@automatalabs/acp-agents 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +53 -17
  2. package/dist/acp-client.d.ts +10 -4
  3. package/dist/acp-client.d.ts.map +1 -1
  4. package/dist/acp-client.js +11 -8
  5. package/dist/agent/acp-agent.d.ts +84 -26
  6. package/dist/agent/acp-agent.d.ts.map +1 -1
  7. package/dist/agent/acp-agent.js +458 -170
  8. package/dist/agent/errors.d.ts +12 -2
  9. package/dist/agent/errors.d.ts.map +1 -1
  10. package/dist/agent/errors.js +38 -9
  11. package/dist/agent/fork.d.ts +7 -5
  12. package/dist/agent/fork.d.ts.map +1 -1
  13. package/dist/agent/fork.js +7 -5
  14. package/dist/agent/messages.d.ts +42 -0
  15. package/dist/agent/messages.d.ts.map +1 -0
  16. package/dist/agent/messages.js +225 -0
  17. package/dist/agent/probe.d.ts +1 -1
  18. package/dist/agent/probe.d.ts.map +1 -1
  19. package/dist/agent/probe.js +5 -1
  20. package/dist/agent/queue.js +2 -2
  21. package/dist/agent/routing.d.ts +19 -1
  22. package/dist/agent/routing.d.ts.map +1 -1
  23. package/dist/agent/routing.js +41 -4
  24. package/dist/agent/stream.d.ts +21 -0
  25. package/dist/agent/stream.d.ts.map +1 -0
  26. package/dist/agent/stream.js +93 -0
  27. package/dist/agent/structured.d.ts +7 -3
  28. package/dist/agent/structured.d.ts.map +1 -1
  29. package/dist/agent/structured.js +24 -14
  30. package/dist/agent/tool-host.d.ts +34 -0
  31. package/dist/agent/tool-host.d.ts.map +1 -0
  32. package/dist/agent/tool-host.js +138 -0
  33. package/dist/agent/tools.d.ts +26 -0
  34. package/dist/agent/tools.d.ts.map +1 -0
  35. package/dist/agent/tools.js +62 -0
  36. package/dist/agent/turn.d.ts +6 -3
  37. package/dist/agent/turn.d.ts.map +1 -1
  38. package/dist/agent/turn.js +15 -67
  39. package/dist/agent/types.d.ts +174 -25
  40. package/dist/agent/types.d.ts.map +1 -1
  41. package/dist/backend.d.ts +4 -3
  42. package/dist/backend.d.ts.map +1 -1
  43. package/dist/backends/codex.d.ts +2 -1
  44. package/dist/backends/codex.d.ts.map +1 -1
  45. package/dist/backends/codex.js +3 -2
  46. package/dist/config-catalog.d.ts +4 -0
  47. package/dist/config-catalog.d.ts.map +1 -1
  48. package/dist/config-catalog.js +1 -0
  49. package/dist/index.d.ts +5 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +6 -0
  52. package/dist/local-mcp-host.d.ts +30 -0
  53. package/dist/local-mcp-host.d.ts.map +1 -0
  54. package/dist/local-mcp-host.js +151 -0
  55. package/dist/protocol-coverage.d.ts +8 -6
  56. package/dist/protocol-coverage.d.ts.map +1 -1
  57. package/dist/protocol-coverage.js +3 -2
  58. package/dist/registry.d.ts +3 -3
  59. package/dist/registry.d.ts.map +1 -1
  60. package/dist/runner.d.ts +5 -0
  61. package/dist/runner.d.ts.map +1 -1
  62. package/dist/runner.js +7 -4
  63. package/dist/structured-output.d.ts +18 -0
  64. package/dist/structured-output.d.ts.map +1 -1
  65. package/dist/structured-output.js +19 -1
  66. package/dist/structured-tool.d.ts +7 -9
  67. package/dist/structured-tool.d.ts.map +1 -1
  68. package/dist/structured-tool.js +14 -102
  69. package/dist/traits.d.ts +51 -0
  70. package/dist/traits.d.ts.map +1 -0
  71. package/dist/traits.js +88 -0
  72. package/package.json +4 -4
@@ -2,12 +2,22 @@ import { WorkflowError } from "@automatalabs/shared-types";
2
2
  import { type ErrorMapContext } from "../errors-map.js";
3
3
  import type { TypedSessionFailure } from "../typed-failures.js";
4
4
  import type { AcpAgentTurn, AcpAgentTurnError } from "./types.js";
5
- /** A deterministic caller error (bad option, misuse): SCRIPT_VALIDATION_ERROR, non-recoverable. */
5
+ /** A deterministic caller error (bad option, misuse): INVALID_ARGUMENT, non-recoverable. */
6
6
  export declare function agentValidationError(message: string, label?: string): WorkflowError;
7
7
  /** Every method except `close()`/`cancel()`/getters after the agent closed, aborted, or its process died. */
8
8
  export declare function agentClosedError(label: string | undefined, backendId: string, detail?: string): WorkflowError;
