@gaunt-sloth/agent 2.0.0-alpha.37 → 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/builtInToolsConfig.js +7 -4
- package/dist/builtInToolsConfig.js.map +1 -1
- 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 +101 -97
- package/dist/modules/interactiveSessionModule.js.map +1 -1
- package/dist/modules/slashCommands.d.ts +8 -2
- package/dist/modules/slashCommands.js +8 -2
- package/dist/modules/slashCommands.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
@@ -20,54 +21,19 @@
|
|
|
20
21
|
* call correct rather than flickering back to a bare id.
|
|
21
22
|
*/
|
|
22
23
|
import { randomUUID } from 'node:crypto';
|
|
23
|
-
|
|
24
|
-
* ACP tool KIND for a gth tool name — the hint a client uses to pick an icon and a UI treatment.
|
|
25
|
-
*
|
|
26
|
-
* Matched by exact name, with `other` as the fallback, because a wrong kind is worse than a
|
|
27
|
-
* generic one: a client that shows a delete glyph for a read is actively misleading about what the
|
|
28
|
-
* agent is doing. Unknown names (custom tools, MCP tools) therefore land on `other` rather than
|
|
29
|
-
* being guessed at from substrings.
|
|
30
|
-
*/
|
|
31
|
-
const TOOL_KINDS = {
|
|
32
|
-
create_directory: 'edit',
|
|
33
|
-
delete_directory: 'delete',
|
|
34
|
-
delete_file: 'delete',
|
|
35
|
-
directory_tree: 'read',
|
|
36
|
-
edit_file: 'edit',
|
|
37
|
-
get_file_info: 'read',
|
|
38
|
-
gth_read_binary: 'read',
|
|
39
|
-
gth_status_update: 'think',
|
|
40
|
-
gth_web_fetch: 'fetch',
|
|
41
|
-
list_allowed_directories: 'read',
|
|
42
|
-
list_directory: 'read',
|
|
43
|
-
list_directory_with_sizes: 'read',
|
|
44
|
-
move_file: 'move',
|
|
45
|
-
read_file: 'read',
|
|
46
|
-
read_multiple_files: 'read',
|
|
47
|
-
run_build: 'execute',
|
|
48
|
-
run_lint: 'execute',
|
|
49
|
-
run_shell_command: 'execute',
|
|
50
|
-
run_single_test: 'execute',
|
|
51
|
-
run_tests: 'execute',
|
|
52
|
-
search_files: 'search',
|
|
53
|
-
write_file: 'edit',
|
|
54
|
-
};
|
|
55
|
-
/** The ACP tool kind for a gth tool name; `other` for anything not built in. */
|
|
56
|
-
export function toolKindFor(name) {
|
|
57
|
-
return TOOL_KINDS[name] ?? 'other';
|
|
58
|
-
}
|
|
24
|
+
import { AcpToolCallTracker, toolKindFor } from '#src/modules/acp/acpToolCalls.js';
|
|
59
25
|
/** One text content block, the shape both message chunks and tool content wrap. */
|
|
60
26
|
function textBlock(text) {
|
|
61
27
|
return { type: 'text', text };
|
|
62
28
|
}
|
|
63
29
|
/**
|
|
64
|
-
* Turns one agent run's event stream into ACP `session/update` payloads.
|
|
30
|
+
* Turns one agent run's event stream into ACP v2 `session/update` payloads.
|
|
65
31
|
*
|
|
66
32
|
* One instance per prompt turn: it holds the message identity of the assistant text run and of the
|
|
67
33
|
* reasoning run, plus the accumulated argument text per tool call. A fresh instance per turn is
|
|
68
34
|
* what makes a new turn a new `messageId`, which is how a client tells two messages apart.
|
|
69
35
|
*/
|
|
70
|
-
export class AcpUpdateMapper {
|
|
36
|
+
export class AcpUpdateMapper extends AcpToolCallTracker {
|
|
71
37
|
/**
|
|
72
38
|
* `messageId` of the assistant text message currently being streamed, or `null` when no text run
|
|
73
39
|
* is open. Cleared whenever something else interrupts the text (a tool call, a reasoning block),
|
|
@@ -77,72 +43,6 @@ export class AcpUpdateMapper {
|
|
|
77
43
|
assistantMessageId = null;
|
|
78
44
|
/** `messageId` of the reasoning message currently being streamed, or `null` outside one. */
|
|
79
45
|
thoughtMessageId = null;
|
|
80
|
-
/**
|
|
81
|
-
* Streamed argument text per tool call id, reassembled from `tool_args` deltas. Held rather than
|
|
82
|
-
* forwarded per delta because ACP has no argument-delta update: `rawInput` is a whole value, so
|
|
83
|
-
* it can only be sent once the deltas stop arriving.
|
|
84
|
-
*/
|
|
85
|
-
toolArgs = new Map();
|
|
86
|
-
/**
|
|
87
|
-
* Tool calls the model has requested but which have neither produced a result nor been claimed by
|
|
88
|
-
* a permission request, in the order they were announced, as `[toolCallId, toolName]`.
|
|
89
|
-
*
|
|
90
|
-
* Kept so the approval bridge can name the tool call a permission request is ABOUT. The gate's
|
|
91
|
-
* `PendingToolInterrupt` carries the tool's name and arguments but no call id — the graph
|
|
92
|
-
* suspends inside the middleware wrapping the call, which is downstream of where the id lives —
|
|
93
|
-
* while the client has already drawn that call from this stream. This queue reconnects the two.
|
|
94
|
-
*/
|
|
95
|
-
openToolCalls = [];
|
|
96
|
-
/**
|
|
97
|
-
* Takes the id of the unclaimed tool call that matches `name` and `args`, removing it from the
|
|
98
|
-
* queue, or `undefined` when there is none.
|
|
99
|
-
*
|
|
100
|
-
* **Matched on the ARGUMENTS, because position is not a reliable discriminator here.** The
|
|
101
|
-
* arguments are the one value both sides hold: the gate's `PendingToolInterrupt` carries them, and
|
|
102
|
-
* this mapper still holds the streamed argument text at claim time (it is only discarded on
|
|
103
|
-
* `tool_result`, which exists solely on the resumed run).
|
|
104
|
-
*
|
|
105
|
-
* **Where the exact match degrades, named rather than implied:** the mapper's side is JSON
|
|
106
|
-
* reassembled from the model's streamed argument deltas, and a local model that ignores
|
|
107
|
-
* `disable_parallel_tool_use` can merge sibling calls' buffers into invalid JSON (`{}{}`,
|
|
108
|
-
* `{"steps":3}{}` — see the AG-UI server's `parseToolArguments` note). {@link parseToolArgs}
|
|
109
|
-
* deliberately does not carry that path's recovery, so such a payload stays a raw string, matches
|
|
110
|
-
* nothing, and falls back to position — on exactly the model class most likely to emit sloppy
|
|
111
|
-
* parallel calls. The fallback is still the best remaining answer and the cost is which row a
|
|
112
|
-
* client attaches the prompt to, never which command the human rules on.
|
|
113
|
-
*
|
|
114
|
-
* **Position was tried and is wrong in both directions**, which is why it is only the fallback.
|
|
115
|
-
* The runner drains suspended calls as a BATCH — every pending call decided in turn before
|
|
116
|
-
* anything resumes — so a model emitting two parallel calls of one tool has both open and neither
|
|
117
|
-
* running. Returning "the most recent" handed BOTH requests the second call's id. But plain
|
|
118
|
-
* oldest-first is no better in the case that is *normal* at the rated rungs: this method is
|
|
119
|
-
* reached only from the human-approval callback, which sits behind the gate's earlier exits
|
|
120
|
-
* (not-gated, deny list, bypass, the hardline floor, the allow list, and the rater's own arms), so
|
|
121
|
-
* a batch where one call is settled without a human and its sibling escalates leaves the settled
|
|
122
|
-
* call's id unclaimed at the head of the queue — and oldest-first then hands the request the
|
|
123
|
-
* wrong one, with every upstream ordering assumption perfectly intact.
|
|
124
|
-
*
|
|
125
|
-
* **Consuming is right either way**, and the queue order still decides between two calls whose
|
|
126
|
-
* arguments are genuinely identical, where either answer is equally true.
|
|
127
|
-
*
|
|
128
|
-
* Absent rather than guessed when nothing matches — a permission request pointing at the WRONG
|
|
129
|
-
* call is worse than one pointing at no call, because a client would then attach the answer to a
|
|
130
|
-
* call the user never saw.
|
|
131
|
-
*/
|
|
132
|
-
claimToolCallId(name, args) {
|
|
133
|
-
const candidates = this.openToolCalls
|
|
134
|
-
.map(([id, toolName], index) => ({ id, toolName, index }))
|
|
135
|
-
.filter((candidate) => candidate.toolName === name);
|
|
136
|
-
if (candidates.length === 0)
|
|
137
|
-
return undefined;
|
|
138
|
-
const wanted = args === undefined ? undefined : canonicalJson(args);
|
|
139
|
-
const matched = wanted === undefined
|
|
140
|
-
? undefined
|
|
141
|
-
: candidates.find((candidate) => canonicalJson(parseToolArgs(this.toolArgs.get(candidate.id))) === wanted);
|
|
142
|
-
const chosen = matched ?? candidates[0];
|
|
143
|
-
this.openToolCalls.splice(chosen.index, 1);
|
|
144
|
-
return chosen.id;
|
|
145
|
-
}
|
|
146
46
|
/**
|
|
147
47
|
* The `session/update` payloads one runtime event produces — usually one, sometimes none
|
|
148
48
|
* (`tool_args`, which only accumulates; the reasoning boundaries, which only move state).
|
|
@@ -181,8 +81,7 @@ export class AcpUpdateMapper {
|
|
|
181
81
|
case 'tool_start': {
|
|
182
82
|
// A tool call ends the open text run: the text that follows the tool is a separate message.
|
|
183
83
|
this.assistantMessageId = null;
|
|
184
|
-
this.
|
|
185
|
-
this.openToolCalls.push([event.id, event.name]);
|
|
84
|
+
this.trackToolStart(event.id, event.name);
|
|
186
85
|
// The CREATING update — the first one a client sees for this id. Everything descriptive is
|
|
187
86
|
// sent here and never resent, because from here on omission means "unchanged".
|
|
188
87
|
return [
|
|
@@ -197,11 +96,11 @@ export class AcpUpdateMapper {
|
|
|
197
96
|
];
|
|
198
97
|
}
|
|
199
98
|
case 'tool_args': {
|
|
200
|
-
this.
|
|
99
|
+
this.appendToolArgs(event.id, event.delta);
|
|
201
100
|
return [];
|
|
202
101
|
}
|
|
203
102
|
case 'tool_end': {
|
|
204
|
-
const rawInput =
|
|
103
|
+
const rawInput = this.rawInputFor(event.id);
|
|
205
104
|
// Status only (plus the arguments, once they are complete). No title, no kind, no name —
|
|
206
105
|
// the client keeps the ones the creating update set.
|
|
207
106
|
return [
|
|
@@ -227,12 +126,7 @@ export class AcpUpdateMapper {
|
|
|
227
126
|
];
|
|
228
127
|
}
|
|
229
128
|
case 'tool_result': {
|
|
230
|
-
this.
|
|
231
|
-
// A call a permission request already claimed is no longer in the queue; that is expected,
|
|
232
|
-
// not a miss.
|
|
233
|
-
const open = this.openToolCalls.findIndex(([id]) => id === event.id);
|
|
234
|
-
if (open >= 0)
|
|
235
|
-
this.openToolCalls.splice(open, 1);
|
|
129
|
+
this.trackToolSettled(event.id);
|
|
236
130
|
return [
|
|
237
131
|
{
|
|
238
132
|
sessionUpdate: 'tool_call_update',
|
|
@@ -245,43 +139,4 @@ export class AcpUpdateMapper {
|
|
|
245
139
|
}
|
|
246
140
|
}
|
|
247
141
|
}
|
|
248
|
-
/**
|
|
249
|
-
* The reassembled tool arguments as a value for `rawInput`, or `undefined` when there is nothing
|
|
250
|
-
* worth sending.
|
|
251
|
-
*
|
|
252
|
-
* Deliberately fail-soft: a model that emits malformed argument JSON is a real and recurring
|
|
253
|
-
* condition (see the AG-UI server's `parseToolArguments` note), and it must not take down the turn
|
|
254
|
-
* that reports it. An unparseable payload is passed through as the raw string so the client can
|
|
255
|
-
* still show what the model asked for.
|
|
256
|
-
*/
|
|
257
|
-
/**
|
|
258
|
-
* A value serialized with object keys in sorted order, so two arguments objects that differ only in
|
|
259
|
-
* key order compare equal.
|
|
260
|
-
*
|
|
261
|
-
* They routinely do: one side is JSON reassembled from a model's streamed argument deltas and the
|
|
262
|
-
* other is the object the graph handed the gate. Comparing raw `JSON.stringify` output would make
|
|
263
|
-
* the exact match in {@link AcpUpdateMapper.claimToolCallId} fail for a reason that has nothing to
|
|
264
|
-
* do with whether the two describe the same call.
|
|
265
|
-
*/
|
|
266
|
-
function canonicalJson(value) {
|
|
267
|
-
if (value === null || typeof value !== 'object')
|
|
268
|
-
return JSON.stringify(value) ?? 'undefined';
|
|
269
|
-
if (Array.isArray(value))
|
|
270
|
-
return `[${value.map(canonicalJson).join(',')}]`;
|
|
271
|
-
const entries = Object.keys(value)
|
|
272
|
-
.sort()
|
|
273
|
-
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
|
|
274
|
-
return `{${entries.join(',')}}`;
|
|
275
|
-
}
|
|
276
|
-
function parseToolArgs(raw) {
|
|
277
|
-
const text = (raw ?? '').trim();
|
|
278
|
-
if (text.length === 0)
|
|
279
|
-
return undefined;
|
|
280
|
-
try {
|
|
281
|
-
return JSON.parse(text);
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
return text;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
142
|
//# sourceMappingURL=acpUpdates.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acpUpdates.js","sourceRoot":"","sources":["../../../src/modules/acp/acpUpdates.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"acpUpdates.js","sourceRoot":"","sources":["../../../src/modules/acp/acpUpdates.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Translates the agent runtime's typed {@link AgentStreamEvent} stream into ACP **v1**
|
|
4
|
+
* `session/update` payloads. (`acpUpdates.ts` is the v2 half.)
|
|
5
|
+
*
|
|
6
|
+
* ## Where v1 differs, and why the mapper could not be shared
|
|
7
|
+
*
|
|
8
|
+
* The two dialects report the same events, but not with the same messages, and each difference
|
|
9
|
+
* changes what a conforming client ends up rendering:
|
|
10
|
+
*
|
|
11
|
+
* - **v1 has a distinct `tool_call` update that CREATES a tool call**, where v2 folded creation into
|
|
12
|
+
* the first `tool_call_update`. Sending only `tool_call_update`s on v1 would leave a client
|
|
13
|
+
* patching a call it was never told about.
|
|
14
|
+
* - **v1 has no `tool_call_content_chunk`.** Its `tool_call_update.content` REPLACES the whole
|
|
15
|
+
* collection, so live tool output is streamed by accumulating it here and resending the
|
|
16
|
+
* collection — the opposite of v2, where each chunk appends and only a `tool_call_update` replaces.
|
|
17
|
+
* - **v1 has no `state_update`.** There is nowhere to report `running` / `requires_action` / `idle`,
|
|
18
|
+
* and no notification carries the stop reason: the turn's outcome is the `session/prompt`
|
|
19
|
+
* RESPONSE. See `acpAgentAppV1.ts`.
|
|
20
|
+
* - **A prompt turn does not echo the user's message.** v1 reserves replay for `session/load`; a
|
|
21
|
+
* client already renders what it sent, so echoing it would draw the message twice.
|
|
22
|
+
*
|
|
23
|
+
* What is NOT duplicated is tool-call identity — the kind hint, the argument reassembly, and the
|
|
24
|
+
* pairing that lets a permission request name the call it is about all come from
|
|
25
|
+
* {@link AcpToolCallTracker}.
|
|
26
|
+
*/
|
|
27
|
+
import type { SessionUpdate } from '@agentclientprotocol/sdk';
|
|
28
|
+
import type { AgentStreamEvent } from '@gaunt-sloth/core/core/types.js';
|
|
29
|
+
import { AcpToolCallTracker } from '#src/modules/acp/acpToolCalls.js';
|
|
30
|
+
/**
|
|
31
|
+
* Turns one agent run's event stream into ACP v1 `session/update` payloads.
|
|
32
|
+
*
|
|
33
|
+
* One instance per prompt turn: it holds the message identity of the assistant text run and of the
|
|
34
|
+
* reasoning run, the accumulated argument text per tool call, and the output accumulated for each
|
|
35
|
+
* running tool. A fresh instance per turn is what makes a new turn a new `messageId`, which is how
|
|
36
|
+
* a client tells two messages apart.
|
|
37
|
+
*/
|
|
38
|
+
export declare class AcpV1UpdateMapper extends AcpToolCallTracker {
|
|
39
|
+
/**
|
|
40
|
+
* `messageId` of the assistant text message currently being streamed, or `null` when no text run
|
|
41
|
+
* is open. Cleared whenever something else interrupts the text (a tool call, a reasoning block),
|
|
42
|
+
* so the text that resumes afterwards is a NEW message rather than an append to the one the
|
|
43
|
+
* client already considers finished.
|
|
44
|
+
*/
|
|
45
|
+
private assistantMessageId;
|
|
46
|
+
/** `messageId` of the reasoning message currently being streamed, or `null` outside one. */
|
|
47
|
+
private thoughtMessageId;
|
|
48
|
+
/**
|
|
49
|
+
* Live output accumulated per tool call, because v1 can only REPLACE a tool call's content.
|
|
50
|
+
*
|
|
51
|
+
* With no append-a-chunk update in the dialect, the only way to show a tool's output as it
|
|
52
|
+
* arrives is to resend everything seen so far; keeping the collection here is what makes each
|
|
53
|
+
* replacement a superset of the last rather than a flicker back to the newest line alone.
|
|
54
|
+
*/
|
|
55
|
+
private readonly toolOutput;
|
|
56
|
+
/**
|
|
57
|
+
* The `session/update` payloads one runtime event produces — usually one, sometimes none
|
|
58
|
+
* (`tool_args`, which only accumulates; the reasoning boundaries, which only move state).
|
|
59
|
+
*/
|
|
60
|
+
map(event: AgentStreamEvent): SessionUpdate[];
|
|
61
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Translates the agent runtime's typed {@link AgentStreamEvent} stream into ACP **v1**
|
|
4
|
+
* `session/update` payloads. (`acpUpdates.ts` is the v2 half.)
|
|
5
|
+
*
|
|
6
|
+
* ## Where v1 differs, and why the mapper could not be shared
|
|
7
|
+
*
|
|
8
|
+
* The two dialects report the same events, but not with the same messages, and each difference
|
|
9
|
+
* changes what a conforming client ends up rendering:
|
|
10
|
+
*
|
|
11
|
+
* - **v1 has a distinct `tool_call` update that CREATES a tool call**, where v2 folded creation into
|
|
12
|
+
* the first `tool_call_update`. Sending only `tool_call_update`s on v1 would leave a client
|
|
13
|
+
* patching a call it was never told about.
|
|
14
|
+
* - **v1 has no `tool_call_content_chunk`.** Its `tool_call_update.content` REPLACES the whole
|
|
15
|
+
* collection, so live tool output is streamed by accumulating it here and resending the
|
|
16
|
+
* collection — the opposite of v2, where each chunk appends and only a `tool_call_update` replaces.
|
|
17
|
+
* - **v1 has no `state_update`.** There is nowhere to report `running` / `requires_action` / `idle`,
|
|
18
|
+
* and no notification carries the stop reason: the turn's outcome is the `session/prompt`
|
|
19
|
+
* RESPONSE. See `acpAgentAppV1.ts`.
|
|
20
|
+
* - **A prompt turn does not echo the user's message.** v1 reserves replay for `session/load`; a
|
|
21
|
+
* client already renders what it sent, so echoing it would draw the message twice.
|
|
22
|
+
*
|
|
23
|
+
* What is NOT duplicated is tool-call identity — the kind hint, the argument reassembly, and the
|
|
24
|
+
* pairing that lets a permission request name the call it is about all come from
|
|
25
|
+
* {@link AcpToolCallTracker}.
|
|
26
|
+
*/
|
|
27
|
+
import { randomUUID } from 'node:crypto';
|
|
28
|
+
import { AcpToolCallTracker, toolKindFor } from '#src/modules/acp/acpToolCalls.js';
|
|
29
|
+
/** One text content block, the shape both message chunks and tool content wrap. */
|
|
30
|
+
function textBlock(text) {
|
|
31
|
+
return { type: 'text', text };
|
|
32
|
+
}
|
|
33
|
+
/** One tool-call content entry wrapping a text block. */
|
|
34
|
+
function toolText(text) {
|
|
35
|
+
return { type: 'content', content: textBlock(text) };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Turns one agent run's event stream into ACP v1 `session/update` payloads.
|
|
39
|
+
*
|
|
40
|
+
* One instance per prompt turn: it holds the message identity of the assistant text run and of the
|
|
41
|
+
* reasoning run, the accumulated argument text per tool call, and the output accumulated for each
|
|
42
|
+
* running tool. A fresh instance per turn is what makes a new turn a new `messageId`, which is how
|
|
43
|
+
* a client tells two messages apart.
|
|
44
|
+
*/
|
|
45
|
+
export class AcpV1UpdateMapper extends AcpToolCallTracker {
|
|
46
|
+
/**
|
|
47
|
+
* `messageId` of the assistant text message currently being streamed, or `null` when no text run
|
|
48
|
+
* is open. Cleared whenever something else interrupts the text (a tool call, a reasoning block),
|
|
49
|
+
* so the text that resumes afterwards is a NEW message rather than an append to the one the
|
|
50
|
+
* client already considers finished.
|
|
51
|
+
*/
|
|
52
|
+
assistantMessageId = null;
|
|
53
|
+
/** `messageId` of the reasoning message currently being streamed, or `null` outside one. */
|
|
54
|
+
thoughtMessageId = null;
|
|
55
|
+
/**
|
|
56
|
+
* Live output accumulated per tool call, because v1 can only REPLACE a tool call's content.
|
|
57
|
+
*
|
|
58
|
+
* With no append-a-chunk update in the dialect, the only way to show a tool's output as it
|
|
59
|
+
* arrives is to resend everything seen so far; keeping the collection here is what makes each
|
|
60
|
+
* replacement a superset of the last rather than a flicker back to the newest line alone.
|
|
61
|
+
*/
|
|
62
|
+
toolOutput = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* The `session/update` payloads one runtime event produces — usually one, sometimes none
|
|
65
|
+
* (`tool_args`, which only accumulates; the reasoning boundaries, which only move state).
|
|
66
|
+
*/
|
|
67
|
+
map(event) {
|
|
68
|
+
switch (event.type) {
|
|
69
|
+
case 'text': {
|
|
70
|
+
this.assistantMessageId ??= randomUUID();
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
sessionUpdate: 'agent_message_chunk',
|
|
74
|
+
messageId: this.assistantMessageId,
|
|
75
|
+
content: textBlock(event.delta),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
case 'reasoning_start': {
|
|
80
|
+
this.assistantMessageId = null;
|
|
81
|
+
this.thoughtMessageId = randomUUID();
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
case 'reasoning_delta': {
|
|
85
|
+
this.thoughtMessageId ??= randomUUID();
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
89
|
+
messageId: this.thoughtMessageId,
|
|
90
|
+
content: textBlock(event.delta),
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
case 'reasoning_end': {
|
|
95
|
+
this.thoughtMessageId = null;
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
case 'tool_start': {
|
|
99
|
+
// A tool call ends the open text run: the text that follows the tool is a separate message.
|
|
100
|
+
this.assistantMessageId = null;
|
|
101
|
+
this.trackToolStart(event.id, event.name);
|
|
102
|
+
this.toolOutput.set(event.id, []);
|
|
103
|
+
// v1's CREATE. `title` is required here, and everything descriptive is sent with it; the
|
|
104
|
+
// later `tool_call_update`s carry only what changed.
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
sessionUpdate: 'tool_call',
|
|
108
|
+
toolCallId: event.id,
|
|
109
|
+
name: event.name,
|
|
110
|
+
title: event.name,
|
|
111
|
+
kind: toolKindFor(event.name),
|
|
112
|
+
status: 'pending',
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
case 'tool_args': {
|
|
117
|
+
this.appendToolArgs(event.id, event.delta);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
case 'tool_end': {
|
|
121
|
+
const rawInput = this.rawInputFor(event.id);
|
|
122
|
+
// Status only (plus the arguments, once they are complete). No title, no kind, no name —
|
|
123
|
+
// the client keeps the ones the creating update set.
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
sessionUpdate: 'tool_call_update',
|
|
127
|
+
toolCallId: event.id,
|
|
128
|
+
status: 'in_progress',
|
|
129
|
+
...(rawInput === undefined ? {} : { rawInput }),
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
case 'tool_output': {
|
|
134
|
+
if (event.id === undefined)
|
|
135
|
+
return [];
|
|
136
|
+
const collected = this.toolOutput.get(event.id) ?? [];
|
|
137
|
+
collected.push(toolText(event.chunk));
|
|
138
|
+
this.toolOutput.set(event.id, collected);
|
|
139
|
+
// The whole collection, because v1 replaces rather than appends. Copied so a later push
|
|
140
|
+
// cannot mutate an update already handed to the transport.
|
|
141
|
+
return [
|
|
142
|
+
{ sessionUpdate: 'tool_call_update', toolCallId: event.id, content: [...collected] },
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
case 'tool_result': {
|
|
146
|
+
this.trackToolSettled(event.id);
|
|
147
|
+
this.toolOutput.delete(event.id);
|
|
148
|
+
// The result REPLACES whatever live output was showing — it is the authoritative record of
|
|
149
|
+
// what the tool produced, and the same thing v2's final update does.
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
sessionUpdate: 'tool_call_update',
|
|
153
|
+
toolCallId: event.id,
|
|
154
|
+
status: event.isError ? 'failed' : 'completed',
|
|
155
|
+
content: [toolText(event.content)],
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=acpUpdatesV1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acpUpdatesV1.js","sourceRoot":"","sources":["../../../src/modules/acp/acpUpdatesV1.ts"],"names":[],"mappings":"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"}
|