@sisu-ai/mw-tool-calling 11.0.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/index.d.ts +17 -0
- package/dist/index.js +18 -291
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @sisu-ai/mw-tool-calling
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Legacy compatibility middleware for provider-native tool call loops.
|
|
4
|
+
|
|
5
|
+
> **Recommended for new code:** use core execution middleware from `@sisu-ai/core`:
|
|
6
|
+
> - `execute` / `executeWith(opts)` for non-streaming
|
|
7
|
+
> - `executeStream` (optionally as `executeStream(opts)`) for streaming
|
|
4
8
|
|
|
5
9
|
[](https://github.com/finger-gun/sisu/actions/workflows/tests.yml)
|
|
6
10
|
[](https://github.com/finger-gun/sisu/actions/workflows/github-code-scanning/codeql)
|
|
@@ -14,7 +18,7 @@ npm i @sisu-ai/mw-tool-calling
|
|
|
14
18
|
```
|
|
15
19
|
|
|
16
20
|
## Behavior
|
|
17
|
-
- `toolCalling`: single-round
|
|
21
|
+
- `toolCalling`: single-round compatibility mode.
|
|
18
22
|
- First turn: calls `ctx.model.generate(messages, { tools, toolChoice:'auto' })`.
|
|
19
23
|
- If assistant returns `tool_calls`, appends the assistant message and executes each tool.
|
|
20
24
|
- Executes each unique `(name, args)` once and responds to every `tool_call_id`.
|
|
@@ -46,7 +50,7 @@ sequenceDiagram
|
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
```
|
|
49
|
-
- `iterativeToolCalling`: multi-round
|
|
53
|
+
- `iterativeToolCalling`: multi-round compatibility mode.
|
|
50
54
|
- Repeats calls with `toolChoice:'auto'` until the model returns a message with no `tool_calls` (max 12 iters).
|
|
51
55
|
|
|
52
56
|
```mermaid
|
|
@@ -88,6 +92,22 @@ agent.use(toolCalling);
|
|
|
88
92
|
agent.use(iterativeToolCalling);
|
|
89
93
|
```
|
|
90
94
|
|
|
95
|
+
## Migration to Core Execution APIs
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { Agent, execute, getExecutionResult } from '@sisu-ai/core';
|
|
99
|
+
import { registerTools } from '@sisu-ai/mw-register-tools';
|
|
100
|
+
import { inputToMessage } from '@sisu-ai/mw-conversation-buffer';
|
|
101
|
+
|
|
102
|
+
const app = new Agent()
|
|
103
|
+
.use(registerTools([myTool]))
|
|
104
|
+
.use(inputToMessage)
|
|
105
|
+
.use(execute);
|
|
106
|
+
|
|
107
|
+
await app.handler()(ctx); // primary path
|
|
108
|
+
console.log(getExecutionResult(ctx)?.text);
|
|
109
|
+
```
|
|
110
|
+
|
|
91
111
|
## Tool Execution Records
|
|
92
112
|
|
|
93
113
|
Each executed tool call is appended to `ctx.state.toolExecutions`:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
import type { Middleware } from "@sisu-ai/core";
|
|
2
|
+
/**
|
|
3
|
+
* Legacy compatibility middleware.
|
|
4
|
+
*
|
|
5
|
+
* @deprecated Prefer core execution middleware from `@sisu-ai/core`:
|
|
6
|
+
* - `.use(execute)` or `.use(executeWith(opts))`
|
|
7
|
+
* - `.use(executeStream)` or `.use(executeStream(opts))`
|
|
8
|
+
*
|
|
9
|
+
* Migration guide: https://github.com/finger-gun/sisu/tree/main/packages/core#execution-apis-recommended
|
|
10
|
+
*/
|
|
2
11
|
export declare const toolCalling: Middleware;
|
|
12
|
+
/**
|
|
13
|
+
* Legacy compatibility middleware.
|
|
14
|
+
*
|
|
15
|
+
* @deprecated Prefer `.use(execute)` (iterative by default) or
|
|
16
|
+
* `.use(executeWith({ strategy: "iterative" }))` from `@sisu-ai/core`.
|
|
17
|
+
*
|
|
18
|
+
* Migration guide: https://github.com/finger-gun/sisu/tree/main/packages/core#execution-apis-recommended
|
|
19
|
+
*/
|
|
3
20
|
export declare const iterativeToolCalling: Middleware;
|
package/dist/index.js
CHANGED
|
@@ -1,297 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
const existing = ctx.state.toolExecutions;
|
|
3
|
-
if (Array.isArray(existing)) {
|
|
4
|
-
existing.push(record);
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
|
-
ctx.state.toolExecutions = [record];
|
|
8
|
-
}
|
|
1
|
+
import { executeWith } from "@sisu-ai/core";
|
|
9
2
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
3
|
+
* Legacy compatibility middleware.
|
|
4
|
+
*
|
|
5
|
+
* @deprecated Prefer core execution middleware from `@sisu-ai/core`:
|
|
6
|
+
* - `.use(execute)` or `.use(executeWith(opts))`
|
|
7
|
+
* - `.use(executeStream)` or `.use(executeStream(opts))`
|
|
8
|
+
*
|
|
9
|
+
* Migration guide: https://github.com/finger-gun/sisu/tree/main/packages/core#execution-apis-recommended
|
|
12
10
|
*/
|
|
13
|
-
function applyAliasesToTools(tools, aliasMap) {
|
|
14
|
-
if (!aliasMap || aliasMap.size === 0) {
|
|
15
|
-
// No aliases configured - return tools as-is with identity map
|
|
16
|
-
const reverseMap = new Map();
|
|
17
|
-
for (const tool of tools) {
|
|
18
|
-
reverseMap.set(tool.name, tool.name);
|
|
19
|
-
}
|
|
20
|
-
return { aliasedTools: tools, reverseMap };
|
|
21
|
-
}
|
|
22
|
-
const aliasedTools = [];
|
|
23
|
-
const reverseMap = new Map();
|
|
24
|
-
for (const tool of tools) {
|
|
25
|
-
const alias = aliasMap.get(tool.name);
|
|
26
|
-
if (alias) {
|
|
27
|
-
// Create new tool object with aliased name
|
|
28
|
-
const aliasedTool = { ...tool, name: alias };
|
|
29
|
-
aliasedTools.push(aliasedTool);
|
|
30
|
-
reverseMap.set(alias, tool.name); // alias -> canonical
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
// No alias for this tool - use as-is
|
|
34
|
-
aliasedTools.push(tool);
|
|
35
|
-
reverseMap.set(tool.name, tool.name); // identity mapping
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return { aliasedTools, reverseMap };
|
|
39
|
-
}
|
|
40
|
-
function hasParseSchema(schema) {
|
|
41
|
-
if (!schema || typeof schema !== "object") {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
const maybeSchema = schema;
|
|
45
|
-
return typeof maybeSchema.parse === "function";
|
|
46
|
-
}
|
|
47
|
-
function normalizeToolArguments(args) {
|
|
48
|
-
if (typeof args !== "string") {
|
|
49
|
-
return args;
|
|
50
|
-
}
|
|
51
|
-
let current = args;
|
|
52
|
-
for (let i = 0; i < 2; i++) {
|
|
53
|
-
if (typeof current !== "string") {
|
|
54
|
-
return current;
|
|
55
|
-
}
|
|
56
|
-
const trimmed = current.trim();
|
|
57
|
-
if (!trimmed) {
|
|
58
|
-
return args;
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
current = JSON.parse(trimmed);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return current;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return current;
|
|
68
|
-
}
|
|
69
11
|
export const toolCalling = async (ctx, next) => {
|
|
70
|
-
await
|
|
71
|
-
const toolList = ctx.tools.list();
|
|
72
|
-
const userAliases = ctx.state.toolAliases;
|
|
73
|
-
const { aliasedTools, reverseMap } = applyAliasesToTools(toolList, userAliases);
|
|
74
|
-
for (let i = 0; i < 6; i++) {
|
|
75
|
-
ctx.log.debug?.("[tool-calling] iteration start", {
|
|
76
|
-
i,
|
|
77
|
-
messages: ctx.messages.length,
|
|
78
|
-
});
|
|
79
|
-
const allowTools = i === 0 ? "auto" : "none";
|
|
80
|
-
const genOpts = {
|
|
81
|
-
toolChoice: allowTools,
|
|
82
|
-
signal: ctx.signal,
|
|
83
|
-
};
|
|
84
|
-
if (allowTools !== "none") {
|
|
85
|
-
genOpts.tools = aliasedTools; // Pass renamed tools to adapter
|
|
86
|
-
genOpts.parallelToolCalls = false;
|
|
87
|
-
}
|
|
88
|
-
const out = await ctx.model.generate(ctx.messages, genOpts);
|
|
89
|
-
if (!out || typeof out !== "object" || !("message" in out)) {
|
|
90
|
-
throw new Error("[tool-calling] model did not return a message");
|
|
91
|
-
}
|
|
92
|
-
const msg = out.message;
|
|
93
|
-
const toolCalls = msg.tool_calls;
|
|
94
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
95
|
-
// Important: include the assistant message that requested tools so tool_call_id has a valid anchor.
|
|
96
|
-
ctx.messages.push(msg);
|
|
97
|
-
ctx.log.info?.("[tool-calling] model requested tools", toolCalls.map((tc) => ({
|
|
98
|
-
id: tc.id,
|
|
99
|
-
name: tc.name,
|
|
100
|
-
hasArgs: typeof tc.arguments !== "undefined",
|
|
101
|
-
})));
|
|
102
|
-
// Execute each unique (name,args) once, but reply to every tool_call_id.
|
|
103
|
-
const cache = new Map();
|
|
104
|
-
const keyOf = (tc) => `${tc.name}:${safeStableStringify(tc.arguments)}`;
|
|
105
|
-
const lastArgsByName = new Map();
|
|
106
|
-
// Pre-pass: fill missing arguments from last seen arguments of same tool name (provider quirk)
|
|
107
|
-
const resolvedCalls = toolCalls.map((tc) => {
|
|
108
|
-
if (typeof tc.arguments === "undefined" &&
|
|
109
|
-
lastArgsByName.has(tc.name)) {
|
|
110
|
-
return { ...tc, arguments: lastArgsByName.get(tc.name) };
|
|
111
|
-
}
|
|
112
|
-
return tc;
|
|
113
|
-
});
|
|
114
|
-
for (const call of resolvedCalls) {
|
|
115
|
-
// Resolve alias back to canonical tool name
|
|
116
|
-
const canonicalName = reverseMap.get(call.name);
|
|
117
|
-
if (!canonicalName)
|
|
118
|
-
throw new Error("Unknown tool: " + call.name);
|
|
119
|
-
const tool = ctx.tools.get(canonicalName);
|
|
120
|
-
if (!tool)
|
|
121
|
-
throw new Error("Unknown tool: " + canonicalName);
|
|
122
|
-
const key = keyOf(call);
|
|
123
|
-
let result = cache.get(key);
|
|
124
|
-
if (result === undefined) {
|
|
125
|
-
const schema = tool.schema;
|
|
126
|
-
const normalizedArgs = normalizeToolArguments(call.arguments);
|
|
127
|
-
const args = hasParseSchema(schema)
|
|
128
|
-
? schema.parse(normalizedArgs)
|
|
129
|
-
: normalizedArgs;
|
|
130
|
-
ctx.log.debug?.("[tool-calling] invoking tool", {
|
|
131
|
-
aliasName: call.name,
|
|
132
|
-
canonicalName: canonicalName,
|
|
133
|
-
id: call.id,
|
|
134
|
-
args,
|
|
135
|
-
});
|
|
136
|
-
// Create restricted context for tool execution
|
|
137
|
-
const toolCtx = {
|
|
138
|
-
memory: ctx.memory,
|
|
139
|
-
signal: ctx.signal,
|
|
140
|
-
log: ctx.log,
|
|
141
|
-
model: ctx.model,
|
|
142
|
-
// Pass deps from state for dependency injection (testing/configuration)
|
|
143
|
-
deps: ctx.state?.toolDeps,
|
|
144
|
-
};
|
|
145
|
-
result = await tool.handler(args, toolCtx);
|
|
146
|
-
cache.set(key, result);
|
|
147
|
-
lastArgsByName.set(call.name, args);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
ctx.log.debug?.("[tool-calling] reusing cached tool result for duplicate call", { name: call.name, id: call.id });
|
|
151
|
-
}
|
|
152
|
-
// Prefer tool_call_id when available (tools API)
|
|
153
|
-
const toolMsg = {
|
|
154
|
-
role: "tool",
|
|
155
|
-
content: JSON.stringify(result),
|
|
156
|
-
...(call.id ? { tool_call_id: call.id } : { name: call.name }),
|
|
157
|
-
};
|
|
158
|
-
ctx.messages.push(toolMsg);
|
|
159
|
-
pushToolExecution(ctx, {
|
|
160
|
-
aliasName: call.name,
|
|
161
|
-
canonicalName,
|
|
162
|
-
callId: call.id,
|
|
163
|
-
args: lastArgsByName.get(call.name),
|
|
164
|
-
result,
|
|
165
|
-
});
|
|
166
|
-
ctx.log.debug?.("[tool-calling] tool result appended", {
|
|
167
|
-
name: call.name,
|
|
168
|
-
id: call.id,
|
|
169
|
-
contentBytes: toolMsg.content.length,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
ctx.log.info?.("[tool-calling] no tool calls; appending assistant message");
|
|
176
|
-
ctx.messages.push(msg);
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
12
|
+
await executeWith({ strategy: "single", maxRounds: 6 })(ctx, next);
|
|
180
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Legacy compatibility middleware.
|
|
16
|
+
*
|
|
17
|
+
* @deprecated Prefer `.use(execute)` (iterative by default) or
|
|
18
|
+
* `.use(executeWith({ strategy: "iterative" }))` from `@sisu-ai/core`.
|
|
19
|
+
*
|
|
20
|
+
* Migration guide: https://github.com/finger-gun/sisu/tree/main/packages/core#execution-apis-recommended
|
|
21
|
+
*/
|
|
181
22
|
export const iterativeToolCalling = async (ctx, next) => {
|
|
182
|
-
await
|
|
183
|
-
const maxIters = 12;
|
|
184
|
-
const toolList = ctx.tools.list();
|
|
185
|
-
const userAliases = ctx.state.toolAliases;
|
|
186
|
-
const { aliasedTools, reverseMap } = applyAliasesToTools(toolList, userAliases);
|
|
187
|
-
for (let i = 0; i < maxIters; i++) {
|
|
188
|
-
ctx.log.debug?.("[iterative-tool-calling] iteration start", {
|
|
189
|
-
i,
|
|
190
|
-
messages: ctx.messages.length,
|
|
191
|
-
});
|
|
192
|
-
const genOpts = {
|
|
193
|
-
toolChoice: "auto",
|
|
194
|
-
tools: aliasedTools, // Pass renamed tools to adapter
|
|
195
|
-
parallelToolCalls: false,
|
|
196
|
-
signal: ctx.signal,
|
|
197
|
-
};
|
|
198
|
-
const out = await ctx.model.generate(ctx.messages, genOpts);
|
|
199
|
-
const msg = out.message;
|
|
200
|
-
const toolCalls = msg.tool_calls;
|
|
201
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
202
|
-
// include the assistant message that requested tools
|
|
203
|
-
ctx.messages.push(msg);
|
|
204
|
-
ctx.log.info?.("[iterative-tool-calling] model requested tools", toolCalls.map((tc) => ({
|
|
205
|
-
id: tc.id,
|
|
206
|
-
name: tc.name,
|
|
207
|
-
hasArgs: typeof tc.arguments !== "undefined",
|
|
208
|
-
})));
|
|
209
|
-
const cache = new Map();
|
|
210
|
-
const keyOf = (tc) => `${tc.name}:${safeStableStringify(tc.arguments)}`;
|
|
211
|
-
const lastArgsByName = new Map();
|
|
212
|
-
const resolvedCalls = toolCalls.map((tc) => {
|
|
213
|
-
if (typeof tc.arguments === "undefined" &&
|
|
214
|
-
lastArgsByName.has(tc.name)) {
|
|
215
|
-
return { ...tc, arguments: lastArgsByName.get(tc.name) };
|
|
216
|
-
}
|
|
217
|
-
return tc;
|
|
218
|
-
});
|
|
219
|
-
for (const call of resolvedCalls) {
|
|
220
|
-
// Resolve alias back to canonical tool name
|
|
221
|
-
const canonicalName = reverseMap.get(call.name);
|
|
222
|
-
if (!canonicalName)
|
|
223
|
-
throw new Error("Unknown tool: " + call.name);
|
|
224
|
-
const tool = ctx.tools.get(canonicalName);
|
|
225
|
-
if (!tool)
|
|
226
|
-
throw new Error("Unknown tool: " + canonicalName);
|
|
227
|
-
const key = keyOf(call);
|
|
228
|
-
let result = cache.get(key);
|
|
229
|
-
if (result === undefined) {
|
|
230
|
-
const schema = tool.schema;
|
|
231
|
-
const normalizedArgs = normalizeToolArguments(call.arguments);
|
|
232
|
-
const args = hasParseSchema(schema)
|
|
233
|
-
? schema.parse(normalizedArgs)
|
|
234
|
-
: normalizedArgs;
|
|
235
|
-
ctx.log.debug?.("[iterative-tool-calling] invoking tool", {
|
|
236
|
-
aliasName: call.name,
|
|
237
|
-
canonicalName: canonicalName,
|
|
238
|
-
id: call.id,
|
|
239
|
-
args,
|
|
240
|
-
});
|
|
241
|
-
// Create restricted context for tool execution
|
|
242
|
-
const toolCtx = {
|
|
243
|
-
memory: ctx.memory,
|
|
244
|
-
signal: ctx.signal,
|
|
245
|
-
log: ctx.log,
|
|
246
|
-
model: ctx.model,
|
|
247
|
-
// Pass deps from state for dependency injection (testing/configuration)
|
|
248
|
-
deps: ctx.state?.toolDeps,
|
|
249
|
-
};
|
|
250
|
-
result = await tool.handler(args, toolCtx);
|
|
251
|
-
cache.set(key, result);
|
|
252
|
-
lastArgsByName.set(call.name, args);
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
ctx.log.debug?.("[iterative-tool-calling] reusing cached result", {
|
|
256
|
-
name: call.name,
|
|
257
|
-
id: call.id,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
const toolMsg = {
|
|
261
|
-
role: "tool",
|
|
262
|
-
content: JSON.stringify(result),
|
|
263
|
-
...(call.id ? { tool_call_id: call.id } : { name: call.name }),
|
|
264
|
-
};
|
|
265
|
-
ctx.messages.push(toolMsg);
|
|
266
|
-
pushToolExecution(ctx, {
|
|
267
|
-
aliasName: call.name,
|
|
268
|
-
canonicalName,
|
|
269
|
-
callId: call.id,
|
|
270
|
-
args: lastArgsByName.get(call.name),
|
|
271
|
-
result,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
continue; // next round may call more tools
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
ctx.log.info?.("[iterative-tool-calling] no tool calls; appending assistant message");
|
|
278
|
-
ctx.messages.push(msg);
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
23
|
+
await executeWith({ strategy: "iterative", maxRounds: 12 })(ctx, next);
|
|
282
24
|
};
|
|
283
|
-
function safeStableStringify(v) {
|
|
284
|
-
try {
|
|
285
|
-
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
286
|
-
const keys = Object.keys(v).sort();
|
|
287
|
-
const obj = {};
|
|
288
|
-
for (const k of keys)
|
|
289
|
-
obj[k] = v[k];
|
|
290
|
-
return JSON.stringify(obj);
|
|
291
|
-
}
|
|
292
|
-
return JSON.stringify(v);
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
return String(v);
|
|
296
|
-
}
|
|
297
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sisu-ai/mw-tool-calling",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@sisu-ai/core": "^2.
|
|
15
|
+
"@sisu-ai/core": "^2.6.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@sisu-ai/core": "2.6.0",
|
|
19
|
+
"@sisu-ai/mw-register-tools": "12.0.0"
|
|
16
20
|
},
|
|
17
21
|
"repository": {
|
|
18
22
|
"type": "git",
|