@automatalabs/acp-agents 1.2.6 → 1.3.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.
Files changed (67) hide show
  1. package/README.md +67 -3
  2. package/dist/acp-client.d.ts +21 -1
  3. package/dist/acp-client.d.ts.map +1 -1
  4. package/dist/acp-client.js +31 -6
  5. package/dist/agent/acp-agent.d.ts +141 -0
  6. package/dist/agent/acp-agent.d.ts.map +1 -0
  7. package/dist/agent/acp-agent.js +832 -0
  8. package/dist/agent/errors.d.ts +17 -0
  9. package/dist/agent/errors.d.ts.map +1 -0
  10. package/dist/agent/errors.js +37 -0
  11. package/dist/agent/events.d.ts +26 -0
  12. package/dist/agent/events.d.ts.map +1 -0
  13. package/dist/agent/events.js +165 -0
  14. package/dist/agent/fork.d.ts +27 -0
  15. package/dist/agent/fork.d.ts.map +1 -0
  16. package/dist/agent/fork.js +28 -0
  17. package/dist/agent/probe.d.ts +14 -0
  18. package/dist/agent/probe.d.ts.map +1 -0
  19. package/dist/agent/probe.js +87 -0
  20. package/dist/agent/process-registry.d.ts +9 -0
  21. package/dist/agent/process-registry.d.ts.map +1 -0
  22. package/dist/agent/process-registry.js +28 -0
  23. package/dist/agent/queue.d.ts +19 -0
  24. package/dist/agent/queue.d.ts.map +1 -0
  25. package/dist/agent/queue.js +90 -0
  26. package/dist/agent/routing.d.ts +27 -0
  27. package/dist/agent/routing.d.ts.map +1 -0
  28. package/dist/agent/routing.js +87 -0
  29. package/dist/agent/structured.d.ts +47 -0
  30. package/dist/agent/structured.d.ts.map +1 -0
  31. package/dist/agent/structured.js +90 -0
  32. package/dist/agent/turn.d.ts +65 -0
  33. package/dist/agent/turn.d.ts.map +1 -0
  34. package/dist/agent/turn.js +187 -0
  35. package/dist/agent/types.d.ts +221 -0
  36. package/dist/agent/types.d.ts.map +1 -0
  37. package/dist/agent/types.js +9 -0
  38. package/dist/backend.d.ts +6 -0
  39. package/dist/backend.d.ts.map +1 -1
  40. package/dist/backends/claude.d.ts +1 -0
  41. package/dist/backends/claude.d.ts.map +1 -1
  42. package/dist/backends/claude.js +6 -1
  43. package/dist/config-catalog.d.ts +173 -0
  44. package/dist/config-catalog.d.ts.map +1 -0
  45. package/dist/config-catalog.js +408 -0
  46. package/dist/index.d.ts +10 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +10 -1
  49. package/dist/permissions.js +5 -0
  50. package/dist/protocol-coverage.d.ts +56 -0
  51. package/dist/protocol-coverage.d.ts.map +1 -1
  52. package/dist/protocol-coverage.js +52 -0
  53. package/dist/registry.d.ts +12 -0
  54. package/dist/registry.d.ts.map +1 -1
  55. package/dist/registry.js +22 -0
  56. package/dist/routing.d.ts +14 -0
  57. package/dist/routing.d.ts.map +1 -0
  58. package/dist/routing.js +53 -0
  59. package/dist/runner.d.ts.map +1 -1
  60. package/dist/runner.js +3 -64
  61. package/dist/session-ref.d.ts +8 -0
  62. package/dist/session-ref.d.ts.map +1 -0
  63. package/dist/session-ref.js +21 -0
  64. package/dist/structured-tool.d.ts +4 -0
  65. package/dist/structured-tool.d.ts.map +1 -1
  66. package/dist/structured-tool.js +5 -0
  67. package/package.json +5 -5
