@gaunt-sloth/agent 2.0.0-alpha.38 → 2.0.0-alpha.39

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 (38) hide show
  1. package/README.md +6 -5
  2. package/cli-acp.js +5 -4
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +4 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/modules/acp/acpAgentApp.d.ts +16 -52
  7. package/dist/modules/acp/acpAgentApp.js +31 -246
  8. package/dist/modules/acp/acpAgentApp.js.map +1 -1
  9. package/dist/modules/acp/acpAgentAppV1.d.ts +52 -0
  10. package/dist/modules/acp/acpAgentAppV1.js +307 -0
  11. package/dist/modules/acp/acpAgentAppV1.js.map +1 -0
  12. package/dist/modules/acp/acpCommon.d.ts +167 -0
  13. package/dist/modules/acp/acpCommon.js +282 -0
  14. package/dist/modules/acp/acpCommon.js.map +1 -0
  15. package/dist/modules/acp/acpPermissions.d.ts +6 -1
  16. package/dist/modules/acp/acpPermissions.js +5 -1
  17. package/dist/modules/acp/acpPermissions.js.map +1 -1
  18. package/dist/modules/acp/acpPermissionsV1.d.ts +45 -0
  19. package/dist/modules/acp/acpPermissionsV1.js +110 -0
  20. package/dist/modules/acp/acpPermissionsV1.js.map +1 -0
  21. package/dist/modules/acp/acpRouter.d.ts +41 -0
  22. package/dist/modules/acp/acpRouter.js +48 -0
  23. package/dist/modules/acp/acpRouter.js.map +1 -0
  24. package/dist/modules/acp/acpStdio.d.ts +9 -3
  25. package/dist/modules/acp/acpStdio.js +11 -5
  26. package/dist/modules/acp/acpStdio.js.map +1 -1
  27. package/dist/modules/acp/acpToolCalls.d.ts +93 -0
  28. package/dist/modules/acp/acpToolCalls.js +193 -0
  29. package/dist/modules/acp/acpToolCalls.js.map +1 -0
  30. package/dist/modules/acp/acpUpdates.d.ts +7 -60
  31. package/dist/modules/acp/acpUpdates.js +10 -155
  32. package/dist/modules/acp/acpUpdates.js.map +1 -1
  33. package/dist/modules/acp/acpUpdatesV1.d.ts +61 -0
  34. package/dist/modules/acp/acpUpdatesV1.js +162 -0
  35. package/dist/modules/acp/acpUpdatesV1.js.map +1 -0
  36. package/dist/modules/interactiveSessionModule.js +90 -94
  37. package/dist/modules/interactiveSessionModule.js.map +1 -1
  38. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @packageDocumentation
3
3
  * The stdio entry point both ACP doors go through — the standalone `gaunt-sloth-acp` bin and
4
- * `gaunt-sloth --acp-agent`.
4
+ * `gaunt-sloth --acp-agent` — serving whichever protocol version the client speaks.
5
5
  *
6
6
  * **It is one function on purpose.** Two entry points spelling the same startup twice is how the
7
7
  * doors drift, and the thing that would drift here is not cosmetic: it is the stdout guarantee
@@ -23,7 +23,8 @@
23
23
  */
24
24
  import { Readable } from 'node:stream';
25
25
  import * as acp from '@agentclientprotocol/sdk/experimental/v2';