9
+ /** Re-code a shared validator's SCRIPT_VALIDATION_ERROR as the SDK's INVALID_ARGUMENT, keeping the
10
+ * message, `recoverable=false`, `agentLabel`, and `details`. Anything else is returned untouched
11
+ * — the runner and `InteractiveSession` keep SCRIPT_VALIDATION_ERROR; only the AcpAgent boundary
12
+ * speaks INVALID_ARGUMENT. */
13
+ export declare function asAgentArgumentError(error: unknown): unknown;
14
+ /** Run a synchronous validation block; a shared validator's SCRIPT_VALIDATION_ERROR surfaces as
15
+ * INVALID_ARGUMENT, everything else propagates as thrown. */
16
+ export declare function validateArguments<T>(validate: () => T): T;
9
17
  /** Map a thrown wire/lifecycle error onto the seam contract — unless `signal` aborted, in which
10
- * case the error is returned untouched so the caller can rethrow `signal.reason` itself. */
18
+ * case the error is returned untouched so the caller can rethrow `signal.reason` itself. A
19
+ * validation error the client raised on the agent's behalf (mode selection, a lifecycle
20
+ * capability the agent does not advertise) is re-coded as INVALID_ARGUMENT. */
11
21
  export declare function mapAgentError(error: unknown, ctx: ErrorMapContext, signal?: AbortSignal): unknown;
12
22
  /** The `prompt()` rejection for a walled turn: `mapTypedSessionFailure` (codes/recoverable/details
13
23
  * unchanged) with the complete `AcpAgentTurn` attached as `error.turn`. */
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/agent/errors.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAsC,MAAM,4BAA4B,CAAC;AAC/F,OAAO,EAA0C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAChG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAElE,mGAAmG;AACnG,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,aAAa,CAKnF;AAED,6GAA6G;AAC7G,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAM7G;AAED;6FAC6F;AAC7F,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAGjG;AAED;4EAC4E;AAC5E,wBAAgB,cAAc,CAC5B,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,YAAY,EAClB,GAAG,EAAE,eAAe,GACnB,iBAAiB,CAInB;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAM9E"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/agent/errors.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAsC,MAAM,4BAA4B,CAAC;AAC/F,OAAO,EAA0C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAChG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAElE,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,aAAa,CAKnF;AAED,6GAA6G;AAC7G,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAM7G;AAED;;;+BAG+B;AAC/B,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO5D;AAED;8DAC8D;AAC9D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAMzD;AAED;;;gFAGgF;AAChF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAGjG;AAED;4EAC4E;AAC5E,wBAAgB,cAAc,CAC5B,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,YAAY,EAClB,GAAG,EAAE,eAAe,GACnB,iBAAiB,CAInB;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAM9E"}
@@ -1,26 +1,55 @@
1
- // AcpAgent error helpers: SDK misuse is uniformly a WorkflowError(SCRIPT_VALIDATION_ERROR); wire
2
- // failures go through the same `mapThrownError` ladder the runner uses; an abort is never mapped
3
- // (the affected promise rejects with `signal.reason` untouched); a typed session failure rejects
4
- // with the runner's mapped error carrying the complete turn.
1
+ // AcpAgent error helpers: SDK misuse is uniformly a WorkflowError(INVALID_ARGUMENT) — the SDK's
2
+ // own guards raise it directly, and a shared validator's SCRIPT_VALIDATION_ERROR (cwd, the reserved
3
+ // "model" config id, the registry, client handlers, systemPrompt support, prompt images, the
4
+ // client's mode-selection and lifecycle-capability gates) is re-coded at the agent boundary with
5
+ // its message, `recoverable=false`, `agentLabel`, and `details` preserved; wire failures go through
6
+ // the same `mapThrownError` ladder the runner uses; an abort is never mapped (the affected promise
7
+ // rejects with `signal.reason` untouched); a typed session failure rejects with the runner's
8
+ // mapped error carrying the complete turn.
5
9
  import { WorkflowError, WorkflowErrorCode, isWorkflowError } from "@automatalabs/shared-types";
6
10
  import { mapThrownError, mapTypedSessionFailure } from "../errors-map.js";