@@ -0,0 +1,87 @@
1
+ // Agent-facing routing over ../routing.js: the registry read (malformed → SCRIPT_VALIDATION_ERROR),
2
+ // the runner's model-spec grammar for `new AcpAgent({ model })`, the ref-driven route the cold
3
+ // statics use (never the default backend), cwd validation that fails BEFORE a process spawns, and
4
+ // the fresh-Backend-instance rule a fork child needs.
5
+ import { isAbsolute } from "node:path";
6
+ import { statSync } from "node:fs";
7
+ import { builtinBackend } from "../backends/builtins.js";
8
+ import { CustomAcpBackend } from "../backends/custom.js";
9
+ import { resolveBackendRegistry } from "../registry.js";
10
+ import { asciiLowercase, resolveModelRoute } from "../routing.js";
11
+ import { agentValidationError } from "./errors.js";
12
+ /** The custom-backend registry for an agent: `backends` merged over `AGENTPRISM_BACKENDS`. A
13
+ * malformed registry is a caller error (SCRIPT_VALIDATION_ERROR), mirroring the runner's wrap. */
14
+ export function resolveAgentRegistry(backends, label) {
15
+ try {
16
+ return resolveBackendRegistry(backends);
17
+ }
18
+ catch (error) {
19
+ throw agentValidationError(error instanceof Error ? error.message : String(error), label);
20
+ }
21
+ }
22
+ /** The runner's routing grammar applied to `new AcpAgent({ model })`. */
23
+ export function resolveAgentRoute(options, registry) {
24
+ return resolveModelRoute(options.model, registry);
25
+ }
26
+ /**
27
+ * Route a cold reopen from a session ref. `ref.backendId` must name a registered custom backend
28
+ * (wins, like `resolveModelRoute`) or a built-in — NEVER the default backend; the ref's `poolKey`
29
+ * must match the currently resolved `poolKey ?? id` (the runner's silent `backend-mismatch` skip,
30
+ * made loud); an optional `model` must stay on the ref's backend: `<ref.backendId>/<inner>` strips
31
+ * the prefix, a spec routing to another known backend is rejected, and an unrouted spec goes
32
+ * VERBATIM to the ref's backend (the same unrouted rule as the runner, applied to this backend).
33
+ */
34
+ export function resolveRefRoute(ref, model, registry, label) {
35
+ const backend = backendNamed(ref.backendId, registry);
36
+ if (!backend) {
37
+ throw agentValidationError(`session ref names backend "${ref.backendId}" which is neither a built-in nor a registered custom backend`, label);
38
+ }
39
+ const expected = backend.poolKey ?? backend.id;
40
+ if (ref.poolKey !== undefined && ref.poolKey !== expected) {
41
+ throw agentValidationError(`session ref pool key "${ref.poolKey}" does not match the currently resolved "${expected}" for "${ref.backendId}"`, label);
42
+ }
43
+ if (model === undefined)
44
+ return { backend, modelSpec: undefined };
45
+ const slash = model.indexOf("/");
46
+ const first = asciiLowercase(slash >= 0 ? model.slice(0, slash) : model);
47
+ const inner = slash >= 0 ? model.slice(slash + 1) : undefined;
48
+ if (first === ref.backendId)
49
+ return { backend, modelSpec: inner };
50
+ if (registry.get(first) || builtinBackend(first)) {
51
+ throw agentValidationError(`model "${model}" routes to "${first}" but the session ref belongs to "${ref.backendId}"`, label);
52
+ }
53
+ return { backend, modelSpec: model };
54
+ }
55
+ /** A fresh `Backend` instance with the same identity as `backend` (a registered name wins, as in
56
+ * routing; else the built-in of that id). A fork child must own its own instance — pooling identity
57
+ * is `poolKey ?? id`, never the object. */
58
+ export function freshBackendFor(backend, registry) {
59
+ return backendNamed(backend.id, registry) ?? backend;
60
+ }
61
+ function backendNamed(name, registry) {
62
+ const custom = registry.get(name);
63
+ if (custom)
64
+ return new CustomAcpBackend(custom);
65
+ return builtinBackend(name);
66
+ }
67
+ /** Stricter than the runner's interactive check and equal to the Claude adapter's own: absolute,
68
+ * existing, and a directory — so the failure happens before a process spawns. */
69
+ export function validateAgentCwd(cwd, label, method) {
70
+ if (typeof cwd !== "string" || cwd.trim() === "" || !isAbsolute(cwd)) {
71
+ throw agentValidationError(`${method} requires cwd to be a non-empty absolute path`, label);
72
+ }
73
+ let stat;
74
+ try {
75
+ stat = statSync(cwd, { throwIfNoEntry: false });
76
+ }
77
+ catch (error) {
78
+ // `throwIfNoEntry` only suppresses ENOENT; EACCES / ELOOP / ENOTDIR on a parent are caller
79
+ // errors too, not raw Node errors.
80
+ const code = error.code;
81
+ const detail = typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
82
+ throw agentValidationError(`${method} cwd is not accessible: ${cwd} (${detail})`, label);
83
+ }
84
+ if (stat?.isDirectory() !== true) {
85
+ throw agentValidationError(`${method} cwd does not exist or is not a directory: ${cwd}`, label);
86
+ }
87
+ }
@@ -0,0 +1,47 @@
1
+ import type { TSchema } from "typebox";
2
+ import type { McpServerConfig } from "@automatalabs/shared-types";
3
+ import type { PooledConnection } from "../acp-client.js";
4
+ import type { Backend, StructuredSource } from "../backend.js";
5
+ import { type StructuredOutputToolHost, type StructuredOutputToolRegistration } from "../structured-tool.js";
6
+ export interface StructuredPlan {
7
+ readonly schema: TSchema | undefined;
8
+ /** An injected StructuredOutput tool is on this session. */
9
+ readonly toolActive: boolean;
10
+ /** The caller's `mcpServers` (+ the injected http server when `toolActive`). */
11
+ readonly mcpServers: McpServerConfig[] | undefined;
12
+ readonly host?: StructuredOutputToolHost;
13
+ readonly registration?: StructuredOutputToolRegistration;
14
+ }
15
+ /** What `planStructured` reads from the agent (kept as a small seam so it needs no class access). */
16
+ export interface StructuredPlanInputs {
17
+ readonly schema: TSchema | undefined;
18
+ readonly backend: Backend;
19
+ readonly mcpServers: McpServerConfig[] | undefined;
20
+ /** The agent's lazily created tool host (one per agent, disposed on close). */
21
+ readonly host: () => StructuredOutputToolHost;
22
+ }
23
+ /** `structured_output`, then `structured_output_2`, `_3`, … — never colliding with a caller's server. */
24
+ export declare function availableMcpServerName(base: string, servers: readonly McpServerConfig[] | undefined): string;
25
+ /** Decide (after initialize, so the capabilities are known) whether this session gets the injected
26
+ * tool, and register it on the agent's host when it does. */
27
+ export declare function planStructured(inputs: StructuredPlanInputs, connection: PooledConnection): Promise<StructuredPlan>;
28
+ /** A per-turn schema is allowed only where the backend carries the schema on the turn and does not
29
+ * embed it in the prompt (Codex among the built-ins). */
30
+ export declare function assertPerTurnSchemaAllowed(backend: Backend, schema: TSchema | undefined, label: string | undefined): void;
31
+ /** The slice of a SessionHandle the result ladder reads. */
32
+ export type StructuredHandle = StructuredSource;
33
+ /**
34
+ * The no-repair ladder for one turn: this turn's StructuredOutput capture (already validated by
35
+ * the tool host) → the backend's native result, validated → a validated JSON block in the final
36
+ * assistant message → otherwise `structuredError` naming every channel that applied.
37
+ */
38
+ export declare function resolveTurnStructured(args: {
39
+ schema: TSchema;
40
+ handle: StructuredHandle;
41
+ backend: Backend;
42
+ captured: unknown;
43
+ }): {
44
+ structured?: unknown;
45
+ structuredError?: string;
46
+ };
47
+ //# sourceMappingURL=structured.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured.d.ts","sourceRoot":"","sources":["../../src/agent/structured.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,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,EAEL,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,4DAA4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;AAehD;;;;GAIG;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"}
@@ -0,0 +1,90 @@
1
+ import { Convert, Errors } from "typebox/value";
2
+ import { extractValidated, validateValue } from "../structured-output.js";
3
+ import { STRUCTURED_OUTPUT_SERVER_NAME, } from "../structured-tool.js";
4
+ import { agentValidationError } from "./errors.js";
5
+ /** The runner's injection rule: the backend opts in AND the initialized agent advertises HTTP MCP. */
6
+ function shouldInjectStructuredOutputTool(schema, backend, capabilities) {
7
+ return Boolean(schema && backend.injectStructuredOutputTool && capabilities?.agent.mcpCapabilities?.http === true);
8
+ }
9
+ /** `structured_output`, then `structured_output_2`, `_3`, … — never colliding with a caller's server. */
10
+ export function availableMcpServerName(base, servers) {
11
+ const used = new Set((servers ?? []).map((server) => server.name));
12
+ let candidate = base;
13
+ let suffix = 2;
14
+ while (used.has(candidate)) {
15
+ candidate = `${base}_${suffix}`;
16
+ suffix += 1;
17
+ }
18
+ return candidate;
19
+ }
20
+ /** Decide (after initialize, so the capabilities are known) whether this session gets the injected
21
+ * tool, and register it on the agent's host when it does. */
22
+ export async function planStructured(inputs, connection) {
23
+ const { schema, backend, mcpServers } = inputs;
24
+ if (!shouldInjectStructuredOutputTool(schema, backend, connection.capabilities)) {
25
+ return { schema, toolActive: false, mcpServers };
26
+ }
27
+ const host = inputs.host();
28
+ const registration = await host.register(schema);
29
+ return {
30
+ schema,
31
+ toolActive: true,
32
+ mcpServers: [
33
+ ...(mcpServers ?? []),
34
+ {
35
+ type: "http",
36
+ name: availableMcpServerName(STRUCTURED_OUTPUT_SERVER_NAME, mcpServers),
37
+ url: registration.url,
38
+ headers: [],
39
+ },
40
+ ],
41
+ host,
42
+ registration,
43
+ };
44
+ }
45
+ /** A per-turn schema is allowed only where the backend carries the schema on the turn and does not
46
+ * embed it in the prompt (Codex among the built-ins). */
47
+ export function assertPerTurnSchemaAllowed(backend, schema, label) {
48
+ if (schema === undefined)
49
+ return;
50
+ if (backend.promptMeta(schema) !== undefined && backend.embedSchemaInPrompt !== true)
51
+ return;
52
+ throw agentValidationError(`per-turn schema is not supported on backend "${backend.id}" (its schema is bound at session open); ` +
53
+ "pass `schema` to the AcpAgent constructor instead", label);
54
+ }
55
+ function describeErrors(schema, value) {
56
+ let converted;
57
+ try {
58
+ converted = Convert(schema, value);
59
+ }
60
+ catch {
61
+ converted = value;
62
+ }
63
+ return Errors(schema, converted)
64
+ .slice(0, 3)
65
+ .map((error) => `${error.instancePath || "/"} ${error.message}`)
66
+ .join("; ");
67
+ }
68
+ /**
69
+ * The no-repair ladder for one turn: this turn's StructuredOutput capture (already validated by
70
+ * the tool host) → the backend's native result, validated → a validated JSON block in the final
71
+ * assistant message → otherwise `structuredError` naming every channel that applied.
72
+ */
73
+ export function resolveTurnStructured(args) {
74
+ const { schema, handle, backend, captured } = args;
75
+ if (captured !== undefined)
76
+ return { structured: captured };
77
+ const reasons = ["no StructuredOutput capture"];
78
+ const native = backend.nativeStructured?.(handle);
79
+ if (native !== undefined && native !== null) {
80
+ const validated = validateValue(native, schema);
81
+ if (validated !== undefined)
82
+ return { structured: validated };
83
+ reasons.push(`native result rejected: ${describeErrors(schema, native)}`);
84
+ }
85
+ const extracted = extractValidated(handle.finalMessageText(), schema);
86
+ if (extracted !== undefined)
87
+ return { structured: extracted };
88
+ reasons.push("no JSON object in the final message");
89
+ return { structuredError: reasons.join("; ") };
90
+ }
@@ -0,0 +1,65 @@
1
+ import type { PromptResponse } from "@agentclientprotocol/sdk";
2
+ import type { AgentHistoryEntry, AgentUsage } from "@automatalabs/shared-types";
3
+ import type { TSchema } from "typebox";
4
+ import type { Backend } from "../backend.js";
5
+ import type { AcpElicitationEvent, AcpPermissionEvent } from "../events.js";
6
+ import type { UsageBaseline } from "../usage.js";
7
+ import type { AgentEventBus } from "./events.js";
8
+ import { type StructuredHandle } from "./structured.js";
9
+ import type { AcpAgentRawRecord, AcpAgentToolCall, AcpAgentTurn, AcpAgentTurnUsage, AcpAgentUpdateRecord } from "./types.js";
10
+ /** The slice of a SessionHandle the collector and builder read (duck-typed for unit tests). */
11
+ export interface TurnHandle extends StructuredHandle {
12
+ readonly history: AgentHistoryEntry[];
13
+ readonly usage: {
14
+ baseline(): UsageBaseline;
15
+ };
16
+ foldedTurnText(): string;
17
+ }
18
+ export declare class TurnCollector {
19
+ #private;
20
+ readonly historyStart: number;
21
+ readonly gaugeBefore: UsageBaseline;
22
+ readonly updates: AcpAgentUpdateRecord[];
23
+ readonly raw: AcpAgentRawRecord[];
24
+ readonly permissions: AcpPermissionEvent[];
25
+ readonly elicitations: AcpElicitationEvent[];
26
+ /** Registers the tap SYNCHRONOUSLY — construct before the wire call so nothing is missed.
27
+ * `retainHistory: false` (the session's `retainSessionLog: false`) means the handle CLEARS its
28
+ * accumulator at `beginTurn()`, so this turn's slice starts at 0, not at the previous length. */
29
+ constructor(bus: Pick<AgentEventBus, "tap">, handle: TurnHandle, options?: {
30
+ retainHistory?: boolean;
31
+ });
32
+ /** The folded tool calls in first-seen order (copies). */
33
+ get toolCalls(): AcpAgentToolCall[];
34
+ stop(): void;
35
+ }
36
+ /**
37
+ * THIS turn's usage. `response.usage` is PER-TURN on every installed adapter (Claude, Codex and pi
38
+ * all report the turn — `PROMPT_USAGE_SCOPES`), so it maps straight to `AgentUsage`; `cost` is the
39
+ * clamped delta of the cumulative `usage_update` cost gauge across the turn (the one channel that
40
+ * IS cumulative), and a response without `usage` falls back to the context-token gauge delta —
41
+ * exactly `UsageAccumulator.delta()`'s two rules. Never a before/after subtraction of accumulator
42
+ * snapshots (the accumulator REPLACES its prompt usage per turn, so that arithmetic would be garbage).
43
+ */
44
+ export declare function turnUsageOf(response: PromptResponse, gaugeBefore: UsageBaseline, gaugeAfter: UsageBaseline): {
45
+ turn: AgentUsage;
46
+ response?: AcpAgentTurnUsage["response"];
47
+ };
48
+ /** Fold one turn into the agent's running session sum; `cost` is the latest cumulative gauge. */
49
+ export declare function addUsage(prev: AgentUsage, turn: AgentUsage, gaugeAfter: UsageBaseline): AgentUsage;
50
+ export interface BuildTurnArgs {
51
+ readonly response: PromptResponse;
52
+ readonly collector: TurnCollector;
53
+ readonly handle: TurnHandle;
54
+ readonly backend: Backend;
55
+ /** The schema active for THIS turn (per-turn override or the session schema), if any. */
56
+ readonly schema: TSchema | undefined;
57
+ /** The injected StructuredOutput capture taken for this turn, if any (`takeCaptured()`). */
58
+ readonly captured: unknown;
59
+ /** The agent's running session sum BEFORE this turn. */
60
+ readonly sessionBefore: AgentUsage;
61
+ }
62
+ /** Assemble the turn: verbatim response, folded text, the turn's history slice (copies), usage
63
+ * per the per-turn model above (with the session sum AFTER this turn), and the structured result. */
64
+ export declare function buildTurn(args: BuildTurnArgs): AcpAgentTurn;
65
+ //# sourceMappingURL=turn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../src/agent/turn.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAA+D,MAAM,0BAA0B,CAAC;AAC5H,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;AACjD,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,KAAK,EACV,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;AAqBD,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;IAKlD;;sGAEkG;gBACtF,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAO;IA6B1G,0DAA0D;IAC1D,IAAI,SAAS,IAAI,gBAAgB,EAAE,CAElC;IAED,IAAI,IAAI,IAAI;CAyCb;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;sGACsG;AACtG,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAyB3D"}
@@ -0,0 +1,187 @@
1
+ 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
+ export class TurnCollector {
8
+ historyStart;
9
+ gaugeBefore;
10
+ updates = [];
11
+ raw = [];
12
+ permissions = [];
13
+ elicitations = [];
14
+ #toolCalls = new Map();
15
+ #active = true;
16
+ #untap;
17
+ /** Registers the tap SYNCHRONOUSLY — construct before the wire call so nothing is missed.
18
+ * `retainHistory: false` (the session's `retainSessionLog: false`) means the handle CLEARS its
19
+ * accumulator at `beginTurn()`, so this turn's slice starts at 0, not at the previous length. */
20
+ constructor(bus, handle, options = {}) {
21
+ this.historyStart = options.retainHistory === false ? 0 : handle.history.length;
22
+ this.gaugeBefore = handle.usage.baseline();
23
+ this.#untap = bus.tap((name, event) => {
24
+ if (!this.#active)
25
+ return;
26
+ switch (name) {
27
+ case "session_update": {
28
+ const update = structuredClone(event.update);
29
+ this.updates.push({ update, receivedAt: Date.now() });
30
+ this.#foldToolCall(update);
31
+ return;
32
+ }
33
+ case "raw_message": {
34
+ const { method, message } = event;
35
+ this.raw.push({ method, message: structuredClone(message), receivedAt: Date.now() });
36
+ return;
37
+ }
38
+ case "permission_request":
39
+ this.permissions.push({ ...event });
40
+ return;
41
+ case "elicitation_request":
42
+ this.elicitations.push({ ...event });
43
+ return;
44
+ default:
45
+ return;
46
+ }
47
+ });
48
+ }
49
+ /** The folded tool calls in first-seen order (copies). */
50
+ get toolCalls() {
51
+ return [...this.#toolCalls.values()].map((call) => ({ ...call }));
52
+ }
53
+ stop() {
54
+ this.#active = false;
55
+ this.#untap();
56
+ }
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
+ }
114
+ /**
115
+ * THIS turn's usage. `response.usage` is PER-TURN on every installed adapter (Claude, Codex and pi
116
+ * all report the turn — `PROMPT_USAGE_SCOPES`), so it maps straight to `AgentUsage`; `cost` is the
117
+ * clamped delta of the cumulative `usage_update` cost gauge across the turn (the one channel that
118
+ * IS cumulative), and a response without `usage` falls back to the context-token gauge delta —
119
+ * exactly `UsageAccumulator.delta()`'s two rules. Never a before/after subtraction of accumulator
120
+ * snapshots (the accumulator REPLACES its prompt usage per turn, so that arithmetic would be garbage).
121
+ */
122
+ export function turnUsageOf(response, gaugeBefore, gaugeAfter) {
123
+ const cost = Math.max(0, gaugeAfter.costAmount - gaugeBefore.costAmount);
124
+ const u = response.usage ?? undefined;
125
+ if (u) {
126
+ return {
127
+ turn: {
128
+ input: u.inputTokens ?? 0,
129
+ output: u.outputTokens ?? 0,
130
+ cacheRead: u.cachedReadTokens ?? 0,
131
+ cacheWrite: u.cachedWriteTokens ?? 0,
132
+ total: u.totalTokens ?? 0,
133
+ cost,
134
+ },
135
+ response: u,
136
+ };
137
+ }
138
+ return {
139
+ turn: {
140
+ input: 0,
141
+ output: 0,
142
+ cacheRead: 0,
143
+ cacheWrite: 0,
144
+ total: Math.max(0, gaugeAfter.contextUsedTokens - gaugeBefore.contextUsedTokens),
145
+ cost,
146
+ },
147
+ };
148
+ }
149
+ /** Fold one turn into the agent's running session sum; `cost` is the latest cumulative gauge. */
150
+ export function addUsage(prev, turn, gaugeAfter) {
151
+ return {
152
+ input: prev.input + turn.input,
153
+ output: prev.output + turn.output,
154
+ cacheRead: prev.cacheRead + turn.cacheRead,
155
+ cacheWrite: prev.cacheWrite + turn.cacheWrite,
156
+ total: prev.total + turn.total,
157
+ cost: gaugeAfter.costAmount,
158
+ };
159
+ }
160
+ /** Assemble the turn: verbatim response, folded text, the turn's history slice (copies), usage
161
+ * per the per-turn model above (with the session sum AFTER this turn), and the structured result. */
162
+ export function buildTurn(args) {
163
+ const { response, collector, handle, backend, schema, captured, sessionBefore } = args;
164
+ const gaugeAfter = handle.usage.baseline();
165
+ const usage = turnUsageOf(response, collector.gaugeBefore, gaugeAfter);
166
+ const historyStart = Math.min(collector.historyStart, handle.history.length);
167
+ const history = handle.history.slice(historyStart).map((entry) => ({ ...entry }));
168
+ const structured = schema ? resolveTurnStructured({ schema, handle, backend, captured }) : {};
169
+ return {
170
+ response,
171
+ stopReason: response.stopReason,
172
+ text: handle.foldedTurnText(),
173
+ updates: collector.updates,
174
+ raw: collector.raw,
175
+ toolCalls: collector.toolCalls,
176
+ permissions: collector.permissions,
177
+ elicitations: collector.elicitations,
178
+ usage: {
179
+ turn: usage.turn,
180
+ session: addUsage(sessionBefore, usage.turn, gaugeAfter),
181
+ ...(usage.response !== undefined ? { response: usage.response } : {}),
182
+ },
183
+ ...(structured.structured !== undefined ? { structured: structured.structured } : {}),
184
+ ...(structured.structuredError !== undefined ? { structuredError: structured.structuredError } : {}),
185
+ history,
186
+ };
187
+ }