@automatalabs/acp-agents 2.0.0 → 3.0.1
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 +53 -17
- package/dist/acp-client.d.ts +10 -4
- package/dist/acp-client.d.ts.map +1 -1
- package/dist/acp-client.js +11 -8
- package/dist/agent/acp-agent.d.ts +84 -26
- package/dist/agent/acp-agent.d.ts.map +1 -1
- package/dist/agent/acp-agent.js +458 -170
- package/dist/agent/errors.d.ts +12 -2
- package/dist/agent/errors.d.ts.map +1 -1
- package/dist/agent/errors.js +38 -9
- package/dist/agent/fork.d.ts +7 -5
- package/dist/agent/fork.d.ts.map +1 -1
- package/dist/agent/fork.js +7 -5
- package/dist/agent/messages.d.ts +42 -0
- package/dist/agent/messages.d.ts.map +1 -0
- package/dist/agent/messages.js +225 -0
- package/dist/agent/probe.d.ts +1 -1
- package/dist/agent/probe.d.ts.map +1 -1
- package/dist/agent/probe.js +5 -1
- package/dist/agent/queue.js +2 -2
- package/dist/agent/routing.d.ts +19 -1
- package/dist/agent/routing.d.ts.map +1 -1
- package/dist/agent/routing.js +41 -4
- package/dist/agent/stream.d.ts +21 -0
- package/dist/agent/stream.d.ts.map +1 -0
- package/dist/agent/stream.js +93 -0
- package/dist/agent/structured.d.ts +7 -3
- package/dist/agent/structured.d.ts.map +1 -1
- package/dist/agent/structured.js +24 -14
- package/dist/agent/tool-host.d.ts +34 -0
- package/dist/agent/tool-host.d.ts.map +1 -0
- package/dist/agent/tool-host.js +138 -0
- package/dist/agent/tools.d.ts +26 -0
- package/dist/agent/tools.d.ts.map +1 -0
- package/dist/agent/tools.js +62 -0
- package/dist/agent/turn.d.ts +6 -3
- package/dist/agent/turn.d.ts.map +1 -1
- package/dist/agent/turn.js +15 -67
- package/dist/agent/types.d.ts +174 -25
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/backend.d.ts +4 -3
- package/dist/backend.d.ts.map +1 -1
- package/dist/backends/codex.d.ts +2 -1
- package/dist/backends/codex.d.ts.map +1 -1
- package/dist/backends/codex.js +3 -2
- package/dist/config-catalog.d.ts +4 -0
- package/dist/config-catalog.d.ts.map +1 -1
- package/dist/config-catalog.js +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/local-mcp-host.d.ts +30 -0
- package/dist/local-mcp-host.d.ts.map +1 -0
- package/dist/local-mcp-host.js +151 -0
- package/dist/protocol-coverage.d.ts +8 -6
- package/dist/protocol-coverage.d.ts.map +1 -1
- package/dist/protocol-coverage.js +3 -2
- package/dist/registry.d.ts +3 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/runner.d.ts +5 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +7 -4
- package/dist/structured-output.d.ts +18 -0
- package/dist/structured-output.d.ts.map +1 -1
- package/dist/structured-output.js +19 -1
- package/dist/structured-tool.d.ts +7 -9
- package/dist/structured-tool.d.ts.map +1 -1
- package/dist/structured-tool.js +14 -102
- package/dist/traits.d.ts +51 -0
- package/dist/traits.d.ts.map +1 -0
- package/dist/traits.js +88 -0
- package/package.json +4 -4
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const DONE = { value: undefined, done: true };
|
|
2
|
+
export class TurnStream {
|
|
3
|
+
#buffer = [];
|
|
4
|
+
#waiters = [];
|
|
5
|
+
#stop;
|
|
6
|
+
/** Nothing more will be pushed: the turn settled (either way) or the consumer left. */
|
|
7
|
+
#closed = false;
|
|
8
|
+
/** The turn's rejection, thrown to the consumer once the buffer has drained. */
|
|
9
|
+
#failure;
|
|
10
|
+
#stopping;
|
|
11
|
+
/** `stop` aborts the turn and resolves once it settled; called at most once. */
|
|
12
|
+
constructor(stop) {
|
|
13
|
+
this.#stop = stop;
|
|
14
|
+
}
|
|
15
|
+
/** Buffer an event, or hand it straight to a waiting `next()`. Ignored once closed. */
|
|
16
|
+
push(event) {
|
|
17
|
+
if (this.#closed)
|
|
18
|
+
return;
|
|
19
|
+
const waiter = this.#waiters.shift();
|
|
20
|
+
if (waiter)
|
|
21
|
+
waiter.resolve({ value: event, done: false });
|
|
22
|
+
else
|
|
23
|
+
this.#buffer.push(event);
|
|
24
|
+
}
|
|
25
|
+
/** The turn resolved; the terminal event is already buffered. */
|
|
26
|
+
end() {
|
|
27
|
+
if (this.#closed)
|
|
28
|
+
return;
|
|
29
|
+
this.#closed = true;
|
|
30
|
+
this.#settleWaiters();
|
|
31
|
+
}
|
|
32
|
+
/** The turn rejected: buffered events are still delivered, then `error` is thrown once. */
|
|
33
|
+
fail(error) {
|
|
34
|
+
if (this.#closed)
|
|
35
|
+
return;
|
|
36
|
+
this.#closed = true;
|
|
37
|
+
this.#failure = { error, thrown: false };
|
|
38
|
+
this.#settleWaiters();
|
|
39
|
+
}
|
|
40
|
+
next() {
|
|
41
|
+
const buffered = this.#buffer.shift();
|
|
42
|
+
if (buffered !== undefined)
|
|
43
|
+
return Promise.resolve({ value: buffered, done: false });
|
|
44
|
+
if (this.#closed)
|
|
45
|
+
return this.#terminal();
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
this.#waiters.push({ resolve, reject });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** The consumer left early: abort the turn, wait for it to settle, then report `done`. */
|
|
51
|
+
async return() {
|
|
52
|
+
await this.#leave();
|
|
53
|
+
return DONE;
|
|
54
|
+
}
|
|
55
|
+
/** Same as `return()`, then rethrow `error` to the caller (async-generator semantics). */
|
|
56
|
+
async throw(error) {
|
|
57
|
+
await this.#leave();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
[Symbol.asyncIterator]() {
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
#terminal() {
|
|
64
|
+
if (this.#failure && !this.#failure.thrown) {
|
|
65
|
+
this.#failure.thrown = true;
|
|
66
|
+
return Promise.reject(this.#failure.error);
|
|
67
|
+
}
|
|
68
|
+
return Promise.resolve(DONE);
|
|
69
|
+
}
|
|
70
|
+
/** Waiters exist only while the buffer is empty, so they get the terminal outcome directly. */
|
|
71
|
+
#settleWaiters() {
|
|
72
|
+
for (const waiter of this.#waiters.splice(0)) {
|
|
73
|
+
if (this.#failure && !this.#failure.thrown) {
|
|
74
|
+
this.#failure.thrown = true;
|
|
75
|
+
waiter.reject(this.#failure.error);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
waiter.resolve(DONE);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
#leave() {
|
|
83
|
+
this.#stopping ??= this.#stop();
|
|
84
|
+
// Undelivered events are the consumer's to discard; a pending `next()` sees `done`, never a
|
|
85
|
+
// rejection — an early exit is not an error (generator `return()` semantics).
|
|
86
|
+
this.#closed = true;
|
|
87
|
+
this.#failure = undefined;
|
|
88
|
+
this.#buffer.length = 0;
|
|
89
|
+
for (const waiter of this.#waiters.splice(0))
|
|
90
|
+
waiter.resolve(DONE);
|
|
91
|
+
return this.#stopping;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -28,12 +28,16 @@ export declare function planStructured(inputs: StructuredPlanInputs, connection:
|
|
|
28
28
|
/** A per-turn schema is allowed only where the backend carries the schema on the turn and does not
|
|
29
29
|
* embed it in the prompt (Codex among the built-ins). */
|
|
30
30
|
export declare function assertPerTurnSchemaAllowed(backend: Backend, schema: TSchema | undefined, label: string | undefined): void;
|
|
31
|
+
/** `schemaRetries` (constructor or per turn): an integer ≥ 0, or INVALID_ARGUMENT naming `where`.
|
|
32
|
+
* `undefined` is the default budget, 0. */
|
|
33
|
+
export declare function validateSchemaRetries(value: unknown, label: string | undefined, where: string): number;
|
|
31
34
|
/** The slice of a SessionHandle the result ladder reads. */
|
|
32
35
|
export type StructuredHandle = StructuredSource;
|
|
33
36
|
/**
|
|
34
|
-
* The
|
|
35
|
-
* the tool host) → the backend's native result, validated → a validated
|
|
36
|
-
* assistant message → otherwise `structuredError` naming every channel
|
|
37
|
+
* The result resolution for ONE turn (no re-prompt here): this turn's StructuredOutput capture
|
|
38
|
+
* (already validated by the tool host) → the backend's native result, validated → a validated
|
|
39
|
+
* JSON block in the final assistant message → otherwise `structuredError` naming every channel
|
|
40
|
+
* that applied. The same three channels the runner's `resolveStructuredOutput` tries per attempt.
|
|
37
41
|
*/
|
|
38
42
|
export declare function resolveTurnStructured(args: {
|
|
39
43
|
schema: TSchema;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structured.d.ts","sourceRoot":"","sources":["../../src/agent/structured.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"structured.d.ts","sourceRoot":"","sources":["../../src/agent/structured.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE/D,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,gCAAgC,EACtC,MAAM,uBAAuB,CAAC;AAG/B,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,IAAI,CAAC,EAAE,wBAAwB,CAAC;IACzC,QAAQ,CAAC,YAAY,CAAC,EAAE,gCAAgC,CAAC;CAC1D;AAED,qGAAqG;AACrG,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,SAAS,CAAC;IACnD,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,wBAAwB,CAAC;CAC/C;AAWD,yGAAyG;AACzG,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,GAAG,MAAM,CAS5G;AAED;8DAC8D;AAC9D,wBAAsB,cAAc,CAAC,MAAM,EAAE,oBAAoB,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAsBxH;AAED;0DAC0D;AAC1D,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAQzH;AAED;4CAC4C;AAC5C,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAStG;AAcD,4DAA4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB,GAAG;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,CAcrD"}
|
package/dist/agent/structured.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Convert, Errors } from "typebox/value";
|
|
2
1
|
import { extractValidated, validateValue } from "../structured-output.js";
|
|
3
|
-
import { STRUCTURED_OUTPUT_SERVER_NAME, } from "../structured-tool.js";
|
|
2
|
+
import { STRUCTURED_OUTPUT_SERVER_NAME, describeSchemaErrors, } from "../structured-tool.js";
|
|
4
3
|
import { agentValidationError } from "./errors.js";
|
|
5
4
|
/** The runner's injection rule: the backend opts in AND the initialized agent advertises HTTP MCP. */
|
|
6
5
|
function shouldInjectStructuredOutputTool(schema, backend, capabilities) {
|
|
@@ -52,23 +51,34 @@ export function assertPerTurnSchemaAllowed(backend, schema, label) {
|
|
|
52
51
|
throw agentValidationError(`per-turn schema is not supported on backend "${backend.id}" (its schema is bound at session open); ` +
|
|
53
52
|
"pass `schema` to the AcpAgent constructor instead", label);
|
|
54
53
|
}
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
/** `schemaRetries` (constructor or per turn): an integer ≥ 0, or INVALID_ARGUMENT naming `where`.
|
|
55
|
+
* `undefined` is the default budget, 0. */
|
|
56
|
+
export function validateSchemaRetries(value, label, where) {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
return 0;
|
|
59
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
60
|
+
throw agentValidationError(`${where}: schemaRetries must be an integer >= 0 (the number of extra repair turns), got ${describeValue(value)}`, label);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
/** The offending value for the message. `JSON.stringify` renders NaN and ±Infinity as `null`,
|
|
65
|
+
* so numbers go through `String`; so does anything JSON cannot serialize (undefined, a symbol,
|
|
66
|
+
* a bigint). */
|
|
67
|
+
function describeValue(value) {
|
|
68
|
+
if (typeof value === "number")
|
|
69
|
+
return String(value);
|
|
57
70
|
try {
|
|
58
|
-
|
|
71
|
+
return JSON.stringify(value) ?? String(value);
|
|
59
72
|
}
|
|
60
73
|
catch {
|
|
61
|
-
|
|
74
|
+
return String(value);
|
|
62
75
|
}
|
|
63
|
-
return Errors(schema, converted)
|
|
64
|
-
.slice(0, 3)
|
|
65
|
-
.map((error) => `${error.instancePath || "/"} ${error.message}`)
|
|
66
|
-
.join("; ");
|
|
67
76
|
}
|
|
68
77
|
/**
|
|
69
|
-
* The
|
|
70
|
-
* the tool host) → the backend's native result, validated → a validated
|
|
71
|
-
* assistant message → otherwise `structuredError` naming every channel
|
|
78
|
+
* The result resolution for ONE turn (no re-prompt here): this turn's StructuredOutput capture
|
|
79
|
+
* (already validated by the tool host) → the backend's native result, validated → a validated
|
|
80
|
+
* JSON block in the final assistant message → otherwise `structuredError` naming every channel
|
|
81
|
+
* that applied. The same three channels the runner's `resolveStructuredOutput` tries per attempt.
|
|
72
82
|
*/
|
|
73
83
|
export function resolveTurnStructured(args) {
|
|
74
84
|
const { schema, handle, backend, captured } = args;
|
|
@@ -80,7 +90,7 @@ export function resolveTurnStructured(args) {
|
|
|
80
90
|
const validated = validateValue(native, schema);
|
|
81
91
|
if (validated !== undefined)
|
|
82
92
|
return { structured: validated };
|
|
83
|
-
reasons.push(`native result rejected: ${
|
|
93
|
+
reasons.push(`native result rejected: ${describeSchemaErrors(schema, native)}`);
|
|
84
94
|
}
|
|
85
95
|
const extracted = extractValidated(handle.finalMessageText(), schema);
|
|
86
96
|
if (extracted !== undefined)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { LocalMcpHttpHost } from "../local-mcp-host.js";
|
|
4
|
+
import type { AcpAgentToolDefinition } from "./types.js";
|
|
5
|
+
/** The injected `mcpServers` entry name (`agent_tools_2`, … when a caller's server holds it). */
|
|
6
|
+
export declare const AGENT_TOOLS_SERVER_NAME = "agent_tools";
|
|
7
|
+
/** What the agent hands the host at call time (the session id exists only once the session is
|
|
8
|
+
* open, and the host must be listening before `session/new` carries its URL). */
|
|
9
|
+
export interface AgentToolHostContext {
|
|
10
|
+
readonly sessionId: string | undefined;
|
|
11
|
+
readonly backendId: string;
|
|
12
|
+
readonly label?: string;
|
|
13
|
+
/** Best-effort ACP `tool_call` correlation for the call the agent is making right now. */
|
|
14
|
+
readonly resolveToolCallId?: (toolName: string) => string | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare class AgentToolHost extends LocalMcpHttpHost {
|
|
17
|
+
#private;
|
|
18
|
+
protected readonly hostName = "agent_tools";
|
|
19
|
+
constructor(tools: readonly AcpAgentToolDefinition[], context: () => AgentToolHostContext);
|
|
20
|
+
/** Bind (once) and return the token URL the `mcpServers` entry points at. */
|
|
21
|
+
listen(): Promise<string>;
|
|
22
|
+
/** The token URL once `listen()` resolved. */
|
|
23
|
+
get url(): string | undefined;
|
|
24
|
+
/** The names served, in definition order. */
|
|
25
|
+
get toolNames(): readonly string[];
|
|
26
|
+
/** `execute` calls currently running. */
|
|
27
|
+
get inFlight(): number;
|
|
28
|
+
/** Abort every running `execute` with `reason` (turn cancellation, the agent's abort, close). */
|
|
29
|
+
abortInFlight(reason?: unknown): void;
|
|
30
|
+
/** Abort what is running, stop listening. Idempotent. */
|
|
31
|
+
dispose(): Promise<void>;
|
|
32
|
+
protected mcpServerFor(token: string, _req: IncomingMessage, res: ServerResponse): Server | undefined;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=tool-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-host.d.ts","sourceRoot":"","sources":["../../src/agent/tool-host.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAUnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGxD,OAAO,KAAK,EAAuB,sBAAsB,EAAsB,MAAM,YAAY,CAAC;AAElG,iGAAiG;AACjG,eAAO,MAAM,uBAAuB,gBAAgB,CAAC;AAErD;kFACkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CACvE;AAMD,qBAAa,aAAc,SAAQ,gBAAgB;;IACjD,SAAS,CAAC,QAAQ,CAAC,QAAQ,iBAAiB;gBAShC,KAAK,EAAE,SAAS,sBAAsB,EAAE,EAAE,OAAO,EAAE,MAAM,oBAAoB;IAWzF,6EAA6E;IACvE,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAO/B,8CAA8C;IAC9C,IAAI,GAAG,IAAI,MAAM,GAAG,SAAS,CAE5B;IAED,6CAA6C;IAC7C,IAAI,SAAS,IAAI,SAAS,MAAM,EAAE,CAEjC;IAED,yCAAyC;IACzC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,iGAAiG;IACjG,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAIrC,yDAAyD;IACnD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM9B,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS;CAyDtG"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The per-agent local tool host: one in-process Streamable HTTP MCP server on 127.0.0.1 behind an
|
|
2
|
+
// unguessable token path (the `StructuredOutputToolHost` pattern), serving `tools/list` and
|
|
3
|
+
// `tools/call` for every `AcpAgentToolDefinition` the agent was given. Arguments are validated
|
|
4
|
+
// against the definition's typebox schema (Convert + Check) before `execute` runs; a validation
|
|
5
|
+
// failure or a thrown `execute` comes back as an MCP result with `isError: true` and the message —
|
|
6
|
+
// never a transport or protocol error, so the agent can read it and recover. Every in-flight
|
|
7
|
+
// `execute` gets an AbortSignal the agent wires to its own abort, turn cancellation, and close.
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { LocalMcpHttpHost } from "../local-mcp-host.js";
|
|
12
|
+
import { validateValue } from "../structured-output.js";
|
|
13
|
+
import { describeSchemaErrors, structuredToolInputSchema } from "../structured-tool.js";
|
|
14
|
+
/** The injected `mcpServers` entry name (`agent_tools_2`, … when a caller's server holds it). */
|
|
15
|
+
export const AGENT_TOOLS_SERVER_NAME = "agent_tools";
|
|
16
|
+
export class AgentToolHost extends LocalMcpHttpHost {
|
|
17
|
+
hostName = "agent_tools";
|
|
18
|
+
#token = randomBytes(16).toString("hex");
|
|
19
|
+
#tools;
|
|
20
|
+
#advertised;
|
|
21
|
+
#context;
|
|
22
|
+
#inFlight = new Set();
|
|
23
|
+
#url;
|
|
24
|
+
#disposed = false;
|
|
25
|
+
constructor(tools, context) {
|
|
26
|
+
super();
|
|
27
|
+
this.#tools = new Map(tools.map((tool) => [tool.name, tool]));
|
|
28
|
+
this.#advertised = tools.map((tool) => ({
|
|
29
|
+
name: tool.name,
|
|
30
|
+
description: tool.description,
|
|
31
|
+
inputSchema: structuredToolInputSchema(tool.inputSchema),
|
|
32
|
+
}));
|
|
33
|
+
this.#context = context;
|
|
34
|
+
}
|
|
35
|
+
/** Bind (once) and return the token URL the `mcpServers` entry points at. */
|
|
36
|
+
async listen() {
|
|
37
|
+
if (this.#disposed)
|
|
38
|
+
throw new Error("agent_tools MCP host is disposed");
|
|
39
|
+
const port = await this.ensureListening();
|
|
40
|
+
this.#url ??= this.urlFor(this.#token, port);
|
|
41
|
+
return this.#url;
|
|
42
|
+
}
|
|
43
|
+
/** The token URL once `listen()` resolved. */
|
|
44
|
+
get url() {
|
|
45
|
+
return this.#url;
|
|
46
|
+
}
|
|
47
|
+
/** The names served, in definition order. */
|
|
48
|
+
get toolNames() {
|
|
49
|
+
return this.#advertised.map((tool) => tool.name);
|
|
50
|
+
}
|
|
51
|
+
/** `execute` calls currently running. */
|
|
52
|
+
get inFlight() {
|
|
53
|
+
return this.#inFlight.size;
|
|
54
|
+
}
|
|
55
|
+
/** Abort every running `execute` with `reason` (turn cancellation, the agent's abort, close). */
|
|
56
|
+
abortInFlight(reason) {
|
|
57
|
+
for (const call of this.#inFlight)
|
|
58
|
+
call.controller.abort(reason);
|
|
59
|
+
}
|
|
60
|
+
/** Abort what is running, stop listening. Idempotent. */
|
|
61
|
+
async dispose() {
|
|
62
|
+
this.#disposed = true;
|
|
63
|
+
this.abortInFlight(new Error("AcpAgent closed while the tool call was running"));
|
|
64
|
+
await this.closeServer();
|
|
65
|
+
}
|
|
66
|
+
mcpServerFor(token, _req, res) {
|
|
67
|
+
if (this.#disposed || token !== this.#token)
|
|
68
|
+
return undefined;
|
|
69
|
+
return this.#createMcpServer(res);
|
|
70
|
+
}
|
|
71
|
+
#createMcpServer(res) {
|
|
72
|
+
const server = new Server({ name: "agentprism-agent-tools", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
73
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: this.#advertised }));
|
|
74
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
75
|
+
const tool = this.#tools.get(request.params.name);
|
|
76
|
+
if (!tool) {
|
|
77
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
|
|
78
|
+
}
|
|
79
|
+
return this.#call(tool, request.params.arguments ?? {}, res);
|
|
80
|
+
});
|
|
81
|
+
return server;
|
|
82
|
+
}
|
|
83
|
+
async #call(tool, args, res) {
|
|
84
|
+
const input = validateValue(args, tool.inputSchema);
|
|
85
|
+
if (input === undefined) {
|
|
86
|
+
return errorResult(`Invalid arguments for tool "${tool.name}": ${describeSchemaErrors(tool.inputSchema, args) || "arguments do not match the input schema"}`);
|
|
87
|
+
}
|
|
88
|
+
const context = this.#context();
|
|
89
|
+
if (context.sessionId === undefined) {
|
|
90
|
+
return errorResult(`Tool "${tool.name}" was called before the agent's session was open`);
|
|
91
|
+
}
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const call = { controller };
|
|
94
|
+
this.#inFlight.add(call);
|
|
95
|
+
// The backend gave up on the request (process death, its own timeout): stop the work.
|
|
96
|
+
const onResponseClosed = () => {
|
|
97
|
+
if (!res.writableFinished)
|
|
98
|
+
controller.abort(new Error("the agent dropped the tool call before it completed"));
|
|
99
|
+
};
|
|
100
|
+
res.once("close", onResponseClosed);
|
|
101
|
+
const ctx = {
|
|
102
|
+
sessionId: context.sessionId,
|
|
103
|
+
backendId: context.backendId,
|
|
104
|
+
...(context.label !== undefined ? { label: context.label } : {}),
|
|
105
|
+
...(() => {
|
|
106
|
+
const toolCallId = context.resolveToolCallId?.(tool.name);
|
|
107
|
+
return toolCallId !== undefined ? { toolCallId } : {};
|
|
108
|
+
})(),
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
const result = await tool.execute(input, ctx);
|
|
113
|
+
return normalizeResult(tool.name, result);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
res.off("close", onResponseClosed);
|
|
120
|
+
this.#inFlight.delete(call);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function errorResult(text) {
|
|
125
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
126
|
+
}
|
|
127
|
+
/** Shape `execute`'s return into a `CallToolResult`; anything outside the contract is an
|
|
128
|
+
* `isError` result naming the tool (JavaScript callers can return `undefined` by mistake). */
|
|
129
|
+
function normalizeResult(name, result) {
|
|
130
|
+
if (typeof result === "string")
|
|
131
|
+
return { content: [{ type: "text", text: result }] };
|
|
132
|
+
if (Array.isArray(result))
|
|
133
|
+
return { content: result };
|
|
134
|
+
if (result !== null && typeof result === "object" && Array.isArray(result.content)) {
|
|
135
|
+
return { content: result.content, ...(result.isError === true ? { isError: true } : {}) };
|
|
136
|
+
}
|
|
137
|
+
return errorResult(`Tool "${name}" returned ${result === null ? "null" : typeof result}; expected a string, an array of content blocks, or { content, isError? }`);
|
|
138
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { McpServerConfig } from "@automatalabs/shared-types";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
3
|
+
import type { PooledConnection } from "../acp-client.js";
|
|
4
|
+
import { type AgentToolHost } from "./tool-host.js";
|
|
5
|
+
import type { AcpAgentToolDefinition } from "./types.js";
|
|
6
|
+
/** The tool-name grammar: MCP-safe, 1–64 characters of `[A-Za-z0-9_-]`. */
|
|
7
|
+
export declare const AGENT_TOOL_NAME_PATTERN: RegExp;
|
|
8
|
+
/** Identity helper that infers `Static<typeof inputSchema>` for `execute`'s input. */
|
|
9
|
+
export declare function defineTool<TInput extends TSchema>(tool: AcpAgentToolDefinition<TInput>): AcpAgentToolDefinition<TInput>;
|
|
10
|
+
/** Constructor-time validation (INVALID_ARGUMENT): an array of well-formed definitions with
|
|
11
|
+
* MCP-safe, unique names and an object-typed `inputSchema`. `undefined` and `[]` are both "no
|
|
12
|
+
* tools". */
|
|
13
|
+
export declare function validateToolDefinitions(tools: unknown, label: string | undefined): AcpAgentToolDefinition[];
|
|
14
|
+
export interface ToolPlanInputs {
|
|
15
|
+
readonly tools: readonly AcpAgentToolDefinition[];
|
|
16
|
+
readonly backendId: string;
|
|
17
|
+
readonly label: string | undefined;
|
|
18
|
+
/** The servers decided so far (the caller's, plus an injected `structured_output`). */
|
|
19
|
+
readonly mcpServers: McpServerConfig[] | undefined;
|
|
20
|
+
/** The agent's lazily created host (one per agent, disposed on close). */
|
|
21
|
+
readonly host: () => AgentToolHost;
|
|
22
|
+
}
|
|
23
|
+
/** After initialize: append the `agent_tools` entry when the agent advertises HTTP MCP, or refuse
|
|
24
|
+
* the open. No tools → the servers pass through untouched and no host is created. */
|
|
25
|
+
export declare function planTools(inputs: ToolPlanInputs, connection: PooledConnection): Promise<McpServerConfig[] | undefined>;
|
|
26
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/agent/tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,OAAO,EAA2B,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,2EAA2E;AAC3E,eAAO,MAAM,uBAAuB,QAA0B,CAAC;AAE/D,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,MAAM,SAAS,OAAO,EAAE,IAAI,EAAE,sBAAsB,CAAC,MAAM,CAAC,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAEvH;AAED;;cAEc;AACd,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,sBAAsB,EAAE,CAiC3G;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,uFAAuF;IACvF,QAAQ,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,SAAS,CAAC;IACnD,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,aAAa,CAAC;CACpC;AAED;sFACsF;AACtF,wBAAsB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,GAAG,SAAS,CAAC,CAe5H"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { agentValidationError } from "./errors.js";
|
|
2
|
+
import { availableMcpServerName } from "./structured.js";
|
|
3
|
+
import { AGENT_TOOLS_SERVER_NAME } from "./tool-host.js";
|
|
4
|
+
/** The tool-name grammar: MCP-safe, 1–64 characters of `[A-Za-z0-9_-]`. */
|
|
5
|
+
export const AGENT_TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
6
|
+
/** Identity helper that infers `Static<typeof inputSchema>` for `execute`'s input. */
|
|
7
|
+
export function defineTool(tool) {
|
|
8
|
+
return tool;
|
|
9
|
+
}
|
|
10
|
+
/** Constructor-time validation (INVALID_ARGUMENT): an array of well-formed definitions with
|
|
11
|
+
* MCP-safe, unique names and an object-typed `inputSchema`. `undefined` and `[]` are both "no
|
|
12
|
+
* tools". */
|
|
13
|
+
export function validateToolDefinitions(tools, label) {
|
|
14
|
+
if (tools === undefined)
|
|
15
|
+
return [];
|
|
16
|
+
if (!Array.isArray(tools))
|
|
17
|
+
throw agentValidationError("AcpAgent `tools` must be an array of tool definitions", label);
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
tools.forEach((tool, index) => {
|
|
20
|
+
const at = `tools[${index}]`;
|
|
21
|
+
if (tool === null || typeof tool !== "object")
|
|
22
|
+
throw agentValidationError(`${at} must be a tool definition object`, label);
|
|
23
|
+
const { name, description, inputSchema, execute } = tool;
|
|
24
|
+
if (typeof name !== "string" || !AGENT_TOOL_NAME_PATTERN.test(name)) {
|
|
25
|
+
throw agentValidationError(`${at}.name must match ${AGENT_TOOL_NAME_PATTERN.source} (got ${JSON.stringify(name)})`, label);
|
|
26
|
+
}
|
|
27
|
+
if (seen.has(name))
|
|
28
|
+
throw agentValidationError(`duplicate tool name "${name}" (tool names must be unique)`, label);
|
|
29
|
+
seen.add(name);
|
|
30
|
+
if (typeof description !== "string")
|
|
31
|
+
throw agentValidationError(`tool "${name}" needs a string description`, label);
|
|
32
|
+
if (inputSchema === null || typeof inputSchema !== "object") {
|
|
33
|
+
throw agentValidationError(`tool "${name}" needs an inputSchema (a typebox schema object)`, label);
|
|
34
|
+
}
|
|
35
|
+
// MCP `tools/call` arguments are an object and `tools/list` advertises `inputSchema` as
|
|
36
|
+
// `type: "object"`: a schema of any other top-level type could never be satisfied by a call,
|
|
37
|
+
// so it is refused here rather than advertised.
|
|
38
|
+
const schemaType = inputSchema.type;
|
|
39
|
+
if (schemaType !== "object") {
|
|
40
|
+
throw agentValidationError(`tool "${name}" inputSchema must be an object schema (typebox Type.Object(...), type: "object"); got type ${JSON.stringify(schemaType) ?? "undefined"}`, label);
|
|
41
|
+
}
|
|
42
|
+
if (typeof execute !== "function")
|
|
43
|
+
throw agentValidationError(`tool "${name}" needs an execute function`, label);
|
|
44
|
+
});
|
|
45
|
+
return tools;
|
|
46
|
+
}
|
|
47
|
+
/** After initialize: append the `agent_tools` entry when the agent advertises HTTP MCP, or refuse
|
|
48
|
+
* the open. No tools → the servers pass through untouched and no host is created. */
|
|
49
|
+
export async function planTools(inputs, connection) {
|
|
50
|
+
const { tools, mcpServers } = inputs;
|
|
51
|
+
if (tools.length === 0)
|
|
52
|
+
return mcpServers;
|
|
53
|
+
if (connection.capabilities?.agent.mcpCapabilities?.http !== true) {
|
|
54
|
+
throw agentValidationError(`function tools need HTTP MCP, but backend "${inputs.backendId}" does not advertise mcpCapabilities.http ` +
|
|
55
|
+
`(${tools.length} tool${tools.length === 1 ? "" : "s"} configured: ${tools.map((tool) => tool.name).join(", ")})`, inputs.label);
|
|
56
|
+
}
|
|
57
|
+
const url = await inputs.host().listen();
|
|
58
|
+
return [
|
|
59
|
+
...(mcpServers ?? []),
|
|
60
|
+
{ type: "http", name: availableMcpServerName(AGENT_TOOLS_SERVER_NAME, mcpServers), url, headers: [] },
|
|
61
|
+
];
|
|
62
|
+
}
|
package/dist/agent/turn.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { AcpElicitationEvent, AcpPermissionEvent } from "../events.js";
|
|
|
6
6
|
import type { UsageBaseline } from "../usage.js";
|
|
7
7
|
import type { AgentEventBus } from "./events.js";
|
|
8
8
|
import { type StructuredHandle } from "./structured.js";
|
|
9
|
-
import type { AcpAgentRawRecord, AcpAgentToolCall, AcpAgentTurn, AcpAgentTurnUsage, AcpAgentUpdateRecord } from "./types.js";
|
|
9
|
+
import type { AcpAgentMessage, AcpAgentRawRecord, AcpAgentToolCall, AcpAgentTurn, AcpAgentTurnUsage, AcpAgentUpdateRecord } from "./types.js";
|
|
10
10
|
/** The slice of a SessionHandle the collector and builder read (duck-typed for unit tests). */
|
|
11
11
|
export interface TurnHandle extends StructuredHandle {
|
|
12
12
|
readonly history: AgentHistoryEntry[];
|
|
@@ -31,6 +31,8 @@ export declare class TurnCollector {
|
|
|
31
31
|
});
|
|
32
32
|
/** The folded tool calls in first-seen order (copies). */
|
|
33
33
|
get toolCalls(): AcpAgentToolCall[];
|
|
34
|
+
/** This turn's messages so far (copies). */
|
|
35
|
+
get messages(): AcpAgentMessage[];
|
|
34
36
|
stop(): void;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
@@ -59,7 +61,8 @@ export interface BuildTurnArgs {
|
|
|
59
61
|
/** The agent's running session sum BEFORE this turn. */
|
|
60
62
|
readonly sessionBefore: AgentUsage;
|
|
61
63
|
}
|
|
62
|
-
/** Assemble the turn: verbatim response, folded text, the turn's
|
|
63
|
-
*
|
|
64
|
+
/** Assemble the turn: verbatim response, folded text, the messages folded from the turn's update
|
|
65
|
+
* records, the turn's history slice (copies), usage per the per-turn model above (with the
|
|
66
|
+
* session sum AFTER this turn), and the structured result. */
|
|
64
67
|
export declare function buildTurn(args: BuildTurnArgs): AcpAgentTurn;
|
|
65
68
|
//# sourceMappingURL=turn.d.ts.map
|
package/dist/agent/turn.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../src/agent/turn.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../src/agent/turn.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAoB,MAAM,cAAc,CAAC;AAC9F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAEpB,+FAA+F;AAC/F,MAAM,WAAW,UAAW,SAAQ,gBAAgB;IAClD,QAAQ,CAAC,OAAO,EAAE,iBAAiB,EAAE,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,IAAI,aAAa,CAAA;KAAE,CAAC;IAC9C,cAAc,IAAI,MAAM,CAAC;CAC1B;AAED,qBAAa,aAAa;;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,EAAE,CAAM;IAC9C,QAAQ,CAAC,GAAG,EAAE,iBAAiB,EAAE,CAAM;IACvC,QAAQ,CAAC,WAAW,EAAE,kBAAkB,EAAE,CAAM;IAChD,QAAQ,CAAC,YAAY,EAAE,mBAAmB,EAAE,CAAM;IAMlD;;sGAEkG;gBACtF,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAO;IA8B1G,0DAA0D;IAC1D,IAAI,SAAS,IAAI,gBAAgB,EAAE,CAElC;IAED,4CAA4C;IAC5C,IAAI,QAAQ,IAAI,eAAe,EAAE,CAEhC;IAED,IAAI,IAAI,IAAI;CAIb;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,aAAa,EAC1B,UAAU,EAAE,aAAa,GACxB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC,UAAU,CAAC,CAAA;CAAE,CA0BhE;AAED,iGAAiG;AACjG,wBAAgB,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,GAAG,UAAU,CASlG;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC;CACpC;AAED;;+DAE+D;AAC/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CA0B3D"}
|
package/dist/agent/turn.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
+
import { MessageFolder } from "./messages.js";
|
|
1
2
|
import { resolveTurnStructured } from "./structured.js";
|
|
2
|
-
function record(value) {
|
|
3
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
4
|
-
? value
|
|
5
|
-
: undefined;
|
|
6
|
-
}
|
|
7
3
|
export class TurnCollector {
|
|
8
4
|
historyStart;
|
|
9
5
|
gaugeBefore;
|
|
@@ -11,7 +7,8 @@ export class TurnCollector {
|
|
|
11
7
|
raw = [];
|
|
12
8
|
permissions = [];
|
|
13
9
|
elicitations = [];
|
|
14
|
-
|
|
10
|
+
/** This turn's messages and tool calls — one fold, fed every update record as it is stored. */
|
|
11
|
+
#messages = new MessageFolder();
|
|
15
12
|
#active = true;
|
|
16
13
|
#untap;
|
|
17
14
|
/** Registers the tap SYNCHRONOUSLY — construct before the wire call so nothing is missed.
|
|
@@ -26,8 +23,9 @@ export class TurnCollector {
|
|
|
26
23
|
switch (name) {
|
|
27
24
|
case "session_update": {
|
|
28
25
|
const update = structuredClone(event.update);
|
|
29
|
-
|
|
30
|
-
this
|
|
26
|
+
const receivedAt = Date.now();
|
|
27
|
+
this.updates.push({ update, receivedAt });
|
|
28
|
+
this.#messages.apply(update, receivedAt);
|
|
31
29
|
return;
|
|
32
30
|
}
|
|
33
31
|
case "raw_message": {
|
|
@@ -48,68 +46,16 @@ export class TurnCollector {
|
|
|
48
46
|
}
|
|
49
47
|
/** The folded tool calls in first-seen order (copies). */
|
|
50
48
|
get toolCalls() {
|
|
51
|
-
return
|
|
49
|
+
return this.#messages.toolCalls;
|
|
50
|
+
}
|
|
51
|
+
/** This turn's messages so far (copies). */
|
|
52
|
+
get messages() {
|
|
53
|
+
return this.#messages.snapshot();
|
|
52
54
|
}
|
|
53
55
|
stop() {
|
|
54
56
|
this.#active = false;
|
|
55
57
|
this.#untap();
|
|
56
58
|
}
|
|
57
|
-
#foldToolCall(update) {
|
|
58
|
-
if (update.sessionUpdate === "tool_call") {
|
|
59
|
-
const existing = this.#toolCalls.get(update.toolCallId);
|
|
60
|
-
const meta = record(update._meta);
|
|
61
|
-
const entry = existing ?? { toolCallId: update.toolCallId, title: update.title, status: "pending" };
|
|
62
|
-
entry.title = update.title;
|
|
63
|
-
if (typeof update.name === "string")
|
|
64
|
-
entry.name = update.name;
|
|
65
|
-
if (update.kind !== undefined && update.kind !== null)
|
|
66
|
-
entry.kind = update.kind;
|
|
67
|
-
if (update.status !== undefined && update.status !== null)
|
|
68
|
-
entry.status = update.status;
|
|
69
|
-
if (update.rawInput !== undefined)
|
|
70
|
-
entry.rawInput = update.rawInput;
|
|
71
|
-
if (update.rawOutput !== undefined)
|
|
72
|
-
entry.rawOutput = update.rawOutput;
|
|
73
|
-
if (update.content !== undefined && update.content !== null)
|
|
74
|
-
entry.content = update.content;
|
|
75
|
-
if (update.locations !== undefined && update.locations !== null)
|
|
76
|
-
entry.locations = update.locations;
|
|
77
|
-
if (meta)
|
|
78
|
-
entry.meta = { ...(entry.meta ?? {}), ...meta };
|
|
79
|
-
if (!existing)
|
|
80
|
-
this.#toolCalls.set(update.toolCallId, entry);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (update.sessionUpdate !== "tool_call_update")
|
|
84
|
-
return;
|
|
85
|
-
const existing = this.#toolCalls.get(update.toolCallId);
|
|
86
|
-
const meta = record(update._meta);
|
|
87
|
-
const entry = existing ?? {
|
|
88
|
-
toolCallId: update.toolCallId,
|
|
89
|
-
title: typeof update.title === "string" ? update.title : "",
|
|
90
|
-
status: "pending",
|
|
91
|
-
};
|
|
92
|
-
if (typeof update.title === "string")
|
|
93
|
-
entry.title = update.title;
|
|
94
|
-
if (typeof update.name === "string")
|
|
95
|
-
entry.name = update.name;
|
|
96
|
-
if (update.kind !== undefined && update.kind !== null)
|
|
97
|
-
entry.kind = update.kind;
|
|
98
|
-
if (update.status !== undefined && update.status !== null)
|
|
99
|
-
entry.status = update.status;
|
|
100
|
-
if (update.rawInput !== undefined)
|
|
101
|
-
entry.rawInput = update.rawInput;
|
|
102
|
-
if (update.rawOutput !== undefined)
|
|
103
|
-
entry.rawOutput = update.rawOutput;
|
|
104
|
-
if (update.content !== undefined && update.content !== null)
|
|
105
|
-
entry.content = update.content;
|
|
106
|
-
if (update.locations !== undefined && update.locations !== null)
|
|
107
|
-
entry.locations = update.locations;
|
|
108
|
-
if (meta)
|
|
109
|
-
entry.meta = { ...(entry.meta ?? {}), ...meta };
|
|
110
|
-
if (!existing)
|
|
111
|
-
this.#toolCalls.set(update.toolCallId, entry);
|
|
112
|
-
}
|
|
113
59
|
}
|
|
114
60
|
/**
|
|
115
61
|
* THIS turn's usage. `response.usage` is PER-TURN on every installed adapter (Claude, Codex and pi
|
|
@@ -157,8 +103,9 @@ export function addUsage(prev, turn, gaugeAfter) {
|
|
|
157
103
|
cost: gaugeAfter.costAmount,
|
|
158
104
|
};
|
|
159
105
|
}
|
|
160
|
-
/** Assemble the turn: verbatim response, folded text, the turn's
|
|
161
|
-
*
|
|
106
|
+
/** Assemble the turn: verbatim response, folded text, the messages folded from the turn's update
|
|
107
|
+
* records, the turn's history slice (copies), usage per the per-turn model above (with the
|
|
108
|
+
* session sum AFTER this turn), and the structured result. */
|
|
162
109
|
export function buildTurn(args) {
|
|
163
110
|
const { response, collector, handle, backend, schema, captured, sessionBefore } = args;
|
|
164
111
|
const gaugeAfter = handle.usage.baseline();
|
|
@@ -173,6 +120,7 @@ export function buildTurn(args) {
|
|
|
173
120
|
updates: collector.updates,
|
|
174
121
|
raw: collector.raw,
|
|
175
122
|
toolCalls: collector.toolCalls,
|
|
123
|
+
messages: collector.messages,
|
|
176
124
|
permissions: collector.permissions,
|
|
177
125
|
elicitations: collector.elicitations,
|
|
178
126
|
usage: {
|