7
- /** A deterministic caller error (bad option, misuse): SCRIPT_VALIDATION_ERROR, non-recoverable. */
11
+ /** A deterministic caller error (bad option, misuse): INVALID_ARGUMENT, non-recoverable. */
8
12
  export function agentValidationError(message, label) {
9
- return new WorkflowError(message, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, {
13
+ return new WorkflowError(message, WorkflowErrorCode.INVALID_ARGUMENT, {
10
14
  recoverable: false,
11
15
  agentLabel: label,
12
16
  });
13
17
  }
14
18
  /** Every method except `close()`/`cancel()`/getters after the agent closed, aborted, or its process died. */
15
19
  export function agentClosedError(label, backendId, detail) {
16
- return new WorkflowError(`AcpAgent (${backendId}) is closed${detail ? `: ${detail}` : ""}`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
20
+ return new WorkflowError(`AcpAgent (${backendId}) is closed${detail ? `: ${detail}` : ""}`, WorkflowErrorCode.INVALID_ARGUMENT, { recoverable: false, agentLabel: label });
21
+ }
22
+ /** Re-code a shared validator's SCRIPT_VALIDATION_ERROR as the SDK's INVALID_ARGUMENT, keeping the
23
+ * message, `recoverable=false`, `agentLabel`, and `details`. Anything else is returned untouched
24
+ * — the runner and `InteractiveSession` keep SCRIPT_VALIDATION_ERROR; only the AcpAgent boundary
25
+ * speaks INVALID_ARGUMENT. */
26
+ export function asAgentArgumentError(error) {
27
+ if (!isWorkflowError(error) || error.code !== WorkflowErrorCode.SCRIPT_VALIDATION_ERROR)
28
+ return error;
29
+ return new WorkflowError(error.message, WorkflowErrorCode.INVALID_ARGUMENT, {
30
+ recoverable: false,
31
+ agentLabel: error.agentLabel,
32
+ details: error.details,
33
+ });
34
+ }
35
+ /** Run a synchronous validation block; a shared validator's SCRIPT_VALIDATION_ERROR surfaces as
36
+ * INVALID_ARGUMENT, everything else propagates as thrown. */
37
+ export function validateArguments(validate) {
38
+ try {
39
+ return validate();
40
+ }
41
+ catch (error) {
42
+ throw asAgentArgumentError(error);
43
+ }
17
44
  }
18
45
  /** Map a thrown wire/lifecycle error onto the seam contract — unless `signal` aborted, in which
19
- * case the error is returned untouched so the caller can rethrow `signal.reason` itself. */
46
+ * case the error is returned untouched so the caller can rethrow `signal.reason` itself. A
47
+ * validation error the client raised on the agent's behalf (mode selection, a lifecycle
48
+ * capability the agent does not advertise) is re-coded as INVALID_ARGUMENT. */
20
49
  export function mapAgentError(error, ctx, signal) {
21
50
  if (signal?.aborted)
22
51
  return error;
23
- return mapThrownError(error, ctx);
52
+ return asAgentArgumentError(mapThrownError(error, ctx));
24
53
  }
25
54
  /** The `prompt()` rejection for a walled turn: `mapTypedSessionFailure` (codes/recoverable/details
26
55
  * unchanged) with the complete `AcpAgentTurn` attached as `error.turn`. */
@@ -1,4 +1,4 @@
1
- import type { AcpSessionOptions, PooledConnection, SessionHandle } from "../acp-client.js";
1
+ import type { AcpSessionOptions, PooledConnection, ReattachPreference, SessionHandle } from "../acp-client.js";
2
2
  import type { Backend } from "../backend.js";
3
3
  import { type ForkSessionTraitRow } from "../protocol-coverage.js";
4
4
  import type { BackendRegistry } from "../registry.js";
@@ -18,10 +18,12 @@ export interface AcquiredFork {
18
18
  * - `live`: the fork response IS the session.
19
19
  * - `id-only`: the response names a persisted copy that is not live; release the fork handle
20
20
  * with `keepOpen` (unregister only — no `session/close`, so the reattach's `register()` never
21
- * replaces a live state and the slot count stays at one), then reattach it (resume preferred,
22
- * load fallback). The catalog/modes come from the reattach response; the bare fork response is
23
- * never read for them.
21
+ * replaces a live state and the slot count stays at one), then reattach it by `reattach` —
22
+ * `resume` preferred with `load` as the fallback (the live `agent.fork()`, whose child is
23
+ * seeded from the parent's snapshot), or `load` preferred with `resume` as the fallback (the
24
+ * cold `AcpAgent.fork(ref)`, which has no parent to seed from and wants the replay). The
25
+ * catalog/modes come from the reattach response; the bare fork response is never read for them.
24
26
  * `prepare` is re-evaluated for the reattach so it sees the same options the fork carried.
25
27
  */
26
- export declare function acquireForkedSession(connection: PooledConnection, sourceSessionId: string, opts: AcpSessionOptions, trait: ForkSessionTraitRow): Promise<AcquiredFork>;
28
+ export declare function acquireForkedSession(connection: PooledConnection, sourceSessionId: string, opts: AcpSessionOptions, trait: ForkSessionTraitRow, reattach?: ReattachPreference): Promise<AcquiredFork>;
27
29
  //# sourceMappingURL=fork.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../../src/agent/fork.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC3F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAoB,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;0FAE0F;AAC1F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,GAAG,mBAAmB,CAG7F;AAED,MAAM,WAAW,YAAY;IAC3B,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;CAC7C;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,gBAAgB,EAC5B,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,YAAY,CAAC,CAOvB"}
1
+ {"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../../src/agent/fork.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC/G,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAoB,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;0FAE0F;AAC1F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,GAAG,mBAAmB,CAG7F;AAED,MAAM,WAAW,YAAY;IAC3B,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;CAC7C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,gBAAgB,EAC5B,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,GAAE,kBAA6B,GACtC,OAAO,CAAC,YAAY,CAAC,CAOvB"}
@@ -12,17 +12,19 @@ export function forkTraitFor(backend, registry) {
12
12
  * - `live`: the fork response IS the session.
13
13
  * - `id-only`: the response names a persisted copy that is not live; release the fork handle
14
14
  * with `keepOpen` (unregister only — no `session/close`, so the reattach's `register()` never
15
- * replaces a live state and the slot count stays at one), then reattach it (resume preferred,
16
- * load fallback). The catalog/modes come from the reattach response; the bare fork response is
17
- * never read for them.
15
+ * replaces a live state and the slot count stays at one), then reattach it by `reattach` —
16
+ * `resume` preferred with `load` as the fallback (the live `agent.fork()`, whose child is
17
+ * seeded from the parent's snapshot), or `load` preferred with `resume` as the fallback (the
18
+ * cold `AcpAgent.fork(ref)`, which has no parent to seed from and wants the replay). The
19
+ * catalog/modes come from the reattach response; the bare fork response is never read for them.
18
20
  * `prepare` is re-evaluated for the reattach so it sees the same options the fork carried.
19
21
  */
20
- export async function acquireForkedSession(connection, sourceSessionId, opts, trait) {
22
+ export async function acquireForkedSession(connection, sourceSessionId, opts, trait, reattach = "resume") {
21
23
  const forkHandle = await connection.forkSession(sourceSessionId, opts);
22
24
  if (trait.disposition !== "id-only")
23
25
  return { handle: forkHandle, method: "fork" };
24
26
  const forkedId = forkHandle.sessionId;
25
27
  await forkHandle.release({ keepOpen: true });
26
- const { handle, method } = await connection.openPreparedReattachedSession(forkedId, () => opts);
28
+ const { handle, method } = await connection.openPreparedReattachedSession(forkedId, () => opts, undefined, reattach);
27
29
  return { handle, method };
28
30
  }
@@ -0,0 +1,42 @@
1
+ import type { ToolCallContent, ToolCallLocation, ToolCallStatus, ToolKind } from "@agentclientprotocol/sdk";
2
+ import type { AcpSessionUpdate } from "../events.js";
3
+ import type { AcpAgentMessage, AcpAgentToolCall, AcpAgentUpdateRecord } from "./types.js";
4
+ export interface MutableToolCall {
5
+ toolCallId: string;
6
+ name?: string;
7
+ title: string;
8
+ kind?: ToolKind;
9
+ status: ToolCallStatus;
10
+ rawInput?: unknown;
11
+ rawOutput?: unknown;
12
+ content?: ToolCallContent[];
13
+ locations?: ToolCallLocation[];
14
+ meta?: Record<string, unknown>;
15
+ }
16
+ type ToolCallUpdate = Extract<AcpSessionUpdate, {
17
+ sessionUpdate: "tool_call" | "tool_call_update";
18
+ }>;
19
+ /** Fold one `tool_call` / `tool_call_update` into `existing` (a fresh entry when undefined):
20
+ * every field the update carries replaces the last seen one, `_meta` merges shallowly, and
21
+ * `status` stays `pending` until the agent sends one. */
22
+ export declare function foldToolCall(existing: MutableToolCall | undefined, update: ToolCallUpdate): MutableToolCall;
23
+ /** A fresh, shallow copy of a message (blocks and tool calls copied one level deep). */
24
+ export declare function copyMessage(message: AcpAgentMessage): AcpAgentMessage;
25
+ export declare class MessageFolder {
26
+ #private;
27
+ /** Mirror `SessionState.beginTurn()`: a boundary, no message in progress, and — like the
28
+ * accumulator under `retainSessionLog: false` — a cleared transcript when `retain` is false.
29
+ * A thought left over from the previous turn becomes its own assistant message first. */
30
+ beginTurn(retain: boolean): void;
31
+ apply(update: AcpSessionUpdate, receivedAt: number): void;
32
+ /** Every folded tool call in first-seen order (copies) — the flattening of the messages'
33
+ * `toolCalls`, which is the same order because a call attaches to the message in progress. */
34
+ get toolCalls(): AcpAgentToolCall[];
35
+ /** The messages so far (copies). A thought still waiting for the content it precedes is
36
+ * reported as a trailing assistant message with no text. */
37
+ snapshot(): AcpAgentMessage[];
38
+ }
39
+ /** Fold verbatim update records (a turn's `updates`, a `replay`) into messages. */
40
+ export declare function foldMessages(records: readonly AcpAgentUpdateRecord[]): AcpAgentMessage[];
41
+ export {};
42
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/agent/messages.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAgB,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAC1H,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE1F,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,KAAK,cAAc,GAAG,OAAO,CAAC,gBAAgB,EAAE;IAAE,aAAa,EAAE,WAAW,GAAG,kBAAkB,CAAA;CAAE,CAAC,CAAC;AAgBrG;;0DAE0D;AAC1D,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,SAAS,EAAE,MAAM,EAAE,cAAc,GAAG,eAAe,CAiB3G;AAYD,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,eAAe,CAQrE;AAED,qBAAa,aAAa;;IAgBxB;;8FAE0F;IAC1F,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAchC,KAAK,CAAC,MAAM,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IA6DzD;mGAC+F;IAC/F,IAAI,SAAS,IAAI,gBAAgB,EAAE,CAElC;IAED;iEAC6D;IAC7D,QAAQ,IAAI,eAAe,EAAE;CAmE9B;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,GAAG,eAAe,EAAE,CAIxF"}
@@ -0,0 +1,225 @@
1
+ function record(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value)
3
+ ? value
4
+ : undefined;
5
+ }
6
+ /** Fold one `tool_call` / `tool_call_update` into `existing` (a fresh entry when undefined):
7
+ * every field the update carries replaces the last seen one, `_meta` merges shallowly, and
8
+ * `status` stays `pending` until the agent sends one. */
9
+ export function foldToolCall(existing, update) {
10
+ const meta = record(update._meta);
11
+ const entry = existing ?? {
12
+ toolCallId: update.toolCallId,
13
+ title: typeof update.title === "string" ? update.title : "",
14
+ status: "pending",
15
+ };
16
+ if (typeof update.title === "string")
17
+ entry.title = update.title;
18
+ if (typeof update.name === "string")
19
+ entry.name = update.name;
20
+ if (update.kind !== undefined && update.kind !== null)
21
+ entry.kind = update.kind;
22
+ if (update.status !== undefined && update.status !== null)
23
+ entry.status = update.status;
24
+ if (update.rawInput !== undefined)
25
+ entry.rawInput = update.rawInput;
26
+ if (update.rawOutput !== undefined)
27
+ entry.rawOutput = update.rawOutput;
28
+ if (update.content !== undefined && update.content !== null)
29
+ entry.content = update.content;
30
+ if (update.locations !== undefined && update.locations !== null)
31
+ entry.locations = update.locations;
32
+ if (meta)
33
+ entry.meta = { ...(entry.meta ?? {}), ...meta };
34
+ return entry;
35
+ }
36
+ /** Append `block` to `blocks`, concatenating a text block onto a trailing text block. */
37
+ function appendBlock(blocks, block) {
38
+ const last = blocks[blocks.length - 1];
39
+ if (block.type === "text" && last?.type === "text") {
40
+ blocks[blocks.length - 1] = { ...last, text: last.text + block.text };
41
+ return;
42
+ }
43
+ blocks.push({ ...block });
44
+ }
45
+ /** A fresh, shallow copy of a message (blocks and tool calls copied one level deep). */
46
+ export function copyMessage(message) {
47
+ return {
48
+ role: message.role,
49
+ content: message.content.map((block) => ({ ...block })),
50
+ toolCalls: message.toolCalls.map((call) => ({ ...call })),
51
+ thoughts: message.thoughts.map((block) => ({ ...block })),
52
+ receivedAt: message.receivedAt,
53
+ };
54
+ }
55
+ export class MessageFolder {
56
+ #messages = [];
57
+ /** The assistant message tool calls and non-text blocks attach to; unset after a user message or
58
+ * a turn boundary. */
59
+ #assistant;
60
+ /** The open run of `user_message_chunk`s. */
61
+ #user;
62
+ /** Thoughts waiting for the assistant content they precede. */
63
+ #pendingThoughts = [];
64
+ #pendingThoughtsAt;
65
+ /** `SessionState.assistantMessageBoundaryPending`: the next text chunk opens a new message. */
66
+ #boundaryPending = true;
67
+ /** `SessionState.activeAssistantMessageId`: a changed ACP `messageId` is a boundary. */
68
+ #activeMessageId;
69
+ #toolCallsById = new Map();
70
+ /** Mirror `SessionState.beginTurn()`: a boundary, no message in progress, and — like the
71
+ * accumulator under `retainSessionLog: false` — a cleared transcript when `retain` is false.
72
+ * A thought left over from the previous turn becomes its own assistant message first. */
73
+ beginTurn(retain) {
74
+ if (retain) {
75
+ this.#flushPendingThoughts();
76
+ }
77
+ else {
78
+ this.#messages.length = 0;
79
+ this.#toolCallsById.clear();
80
+ this.#pendingThoughts = [];
81
+ this.#pendingThoughtsAt = undefined;
82
+ }
83
+ this.#assistant = undefined;
84
+ this.#user = undefined;
85
+ this.#markBoundary();
86
+ }
87
+ apply(update, receivedAt) {
88
+ switch (update.sessionUpdate) {
89
+ case "agent_message_chunk": {
90
+ this.#user = undefined;
91
+ this.#applyMessageId(update.messageId);
92
+ // A pending boundary opens a new message for ANY block. The text fold consumes it only on
93
+ // a text chunk, but a non-text block consuming it here never moves a text chunk between
94
+ // messages: the message it opens holds no earlier text, so the next text chunk is its
95
+ // first either way.
96
+ const target = this.#boundaryPending || !this.#assistant ? this.#openAssistant(receivedAt) : this.#assistant;
97
+ this.#boundaryPending = false;
98
+ appendBlock(target.content, update.content);
99
+ return;
100
+ }
101
+ case "agent_thought_chunk": {
102
+ this.#user = undefined;
103
+ this.#markBoundary();
104
+ appendBlock(this.#pendingThoughts, update.content);
105
+ this.#pendingThoughtsAt ??= receivedAt;
106
+ return;
107
+ }
108
+ case "tool_call":
109
+ case "tool_call_update": {
110
+ this.#user = undefined;
111
+ this.#markBoundary();
112
+ const existing = this.#toolCallsById.get(update.toolCallId);
113
+ if (existing) {
114
+ foldToolCall(existing, update);
115
+ return;
116
+ }
117
+ const target = this.#assistant ?? this.#openAssistant(receivedAt);
118
+ this.#adoptPendingThoughts(target);
119
+ const entry = foldToolCall(undefined, update);
120
+ target.toolCalls.push(entry);
121
+ this.#toolCallsById.set(entry.toolCallId, entry);
122
+ return;
123
+ }
124
+ case "user_message_chunk": {
125
+ this.#markBoundary();
126
+ this.#assistant = undefined;
127
+ this.#flushPendingThoughts();
128
+ if (!this.#user) {
129
+ this.#user = { role: "user", content: [], toolCalls: [], thoughts: [], receivedAt };
130
+ this.#messages.push(this.#user);
131
+ }
132
+ appendBlock(this.#user.content, update.content);
133
+ return;
134
+ }
135
+ case "plan":
136
+ case "plan_update":
137
+ case "plan_removed": {
138
+ this.#user = undefined;
139
+ this.#markBoundary();
140
+ return;
141
+ }
142
+ default:
143
+ return;
144
+ }
145
+ }
146
+ /** Every folded tool call in first-seen order (copies) — the flattening of the messages'
147
+ * `toolCalls`, which is the same order because a call attaches to the message in progress. */
148
+ get toolCalls() {
149
+ return this.#messages.flatMap((message) => message.toolCalls.map((call) => ({ ...call })));
150
+ }
151
+ /** The messages so far (copies). A thought still waiting for the content it precedes is
152
+ * reported as a trailing assistant message with no text. */
153
+ snapshot() {
154
+ const messages = this.#messages.map(copyMessage);
155
+ if (this.#pendingThoughts.length > 0) {
156
+ messages.push({
157
+ role: "assistant",
158
+ content: [],
159
+ toolCalls: [],
160
+ thoughts: this.#pendingThoughts.map((block) => ({ ...block })),
161
+ receivedAt: this.#pendingThoughtsAt,
162
+ });
163
+ }
164
+ return messages;
165
+ }
166
+ #markBoundary() {
167
+ this.#boundaryPending = true;
168
+ this.#activeMessageId = undefined;
169
+ }
170
+ /** `SessionState.beginAssistantMessageChunk`'s id rule: a chunk whose `messageId` differs from
171
+ * the active one marks a boundary; an id is remembered whether or not the chunk carries text. */
172
+ #applyMessageId(messageId) {
173
+ const id = messageId ?? undefined;
174
+ if (id !== undefined && this.#activeMessageId !== undefined && id !== this.#activeMessageId) {
175
+ this.#boundaryPending = true;
176
+ }
177
+ if (id !== undefined)
178
+ this.#activeMessageId = id;
179
+ }
180
+ /** Open a new assistant message (adopting any pending thoughts, which then date it). The
181
+ * boundary flag is the caller's: a text or non-text block consumes it, a tool call does not
182
+ * (text after a tool call is always a new message, exactly like the text fold). */
183
+ #openAssistant(receivedAt) {
184
+ const message = {
185
+ role: "assistant",
186
+ content: [],
187
+ toolCalls: [],
188
+ thoughts: [],
189
+ receivedAt: this.#pendingThoughtsAt ?? receivedAt,
190
+ };
191
+ this.#adoptPendingThoughts(message);
192
+ this.#messages.push(message);
193
+ this.#assistant = message;
194
+ return message;
195
+ }
196
+ #adoptPendingThoughts(message) {
197
+ if (this.#pendingThoughts.length === 0)
198
+ return;
199
+ message.thoughts.push(...this.#pendingThoughts);
200
+ this.#pendingThoughts = [];
201
+ this.#pendingThoughtsAt = undefined;
202
+ }
203
+ /** A thought that no assistant content followed (a user message or a turn boundary came first)
204
+ * is an assistant message of its own; it is never the attach target for later content. */
205
+ #flushPendingThoughts() {
206
+ if (this.#pendingThoughts.length === 0)
207
+ return;
208
+ this.#messages.push({
209
+ role: "assistant",
210
+ content: [],
211
+ toolCalls: [],
212
+ thoughts: this.#pendingThoughts,
213
+ receivedAt: this.#pendingThoughtsAt,
214
+ });
215
+ this.#pendingThoughts = [];
216
+ this.#pendingThoughtsAt = undefined;
217
+ }
218
+ }
219
+ /** Fold verbatim update records (a turn's `updates`, a `replay`) into messages. */
220
+ export function foldMessages(records) {
221
+ const folder = new MessageFolder();
222
+ for (const { update, receivedAt } of records)
223
+ folder.apply(update, receivedAt);
224
+ return folder.snapshot();
225
+ }
@@ -8,7 +8,7 @@ import type { AcpAgentCatalog, AcpAgentProbeOptions } from "./types.js";
8
8
  export declare function createAgentProbeRunner(backends: Record<string, CustomBackendConfig> | undefined): ValidateProbeRunner;
