File size: 4,423 Bytes
68d7816
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import type { ModelCapability } from '#/llm/capability';
import type { ToolDescription } from '#/llm/message';
import type { ToolDefinition } from '#/tool/tool';

export const SELECT_TOOLS_TOOL_NAME = 'select_tools';

export interface LoadToolsResult {
  readonly toLoad: readonly string[];
  readonly alreadyAvailable: readonly string[];
  readonly unknown: readonly string[];
}

export interface ToolSelectState {
  enabled(): boolean;
  isLoadable(name: string): boolean;
  isLoaded(name: string): boolean;
  load(names: readonly string[]): LoadToolsResult;
  pendingSchemas(): ToolDescription[];
  announcement(): string | undefined;
  markSchemasLanded(): void;
  markAnnounced(): void;
  reset(): void;
}

export interface CreateToolSelectStateOptions {
  loadable: () => readonly ToolDefinition[];
  enabled: () => boolean;
}

export function isToolSelectEnabled(capability: ModelCapability): boolean {
  return capability.dynamically_loaded_tools === true && capability.tool_use;
}

export function renderLoadableToolsAnnouncement(
  added: readonly string[],
  removed: readonly string[],
): string {
  const sections: string[] = [];
  if (added.length > 0) {
    sections.push(`<tools_added>\n${added.join('\n')}\n</tools_added>`);
  }
  if (removed.length > 0) {
    sections.push(`<tools_removed>\n${removed.join('\n')}\n</tools_removed>`);
  }
  sections.push(
    'Use the select_tools tool with exact names to load full tool definitions before calling them. ' +
      'Names listed as removed are no longer loadable — do not select them. ' +
      'Fold all announcements in this conversation in order to get the current list.',
  );
  return sections.join('\n\n');
}

export function createToolSelectState({
  loadable,
  enabled,
}: CreateToolSelectStateOptions): ToolSelectState {
  const pending = new Set<string>();
  const landed = new Set<string>();
  const announced = new Set<string>();
  let pendingAnnouncement: { added: string[]; removed: string[] } | undefined;

  const loadableNames = () => new Set(loadable().map((tool) => tool.name));

  const schemaOf = (name: string): ToolDescription | undefined => {
    const tool = loadable().find((entry) => entry.name === name);
    if (tool === undefined) return undefined;
    return { name: tool.name, description: tool.description, parameters: tool.parameters };
  };

  return {
    enabled,
    isLoadable: (name) => loadableNames().has(name),
    isLoaded: (name) => pending.has(name) || landed.has(name),
    load: (names) => {
      const loadableSet = loadableNames();
      const toLoad: string[] = [];
      const alreadyAvailable: string[] = [];
      const unknown: string[] = [];
      for (const name of new Set(names)) {
        if (pending.has(name) || landed.has(name)) {
          alreadyAvailable.push(name);
        } else if (loadableSet.has(name)) {
          toLoad.push(name);
        } else {
          unknown.push(name);
        }
      }
      for (const name of toLoad) pending.add(name);
      return { toLoad, alreadyAvailable, unknown };
    },
    pendingSchemas: () =>
      [...pending]
        .toSorted((a, b) => a.localeCompare(b))
        .flatMap((name) => {
          const schema = schemaOf(name);
          return schema === undefined ? [] : [schema];
        }),
    announcement: () => {
      if (!enabled()) return undefined;
      const names = loadable()
        .map((tool) => tool.name)
        .toSorted((a, b) => a.localeCompare(b));
      const namesSet = new Set(names);
      const added = names.filter((name) => !announced.has(name));
      const removed = [...announced]
        .filter((name) => !namesSet.has(name))
        .toSorted((a, b) => a.localeCompare(b));
      if (added.length === 0 && removed.length === 0) return undefined;
      pendingAnnouncement = { added, removed };
      return renderLoadableToolsAnnouncement(added, removed);
    },
    markSchemasLanded: () => {
      for (const name of pending) landed.add(name);
      pending.clear();
    },
    markAnnounced: () => {
      if (pendingAnnouncement === undefined) return;
      for (const name of pendingAnnouncement.added) announced.add(name);
      for (const name of pendingAnnouncement.removed) announced.delete(name);
      pendingAnnouncement = undefined;
    },
    reset: () => {
      pending.clear();
      landed.clear();
      announced.clear();
      pendingAnnouncement = undefined;
    },
  };
}