@jxsuite/ai 0.37.2 → 0.39.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 +48 -14
- package/package.json +5 -3
- package/src/cache-hints.ts +111 -0
- package/src/chat-state.ts +100 -10
- package/src/core-types.ts +97 -0
- package/src/gateway/chat.ts +224 -0
- package/src/gateway/index.ts +33 -0
- package/src/gateway/models.ts +22 -0
- package/src/gateway/normalize.ts +302 -0
- package/src/gateway/problem.ts +80 -0
- package/src/gateway/sse.ts +42 -0
- package/src/gateway/types.ts +87 -0
- package/src/gateway/upstream-error.ts +54 -0
- package/src/index.ts +14 -2
- package/src/streaming-client.ts +182 -49
- package/src/tools.ts +153 -14
package/src/tools.ts
CHANGED
|
@@ -5,16 +5,33 @@
|
|
|
5
5
|
* can call. Tools have JSON Schema parameter definitions and execute functions.
|
|
6
6
|
* The registry validates arguments before execution.
|
|
7
7
|
*
|
|
8
|
+
* Every call carries a {@link ToolContext}: its own signal, its call id, who it acts for, the turn's
|
|
9
|
+
* write ledger and the session's facts (specs/ai.md §3.7). A caller that has none gets a detached
|
|
10
|
+
* one, so a tool run outside a turn (a test, a command) records into nothing and cannot be stopped.
|
|
11
|
+
*
|
|
12
|
+
* @docs extending/embedding/assistant-harness
|
|
8
13
|
* @license MIT
|
|
9
14
|
* @module @jxsuite/ai/tools
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
import type {
|
|
18
|
+
AiWrite,
|
|
19
|
+
JsonValue,
|
|
20
|
+
SessionFacts,
|
|
21
|
+
ToolContext,
|
|
22
|
+
ToolResult,
|
|
23
|
+
WriteLedger,
|
|
24
|
+
} from "./core-types.ts";
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
Actor,
|
|
28
|
+
AiWrite,
|
|
29
|
+
JsonValue,
|
|
30
|
+
SessionFacts,
|
|
31
|
+
ToolContext,
|
|
32
|
+
ToolResult,
|
|
33
|
+
WriteLedger,
|
|
34
|
+
} from "./core-types.ts";
|
|
18
35
|
|
|
19
36
|
/**
|
|
20
37
|
* JSON Schema fragment describing a tool's parameters. Only the subset the registry's lightweight
|
|
@@ -33,7 +50,12 @@ export interface ToolDefinition {
|
|
|
33
50
|
parameters: JSONSchema;
|
|
34
51
|
strict: boolean;
|
|
35
52
|
llmStrict: boolean;
|
|
36
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The call suspends the turn on a person rather than doing work (`ask_user`), so a round that
|
|
55
|
+
* made only such calls does not spend the turn's work budget.
|
|
56
|
+
*/
|
|
57
|
+
interactive?: boolean;
|
|
58
|
+
execute: (args: object, ctx: ToolContext) => Promise<ToolResult> | ToolResult;
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
export interface ToolRegistry {
|
|
@@ -41,7 +63,11 @@ export interface ToolRegistry {
|
|
|
41
63
|
list: () => ToolDefinition[];
|
|
42
64
|
listForLLM: () => object[];
|
|
43
65
|
validate: (toolName: string, args: object) => { valid: boolean; errors?: string[] };
|
|
44
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Run a tool with the call's context; a registry that wraps another forwards `ctx` unchanged.
|
|
68
|
+
* Omitted, the call gets a detached context ({@link createToolContext}).
|
|
69
|
+
*/
|
|
70
|
+
execute: (toolName: string, args: object, ctx?: ToolContext) => Promise<ToolResult>;
|
|
45
71
|
getDefinition: (toolName: string) => ToolDefinition | undefined;
|
|
46
72
|
}
|
|
47
73
|
|
|
@@ -82,8 +108,9 @@ export function toolError(error: string): ToolResult {
|
|
|
82
108
|
* @param {boolean} [opts.llmStrict] - Whether to send OpenAI `strict: true` in the function schema
|
|
83
109
|
* (default false). Only enable if the parameter schema is OpenAI-strict-compliant
|
|
84
110
|
* (additionalProperties:false everywhere, all properties in `required`, every property typed).
|
|
85
|
-
* @param {
|
|
86
|
-
*
|
|
111
|
+
* @param {boolean} [opts.interactive] - The call suspends the turn on a person (`ask_user`)
|
|
112
|
+
* @param {(args: object, ctx: ToolContext) => Promise<ToolResult> | ToolResult} opts.execute - The
|
|
113
|
+
* tool implementation, handed the call's context
|
|
87
114
|
* @returns {ToolDefinition}
|
|
88
115
|
*/
|
|
89
116
|
export function createToolDefinition({
|
|
@@ -92,6 +119,7 @@ export function createToolDefinition({
|
|
|
92
119
|
parameters,
|
|
93
120
|
strict = true,
|
|
94
121
|
llmStrict = false,
|
|
122
|
+
interactive,
|
|
95
123
|
execute,
|
|
96
124
|
}: {
|
|
97
125
|
name: string;
|
|
@@ -99,9 +127,119 @@ export function createToolDefinition({
|
|
|
99
127
|
parameters: JSONSchema;
|
|
100
128
|
strict?: boolean;
|
|
101
129
|
llmStrict?: boolean;
|
|
102
|
-
|
|
130
|
+
interactive?: boolean;
|
|
131
|
+
execute: (args: object, ctx: ToolContext) => Promise<ToolResult> | ToolResult;
|
|
103
132
|
}): ToolDefinition {
|
|
104
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
name,
|
|
135
|
+
description,
|
|
136
|
+
parameters,
|
|
137
|
+
strict,
|
|
138
|
+
llmStrict,
|
|
139
|
+
...(interactive === undefined ? {} : { interactive }),
|
|
140
|
+
execute,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── ToolContext ─────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A write ledger: the writes one turn's tools record, in order.
|
|
148
|
+
*
|
|
149
|
+
* @param {(write: AiWrite) => void} [onRecord] - Told of each write as it is recorded
|
|
150
|
+
* @returns {WriteLedger}
|
|
151
|
+
*/
|
|
152
|
+
export function createLedger(onRecord?: (write: AiWrite) => void): WriteLedger {
|
|
153
|
+
const writes: AiWrite[] = [];
|
|
154
|
+
return {
|
|
155
|
+
record(write: AiWrite) {
|
|
156
|
+
writes.push(write);
|
|
157
|
+
onRecord?.(write);
|
|
158
|
+
},
|
|
159
|
+
writes,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Per-session facts: JSON a tool remembers across one conversation's turns.
|
|
165
|
+
*
|
|
166
|
+
* @param {string | null} [sessionId]
|
|
167
|
+
* @param {Readonly<Record<string, JsonValue>>} [initial] - Facts to start from (a restored session)
|
|
168
|
+
* @returns {SessionFacts & { toJSON: () => Record<string, JsonValue> }}
|
|
169
|
+
*/
|
|
170
|
+
export function createSessionFacts(
|
|
171
|
+
sessionId: string | null = null,
|
|
172
|
+
initial: Readonly<Record<string, JsonValue>> = {},
|
|
173
|
+
): SessionFacts & { toJSON: () => Record<string, JsonValue> } {
|
|
174
|
+
const facts = new Map<string, JsonValue>(Object.entries(initial));
|
|
175
|
+
return {
|
|
176
|
+
sessionId,
|
|
177
|
+
get: (key: string) => facts.get(key),
|
|
178
|
+
set: (key: string, value: JsonValue) => {
|
|
179
|
+
facts.set(key, value);
|
|
180
|
+
},
|
|
181
|
+
toJSON: () => Object.fromEntries(facts),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A signal that never aborts: the one a detached call gets. */
|
|
186
|
+
const NEVER = new AbortController().signal;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A tool call's context. Every field not given is detached: a signal that never aborts, an empty
|
|
190
|
+
* call id, a fresh ledger nothing files, empty session facts and a progress sink that drops
|
|
191
|
+
* everything. That is what a call made outside any turn gets.
|
|
192
|
+
*
|
|
193
|
+
* @param {Partial<ToolContext>} [init]
|
|
194
|
+
* @returns {ToolContext}
|
|
195
|
+
*/
|
|
196
|
+
export function createToolContext(init: Partial<ToolContext> = {}): ToolContext {
|
|
197
|
+
const session = init.session ?? createSessionFacts();
|
|
198
|
+
return {
|
|
199
|
+
actor: init.actor ?? {
|
|
200
|
+
id: `assistant:${session.sessionId ?? "local"}:`,
|
|
201
|
+
kind: "assistant",
|
|
202
|
+
sessionId: session.sessionId,
|
|
203
|
+
turnId: "",
|
|
204
|
+
},
|
|
205
|
+
callId: init.callId ?? "",
|
|
206
|
+
ledger: init.ledger ?? createLedger(),
|
|
207
|
+
progress: init.progress ?? (() => {}),
|
|
208
|
+
session,
|
|
209
|
+
signal: init.signal ?? NEVER,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A call's own signal, linked to its turn's: it aborts when the turn does, with the turn's reason,
|
|
215
|
+
* and {@link release} unlinks it once the call settles, so a Stop later in the turn reaches no call
|
|
216
|
+
* that has already finished.
|
|
217
|
+
*
|
|
218
|
+
* Linked by hand, with one `{ once: true }` listener, rather than with `AbortSignal.any`, so page
|
|
219
|
+
* webviews and the Worker's ES2023 typing agree.
|
|
220
|
+
*
|
|
221
|
+
* @param {AbortSignal} turn
|
|
222
|
+
* @returns {{ readonly signal: AbortSignal; release(): void }}
|
|
223
|
+
*/
|
|
224
|
+
export function linkCallSignal(turn: AbortSignal): {
|
|
225
|
+
readonly signal: AbortSignal;
|
|
226
|
+
release: () => void;
|
|
227
|
+
} {
|
|
228
|
+
const call = new AbortController();
|
|
229
|
+
if (turn.aborted) {
|
|
230
|
+
call.abort(turn.reason);
|
|
231
|
+
return { release: () => {}, signal: call.signal };
|
|
232
|
+
}
|
|
233
|
+
const onAbort = () => {
|
|
234
|
+
call.abort(turn.reason);
|
|
235
|
+
};
|
|
236
|
+
turn.addEventListener("abort", onAbort, { once: true });
|
|
237
|
+
return {
|
|
238
|
+
release: () => {
|
|
239
|
+
turn.removeEventListener("abort", onAbort);
|
|
240
|
+
},
|
|
241
|
+
signal: call.signal,
|
|
242
|
+
};
|
|
105
243
|
}
|
|
106
244
|
|
|
107
245
|
// ─── ToolRegistry ────────────────────────────────────────────────────────────
|
|
@@ -256,9 +394,10 @@ export function createToolRegistry() {
|
|
|
256
394
|
*
|
|
257
395
|
* @param {string} toolName
|
|
258
396
|
* @param {object} args
|
|
397
|
+
* @param {ToolContext} [ctx] - The call's context; omitted, a detached one
|
|
259
398
|
* @returns {Promise<ToolResult>}
|
|
260
399
|
*/
|
|
261
|
-
async execute(toolName: string, args: object) {
|
|
400
|
+
async execute(toolName: string, args: object, ctx: ToolContext = createToolContext()) {
|
|
262
401
|
const tool = _tools.get(toolName);
|
|
263
402
|
if (!tool) {
|
|
264
403
|
return toolError(`Unknown tool: "${toolName}"`);
|
|
@@ -271,7 +410,7 @@ export function createToolRegistry() {
|
|
|
271
410
|
}
|
|
272
411
|
|
|
273
412
|
try {
|
|
274
|
-
return await tool.execute(args);
|
|
413
|
+
return await tool.execute(args, ctx);
|
|
275
414
|
} catch (error) {
|
|
276
415
|
return toolError(`Tool "${toolName}" execution error: ${(error as Error).message}`);
|
|
277
416
|
}
|