9
9
  /** The catalog behind `AcpAgent.probe` (see the file header). Per-target failures never throw
10
10
  * (`probed: false` with a redacted `error`); a bad `modelFilter`, `probeTimeoutMs`, or
11
- * `probeConcurrency` throws a TypeError and a malformed registry a SCRIPT_VALIDATION_ERROR — all
11
+ * `probeConcurrency` throws a TypeError and a malformed registry an INVALID_ARGUMENT — all
12
12
  * before any process spawns. */
13
13
  export declare function probeCatalog(options?: AcpAgentProbeOptions): Promise<AcpAgentCatalog>;
14
14
  //# sourceMappingURL=probe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/agent/probe.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAInF,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAExE;;;wEAGwE;AACxE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,SAAS,GAAG,mBAAmB,CAuCrH;AAED;;;iCAGiC;AACjC,wBAAsB,YAAY,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,eAAe,CAAC,CA8B/F"}
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/agent/probe.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAKnF,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAExE;;;wEAGwE;AACxE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,SAAS,GAAG,mBAAmB,CA0CrH;AAED;;;iCAGiC;AACjC,wBAAsB,YAAY,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,eAAe,CAAC,CA8B/F"}
@@ -3,6 +3,7 @@ import { buildHarnessModelsView, buildModelFilter, probeHarnessConfig, } from ".
3
3
  import { mapThrownError } from "../errors-map.js";