26
- import { announceAcpStart, createAcpAgentApp } from '#src/modules/acp/acpAgentApp.js';
26
+ import { announceAcpStart } from '#src/modules/acp/acpCommon.js';
27
+ import { createAcpAgentRouter } from '#src/modules/acp/acpRouter.js';
27
28
  /**
28
29
  * Sends everything written to `process.stdout` to stderr instead, and returns a sink holding the
29
30
  * ORIGINAL writer.
@@ -46,7 +47,13 @@ function captureStdoutForProtocol() {
46
47
  });
47
48
  }
48
49
  /**
49
- * Serves ACP v2 over stdio until the client disconnects.
50
+ * Serves ACP over stdio until the client disconnects, in whichever protocol version the client
51
+ * asks for on its first message.
52
+ *
53
+ * The dialect is not chosen here and is not configurable: `acpRouter.ts` reads it off the
54
+ * `initialize` and hands the connection to the matching app. A flag would be the wrong shape —
55
+ * an editor spawns this command with no arguments, so a version it had to be told would be a
56
+ * version it never gets.
50
57
  *
51
58
  * Resolves when the connection closes, so a bin can `await` it and exit cleanly rather than
52
59
  * holding the event loop open on a socket nobody is reading.
@@ -57,8 +64,7 @@ export async function startAcpServer(options = {}) {
57
64
  // there so the guarantee is exercised on every real start rather than only under test.
58
65
  const protocolOut = output ?? captureStdoutForProtocol();
59
66
  const protocolIn = input ?? Readable.toWeb(process.stdin);
60
- const app = createAcpAgentApp(appOptions);
61
- const connection = app.connect(acp.ndJsonStream(protocolOut, protocolIn));
67
+ const connection = createAcpAgentRouter(appOptions).connect(acp.ndJsonStream(protocolOut, protocolIn));
62
68
  announceAcpStart();
63
69
  await connection.closed;
64
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"acpStdio.js","sourceRoot":"","sources":["../../../src/modules/acp/acpStdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,GAAG,MAAM,0CAA0C,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AActF;;;;;;;GAOG;AACH,SAAS,wBAAwB;IAC/B,MAAM,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,8DAA8D;IAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAU,EAAE,GAAG,IAAW,EAAE,EAAE;IACrD,8DAA8D;IAC7D,aAAqB,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAgC,CAAC;IACzE,OAAO,IAAI,cAAc,CAAa;QACpC,KAAK,CAAC,KAAK;YACT,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAO,GAAoB,EAAE;IAChE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,GAAG,OAAO,CAAC;IACjD,+FAA+F;IAC/F,uFAAuF;IACvF,MAAM,WAAW,GAAG,MAAM,IAAI,wBAAwB,EAAE,CAAC;IACzD,MAAM,UAAU,GAAG,KAAK,IAAK,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAgC,CAAC;IAE1F,MAAM,GAAG,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;IAC1E,gBAAgB,EAAE,CAAC;IACnB,MAAM,UAAU,CAAC,MAAM,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"acpStdio.js","sourceRoot":"","sources":["../../../src/modules/acp/acpStdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,GAAG,MAAM,0CAA0C,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAarE;;;;;;;GAOG;AACH,SAAS,wBAAwB;IAC/B,MAAM,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,8DAA8D;IAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAU,EAAE,GAAG,IAAW,EAAE,EAAE;IACrD,8DAA8D;IAC7D,aAAqB,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAgC,CAAC;IACzE,OAAO,IAAI,cAAc,CAAa;QACpC,KAAK,CAAC,KAAK;YACT,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAO,GAAoB,EAAE;IAChE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,GAAG,OAAO,CAAC;IACjD,+FAA+F;IAC/F,uFAAuF;IACvF,MAAM,WAAW,GAAG,MAAM,IAAI,wBAAwB,EAAE,CAAC;IACzD,MAAM,UAAU,GAAG,KAAK,IAAK,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAgC,CAAC;IAE1F,MAAM,UAAU,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC,OAAO,CACzD,GAAG,CAAC,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC,CAC1C,CAAC;IACF,gBAAgB,EAAE,CAAC;IACnB,MAAM,UAAU,CAAC,MAAM,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * Tool-call identity, shared by both ACP dialects.
4
+ *
5
+ * The two dialects disagree about how a tool call is REPORTED — v1 has a distinct `tool_call`
6
+ * update that creates one and a `tool_call_update` that patches it, v2 folds creation into the
7
+ * first `tool_call_update` — but they agree completely about what a tool call IS. The kind hint, the
8
+ * reassembly of streamed argument deltas, and the pairing of a permission request with the call it
9
+ * is about are the same problem on both surfaces, so they are solved once here.
10
+ *
11
+ * **The pairing especially.** {@link AcpToolCallTracker.claimToolCallId} is the piece that took
12
+ * three attempts to get right, and a second copy of it living in the other dialect's mapper is how
13
+ * one of them silently regresses to attaching a permission prompt to the wrong row.
14
+ */
15
+ import type { ToolKind } from '@agentclientprotocol/sdk';
16
+ /**
17
+ * The ACP tool kind for a gth tool name; `other` for anything not built in.
18
+ *
19
+ * Typed against **v1's** `ToolKind`, which is the closed union of the ten defined kinds; v2 widens
20
+ * the same union with a catch-all for future values. Typing against the stricter one is what lets a
21
+ * single function serve both — a v1 kind is always a valid v2 kind, and the reverse is not true.
22
+ */
23
+ export declare function toolKindFor(name: string): ToolKind;
24
+ /**
25
+ * Tracks the tool calls a turn has opened, so a permission request can name the one it is about.
26
+ *
27
+ * A dialect's update mapper extends this and keeps the fields fed: {@link trackToolStart} when the
28
+ * model announces a call, {@link appendToolArgs} for each argument delta, {@link trackToolSettled}
29
+ * when a result lands.
30
+ */
31
+ export declare abstract class AcpToolCallTracker {
32
+ /**
33
+ * Streamed argument text per tool call id, reassembled from `tool_args` deltas. Held rather than
34
+ * forwarded per delta because ACP has no argument-delta update: `rawInput` is a whole value, so
35
+ * it can only be sent once the deltas stop arriving.
36
+ */
37
+ protected readonly toolArgs: Map<string, string>;
38
+ /**
39
+ * Tool calls the model has requested but which have neither produced a result nor been claimed by
40
+ * a permission request, in the order they were announced, as `[toolCallId, toolName]`.
41
+ *
42
+ * Kept so the approval bridge can name the tool call a permission request is ABOUT. The gate's
43
+ * `PendingToolInterrupt` carries the tool's name and arguments but no call id — the graph
44
+ * suspends inside the middleware wrapping the call, which is downstream of where the id lives —
45
+ * while the client has already drawn that call from this stream. This queue reconnects the two.
46
+ */
47
+ protected readonly openToolCalls: Array<[string, string]>;
48
+ /** Records a newly announced call and opens its argument buffer. */
49
+ protected trackToolStart(id: string, name: string): void;
50
+ /** Accumulates one streamed argument delta. */
51
+ protected appendToolArgs(id: string, delta: string): void;
52
+ /** The reassembled arguments for a call, as a value for `rawInput`. */
53
+ protected rawInputFor(id: string): unknown;
54
+ /** Forgets a call that has produced its result. */
55
+ protected trackToolSettled(id: string): void;
56
+ /**
57
+ * Takes the id of the unclaimed tool call that matches `name` and `args`, removing it from the
58
+ * queue, or `undefined` when there is none.
59
+ *
60
+ * **Matched on the ARGUMENTS, because position is not a reliable discriminator here.** The
61
+ * arguments are the one value both sides hold: the gate's `PendingToolInterrupt` carries them, and
62
+ * this tracker still holds the streamed argument text at claim time (it is only discarded on
63
+ * `tool_result`, which exists solely on the resumed run).
64
+ *
65
+ * **Where the exact match degrades, named rather than implied:** this side is JSON reassembled
66
+ * from the model's streamed argument deltas, and a local model that ignores
67
+ * `disable_parallel_tool_use` can merge sibling calls' buffers into invalid JSON (`{}{}`,
68
+ * `{"steps":3}{}` — see the AG-UI server's `parseToolArguments` note). {@link parseToolArgs}
69
+ * deliberately does not carry that path's recovery, so such a payload stays a raw string, matches
70
+ * nothing, and falls back to position — on exactly the model class most likely to emit sloppy
71
+ * parallel calls. The fallback is still the best remaining answer and the cost is which row a
72
+ * client attaches the prompt to, never which command the human rules on.
73
+ *
74
+ * **Position was tried and is wrong in both directions**, which is why it is only the fallback.
75
+ * The runner drains suspended calls as a BATCH — every pending call decided in turn before
76
+ * anything resumes — so a model emitting two parallel calls of one tool has both open and neither
77
+ * running. Returning "the most recent" handed BOTH requests the second call's id. But plain
78
+ * oldest-first is no better in the case that is *normal* at the rated rungs: this method is
79
+ * reached only from the human-approval callback, which sits behind the gate's earlier exits
80
+ * (not-gated, deny list, bypass, the hardline floor, the allow list, and the rater's own arms), so
81
+ * a batch where one call is settled without a human and its sibling escalates leaves the settled
82
+ * call's id unclaimed at the head of the queue — and oldest-first then hands the request the
83
+ * wrong one, with every upstream ordering assumption perfectly intact.
84
+ *
85
+ * **Consuming is right either way**, and the queue order still decides between two calls whose
86
+ * arguments are genuinely identical, where either answer is equally true.
87
+ *
88
+ * Absent rather than guessed when nothing matches — a permission request pointing at the WRONG
89
+ * call is worse than one pointing at no call, because a client would then attach the answer to a
90
+ * call the user never saw.
91
+ */
92
+ claimToolCallId(name: string, args?: Record<string, unknown>): string | undefined;
93
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * Tool-call identity, shared by both ACP dialects.
4
+ *
5
+ * The two dialects disagree about how a tool call is REPORTED — v1 has a distinct `tool_call`
6
+ * update that creates one and a `tool_call_update` that patches it, v2 folds creation into the
7
+ * first `tool_call_update` — but they agree completely about what a tool call IS. The kind hint, the
8
+ * reassembly of streamed argument deltas, and the pairing of a permission request with the call it
9
+ * is about are the same problem on both surfaces, so they are solved once here.
10
+ *
11
+ * **The pairing especially.** {@link AcpToolCallTracker.claimToolCallId} is the piece that took
12
+ * three attempts to get right, and a second copy of it living in the other dialect's mapper is how
13
+ * one of them silently regresses to attaching a permission prompt to the wrong row.
14
+ */
15
+ /**
16
+ * ACP tool KIND for a gth tool name — the hint a client uses to pick an icon and a UI treatment.
17
+ *
18
+ * Matched by exact name, with `other` as the fallback, because a wrong kind is worse than a
19
+ * generic one: a client that shows a delete glyph for a read is actively misleading about what the
20
+ * agent is doing. Unknown names (custom tools, MCP tools) therefore land on `other` rather than
21
+ * being guessed at from substrings.
22
+ */
23
+ const TOOL_KINDS = {
24
+ create_directory: 'edit',
25
+ delete_directory: 'delete',
26
+ delete_file: 'delete',
27
+ directory_tree: 'read',
28
+ edit_file: 'edit',
29
+ get_file_info: 'read',
30
+ gth_read_binary: 'read',
31
+ gth_status_update: 'think',
32
+ gth_web_fetch: 'fetch',
33
+ list_allowed_directories: 'read',
34
+ list_directory: 'read',
35
+ list_directory_with_sizes: 'read',
36
+ move_file: 'move',
37
+ read_file: 'read',
38
+ read_multiple_files: 'read',
39
+ run_build: 'execute',
40
+ run_lint: 'execute',
41
+ run_shell_command: 'execute',
42
+ run_single_test: 'execute',
43
+ run_tests: 'execute',
44
+ search_files: 'search',
45
+ write_file: 'edit',
46
+ };
47
+ /**
48
+ * The ACP tool kind for a gth tool name; `other` for anything not built in.
49
+ *
50
+ * Typed against **v1's** `ToolKind`, which is the closed union of the ten defined kinds; v2 widens
51
+ * the same union with a catch-all for future values. Typing against the stricter one is what lets a
52
+ * single function serve both — a v1 kind is always a valid v2 kind, and the reverse is not true.
53
+ */
54
+ export function toolKindFor(name) {
55
+ return TOOL_KINDS[name] ?? 'other';
56
+ }
57
+ /**
58
+ * Tracks the tool calls a turn has opened, so a permission request can name the one it is about.
59
+ *
60
+ * A dialect's update mapper extends this and keeps the fields fed: {@link trackToolStart} when the
61
+ * model announces a call, {@link appendToolArgs} for each argument delta, {@link trackToolSettled}
62
+ * when a result lands.
63
+ */
64
+ export class AcpToolCallTracker {
65
+ /**
66
+ * Streamed argument text per tool call id, reassembled from `tool_args` deltas. Held rather than
67
+ * forwarded per delta because ACP has no argument-delta update: `rawInput` is a whole value, so
68
+ * it can only be sent once the deltas stop arriving.
69
+ */
70
+ toolArgs = new Map();
71
+ /**
72
+ * Tool calls the model has requested but which have neither produced a result nor been claimed by
73
+ * a permission request, in the order they were announced, as `[toolCallId, toolName]`.
74
+ *
75
+ * Kept so the approval bridge can name the tool call a permission request is ABOUT. The gate's
76
+ * `PendingToolInterrupt` carries the tool's name and arguments but no call id — the graph
77
+ * suspends inside the middleware wrapping the call, which is downstream of where the id lives —
78
+ * while the client has already drawn that call from this stream. This queue reconnects the two.
79
+ */
80
+ openToolCalls = [];
81
+ /** Records a newly announced call and opens its argument buffer. */
82
+ trackToolStart(id, name) {
83
+ this.toolArgs.set(id, '');
84
+ this.openToolCalls.push([id, name]);
85
+ }
86
+ /** Accumulates one streamed argument delta. */
87
+ appendToolArgs(id, delta) {
88
+ this.toolArgs.set(id, (this.toolArgs.get(id) ?? '') + delta);
89
+ }
90
+ /** The reassembled arguments for a call, as a value for `rawInput`. */
91
+ rawInputFor(id) {
92
+ return parseToolArgs(this.toolArgs.get(id));
93
+ }
94
+ /** Forgets a call that has produced its result. */
95
+ trackToolSettled(id) {
96
+ this.toolArgs.delete(id);
97
+ // A call a permission request already claimed is no longer in the queue; that is expected,
98
+ // not a miss.
99
+ const open = this.openToolCalls.findIndex(([openId]) => openId === id);
100
+ if (open >= 0)
101
+ this.openToolCalls.splice(open, 1);
102
+ }
103
+ /**
104
+ * Takes the id of the unclaimed tool call that matches `name` and `args`, removing it from the
105
+ * queue, or `undefined` when there is none.
106
+ *
107
+ * **Matched on the ARGUMENTS, because position is not a reliable discriminator here.** The
108
+ * arguments are the one value both sides hold: the gate's `PendingToolInterrupt` carries them, and
109
+ * this tracker still holds the streamed argument text at claim time (it is only discarded on
110
+ * `tool_result`, which exists solely on the resumed run).
111
+ *
112
+ * **Where the exact match degrades, named rather than implied:** this side is JSON reassembled
113
+ * from the model's streamed argument deltas, and a local model that ignores
114
+ * `disable_parallel_tool_use` can merge sibling calls' buffers into invalid JSON (`{}{}`,
115
+ * `{"steps":3}{}` — see the AG-UI server's `parseToolArguments` note). {@link parseToolArgs}
116
+ * deliberately does not carry that path's recovery, so such a payload stays a raw string, matches
117
+ * nothing, and falls back to position — on exactly the model class most likely to emit sloppy
118
+ * parallel calls. The fallback is still the best remaining answer and the cost is which row a
119
+ * client attaches the prompt to, never which command the human rules on.
120
+ *
121
+ * **Position was tried and is wrong in both directions**, which is why it is only the fallback.
122
+ * The runner drains suspended calls as a BATCH — every pending call decided in turn before
123
+ * anything resumes — so a model emitting two parallel calls of one tool has both open and neither
124
+ * running. Returning "the most recent" handed BOTH requests the second call's id. But plain
125
+ * oldest-first is no better in the case that is *normal* at the rated rungs: this method is
126
+ * reached only from the human-approval callback, which sits behind the gate's earlier exits
127
+ * (not-gated, deny list, bypass, the hardline floor, the allow list, and the rater's own arms), so
128
+ * a batch where one call is settled without a human and its sibling escalates leaves the settled
129
+ * call's id unclaimed at the head of the queue — and oldest-first then hands the request the
130
+ * wrong one, with every upstream ordering assumption perfectly intact.
131
+ *
132
+ * **Consuming is right either way**, and the queue order still decides between two calls whose
133
+ * arguments are genuinely identical, where either answer is equally true.
134
+ *
135
+ * Absent rather than guessed when nothing matches — a permission request pointing at the WRONG
136
+ * call is worse than one pointing at no call, because a client would then attach the answer to a
137
+ * call the user never saw.
138
+ */
139
+ claimToolCallId(name, args) {
140
+ const candidates = this.openToolCalls
141
+ .map(([id, toolName], index) => ({ id, toolName, index }))
142
+ .filter((candidate) => candidate.toolName === name);
143
+ if (candidates.length === 0)
144
+ return undefined;
145
+ const wanted = args === undefined ? undefined : canonicalJson(args);
146
+ const matched = wanted === undefined
147
+ ? undefined
148
+ : candidates.find((candidate) => canonicalJson(parseToolArgs(this.toolArgs.get(candidate.id))) === wanted);
149
+ const chosen = matched ?? candidates[0];
150
+ this.openToolCalls.splice(chosen.index, 1);
151
+ return chosen.id;
152
+ }
153
+ }
154
+ /**
155
+ * A value serialized with object keys in sorted order, so two arguments objects that differ only in
156
+ * key order compare equal.
157
+ *
158
+ * They routinely do: one side is JSON reassembled from a model's streamed argument deltas and the
159
+ * other is the object the graph handed the gate. Comparing raw `JSON.stringify` output would make
160
+ * the exact match in {@link AcpToolCallTracker.claimToolCallId} fail for a reason that has nothing
161
+ * to do with whether the two describe the same call.
162
+ */
163
+ function canonicalJson(value) {
164
+ if (value === null || typeof value !== 'object')
165
+ return JSON.stringify(value) ?? 'undefined';
166
+ if (Array.isArray(value))
167
+ return `[${value.map(canonicalJson).join(',')}]`;
168
+ const entries = Object.keys(value)
169
+ .sort()
170
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
171
+ return `{${entries.join(',')}}`;
172
+ }
173
+ /**
174
+ * The reassembled tool arguments as a value for `rawInput`, or `undefined` when there is nothing
175
+ * worth sending.
176
+ *
177
+ * Deliberately fail-soft: a model that emits malformed argument JSON is a real and recurring
178
+ * condition (see the AG-UI server's `parseToolArguments` note), and it must not take down the turn
179
+ * that reports it. An unparseable payload is passed through as the raw string so the client can
180
+ * still show what the model asked for.
181
+ */
182
+ function parseToolArgs(raw) {
183
+ const text = (raw ?? '').trim();
184
+ if (text.length === 0)
185
+ return undefined;
186
+ try {
187
+ return JSON.parse(text);
188
+ }
189
+ catch {
190
+ return text;
191
+ }
192
+ }
193
+ //# sourceMappingURL=acpToolCalls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acpToolCalls.js","sourceRoot":"","sources":["../../../src/modules/acp/acpToolCalls.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * @packageDocumentation
3
- * Translates the agent runtime's typed {@link AgentStreamEvent} stream into ACP v2
4
- * `session/update` payloads.
3
+ * Translates the agent runtime's typed {@link AgentStreamEvent} stream into ACP **v2**
4
+ * `session/update` payloads. (`acpUpdatesV1.ts` is the v1 half; both extend the tool-call tracker
5
+ * in `acpToolCalls.ts`, which is where the parts that are not about the dialect live.)
5
6
  *
6
7
  * Kept as a pure, stateful-but-transport-free mapper rather than inlined in the request handlers
7
8
  * for two reasons. It is the half of the ACP surface with real logic — message identity, tool-call
@@ -19,18 +20,17 @@
19
20
  * carries only what actually changed — which is what makes a client's rendering of a running tool
20
21
  * call correct rather than flickering back to a bare id.
21
22
  */
22
- import type { SessionUpdate, ToolKind } from '@agentclientprotocol/sdk/experimental/v2';
23
+ import type { SessionUpdate } from '@agentclientprotocol/sdk/experimental/v2';
23
24
  import type { AgentStreamEvent } from '@gaunt-sloth/core/core/types.js';
24
- /** The ACP tool kind for a gth tool name; `other` for anything not built in. */
25
- export declare function toolKindFor(name: string): ToolKind;
25
+ import { AcpToolCallTracker } from '#src/modules/acp/acpToolCalls.js';
26
26
  /**
27
- * Turns one agent run's event stream into ACP `session/update` payloads.
27
+ * Turns one agent run's event stream into ACP v2 `session/update` payloads.
28
28
  *
29
29
  * One instance per prompt turn: it holds the message identity of the assistant text run and of the
30
30
  * reasoning run, plus the accumulated argument text per tool call. A fresh instance per turn is
31
31
  * what makes a new turn a new `messageId`, which is how a client tells two messages apart.
32
32
  */
33
- export declare class AcpUpdateMapper {
33
+ export declare class AcpUpdateMapper extends AcpToolCallTracker {
34
34
  /**
35
35
  * `messageId` of the assistant text message currently being streamed, or `null` when no text run
36
36
  * is open. Cleared whenever something else interrupts the text (a tool call, a reasoning block),
@@ -40,59 +40,6 @@ export declare class AcpUpdateMapper {
40
40
  private assistantMessageId;
41
41
  /** `messageId` of the reasoning message currently being streamed, or `null` outside one. */
42
42
  private thoughtMessageId;
43
- /**
44
- * Streamed argument text per tool call id, reassembled from `tool_args` deltas. Held rather than
45
- * forwarded per delta because ACP has no argument-delta update: `rawInput` is a whole value, so
46
- * it can only be sent once the deltas stop arriving.
47
- */
48
- private readonly toolArgs;
49
- /**
50
- * Tool calls the model has requested but which have neither produced a result nor been claimed by
51
- * a permission request, in the order they were announced, as `[toolCallId, toolName]`.
52
- *
53
- * Kept so the approval bridge can name the tool call a permission request is ABOUT. The gate's
54
- * `PendingToolInterrupt` carries the tool's name and arguments but no call id — the graph
55
- * suspends inside the middleware wrapping the call, which is downstream of where the id lives —
56
- * while the client has already drawn that call from this stream. This queue reconnects the two.
57
- */
58
- private readonly openToolCalls;
59
- /**
60
- * Takes the id of the unclaimed tool call that matches `name` and `args`, removing it from the
61
- * queue, or `undefined` when there is none.
62
- *
63
- * **Matched on the ARGUMENTS, because position is not a reliable discriminator here.** The
64
- * arguments are the one value both sides hold: the gate's `PendingToolInterrupt` carries them, and
65
- * this mapper still holds the streamed argument text at claim time (it is only discarded on
66
- * `tool_result`, which exists solely on the resumed run).
67
- *
68
- * **Where the exact match degrades, named rather than implied:** the mapper's side is JSON
69
- * reassembled from the model's streamed argument deltas, and a local model that ignores
70
- * `disable_parallel_tool_use` can merge sibling calls' buffers into invalid JSON (`{}{}`,
71
- * `{"steps":3}{}` — see the AG-UI server's `parseToolArguments` note). {@link parseToolArgs}
72
- * deliberately does not carry that path's recovery, so such a payload stays a raw string, matches
73
- * nothing, and falls back to position — on exactly the model class most likely to emit sloppy
74
- * parallel calls. The fallback is still the best remaining answer and the cost is which row a
75
- * client attaches the prompt to, never which command the human rules on.
76
- *
77
- * **Position was tried and is wrong in both directions**, which is why it is only the fallback.
78
- * The runner drains suspended calls as a BATCH — every pending call decided in turn before
79
- * anything resumes — so a model emitting two parallel calls of one tool has both open and neither
80
- * running. Returning "the most recent" handed BOTH requests the second call's id. But plain
81
- * oldest-first is no better in the case that is *normal* at the rated rungs: this method is
82
- * reached only from the human-approval callback, which sits behind the gate's earlier exits
83
- * (not-gated, deny list, bypass, the hardline floor, the allow list, and the rater's own arms), so
84
- * a batch where one call is settled without a human and its sibling escalates leaves the settled
85
- * call's id unclaimed at the head of the queue — and oldest-first then hands the request the
86
- * wrong one, with every upstream ordering assumption perfectly intact.
87
- *
88
- * **Consuming is right either way**, and the queue order still decides between two calls whose
89
- * arguments are genuinely identical, where either answer is equally true.
90
- *
91
- * Absent rather than guessed when nothing matches — a permission request pointing at the WRONG
92
- * call is worse than one pointing at no call, because a client would then attach the answer to a
93
- * call the user never saw.
94
- */
95
- claimToolCallId(name: string, args?: Record<string, unknown>): string | undefined;
96
43
  /**
97
44
  * The `session/update` payloads one runtime event produces — usually one, sometimes none
98
45
  * (`tool_args`, which only accumulates; the reasoning boundaries, which only move state).