@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 CHANGED
@@ -1,6 +1,10 @@
1
1
  # @sisu-ai/mw-tool-calling
2
2
 
3
- Execute provider-native tool call loops with robust iteration, validation, and tool result handling.
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
  [![Tests](https://github.com/finger-gun/sisu/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/finger-gun/sisu/actions/workflows/tests.yml)
6
10
  [![CodeQL](https://github.com/finger-gun/sisu/actions/workflows/github-code-scanning/codeql/badge.svg)](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 tool calling.
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 tool calling.
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
- function pushToolExecution(ctx, record) {
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
- * Apply user-configured aliases to tools before passing to adapter.
11
- * Creates new tool objects with aliased names while keeping originals in registry.
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 next();
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 next();
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": "11.0.0",
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.5.0"
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",