4
4
  import { registryWithRunBackends } from "../registry.js";
5
5
  import { resolveModelRoute } from "../routing.js";
6
+ import { describeBackendTraits } from "../traits.js";
6
7
  import { releaseOnExit, retainOnExit } from "./process-registry.js";
7
8
  import { resolveAgentRegistry } from "./routing.js";
8
9
  /** A `ValidateProbeRunner` over a dedicated connection per probe: spawn, initialize, one no-prompt
@@ -34,6 +35,9 @@ export function createAgentProbeRunner(backends) {
34
35
  ...(route.backend.defaultModeId === undefined ? {} : { defaultModeId: route.backend.defaultModeId }),
35
36
  options: handle.advertisedConfigOptions,
36
37
  modes: handle.modes ?? null,
38
+ // The live refinement: pi and the Codex fork advertise their system-prompt channel and the
39
+ // two vendor extensions at initialize; Claude advertises nothing and reports the tables.
40
+ traits: describeBackendTraits(route.backend, registry, connection.capabilities),
37
41
  };
38
42
  }
39
43
  finally {
@@ -54,7 +58,7 @@ export function createAgentProbeRunner(backends) {
54
58
  }
55
59
  /** The catalog behind `AcpAgent.probe` (see the file header). Per-target failures never throw
56
60
  * (`probed: false` with a redacted `error`); a bad `modelFilter`, `probeTimeoutMs`, or
57
- * `probeConcurrency` throws a TypeError and a malformed registry a SCRIPT_VALIDATION_ERROR — all
61
+ * `probeConcurrency` throws a TypeError and a malformed registry an INVALID_ARGUMENT — all
58
62
  * before any process spawns. */
