@jxsuite/ai 0.39.5 → 0.41.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/package.json +8 -5
- package/src/catalog/catalog.ts +166 -0
- package/src/catalog/index.ts +17 -0
- package/src/catalog/mcp.ts +80 -0
- package/src/harness/index.ts +2 -1
- package/src/harness/policy.ts +2 -0
- package/src/harness/run.ts +39 -5
- package/src/harness/types.ts +44 -1
- package/src/jx-tools/doc-tools.ts +517 -0
- package/src/jx-tools/entries.ts +129 -0
- package/src/jx-tools/facts.ts +96 -0
- package/src/jx-tools/index.ts +29 -0
- package/src/jx-tools/types.ts +118 -0
- package/src/tools.ts +201 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "AI infrastructure for Jx Suite — streaming LLM client abstraction, tool registry, and reactive chat state",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"./chat-state": "./src/chat-state.ts",
|
|
21
21
|
"./gateway": "./src/gateway/index.ts",
|
|
22
22
|
"./harness": "./src/harness/index.ts",
|
|
23
|
-
"./messages": "./src/messages/index.ts"
|
|
23
|
+
"./messages": "./src/messages/index.ts",
|
|
24
|
+
"./jx-tools": "./src/jx-tools/index.ts",
|
|
25
|
+
"./catalog": "./src/catalog/index.ts"
|
|
24
26
|
},
|
|
25
27
|
"publishConfig": {
|
|
26
28
|
"provenance": true
|
|
@@ -31,11 +33,12 @@
|
|
|
31
33
|
"upgrade": "bun update --latest && bun install"
|
|
32
34
|
},
|
|
33
35
|
"dependencies": {
|
|
34
|
-
"@jxsuite/protocol": "^
|
|
36
|
+
"@jxsuite/protocol": "^5.0.0",
|
|
37
|
+
"@jxsuite/schema": "^4.1.0",
|
|
35
38
|
"@vue/reactivity": "3.5.44"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@cloudflare/workers-types": "^5.
|
|
39
|
-
"@happy-dom/global-registrator": "^21.0.
|
|
41
|
+
"@cloudflare/workers-types": "^5.20261026.1",
|
|
42
|
+
"@happy-dom/global-registrator": "^21.0.4"
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool catalogs (specs/ai.md §3.10): which tools a host has (membership), and which of them the
|
|
3
|
+
* model is offered and may call right now (advertisement and the gate).
|
|
4
|
+
*
|
|
5
|
+
* @module @jxsuite/ai/catalog
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
10
|
+
import { createToolRegistry, toolError } from "../tools.ts";
|
|
11
|
+
import type {
|
|
12
|
+
ToolAnnotations,
|
|
13
|
+
ToolArguments,
|
|
14
|
+
ToolContext,
|
|
15
|
+
ToolDefinition,
|
|
16
|
+
ToolRegistry,
|
|
17
|
+
} from "../tools.ts";
|
|
18
|
+
|
|
19
|
+
/** What a tool's name may be: what every provider and MCP accept. */
|
|
20
|
+
export const TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
21
|
+
|
|
22
|
+
/** A catalog member: a tool definition with the title and the hints a catalog requires. */
|
|
23
|
+
export type CatalogEntry = ToolDefinition & {
|
|
24
|
+
readonly title: string;
|
|
25
|
+
readonly annotations: ToolAnnotations;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** A host's tools, each name once, in the order they were given. */
|
|
29
|
+
export interface Catalog {
|
|
30
|
+
readonly entries: readonly CatalogEntry[];
|
|
31
|
+
/** The member named `name`. */
|
|
32
|
+
readonly get: (name: string) => CatalogEntry | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Why a catalog refused a tool. */
|
|
36
|
+
export class CatalogError extends Error {
|
|
37
|
+
/**
|
|
38
|
+
* `name`: the name is not {@link TOOL_NAME_PATTERN}. `duplicate`: two tools share it. `schema`:
|
|
39
|
+
* its parameters are not an object schema. `annotations`: it has no title or no hints.
|
|
40
|
+
*/
|
|
41
|
+
readonly code: "name" | "duplicate" | "schema" | "annotations";
|
|
42
|
+
|
|
43
|
+
constructor(code: CatalogError["code"], message: string) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "CatalogError";
|
|
46
|
+
this.code = code;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The entry, or the reason it cannot be one. */
|
|
51
|
+
function admit(tool: ToolDefinition): CatalogEntry {
|
|
52
|
+
if (!TOOL_NAME_PATTERN.test(tool.name)) {
|
|
53
|
+
throw new CatalogError("name", `Tool name "${tool.name}" is not ${TOOL_NAME_PATTERN}.`);
|
|
54
|
+
}
|
|
55
|
+
if (tool.parameters.type !== "object") {
|
|
56
|
+
throw new CatalogError("schema", `Tool "${tool.name}" parameters are not an object schema.`);
|
|
57
|
+
}
|
|
58
|
+
const { annotations, title } = tool;
|
|
59
|
+
if (title === undefined || annotations === undefined) {
|
|
60
|
+
throw new CatalogError("annotations", `Tool "${tool.name}" has no title or no annotations.`);
|
|
61
|
+
}
|
|
62
|
+
return { ...tool, annotations, title };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A catalog of `groups`, in order. Membership is decided here, once: a tool is a member or the
|
|
67
|
+
* catalog is not built.
|
|
68
|
+
*
|
|
69
|
+
* @param {...(readonly ToolDefinition[])} groups
|
|
70
|
+
* @returns {Catalog}
|
|
71
|
+
* @throws {CatalogError} For an invalid or duplicate name, a non-object parameter schema, or a
|
|
72
|
+
* missing title or annotations
|
|
73
|
+
*/
|
|
74
|
+
export function createCatalog(...groups: readonly (readonly ToolDefinition[])[]): Catalog {
|
|
75
|
+
const byName = new Map<string, CatalogEntry>();
|
|
76
|
+
for (const tool of groups.flat()) {
|
|
77
|
+
if (byName.has(tool.name)) {
|
|
78
|
+
throw new CatalogError("duplicate", `Tool "${tool.name}" is in the catalog twice.`);
|
|
79
|
+
}
|
|
80
|
+
byName.set(tool.name, admit(tool));
|
|
81
|
+
}
|
|
82
|
+
const entries = [...byName.values()];
|
|
83
|
+
return { entries, get: (name) => byName.get(name) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether `entry` may be called with `args` right now: null, or what it requires (the end of the
|
|
88
|
+
* sentence the model reads, such as `"an open project"`). `args` is null when the question is
|
|
89
|
+
* whether to offer the tool at all.
|
|
90
|
+
*/
|
|
91
|
+
export type Gate = (entry: CatalogEntry, args: ToolArguments | null) => string | null;
|
|
92
|
+
|
|
93
|
+
/** How a host sees its catalog this round. */
|
|
94
|
+
export interface CatalogView {
|
|
95
|
+
/** Refuses a member, at listing and at call time alike. */
|
|
96
|
+
readonly gate?: Gate;
|
|
97
|
+
/**
|
|
98
|
+
* Narrows what the model is offered, without refusing the call: a member it returns false for is
|
|
99
|
+
* not listed, and a call to it still runs.
|
|
100
|
+
*/
|
|
101
|
+
readonly advertise?: (entry: CatalogEntry) => boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A catalog with the plain registry that validates and runs its members. */
|
|
105
|
+
interface CatalogRun {
|
|
106
|
+
members: Catalog;
|
|
107
|
+
runner: ToolRegistry;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The registry a turn reads its tools from: `list()` is the members, `listForLLM()` the members the
|
|
112
|
+
* view admits, and `execute` refuses a call the gate refuses, naming what it requires. `catalog`
|
|
113
|
+
* may be a function, read on every call, for a host whose membership follows its own state.
|
|
114
|
+
*
|
|
115
|
+
* @param {Catalog | (() => Catalog)} catalog
|
|
116
|
+
* @param {CatalogView} [view]
|
|
117
|
+
* @returns {ToolRegistry}
|
|
118
|
+
*/
|
|
119
|
+
export function catalogRegistry(
|
|
120
|
+
catalog: Catalog | (() => Catalog),
|
|
121
|
+
{ advertise, gate }: CatalogView = {},
|
|
122
|
+
): ToolRegistry {
|
|
123
|
+
const runners = new WeakMap<Catalog, ToolRegistry>();
|
|
124
|
+
|
|
125
|
+
/** The catalog now, and the registry that validates and runs its members. */
|
|
126
|
+
function current(): CatalogRun {
|
|
127
|
+
const members = isFunction(catalog) ? catalog() : catalog;
|
|
128
|
+
let runner = runners.get(members);
|
|
129
|
+
if (!runner) {
|
|
130
|
+
runner = createToolRegistry();
|
|
131
|
+
for (const entry of members.entries) {
|
|
132
|
+
runner.register(entry);
|
|
133
|
+
}
|
|
134
|
+
runners.set(members, runner);
|
|
135
|
+
}
|
|
136
|
+
return { members, runner };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function offered(entry: CatalogEntry): boolean {
|
|
140
|
+
return (gate?.(entry, null) ?? null) === null && (advertise?.(entry) ?? true);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
register() {
|
|
145
|
+
throw new TypeError("A catalog's members are given to createCatalog, not registered.");
|
|
146
|
+
},
|
|
147
|
+
list: () => [...current().members.entries],
|
|
148
|
+
listForLLM() {
|
|
149
|
+
const { members, runner } = current();
|
|
150
|
+
const names = new Set(members.entries.filter(offered).map((entry) => entry.name));
|
|
151
|
+
// The runner's own listing, so the bytes are the ones a plain registry sends.
|
|
152
|
+
return runner.listForLLM().filter((_tool, at) => names.has(members.entries[at]!.name));
|
|
153
|
+
},
|
|
154
|
+
getDefinition: (name) => current().members.get(name),
|
|
155
|
+
validate: (name, args) => current().runner.validate(name, args),
|
|
156
|
+
async execute(name: string, args: ToolArguments, ctx?: ToolContext) {
|
|
157
|
+
const { members, runner } = current();
|
|
158
|
+
const entry = members.get(name);
|
|
159
|
+
const requires = entry ? (gate?.(entry, args) ?? null) : null;
|
|
160
|
+
if (requires !== null) {
|
|
161
|
+
return toolError(`Tool "${name}" is not available right now — it requires ${requires}.`);
|
|
162
|
+
}
|
|
163
|
+
return runner.execute(name, args, ctx);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@jxsuite/ai/catalog`: a host's tools as one catalog (specs/ai.md §3.10). Membership is decided
|
|
3
|
+
* once, by `createCatalog`, which refuses a duplicate or malformed tool; what the model is offered
|
|
4
|
+
* each round, and which calls are refused, is the host's view over it. `toMcpTool` and
|
|
5
|
+
* `toCallToolResult` project members and results into MCP's shapes.
|
|
6
|
+
*
|
|
7
|
+
* Worker-safe: the module graph is this directory, `../tools.ts` and `@jxsuite/schema/guards`.
|
|
8
|
+
*
|
|
9
|
+
* @module @jxsuite/ai/catalog
|
|
10
|
+
* @license MIT
|
|
11
|
+
* @docs extending/embedding/assistant-harness
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { catalogRegistry, CatalogError, createCatalog, TOOL_NAME_PATTERN } from "./catalog.ts";
|
|
15
|
+
export type { Catalog, CatalogEntry, CatalogView, Gate } from "./catalog.ts";
|
|
16
|
+
export { toCallToolResult, toMcpTool } from "./mcp.ts";
|
|
17
|
+
export type { McpCallToolResult, McpToolJson } from "./mcp.ts";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A catalog member as the Model Context Protocol names a tool, and a tool result as MCP returns one
|
|
3
|
+
* (specs/ai.md §3.10). Pure data: no MCP SDK is imported, so any host can serve them.
|
|
4
|
+
*
|
|
5
|
+
* @module @jxsuite/ai/catalog
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Undoability } from "../jx-tools/types.ts";
|
|
10
|
+
import type { JSONSchema, ToolResult } from "../tools.ts";
|
|
11
|
+
import type { CatalogEntry } from "./catalog.ts";
|
|
12
|
+
|
|
13
|
+
/** A tool as an MCP `tools/list` result names it. */
|
|
14
|
+
export interface McpToolJson {
|
|
15
|
+
name: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: JSONSchema;
|
|
19
|
+
/** All four, always stated: MCP's defaults are the opposite of a safe guess for some. */
|
|
20
|
+
annotations: {
|
|
21
|
+
readOnlyHint: boolean;
|
|
22
|
+
destructiveHint: boolean;
|
|
23
|
+
idempotentHint: boolean;
|
|
24
|
+
openWorldHint: boolean;
|
|
25
|
+
};
|
|
26
|
+
_meta: { "com.jxsuite/undoable": Undoability };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `entry` as an MCP tool. `undoable` is the serving host's: whether it can take the tool's writes
|
|
31
|
+
* back.
|
|
32
|
+
*
|
|
33
|
+
* @param {CatalogEntry} entry
|
|
34
|
+
* @param {{ undoable: Undoability }} facts
|
|
35
|
+
* @returns {McpToolJson}
|
|
36
|
+
*/
|
|
37
|
+
export function toMcpTool(
|
|
38
|
+
entry: CatalogEntry,
|
|
39
|
+
{ undoable }: { undoable: Undoability },
|
|
40
|
+
): McpToolJson {
|
|
41
|
+
const { destructiveHint, idempotentHint, openWorldHint, readOnlyHint } = entry.annotations;
|
|
42
|
+
return {
|
|
43
|
+
_meta: { "com.jxsuite/undoable": undoable },
|
|
44
|
+
annotations: { destructiveHint, idempotentHint, openWorldHint, readOnlyHint },
|
|
45
|
+
description: entry.description,
|
|
46
|
+
inputSchema: entry.parameters,
|
|
47
|
+
name: entry.name,
|
|
48
|
+
title: entry.title,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** An MCP `tools/call` result. */
|
|
53
|
+
export interface McpCallToolResult {
|
|
54
|
+
content: { type: "text"; text: string }[];
|
|
55
|
+
isError?: true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A tool result as MCP returns it: the summary, then the data as JSON, each its own text block; a
|
|
60
|
+
* failure is its error, flagged. A success with neither is the result itself, as JSON.
|
|
61
|
+
*
|
|
62
|
+
* @param {ToolResult} result
|
|
63
|
+
* @returns {McpCallToolResult}
|
|
64
|
+
*/
|
|
65
|
+
export function toCallToolResult(result: ToolResult): McpCallToolResult {
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
return { content: [{ text: result.error ?? "The tool failed.", type: "text" }], isError: true };
|
|
68
|
+
}
|
|
69
|
+
const texts: string[] = [];
|
|
70
|
+
if (result.summary !== undefined) {
|
|
71
|
+
texts.push(result.summary);
|
|
72
|
+
}
|
|
73
|
+
if (result.data !== undefined) {
|
|
74
|
+
texts.push(JSON.stringify(result.data));
|
|
75
|
+
}
|
|
76
|
+
if (texts.length === 0) {
|
|
77
|
+
texts.push(JSON.stringify(result));
|
|
78
|
+
}
|
|
79
|
+
return { content: texts.map((text) => ({ text, type: "text" })) };
|
|
80
|
+
}
|
package/src/harness/index.ts
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
export { LaneBusyError, createTurnLock } from "./lock.ts";
|
|
17
17
|
export { fromStreamingClient, joinSystem } from "./model.ts";
|
|
18
18
|
export { CHARS_PER_TOKEN, DEFAULT_TURN_POLICY } from "./policy.ts";
|
|
19
|
-
export { runTurn } from "./run.ts";
|
|
19
|
+
export { assistantActorId, runTurn } from "./run.ts";
|
|
20
20
|
|
|
21
21
|
export type {
|
|
22
22
|
HarnessEvent,
|
|
23
|
+
InteractionStrategy,
|
|
23
24
|
ModelFn,
|
|
24
25
|
ModelRequest,
|
|
25
26
|
TurnError,
|
package/src/harness/policy.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @license MIT
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { proceedAlways } from "../tools.ts";
|
|
8
9
|
import type { TurnPolicy } from "./types.ts";
|
|
9
10
|
|
|
10
11
|
/** Average characters per token: the estimate every budget used before providers reported counts. */
|
|
@@ -36,4 +37,5 @@ export const DEFAULT_TURN_POLICY: TurnPolicy = {
|
|
|
36
37
|
);
|
|
37
38
|
},
|
|
38
39
|
emptyText: "The model sent back an empty reply.",
|
|
40
|
+
approval: proceedAlways,
|
|
39
41
|
};
|
package/src/harness/run.ts
CHANGED
|
@@ -35,8 +35,9 @@ import {
|
|
|
35
35
|
createToolContext,
|
|
36
36
|
invokeTool,
|
|
37
37
|
linkCallSignal,
|
|
38
|
+
toolError,
|
|
38
39
|
} from "../tools.ts";
|
|
39
|
-
import type { Actor } from "../tools.ts";
|
|
40
|
+
import type { Actor, ToolCall, ToolContext } from "../tools.ts";
|
|
40
41
|
import { LaneBusyError } from "./lock.ts";
|
|
41
42
|
import { joinSystem } from "./model.ts";
|
|
42
43
|
import { DEFAULT_TURN_POLICY } from "./policy.ts";
|
|
@@ -136,6 +137,15 @@ function createEventQueue() {
|
|
|
136
137
|
};
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The id a turn's {@link Actor} carries: `assistant:<sessionId|local>:<turnId>`. A host that keys
|
|
142
|
+
* anything on the actor before the turn starts (Studio's batch lane, specs/ai.md §3.3) builds it
|
|
143
|
+
* here, so the key it opens cannot drift from the one each call's context will name.
|
|
144
|
+
*/
|
|
145
|
+
export function assistantActorId(sessionId: string | null, turnId: string): string {
|
|
146
|
+
return `assistant:${sessionId ?? "local"}:${turnId}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
139
149
|
/**
|
|
140
150
|
* Start one turn. It begins at once and runs to its end whether or not anyone reads its events;
|
|
141
151
|
* `onEvent` hears each one synchronously, in order, before the engine takes its next step.
|
|
@@ -182,7 +192,7 @@ export function runTurn(input: TurnInput): TurnRun {
|
|
|
182
192
|
const session = input.session ?? createSessionFacts();
|
|
183
193
|
const ledger = input.ledger ?? createLedger();
|
|
184
194
|
const sessionId = input.sessionId === undefined ? session.sessionId : input.sessionId;
|
|
185
|
-
const actorId =
|
|
195
|
+
const actorId = assistantActorId(sessionId, turnId);
|
|
186
196
|
const actor: Actor = input.modelInfo
|
|
187
197
|
? { id: actorId, kind: "assistant", model: input.modelInfo.model, sessionId, turnId }
|
|
188
198
|
: { id: actorId, kind: "assistant", sessionId, turnId };
|
|
@@ -217,6 +227,30 @@ export function runTurn(input: TurnInput): TurnRun {
|
|
|
217
227
|
.slice(answered + 1)
|
|
218
228
|
.some((message) => message.role === "assistant" && !isEmptyAssistant(message));
|
|
219
229
|
|
|
230
|
+
const interactions = input.interactions ?? { mode: "none" };
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Invoke one call to its result. A call that needs a person is put to the host's strategy and
|
|
234
|
+
* invoked again with the answer, so no tool body ever awaits a person (specs/ai.md §3.11).
|
|
235
|
+
*/
|
|
236
|
+
async function invoke(call: ToolCall, ctx: ToolContext, callSignal: AbortSignal) {
|
|
237
|
+
let outcome = await invokeTool(input.tools, call, ctx, { approval: policy.approval });
|
|
238
|
+
while (outcome.status === "needs_input") {
|
|
239
|
+
if (interactions.mode === "none") {
|
|
240
|
+
return toolError(`${call.name} needs a person, and none is available here.`);
|
|
241
|
+
}
|
|
242
|
+
const { request } = outcome;
|
|
243
|
+
emit({ type: "interaction_request", callId: call.callId, request });
|
|
244
|
+
const response = await interactions.resolve(request, {
|
|
245
|
+
callId: call.callId,
|
|
246
|
+
signal: callSignal,
|
|
247
|
+
});
|
|
248
|
+
emit({ type: "interaction_settled", callId: call.callId, response });
|
|
249
|
+
outcome = await invokeTool(input.tools, call, ctx, { approval: policy.approval, response });
|
|
250
|
+
}
|
|
251
|
+
return outcome.result;
|
|
252
|
+
}
|
|
253
|
+
|
|
220
254
|
async function loop(): Promise<TurnOutcome> {
|
|
221
255
|
const appended: ChatMessage[] = [];
|
|
222
256
|
const applied: string[] = [];
|
|
@@ -412,11 +446,11 @@ export function runTurn(input: TurnInput): TurnRun {
|
|
|
412
446
|
});
|
|
413
447
|
let result;
|
|
414
448
|
try {
|
|
415
|
-
|
|
416
|
-
input.tools,
|
|
449
|
+
result = await invoke(
|
|
417
450
|
{ callId, name: call.name, argumentsText: call.argumentsText },
|
|
418
451
|
ctx,
|
|
419
|
-
|
|
452
|
+
link.signal,
|
|
453
|
+
);
|
|
420
454
|
} finally {
|
|
421
455
|
link.release();
|
|
422
456
|
}
|
package/src/harness/types.ts
CHANGED
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
* @license MIT
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
AiWrite,
|
|
12
|
+
ApprovalPolicy,
|
|
13
|
+
InteractionRequest,
|
|
14
|
+
InteractionResponse,
|
|
15
|
+
SessionFacts,
|
|
16
|
+
ToolRegistry,
|
|
17
|
+
ToolResult,
|
|
18
|
+
WriteLedger,
|
|
19
|
+
} from "../tools.ts";
|
|
11
20
|
import type {
|
|
12
21
|
ChatMessage,
|
|
13
22
|
JsonValue,
|
|
@@ -74,8 +83,28 @@ export interface TurnPolicy {
|
|
|
74
83
|
}) => string;
|
|
75
84
|
/** What a turn that drew nothing says. */
|
|
76
85
|
readonly emptyText: string;
|
|
86
|
+
/** Whether each call needs the author's approval before it runs. Defaults to `proceedAlways`. */
|
|
87
|
+
readonly approval: ApprovalPolicy;
|
|
77
88
|
}
|
|
78
89
|
|
|
90
|
+
/**
|
|
91
|
+
* How a turn gets a person's answer when a call needs one (specs/ai.md §3.11): `inline` asks the
|
|
92
|
+
* host and waits on the answer within the turn, and `none` answers that nobody is available.
|
|
93
|
+
*/
|
|
94
|
+
export type InteractionStrategy =
|
|
95
|
+
| {
|
|
96
|
+
readonly mode: "inline";
|
|
97
|
+
/**
|
|
98
|
+
* The person's answer to `request`. Resolve it when the call's `signal` aborts too, with the
|
|
99
|
+
* answer a stopped turn gives (for a question, unanswered and not skipped).
|
|
100
|
+
*/
|
|
101
|
+
readonly resolve: (
|
|
102
|
+
request: InteractionRequest,
|
|
103
|
+
at: { readonly callId: string; readonly signal: AbortSignal },
|
|
104
|
+
) => Promise<InteractionResponse>;
|
|
105
|
+
}
|
|
106
|
+
| { readonly mode: "none" };
|
|
107
|
+
|
|
79
108
|
/** What a host can do between the engine's steps. */
|
|
80
109
|
export interface TurnHooks {
|
|
81
110
|
/** After a call has settled, before its result is reported. */
|
|
@@ -123,6 +152,8 @@ export interface TurnInput {
|
|
|
123
152
|
readonly now?: () => number;
|
|
124
153
|
readonly policy?: Partial<TurnPolicy>;
|
|
125
154
|
readonly hooks?: TurnHooks;
|
|
155
|
+
/** How a call that needs a person gets their answer. Defaults to `{ mode: "none" }`. */
|
|
156
|
+
readonly interactions?: InteractionStrategy;
|
|
126
157
|
/** What the conversation's tools remember across turns. Defaults to a fresh set. */
|
|
127
158
|
readonly session?: SessionFacts;
|
|
128
159
|
/** Where the turn's tools record their writes. Defaults to a fresh ledger. */
|
|
@@ -218,6 +249,18 @@ export type HarnessEvent = EventBase &
|
|
|
218
249
|
readonly name: string;
|
|
219
250
|
readonly interactive: boolean;
|
|
220
251
|
}
|
|
252
|
+
| {
|
|
253
|
+
/** The call needs a person before it can run, and the host was asked. */
|
|
254
|
+
readonly type: "interaction_request";
|
|
255
|
+
readonly callId: string;
|
|
256
|
+
readonly request: InteractionRequest;
|
|
257
|
+
}
|
|
258
|
+
| {
|
|
259
|
+
/** The person answered (or the turn's stop answered for them). */
|
|
260
|
+
readonly type: "interaction_settled";
|
|
261
|
+
readonly callId: string;
|
|
262
|
+
readonly response: InteractionResponse;
|
|
263
|
+
}
|
|
221
264
|
| { readonly type: "tool_progress"; readonly callId: string; readonly progress: JsonValue }
|
|
222
265
|
| { readonly type: "write"; readonly callId: string; readonly write: AiWrite }
|
|
223
266
|
| {
|