@gaunt-sloth/agent 2.0.0-alpha.9 → 2.0.0-beta.1
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 +73 -23
- package/cli-acp.js +21 -24
- package/dist/builtInToolsConfig.d.ts +17 -3
- package/dist/builtInToolsConfig.js +43 -41
- package/dist/builtInToolsConfig.js.map +1 -1
- package/dist/core/debugCapture.d.ts +7 -49
- package/dist/core/debugCapture.js +1 -1
- package/dist/core/debugCapture.js.map +1 -1
- package/dist/core/resolveAgentFactory.d.ts +10 -9
- package/dist/core/resolveAgentFactory.js +11 -12
- package/dist/core/resolveAgentFactory.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/OAuthClientProviderImpl.js.map +1 -1
- package/dist/mcp/tlsTrust.d.ts +61 -0
- package/dist/mcp/tlsTrust.js +109 -0
- package/dist/mcp/tlsTrust.js.map +1 -0
- package/dist/middleware/binaryContentInjectionMiddleware.d.ts +8 -1
- package/dist/middleware/binaryContentInjectionMiddleware.js +11 -2
- package/dist/middleware/binaryContentInjectionMiddleware.js.map +1 -1
- package/dist/middleware/frontendImageInjectionMiddleware.d.ts +107 -0
- package/dist/middleware/frontendImageInjectionMiddleware.js +167 -0
- package/dist/middleware/frontendImageInjectionMiddleware.js.map +1 -0
- package/dist/middleware/registry.js +49 -1
- package/dist/middleware/registry.js.map +1 -1
- package/dist/middleware/types.d.ts +16 -2
- package/dist/modules/a2a/A2AClientWrapper.d.ts +56 -1
- package/dist/modules/a2a/A2AClientWrapper.js +131 -31
- package/dist/modules/a2a/A2AClientWrapper.js.map +1 -1
- package/dist/modules/acp/acpAgentApp.d.ts +57 -0
- package/dist/modules/acp/acpAgentApp.js +378 -0
- package/dist/modules/acp/acpAgentApp.js.map +1 -0
- 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 +65 -0
- package/dist/modules/acp/acpPermissions.js +162 -0
- package/dist/modules/acp/acpPermissions.js.map +1 -0
- 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 +47 -0
- package/dist/modules/acp/acpStdio.js +71 -0
- package/dist/modules/acp/acpStdio.js.map +1 -0
- 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 +48 -0
- package/dist/modules/acp/acpUpdates.js +142 -0
- package/dist/modules/acp/acpUpdates.js.map +1 -0
- 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/apiAgUiModule.d.ts +87 -0
- package/dist/modules/apiAgUiModule.js +193 -28
- package/dist/modules/apiAgUiModule.js.map +1 -1
- package/dist/modules/interactiveSessionModule.js +497 -51
- package/dist/modules/interactiveSessionModule.js.map +1 -1
- package/dist/modules/slashCommands.d.ts +582 -0
- package/dist/modules/slashCommands.js +1001 -0
- package/dist/modules/slashCommands.js.map +1 -0
- package/dist/resolvers.js +119 -9
- package/dist/resolvers.js.map +1 -1
- package/dist/tools/GthCustomToolkit.js +95 -11
- package/dist/tools/GthCustomToolkit.js.map +1 -1
- package/dist/tools/GthDevToolkit.d.ts +8 -5
- package/dist/tools/GthDevToolkit.js +89 -43
- package/dist/tools/GthDevToolkit.js.map +1 -1
- package/dist/tools/GthFileSystemToolkit.d.ts +63 -0
- package/dist/tools/GthFileSystemToolkit.js +506 -105
- package/dist/tools/GthFileSystemToolkit.js.map +1 -1
- package/dist/tools/McpResourceTool.d.ts +31 -0
- package/dist/tools/McpResourceTool.js +106 -0
- package/dist/tools/McpResourceTool.js.map +1 -0
- package/dist/tools/gthChecklistTool.d.ts +30 -0
- package/dist/tools/gthChecklistTool.js +80 -0
- package/dist/tools/gthChecklistTool.js.map +1 -0
- package/dist/tools/gthGrepTool.d.ts +54 -0
- package/dist/tools/gthGrepTool.js +482 -0
- package/dist/tools/gthGrepTool.js.map +1 -0
- package/dist/tools/shell/env.js +1 -1
- package/dist/tools/shell/env.js.map +1 -1
- package/dist/tools/shell/outputBuffer.js.map +1 -1
- package/dist/tools/shell/workDir.d.ts +1 -10
- package/dist/tools/shell/workDir.js +10 -36
- package/dist/tools/shell/workDir.js.map +1 -1
- package/dist/utils/mcpAuthError.d.ts +36 -0
- package/dist/utils/mcpAuthError.js +89 -0
- package/dist/utils/mcpAuthError.js.map +1 -0
- package/package.json +13 -13
- package/dist/core/GthDeepAgent.d.ts +0 -181
- package/dist/core/GthDeepAgent.js +0 -682
- package/dist/core/GthDeepAgent.js.map +0 -1
- package/dist/core/deepAgentPermissions.d.ts +0 -115
- package/dist/core/deepAgentPermissions.js +0 -282
- package/dist/core/deepAgentPermissions.js.map +0 -1
- package/dist/core/gthAcpServer.d.ts +0 -27
- package/dist/core/gthAcpServer.js +0 -82
- package/dist/core/gthAcpServer.js.map +0 -1
- package/dist/core/gthDeepAgentFactory.d.ts +0 -8
- package/dist/core/gthDeepAgentFactory.js +0 -9
- package/dist/core/gthDeepAgentFactory.js.map +0 -1
- package/dist/modules/acpModule.d.ts +0 -40
- package/dist/modules/acpModule.js +0 -78
- package/dist/modules/acpModule.js.map +0 -1
- package/dist/tools/shell/allowlist.d.ts +0 -11
- package/dist/tools/shell/allowlist.js +0 -12
- package/dist/tools/shell/allowlist.js.map +0 -1
- package/dist/tools/shell/arity.d.ts +0 -11
- package/dist/tools/shell/arity.js +0 -12
- package/dist/tools/shell/arity.js.map +0 -1
- package/dist/tools/shell/hardline.d.ts +0 -15
- package/dist/tools/shell/hardline.js +0 -88
- package/dist/tools/shell/hardline.js.map +0 -1
- package/dist/tools/shell/normalize.d.ts +0 -10
- package/dist/tools/shell/normalize.js +0 -11
- package/dist/tools/shell/normalize.js.map +0 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Bridges the agent's tool-approval gate to ACP **v1**'s `session/request_permission`.
|
|
4
|
+
* (`acpPermissions.ts` is the v2 half and carries the reasoning both share.)
|
|
5
|
+
*
|
|
6
|
+
* **The gate must reach a human on this surface too.** [[EXT-54]] records what happens otherwise:
|
|
7
|
+
* a server that wires no approval callback does not get a quieter gate, it gets a gated tool that
|
|
8
|
+
* silently does nothing. A new dialect is a new surface, and the hole reopens per surface unless
|
|
9
|
+
* each one answers it.
|
|
10
|
+
*
|
|
11
|
+
* ## What v1 has instead of v2's `subject`
|
|
12
|
+
*
|
|
13
|
+
* v2 carries a request-level `title`, an optional `description`, and a structured `subject` that
|
|
14
|
+
* can say "this is a shell command, here it is, and here is its cwd". **v1 has none of those**: a
|
|
15
|
+
* `RequestPermissionRequest` is a session id, a `ToolCallUpdate`, and the options. So:
|
|
16
|
+
*
|
|
17
|
+
* - the command travels as `rawInput`, which is where a v1 client reads a tool call's arguments;
|
|
18
|
+
* - the gate's explanation — the rater's verdict, the escalation entry that fired, what a
|
|
19
|
+
* remembered answer would store — travels as the tool call's `content`, the only free-text field
|
|
20
|
+
* in the request. Dropping it is not an option: a user asked to rule on a call their own
|
|
21
|
+
* configuration should have approved reads an unexplained prompt as the gate malfunctioning.
|
|
22
|
+
*
|
|
23
|
+
* The `toolCall` field is an UPSERT against the call the client is already rendering, so every
|
|
24
|
+
* descriptive field is sent with the same values the creating `tool_call` update carried. Re-sending
|
|
25
|
+
* them is a no-op for a call the client knows, and it is what lets the request stand on its own for
|
|
26
|
+
* a call the client has never seen — which is what a minted id means.
|
|
27
|
+
*
|
|
28
|
+
* ## Untrusted text crosses this boundary as DATA, deliberately
|
|
29
|
+
*
|
|
30
|
+
* The command, the rater's reason and the escalation provenance are all model- or third-party-
|
|
31
|
+
* authored, and they are not painted through `core/shell/framing` here. That is not an oversight:
|
|
32
|
+
* these values leave as JSON fields of a structured request and the client draws them in its own
|
|
33
|
+
* UI, where terminal control codes are inert, and framing them would ship a line-number gutter into
|
|
34
|
+
* a GUI. What matters is that they stay in their own fields rather than being interpolated into the
|
|
35
|
+
* prompt's own prose, so nothing the model writes can impersonate the request itself.
|
|
36
|
+
*/
|
|
37
|
+
import { ACP_PERMISSION_OPTIONS } from '#src/modules/acp/acpPermissions.js';
|
|
38
|
+
import { toolKindFor } from '#src/modules/acp/acpToolCalls.js';
|
|
39
|
+
/** The gated tool whose argument is a shell command. */
|
|
40
|
+
const SHELL_TOOL = 'run_shell_command';
|
|
41
|
+
/**
|
|
42
|
+
* The human-readable explanation for the prompt: everything the gate knows about WHY this call
|
|
43
|
+
* reached a human.
|
|
44
|
+
*
|
|
45
|
+
* Assembled from the same three sources the terminal surfaces show, for the same reason. Empty when
|
|
46
|
+
* the gate has nothing to add, in which case no content is attached rather than an empty block.
|
|
47
|
+
*/
|
|
48
|
+
function descriptionFor(pending) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (pending.safetyVerdict) {
|
|
51
|
+
parts.push(`AI rater: ${pending.safetyVerdict.outcome} — ${pending.safetyVerdict.reason}`.trimEnd());
|
|
52
|
+
}
|
|
53
|
+
if (pending.escalatedBy) {
|
|
54
|
+
parts.push(`Your approvals.escalate list matched this call: ${pending.escalatedBy}`);
|
|
55
|
+
}
|
|
56
|
+
if (pending.grantPreview) {
|
|
57
|
+
parts.push(`"Allow and remember" will store: ${pending.grantPreview}`);
|
|
58
|
+
}
|
|
59
|
+
return parts.length > 0 ? parts.join('\n\n') : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** The shell command a pending call would run, when it is one and it is a plain string. */
|
|
62
|
+
function shellCommandOf(pending) {
|
|
63
|
+
if (pending.name !== SHELL_TOOL)
|
|
64
|
+
return undefined;
|
|
65
|
+
const command = pending.args.command;
|
|
66
|
+
return typeof command === 'string' ? command : undefined;
|
|
67
|
+
}
|
|
68
|
+
/** One tool-call content entry wrapping a line of text. */
|
|
69
|
+
function toolText(text) {
|
|
70
|
+
return { type: 'content', content: { type: 'text', text } };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What the request puts in front of the human, beyond the tool's name and arguments.
|
|
74
|
+
*
|
|
75
|
+
* The command comes first when there is one: v2 can say "this is a command" structurally and v1
|
|
76
|
+
* cannot, so on this dialect the only way to put it where a person ruling on it will read it is to
|
|
77
|
+
* spell it out. The gate's own explanation follows.
|
|
78
|
+
*/
|
|
79
|
+
function explanationFor(pending) {
|
|
80
|
+
const command = shellCommandOf(pending);
|
|
81
|
+
const description = descriptionFor(pending);
|
|
82
|
+
return [
|
|
83
|
+
...(command === undefined ? [] : [toolText(`Shell command: ${command}`)]),
|
|
84
|
+
...(description === undefined ? [] : [toolText(description)]),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
/** The ACP v1 permission request for one pending tool call the gate escalated to a human. */
|
|
88
|
+
export function permissionRequestForV1(options) {
|
|
89
|
+
const { sessionId, pending, toolCallId } = options;
|
|
90
|
+
const content = explanationFor(pending);
|
|
91
|
+
return {
|
|
92
|
+
sessionId,
|
|
93
|
+
toolCall: {
|
|
94
|
+
// A request about a call the update stream never announced still needs an id. It is minted
|
|
95
|
+
// rather than omitted — the field is required — and carries the same descriptive fields a
|
|
96
|
+
// creating update would, so the client renders the tool by name instead of an empty row.
|
|
97
|
+
toolCallId: toolCallId ?? `permission-${pending.name}`,
|
|
98
|
+
name: pending.name,
|
|
99
|
+
// The SAME title the creating update sent. This request is an upsert against a call the
|
|
100
|
+
// client is already drawing, so a different title here would rename that row mid-flight.
|
|
101
|
+
title: pending.name,
|
|
102
|
+
kind: toolKindFor(pending.name),
|
|
103
|
+
status: 'pending',
|
|
104
|
+
rawInput: pending.args,
|
|
105
|
+
...(content.length === 0 ? {} : { content }),
|
|
106
|
+
},
|
|
107
|
+
options: [...ACP_PERMISSION_OPTIONS],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=acpPermissionsV1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acpPermissionsV1.js","sourceRoot":"","sources":["../../../src/modules/acp/acpPermissionsV1.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Picks the ACP dialect a connection gets, from the connection itself.
|
|
4
|
+
*
|
|
5
|
+
* ## The problem, and why the SDK solves it rather than us
|
|
6
|
+
*
|
|
7
|
+
* There is one stdio connection and the protocol version is knowable only from the first
|
|
8
|
+
* `initialize` — which is also the first thing the chosen app has to handle. So a dispatcher has to
|
|
9
|
+
* read a message it is not allowed to consume, and pushing it back onto a `ReadableStream` is the
|
|
10
|
+
* part that is awkward to do correctly.
|
|
11
|
+
*
|
|
12
|
+
* `@agentclientprotocol/sdk` already does exactly this. {@link acp.agentProtocolRouter} takes the
|
|
13
|
+
* first wire item, requires it to be an `initialize`, selects the highest configured version that
|
|
14
|
+
* does not exceed the client's requested one, and re-enqueues the request into a fresh readable in
|
|
15
|
+
* front of the rest of the stream. Nothing later is touched. Using it means the sniff, the pushback
|
|
16
|
+
* and the not-configured / not-an-initialize error paths are the SDK's problem, on the same pinned
|
|
17
|
+
* version as the two apps it routes between.
|
|
18
|
+
*
|
|
19
|
+
* It is exported from the `experimental/v2` entry point rather than from `experimental/server` —
|
|
20
|
+
* `AcpServer` there is an HTTP/WebSocket transport and does not route by version.
|
|
21
|
+
*
|
|
22
|
+
* ## What each side gets
|
|
23
|
+
*
|
|
24
|
+
* A client asking for **1** reaches the v1 app; a client asking for **2 or higher** reaches the v2
|
|
25
|
+
* app. Neither app sees the other's traffic and neither is a translation layer over the other:
|
|
26
|
+
* `acpAgentAppV1.ts` and `acpAgentApp.ts` implement their own dialects, which differ in more than
|
|
27
|
+
* naming (see either module's doc).
|
|
28
|
+
*
|
|
29
|
+
* A version below 1 gets the SDK's `unsupported ACP protocol version` error, which is the honest
|
|
30
|
+
* answer — there is no ACP dialect this agent could serve such a client in.
|
|
31
|
+
*/
|
|
32
|
+
import * as acp from '@agentclientprotocol/sdk/experimental/v2';
|
|
33
|
+
import type { AcpAgentAppOptions } from '#src/modules/acp/acpCommon.js';
|
|
34
|
+
/**
|
|
35
|
+
* Builds the version-dispatching front door: both apps, wired behind one connector.
|
|
36
|
+
*
|
|
37
|
+
* `options` reaches both apps unchanged, so a caller cannot end up with a configured v2 surface and
|
|
38
|
+
* a default v1 one — a divergence that would show up only against whichever client the tests do not
|
|
39
|
+
* use.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createAcpAgentRouter(options?: AcpAgentAppOptions): acp.AgentProtocolRouter;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Picks the ACP dialect a connection gets, from the connection itself.
|
|
4
|
+
*
|
|
5
|
+
* ## The problem, and why the SDK solves it rather than us
|
|
6
|
+
*
|
|
7
|
+
* There is one stdio connection and the protocol version is knowable only from the first
|
|
8
|
+
* `initialize` — which is also the first thing the chosen app has to handle. So a dispatcher has to
|
|
9
|
+
* read a message it is not allowed to consume, and pushing it back onto a `ReadableStream` is the
|
|
10
|
+
* part that is awkward to do correctly.
|
|
11
|
+
*
|
|
12
|
+
* `@agentclientprotocol/sdk` already does exactly this. {@link acp.agentProtocolRouter} takes the
|
|
13
|
+
* first wire item, requires it to be an `initialize`, selects the highest configured version that
|
|
14
|
+
* does not exceed the client's requested one, and re-enqueues the request into a fresh readable in
|
|
15
|
+
* front of the rest of the stream. Nothing later is touched. Using it means the sniff, the pushback
|
|
16
|
+
* and the not-configured / not-an-initialize error paths are the SDK's problem, on the same pinned
|
|
17
|
+
* version as the two apps it routes between.
|
|
18
|
+
*
|
|
19
|
+
* It is exported from the `experimental/v2` entry point rather than from `experimental/server` —
|
|
20
|
+
* `AcpServer` there is an HTTP/WebSocket transport and does not route by version.
|
|
21
|
+
*
|
|
22
|
+
* ## What each side gets
|
|
23
|
+
*
|
|
24
|
+
* A client asking for **1** reaches the v1 app; a client asking for **2 or higher** reaches the v2
|
|
25
|
+
* app. Neither app sees the other's traffic and neither is a translation layer over the other:
|
|
26
|
+
* `acpAgentAppV1.ts` and `acpAgentApp.ts` implement their own dialects, which differ in more than
|
|
27
|
+
* naming (see either module's doc).
|
|
28
|
+
*
|
|
29
|
+
* A version below 1 gets the SDK's `unsupported ACP protocol version` error, which is the honest
|
|
30
|
+
* answer — there is no ACP dialect this agent could serve such a client in.
|
|
31
|
+
*/
|
|
32
|
+
import * as acp from '@agentclientprotocol/sdk/experimental/v2';
|
|
33
|
+
import { createAcpAgentApp } from '#src/modules/acp/acpAgentApp.js';
|
|
34
|
+
import { createAcpV1AgentApp } from '#src/modules/acp/acpAgentAppV1.js';
|
|
35
|
+
/**
|
|
36
|
+
* Builds the version-dispatching front door: both apps, wired behind one connector.
|
|
37
|
+
*
|
|
38
|
+
* `options` reaches both apps unchanged, so a caller cannot end up with a configured v2 surface and
|
|
39
|
+
* a default v1 one — a divergence that would show up only against whichever client the tests do not
|
|
40
|
+
* use.
|
|
41
|
+
*/
|
|
42
|
+
export function createAcpAgentRouter(options = {}) {
|
|
43
|
+
return acp
|
|
44
|
+
.agentProtocolRouter()
|
|
45
|
+
.withV1(createAcpV1AgentApp(options))
|
|
46
|
+
.withV2(createAcpAgentApp(options));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=acpRouter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acpRouter.js","sourceRoot":"","sources":["../../../src/modules/acp/acpRouter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,GAAG,MAAM,0CAA0C,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAGxE;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAO,GAAuB,EAAE;IACnE,OAAO,GAAG;SACP,mBAAmB,EAAE;SACrB,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;SACpC,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* The stdio entry point both ACP doors go through — the standalone `gaunt-sloth-acp` bin and
|
|
4
|
+
* `gaunt-sloth --acp-agent` — serving whichever protocol version the client speaks.
|
|
5
|
+
*
|
|
6
|
+
* **It is one function on purpose.** Two entry points spelling the same startup twice is how the
|
|
7
|
+
* doors drift, and the thing that would drift here is not cosmetic: it is the stdout guarantee
|
|
8
|
+
* below, which is invisible until an editor cannot parse a frame.
|
|
9
|
+
*
|
|
10
|
+
* ## stdout belongs to the protocol, and nothing else may touch it
|
|
11
|
+
*
|
|
12
|
+
* An ACP host reads JSON-RPC off the agent's stdout. Any other byte there is a framing error, not
|
|
13
|
+
* a message someone reads — and this codebase writes to stdout constantly: `displayInfo` and
|
|
14
|
+
* friends go through `console.log`/`console.info`, the streaming path writes to
|
|
15
|
+
* `process.stdout` directly, and the dev/custom toolkits pipe a child process's stdout through.
|
|
16
|
+
* A single status line would corrupt the stream.
|
|
17
|
+
*
|
|
18
|
+
* So the transport captures the REAL stdout writer first, and then `process.stdout.write` is
|
|
19
|
+
* redirected to stderr for the life of the process. Everything that thought it was printing to the
|
|
20
|
+
* user keeps working and lands on stderr, where a host shows it as agent output; the protocol
|
|
21
|
+
* keeps the channel it needs. Redirecting rather than silencing is deliberate — a warning nobody
|
|
22
|
+
* can see is how a misconfigured agent looks identical to a working one.
|
|
23
|
+
*/
|
|
24
|
+
import type { AcpAgentAppOptions } from '#src/modules/acp/acpCommon.js';
|
|
25
|
+
/** Seams for the tests; production passes nothing. */
|
|
26
|
+
export interface AcpStdioOptions extends AcpAgentAppOptions {
|
|
27
|
+
/** Byte stream the client writes to. Defaults to the process's stdin. */
|
|
28
|
+
input?: ReadableStream<Uint8Array>;
|
|
29
|
+
/**
|
|
30
|
+
* Byte sink the protocol writes to. Defaults to the process's real stdout, captured before the
|
|
31
|
+
* redirect below so the redirect cannot swallow the protocol along with everything else.
|
|
32
|
+
*/
|
|
33
|
+
output?: WritableStream<Uint8Array>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Serves ACP over stdio until the client disconnects, in whichever protocol version the client
|
|
37
|
+
* asks for on its first message.
|
|
38
|
+
*
|
|
39
|
+
* The dialect is not chosen here and is not configurable: `acpRouter.ts` reads it off the
|
|
40
|
+
* `initialize` and hands the connection to the matching app. A flag would be the wrong shape —
|
|
41
|
+
* an editor spawns this command with no arguments, so a version it had to be told would be a
|
|
42
|
+
* version it never gets.
|
|
43
|
+
*
|
|
44
|
+
* Resolves when the connection closes, so a bin can `await` it and exit cleanly rather than
|
|
45
|
+
* holding the event loop open on a socket nobody is reading.
|
|
46
|
+
*/
|
|
47
|
+
export declare function startAcpServer(options?: AcpStdioOptions): Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* The stdio entry point both ACP doors go through — the standalone `gaunt-sloth-acp` bin and
|
|
4
|
+
* `gaunt-sloth --acp-agent` — serving whichever protocol version the client speaks.
|
|
5
|
+
*
|
|
6
|
+
* **It is one function on purpose.** Two entry points spelling the same startup twice is how the
|
|
7
|
+
* doors drift, and the thing that would drift here is not cosmetic: it is the stdout guarantee
|
|
8
|
+
* below, which is invisible until an editor cannot parse a frame.
|
|
9
|
+
*
|
|
10
|
+
* ## stdout belongs to the protocol, and nothing else may touch it
|
|
11
|
+
*
|
|
12
|
+
* An ACP host reads JSON-RPC off the agent's stdout. Any other byte there is a framing error, not
|
|
13
|
+
* a message someone reads — and this codebase writes to stdout constantly: `displayInfo` and
|
|
14
|
+
* friends go through `console.log`/`console.info`, the streaming path writes to
|
|
15
|
+
* `process.stdout` directly, and the dev/custom toolkits pipe a child process's stdout through.
|
|
16
|
+
* A single status line would corrupt the stream.
|
|
17
|
+
*
|
|
18
|
+
* So the transport captures the REAL stdout writer first, and then `process.stdout.write` is
|
|
19
|
+
* redirected to stderr for the life of the process. Everything that thought it was printing to the
|
|
20
|
+
* user keeps working and lands on stderr, where a host shows it as agent output; the protocol
|
|
21
|
+
* keeps the channel it needs. Redirecting rather than silencing is deliberate — a warning nobody
|
|
22
|
+
* can see is how a misconfigured agent looks identical to a working one.
|
|
23
|
+
*/
|
|
24
|
+
import { Readable } from 'node:stream';
|
|
25
|
+
import * as acp from '@agentclientprotocol/sdk/experimental/v2';
|
|
26
|
+
import { announceAcpStart } from '#src/modules/acp/acpCommon.js';
|
|
27
|
+
import { createAcpAgentRouter } from '#src/modules/acp/acpRouter.js';
|
|
28
|
+
/**
|
|
29
|
+
* Sends everything written to `process.stdout` to stderr instead, and returns a sink holding the
|
|
30
|
+
* ORIGINAL writer.
|
|
31
|
+
*
|
|
32
|
+
* The capture must happen before the redirect, and the sink must hold the captured function rather
|
|
33
|
+
* than reach for `process.stdout.write` at write time — otherwise the transport's own frames go
|
|
34
|
+
* through the redirect too and the agent talks to itself.
|
|
35
|
+
*/
|
|
36
|
+
function captureStdoutForProtocol() {
|
|
37
|
+
const writeToRealStdout = process.stdout.write.bind(process.stdout);
|
|
38
|
+
const writeToStderr = process.stderr.write.bind(process.stderr);
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
process.stdout.write = ((chunk, ...rest) =>
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
writeToStderr(chunk, ...rest));
|
|
43
|
+
return new WritableStream({
|
|
44
|
+
write(chunk) {
|
|
45
|
+
writeToRealStdout(chunk);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
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.
|
|
57
|
+
*
|
|
58
|
+
* Resolves when the connection closes, so a bin can `await` it and exit cleanly rather than
|
|
59
|
+
* holding the event loop open on a socket nobody is reading.
|
|
60
|
+
*/
|
|
61
|
+
export async function startAcpServer(options = {}) {
|
|
62
|
+
const { input, output, ...appOptions } = options;
|
|
63
|
+
// Before anything can print. `announceAcpStart` below is the first thing that tries, and it is
|
|
64
|
+
// there so the guarantee is exercised on every real start rather than only under test.
|
|
65
|
+
const protocolOut = output ?? captureStdoutForProtocol();
|
|
66
|
+
const protocolIn = input ?? Readable.toWeb(process.stdin);
|
|
67
|
+
const connection = createAcpAgentRouter(appOptions).connect(acp.ndJsonStream(protocolOut, protocolIn));
|
|
68
|
+
announceAcpStart();
|
|
69
|
+
await connection.closed;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=acpStdio.js.map
|
|
@@ -0,0 +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,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"}
|