59
63
  export async function probeCatalog(options = {}) {
60
64
  // Validation only: a bad regex must surface BEFORE any spawn (the MCP handler does the same).
@@ -1,5 +1,5 @@
1
- // The per-agent FIFO. `prompt`, `fork`, `setMode`, `setConfigOptions`, `close` and the implicit
2
- // open all run through one SerialQueue so no two of them ever overlap (a second concurrent
1
+ // The per-agent FIFO. `prompt`, `fork`, `setModel`, `setMode`, `setConfigOptions`, `close` and the
2
+ // implicit open all run through one SerialQueue so no two of them ever overlap (a second concurrent
3
3
  // `SessionHandle.prompt` would clobber the handle's active turn; pi rejects a fork while the
4
4
  // source has a turn in flight; Claude would copy a partial transcript). Pure: no ACP imports.
5
5
  export class SerialQueue {
@@ -4,7 +4,8 @@ import { type BackendRegistry, type CustomBackendConfig } from "../registry.js";
4
4
  import { type ModelRoute } from "../routing.js";
5
5
  import type { AcpAgentOptions } from "./types.js";
6
6
  /** The custom-backend registry for an agent: `backends` merged over `AGENTPRISM_BACKENDS`. A
7
- * malformed registry is a caller error (SCRIPT_VALIDATION_ERROR), mirroring the runner's wrap. */
7
+ * malformed registry is a caller error (INVALID_ARGUMENT; the runner's wrap says
8
+ * SCRIPT_VALIDATION_ERROR for the same condition). */
8
9
  export declare function resolveAgentRegistry(backends: Record<string, CustomBackendConfig> | undefined, label?: string): BackendRegistry;
9
10
  /** The runner's routing grammar applied to `new AcpAgent({ model })`. */
10
11
  export declare function resolveAgentRoute(options: Pick<AcpAgentOptions, "model">, registry: BackendRegistry): ModelRoute;
@@ -17,6 +18,23 @@ export declare function resolveAgentRoute(options: Pick<AcpAgentOptions, "model"
17
18
  * VERBATIM to the ref's backend (the same unrouted rule as the runner, applied to this backend).
18
19
  */
19
20
  export declare function resolveRefRoute(ref: AgentSessionRef, model: string | undefined, registry: BackendRegistry, label?: string): ModelRoute;
21
+ /**
22
+ * The model rule `fork()`, `setModel()` and a per-turn `model` share: `spec` goes through the
23
+ * runner's grammar (`resolveModelRoute` — a registered name wins, an unrouted spec goes to the
24
+ * default backend) and the route must land on THIS agent's backend, poolKey-equal; otherwise
25
+ * INVALID_ARGUMENT naming both backends. The remainder is the verbatim model id (`undefined` for a
26
+ * backend-only spec — `fork()` reads that as "no selection"; a switch rejects it, see
27
+ * `resolveModelSwitch`).
28
+ */
29
+ export declare function resolveSameBackendModel(spec: string, backend: Backend, registry: BackendRegistry, label: string | undefined, method: string): ModelRoute;
30
+ /** What a mid-session switch selects: `spec` resolved with `resolveSameBackendModel`, with a
31
+ * blank spec or a backend-only spec (`"claude"`, `"claude/"`) rejected — there is no wire form
32
+ * for "unselect", so a switch always names a model id. Returns the verbatim id to send and the
33
+ * routed `<backendId>/<id>` form the agent's `model` takes once it applied. */
34
+ export declare function resolveModelSwitch(spec: unknown, backend: Backend, registry: BackendRegistry, label: string | undefined, method: string): {
35
+ modelSpec: string;
36
+ model: string;
37
+ };
20
38
  /** A fresh `Backend` instance with the same identity as `backend` (a registered name wins, as in
21
39
  * routing; else the built-in of that id). A fork child must own its own instance — pooling identity
22
40
  * is `poolKey ?? id`, never the object. */
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/agent/routing.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C,OAAO,EAA0B,KAAK,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACxG,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAEnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;mGACmG;AACnG,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,SAAS,EACzD,KAAK,CAAC,EAAE,MAAM,GACb,eAAe,CAMjB;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,eAAe,GAAG,UAAU,CAEhH;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,QAAQ,EAAE,eAAe,EACzB,KAAK,CAAC,EAAE,MAAM,GACb,UAAU,CA4BZ;AAED;;4CAE4C;AAC5C,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAEpF;AAQD;kFACkF;AAClF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,MAAM,CAiB/G"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/agent/routing.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C,OAAO,EAA0B,KAAK,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACxG,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAEnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;uDAEuD;AACvD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,SAAS,EACzD,KAAK,CAAC,EAAE,MAAM,GACb,eAAe,CAMjB;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,eAAe,GAAG,UAAU,CAEhH;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,QAAQ,EAAE,eAAe,EACzB,KAAK,CAAC,EAAE,MAAM,GACb,UAAU,CA4BZ;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,MAAM,GACb,UAAU,CAYZ;AAED;;;gFAGgF;AAChF,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,MAAM,GACb;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAYtC;AAED;;4CAE4C;AAC5C,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAEpF;AAQD;kFACkF;AAClF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,MAAM,CAiB/G"}
@@ -1,7 +1,8 @@
1
- // Agent-facing routing over ../routing.js: the registry read (malformed → SCRIPT_VALIDATION_ERROR),
1
+ // Agent-facing routing over ../routing.js: the registry read (malformed → INVALID_ARGUMENT),
2
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.
3
+ // statics use (never the default backend), the same-backend rule a fork override and a
4
+ // mid-session model switch (`setModel`, a per-turn `model`) must satisfy, cwd validation that
5
+ // fails BEFORE a process spawns, and the fresh-Backend-instance rule a fork child needs.
5
6
  import { isAbsolute } from "node:path";
6
7
  import { statSync } from "node:fs";
7
8
  import { builtinBackend } from "../backends/builtins.js";
@@ -10,7 +11,8 @@ import { resolveBackendRegistry } from "../registry.js";
10
11
  import { asciiLowercase, resolveModelRoute } from "../routing.js";
11
12
  import { agentValidationError } from "./errors.js";
12
13
  /** 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
+ * malformed registry is a caller error (INVALID_ARGUMENT; the runner's wrap says
15
+ * SCRIPT_VALIDATION_ERROR for the same condition). */
14
16
  export function resolveAgentRegistry(backends, label) {
15
17
  try {
16
18
  return resolveBackendRegistry(backends);
@@ -52,6 +54,41 @@ export function resolveRefRoute(ref, model, registry, label) {
52
54
  }
53
55
  return { backend, modelSpec: model };
54
56
  }
57
+ /**
58
+ * The model rule `fork()`, `setModel()` and a per-turn `model` share: `spec` goes through the
59
+ * runner's grammar (`resolveModelRoute` — a registered name wins, an unrouted spec goes to the
60
+ * default backend) and the route must land on THIS agent's backend, poolKey-equal; otherwise
61
+ * INVALID_ARGUMENT naming both backends. The remainder is the verbatim model id (`undefined` for a
62
+ * backend-only spec — `fork()` reads that as "no selection"; a switch rejects it, see
63
+ * `resolveModelSwitch`).
64
+ */
65
+ export function resolveSameBackendModel(spec, backend, registry, label, method) {
66
+ const route = resolveModelRoute(spec, registry);
67
+ const routedPool = route.backend.poolKey ?? route.backend.id;
68
+ const ownPool = backend.poolKey ?? backend.id;
69
+ if (route.backend.id !== backend.id || routedPool !== ownPool) {
70
+ // Same id, different pool (a registry entry shadowing this built-in's name): say which pool.
71
+ const pooled = route.backend.id === backend.id;
72
+ const routed = `backend "${route.backend.id}"${pooled ? ` (pool "${routedPool}")` : ""}`;
73
+ const own = `backend "${backend.id}"${pooled ? ` (pool "${ownPool}")` : ""}`;
74
+ throw agentValidationError(`${method}: model "${spec}" routes to ${routed} but must stay on ${own}`, label);
75
+ }
76
+ return route;
77
+ }
78
+ /** What a mid-session switch selects: `spec` resolved with `resolveSameBackendModel`, with a
79
+ * blank spec or a backend-only spec (`"claude"`, `"claude/"`) rejected — there is no wire form
80
+ * for "unselect", so a switch always names a model id. Returns the verbatim id to send and the
81
+ * routed `<backendId>/<id>` form the agent's `model` takes once it applied. */
82
+ export function resolveModelSwitch(spec, backend, registry, label, method) {
83
+ if (typeof spec !== "string" || spec.trim() === "") {
84
+ throw agentValidationError(`${method} requires a non-empty model spec ("${backend.id}/<model id>")`, label);
85
+ }
86
+ const { modelSpec } = resolveSameBackendModel(spec, backend, registry, label, method);
87
+ if (modelSpec === undefined || modelSpec.trim() === "") {
88
+ throw agentValidationError(`${method}: model "${spec}" names backend "${backend.id}" but no model id; use "${backend.id}/<model id>"`, label);
89
+ }
90
+ return { modelSpec, model: `${backend.id}/${modelSpec}` };
91
+ }
55
92
  /** A fresh `Backend` instance with the same identity as `backend` (a registered name wins, as in
56
93
  * routing; else the built-in of that id). A fork child must own its own instance — pooling identity
57
94
  * is `poolKey ?? id`, never the object. */
@@ -0,0 +1,21 @@
1
+ import type { AcpAgentStream, AcpAgentStreamEvent } from "./types.js";
2
+ type Result = IteratorResult<AcpAgentStreamEvent, undefined>;
3
+ export declare class TurnStream implements AcpAgentStream {
4
+ #private;
5
+ /** `stop` aborts the turn and resolves once it settled; called at most once. */
6
+ constructor(stop: () => Promise<void>);
7
+ /** Buffer an event, or hand it straight to a waiting `next()`. Ignored once closed. */
8
+ push(event: AcpAgentStreamEvent): void;
9
+ /** The turn resolved; the terminal event is already buffered. */
10
+ end(): void;
11
+ /** The turn rejected: buffered events are still delivered, then `error` is thrown once. */
12
+ fail(error: unknown): void;
13
+ next(): Promise<Result>;
14
+ /** The consumer left early: abort the turn, wait for it to settle, then report `done`. */
15
+ return(): Promise<Result>;
16
+ /** Same as `return()`, then rethrow `error` to the caller (async-generator semantics). */
17
+ throw(error?: unknown): Promise<Result>;
18
+ [Symbol.asyncIterator](): AcpAgentStream;
19
+ }
20
+ export {};
21
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/agent/stream.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtE,KAAK,MAAM,GAAG,cAAc,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;AAS7D,qBAAa,UAAW,YAAW,cAAc;;IAU/C,gFAAgF;gBACpE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC;IAIrC,uFAAuF;IACvF,IAAI,CAAC,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAOtC,iEAAiE;IACjE,GAAG,IAAI,IAAI;IAMX,2FAA2F;IAC3F,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAO1B,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IASvB,0FAA0F;IACpF,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAK/B,0FAA0F;IACpF,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAK7C,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,cAAc;CAkCzC"}