@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.
- package/README.md +6 -5
- package/cli-acp.js +5 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/acp/acpAgentApp.d.ts +16 -52
- package/dist/modules/acp/acpAgentApp.js +31 -246
- package/dist/modules/acp/acpAgentApp.js.map +1 -1
- package/dist/modules/acp/acpAgentAppV1.d.ts +52 -0
- package/dist/modules/acp/acpAgentAppV1.js +307 -0
- package/dist/modules/acp/acpAgentAppV1.js.map +1 -0
- package/dist/modules/acp/acpCommon.d.ts +167 -0
- package/dist/modules/acp/acpCommon.js +282 -0
- package/dist/modules/acp/acpCommon.js.map +1 -0
- package/dist/modules/acp/acpPermissions.d.ts +6 -1
- package/dist/modules/acp/acpPermissions.js +5 -1
- package/dist/modules/acp/acpPermissions.js.map +1 -1
- package/dist/modules/acp/acpPermissionsV1.d.ts +45 -0
- package/dist/modules/acp/acpPermissionsV1.js +110 -0
- package/dist/modules/acp/acpPermissionsV1.js.map +1 -0
- package/dist/modules/acp/acpRouter.d.ts +41 -0
- package/dist/modules/acp/acpRouter.js +48 -0
- package/dist/modules/acp/acpRouter.js.map +1 -0
- package/dist/modules/acp/acpStdio.d.ts +9 -3
- package/dist/modules/acp/acpStdio.js +11 -5
- package/dist/modules/acp/acpStdio.js.map +1 -1
- package/dist/modules/acp/acpToolCalls.d.ts +93 -0
- package/dist/modules/acp/acpToolCalls.js +193 -0
- package/dist/modules/acp/acpToolCalls.js.map +1 -0
- package/dist/modules/acp/acpUpdates.d.ts +7 -60
- package/dist/modules/acp/acpUpdates.js +10 -155
- package/dist/modules/acp/acpUpdates.js.map +1 -1
- package/dist/modules/acp/acpUpdatesV1.d.ts +61 -0
- package/dist/modules/acp/acpUpdatesV1.js +162 -0
- package/dist/modules/acp/acpUpdatesV1.js.map +1 -0
- package/dist/modules/interactiveSessionModule.js +90 -94
- package/dist/modules/interactiveSessionModule.js.map +1 -1
- 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
|
|
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
|
|
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
|
|
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,
|
|
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":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;;;;GAOG;AACH,MAAM,UAAU,GAAuC;IACrD,gBAAgB,EAAE,MAAM;IACxB,gBAAgB,EAAE,QAAQ;IAC1B,WAAW,EAAE,QAAQ;IACrB,cAAc,EAAE,MAAM;IACtB,SAAS,EAAE,MAAM;IACjB,aAAa,EAAE,MAAM;IACrB,eAAe,EAAE,MAAM;IACvB,iBAAiB,EAAE,OAAO;IAC1B,aAAa,EAAE,OAAO;IACtB,wBAAwB,EAAE,MAAM;IAChC,cAAc,EAAE,MAAM;IACtB,yBAAyB,EAAE,MAAM;IACjC,SAAS,EAAE,MAAM;IACjB,SAAS,EAAE,MAAM;IACjB,mBAAmB,EAAE,MAAM;IAC3B,SAAS,EAAE,SAAS;IACpB,QAAQ,EAAE,SAAS;IACnB,iBAAiB,EAAE,SAAS;IAC5B,eAAe,EAAE,SAAS;IAC1B,SAAS,EAAE,SAAS;IACpB,YAAY,EAAE,QAAQ;IACtB,UAAU,EAAE,MAAM;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAgB,kBAAkB;IACtC;;;;OAIG;IACgB,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAExD;;;;;;;;OAQG;IACgB,aAAa,GAA4B,EAAE,CAAC;IAE/D,oEAAoE;IAC1D,cAAc,CAAC,EAAU,EAAE,IAAY;QAC/C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;IACtC,CAAC;IAED,+CAA+C;IACrC,cAAc,CAAC,EAAU,EAAE,KAAa;QAChD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC;IAC/D,CAAC;IAED,uEAAuE;IAC7D,WAAW,CAAC,EAAU;QAC9B,OAAO,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,mDAAmD;IACzC,gBAAgB,CAAC,EAAU;QACnC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACzB,2FAA2F;QAC3F,cAAc;QACd,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC;QACvE,IAAI,IAAI,IAAI,CAAC;YAAE,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACpD,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,eAAe,CAAC,IAAY,EAAE,IAA8B;QAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa;aAClC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;aACzD,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;QACtD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACpE,MAAM,OAAO,GACX,MAAM,KAAK,SAAS;YAClB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,UAAU,CAAC,IAAI,CACb,CAAC,SAAS,EAAE,EAAE,CAAC,aAAa,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,MAAM,CACxF,CAAC;QACR,MAAM,MAAM,GAAG,OAAO,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;QACxC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAC3C,OAAO,MAAM,CAAC,EAAE,CAAC;IACnB,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC;IAC7F,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAC3E,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAgC,CAAC;SAC1D,IAAI,EAAE;SACN,GAAG,CACF,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,aAAa,CAAE,KAAiC,CAAC,GAAG,CAAC,CAAC,EAAE,CAC5F,CAAC;IACJ,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClC,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,aAAa,CAAC,GAAuB;IAC5C,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -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
|
|
23
|
+
import type { SessionUpdate } from '@agentclientprotocol/sdk/experimental/v2';
|
|
23
24
|
import type { AgentStreamEvent } from '@gaunt-sloth/core/core/types.js';
|
|
24
|
-
|
|
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).
|