@gaunt-sloth/agent 2.0.0-alpha.35 → 2.0.0-alpha.36
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 +9 -10
- package/cli-acp.js +20 -24
- package/dist/builtInToolsConfig.d.ts +2 -2
- package/dist/builtInToolsConfig.js +2 -2
- package/dist/core/debugCapture.d.ts +4 -5
- 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 +2 -4
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/middleware/frontendImageInjectionMiddleware.d.ts +3 -2
- package/dist/middleware/frontendImageInjectionMiddleware.js +3 -2
- package/dist/middleware/frontendImageInjectionMiddleware.js.map +1 -1
- package/dist/middleware/registry.js +16 -3
- package/dist/middleware/registry.js.map +1 -1
- package/dist/modules/acp/acpAgentApp.d.ts +93 -0
- package/dist/modules/acp/acpAgentApp.js +593 -0
- package/dist/modules/acp/acpAgentApp.js.map +1 -0
- package/dist/modules/acp/acpPermissions.d.ts +60 -0
- package/dist/modules/acp/acpPermissions.js +158 -0
- package/dist/modules/acp/acpPermissions.js.map +1 -0
- package/dist/modules/acp/acpStdio.d.ts +41 -0
- package/dist/modules/acp/acpStdio.js +65 -0
- package/dist/modules/acp/acpStdio.js.map +1 -0
- package/dist/modules/acp/acpUpdates.d.ts +101 -0
- package/dist/modules/acp/acpUpdates.js +287 -0
- package/dist/modules/acp/acpUpdates.js.map +1 -0
- package/dist/modules/apiAgUiModule.js +8 -16
- package/dist/modules/apiAgUiModule.js.map +1 -1
- package/dist/modules/interactiveSessionModule.js +39 -6
- package/dist/modules/interactiveSessionModule.js.map +1 -1
- package/dist/tools/GthCustomToolkit.js +1 -1
- package/dist/tools/GthCustomToolkit.js.map +1 -1
- package/dist/tools/GthDevToolkit.d.ts +2 -2
- package/dist/tools/GthDevToolkit.js +8 -13
- package/dist/tools/GthDevToolkit.js.map +1 -1
- package/dist/tools/gthChecklistTool.js +4 -5
- package/dist/tools/gthChecklistTool.js.map +1 -1
- package/dist/tools/gthGrepTool.js +6 -6
- 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/package.json +8 -9
- package/dist/core/GthDeepAgent.d.ts +0 -128
- package/dist/core/GthDeepAgent.js +0 -712
- package/dist/core/GthDeepAgent.js.map +0 -1
- package/dist/core/deepAgentPermissions.d.ts +0 -138
- package/dist/core/deepAgentPermissions.js +0 -309
- 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 -11
- package/dist/core/gthDeepAgentFactory.js +0 -17
- package/dist/core/gthDeepAgentFactory.js.map +0 -1
- package/dist/core/subagentProfiles.d.ts +0 -50
- package/dist/core/subagentProfiles.js +0 -76
- package/dist/core/subagentProfiles.js.map +0 -1
- package/dist/modules/acpModule.d.ts +0 -40
- package/dist/modules/acpModule.js +0 -86
- package/dist/modules/acpModule.js.map +0 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Bridges the agent's tool-approval gate to ACP v2's `session/request_permission`.
|
|
4
|
+
*
|
|
5
|
+
* **This is the reason the ACP server drives {@link GthAgentRunner} rather than an agent
|
|
6
|
+
* directly.** The runner is what owns the gate — the bypass check, the allow-list, the AI rater,
|
|
7
|
+
* the deny list, and finally the human callback — and a surface that skips it does not get a
|
|
8
|
+
* quieter gate, it gets a gated tool that silently does nothing (the hole [[EXT-54]] records
|
|
9
|
+
* against the AG-UI and the old ACP servers). Everything here is the last hop of that gate: the
|
|
10
|
+
* runner has already decided the call needs a human, and this turns "needs a human" into a request
|
|
11
|
+
* an editor can put in front of one, then turns the answer back into a decision the runner
|
|
12
|
+
* understands.
|
|
13
|
+
*
|
|
14
|
+
* ## Untrusted text crosses this boundary as DATA, deliberately
|
|
15
|
+
*
|
|
16
|
+
* The command, the rater's reason and the escalation provenance are all model- or third-party-
|
|
17
|
+
* authored. On the terminal surfaces they are painted through `core/shell/framing` because a
|
|
18
|
+
* carriage return there reaches column 0 and an escape sequence clears the screen. **That renderer
|
|
19
|
+
* is not applied here, and its absence is not an oversight:** these values leave as JSON fields of
|
|
20
|
+
* a structured request, and the client draws them in its own UI, where terminal control codes are
|
|
21
|
+
* inert. Framing them would ship a line-number gutter and box drawing into a GUI. What matters on
|
|
22
|
+
* this surface is that they stay in their own fields rather than being interpolated into the
|
|
23
|
+
* prompt's own prose — so a client can style them as untrusted, and nothing the model writes can
|
|
24
|
+
* impersonate the request's title.
|
|
25
|
+
*/
|
|
26
|
+
import type { PermissionOption, RequestPermissionOutcome, RequestPermissionRequest, SessionId } from '@agentclientprotocol/sdk/experimental/v2';
|
|
27
|
+
import type { PendingToolInterrupt, ToolApprovalDecision } from '@gaunt-sloth/core/core/types.js';
|
|
28
|
+
/**
|
|
29
|
+
* The four answers offered on every permission request, one per ACP `PermissionOptionKind`.
|
|
30
|
+
*
|
|
31
|
+
* The set is fixed rather than computed from what the gate would accept, because a menu that
|
|
32
|
+
* appears and disappears per call is a menu a user cannot learn. What each one MEANS is in
|
|
33
|
+
* {@link decisionForOutcome}; the labels say it in words, since a bare kind hint does not tell
|
|
34
|
+
* anyone what "always" persists to.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ACP_PERMISSION_OPTIONS: readonly PermissionOption[];
|
|
37
|
+
/** The ACP permission request for one pending tool call the gate escalated to a human. */
|
|
38
|
+
export declare function permissionRequestFor(options: {
|
|
39
|
+
sessionId: SessionId;
|
|
40
|
+
pending: PendingToolInterrupt;
|
|
41
|
+
/** The tool call id the update stream already announced for this call, when it is known. */
|
|
42
|
+
toolCallId?: string;
|
|
43
|
+
/** The session's working directory — the `cwd` a `command` subject is required to carry. */
|
|
44
|
+
cwd: string;
|
|
45
|
+
}): RequestPermissionRequest;
|
|
46
|
+
/**
|
|
47
|
+
* The gate decision an ACP permission outcome means.
|
|
48
|
+
*
|
|
49
|
+
* **Every path that is not an explicit allow is a reject**, including an option id we do not
|
|
50
|
+
* recognise and an outcome variant that postdates this code. That polarity is the whole safety
|
|
51
|
+
* property: a client that answers with something unexpected must not be able to run a command,
|
|
52
|
+
* and a future ACP outcome must fail toward refusing rather than toward executing.
|
|
53
|
+
*
|
|
54
|
+
* `allow-always` maps to the `always` scope, which persists to the project allow-list — the same
|
|
55
|
+
* thing the terminal menu's *always approve* does. `reject-always` maps to the `session` scope
|
|
56
|
+
* because that is the strongest refusal the gate actually persists; ACP's `reject_always` is a UI
|
|
57
|
+
* hint, and promising a persistence nothing implements would be worse than serving it with the
|
|
58
|
+
* refusal that does exist. The option's own label says "for this session" so the user is told.
|
|
59
|
+
*/
|
|
60
|
+
export declare function decisionForOutcome(outcome: RequestPermissionOutcome): ToolApprovalDecision;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Bridges the agent's tool-approval gate to ACP v2's `session/request_permission`.
|
|
4
|
+
*
|
|
5
|
+
* **This is the reason the ACP server drives {@link GthAgentRunner} rather than an agent
|
|
6
|
+
* directly.** The runner is what owns the gate — the bypass check, the allow-list, the AI rater,
|
|
7
|
+
* the deny list, and finally the human callback — and a surface that skips it does not get a
|
|
8
|
+
* quieter gate, it gets a gated tool that silently does nothing (the hole [[EXT-54]] records
|
|
9
|
+
* against the AG-UI and the old ACP servers). Everything here is the last hop of that gate: the
|
|
10
|
+
* runner has already decided the call needs a human, and this turns "needs a human" into a request
|
|
11
|
+
* an editor can put in front of one, then turns the answer back into a decision the runner
|
|
12
|
+
* understands.
|
|
13
|
+
*
|
|
14
|
+
* ## Untrusted text crosses this boundary as DATA, deliberately
|
|
15
|
+
*
|
|
16
|
+
* The command, the rater's reason and the escalation provenance are all model- or third-party-
|
|
17
|
+
* authored. On the terminal surfaces they are painted through `core/shell/framing` because a
|
|
18
|
+
* carriage return there reaches column 0 and an escape sequence clears the screen. **That renderer
|
|
19
|
+
* is not applied here, and its absence is not an oversight:** these values leave as JSON fields of
|
|
20
|
+
* a structured request, and the client draws them in its own UI, where terminal control codes are
|
|
21
|
+
* inert. Framing them would ship a line-number gutter and box drawing into a GUI. What matters on
|
|
22
|
+
* this surface is that they stay in their own fields rather than being interpolated into the
|
|
23
|
+
* prompt's own prose — so a client can style them as untrusted, and nothing the model writes can
|
|
24
|
+
* impersonate the request's title.
|
|
25
|
+
*/
|
|
26
|
+
import { toolKindFor } from '#src/modules/acp/acpUpdates.js';
|
|
27
|
+
/** The gated tool whose argument is a shell command, and so has an ACP `command` subject. */
|
|
28
|
+
const SHELL_TOOL = 'run_shell_command';
|
|
29
|
+
/**
|
|
30
|
+
* The four answers offered on every permission request, one per ACP `PermissionOptionKind`.
|
|
31
|
+
*
|
|
32
|
+
* The set is fixed rather than computed from what the gate would accept, because a menu that
|
|
33
|
+
* appears and disappears per call is a menu a user cannot learn. What each one MEANS is in
|
|
34
|
+
* {@link decisionForOutcome}; the labels say it in words, since a bare kind hint does not tell
|
|
35
|
+
* anyone what "always" persists to.
|
|
36
|
+
*/
|
|
37
|
+
export const ACP_PERMISSION_OPTIONS = [
|
|
38
|
+
{ optionId: 'allow-once', name: 'Allow once', kind: 'allow_once' },
|
|
39
|
+
{ optionId: 'allow-always', name: 'Allow and remember', kind: 'allow_always' },
|
|
40
|
+
{ optionId: 'reject-once', name: 'Reject once', kind: 'reject_once' },
|
|
41
|
+
{ optionId: 'reject-always', name: 'Reject for this session', kind: 'reject_always' },
|
|
42
|
+
];
|
|
43
|
+
/** The shell command a pending call would run, when it is one and it is a plain string. */
|
|
44
|
+
function shellCommandOf(pending) {
|
|
45
|
+
if (pending.name !== SHELL_TOOL)
|
|
46
|
+
return undefined;
|
|
47
|
+
const command = pending.args.command;
|
|
48
|
+
return typeof command === 'string' ? command : undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The structured `subject` for a pending call: a `command` subject when the gate stopped a shell
|
|
52
|
+
* command (so the client can show the command as a command), a `tool_call` subject otherwise.
|
|
53
|
+
*
|
|
54
|
+
* `toolCallId` correlates the request with the tool call the client is ALREADY rendering from the
|
|
55
|
+
* `session/update` stream — the model emits its tool call before the graph suspends on the gate,
|
|
56
|
+
* so by the time a permission request goes out the client has drawn the call and is waiting on it.
|
|
57
|
+
* Without the correlation the prompt is about a tool call the client cannot point at.
|
|
58
|
+
*/
|
|
59
|
+
function subjectFor(pending, toolCallId, cwd) {
|
|
60
|
+
const command = shellCommandOf(pending);
|
|
61
|
+
if (command !== undefined) {
|
|
62
|
+
return {
|
|
63
|
+
type: 'command',
|
|
64
|
+
command,
|
|
65
|
+
cwd,
|
|
66
|
+
...(toolCallId === undefined ? {} : { toolCallId }),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
type: 'tool_call',
|
|
71
|
+
toolCall: {
|
|
72
|
+
// A `tool_call` subject has nowhere to put "no id", so a call the update stream never
|
|
73
|
+
// announced still needs one. It is minted rather than omitted, and it is the same shape a
|
|
74
|
+
// creating `tool_call_update` would carry, so a client that renders the subject shows the
|
|
75
|
+
// tool by name instead of an empty row.
|
|
76
|
+
toolCallId: toolCallId ?? `permission-${pending.name}`,
|
|
77
|
+
name: pending.name,
|
|
78
|
+
title: pending.name,
|
|
79
|
+
kind: toolKindFor(pending.name),
|
|
80
|
+
status: 'pending',
|
|
81
|
+
rawInput: pending.args,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The human-readable explanation for the prompt: everything the gate knows about WHY this call
|
|
87
|
+
* reached a human.
|
|
88
|
+
*
|
|
89
|
+
* Assembled from the same three sources the terminal surfaces show — the rater's verdict, the
|
|
90
|
+
* declared `approvals.escalate` entry that fired, and what a remembered answer would store — for
|
|
91
|
+
* the same reason: a user asked to rule on a call their configuration should have approved reads
|
|
92
|
+
* an unexplained prompt as the gate malfunctioning. Empty when the gate has nothing to add, in
|
|
93
|
+
* which case the field is omitted rather than sent as an empty string.
|
|
94
|
+
*/
|
|
95
|
+
function descriptionFor(pending) {
|
|
96
|
+
const parts = [];
|
|
97
|
+
if (pending.safetyVerdict) {
|
|
98
|
+
parts.push(`AI rater: ${pending.safetyVerdict.outcome} — ${pending.safetyVerdict.reason}`.trimEnd());
|
|
99
|
+
}
|
|
100
|
+
if (pending.escalatedBy) {
|
|
101
|
+
parts.push(`Your approvals.escalate list matched this call: ${pending.escalatedBy}`);
|
|
102
|
+
}
|
|
103
|
+
if (pending.grantPreview) {
|
|
104
|
+
parts.push(`"Allow and remember" will store: ${pending.grantPreview}`);
|
|
105
|
+
}
|
|
106
|
+
return parts.length > 0 ? parts.join('\n\n') : undefined;
|
|
107
|
+
}
|
|
108
|
+
/** The ACP permission request for one pending tool call the gate escalated to a human. */
|
|
109
|
+
export function permissionRequestFor(options) {
|
|
110
|
+
const { sessionId, pending, toolCallId, cwd } = options;
|
|
111
|
+
const command = shellCommandOf(pending);
|
|
112
|
+
const description = descriptionFor(pending);
|
|
113
|
+
return {
|
|
114
|
+
sessionId,
|
|
115
|
+
// The title is the AGENT's own words and carries no untrusted text: what the model wrote is in
|
|
116
|
+
// the subject, where a client can style it as the model's.
|
|
117
|
+
title: command !== undefined ? 'Run a shell command' : `Run the ${pending.name} tool`,
|
|
118
|
+
...(description === undefined ? {} : { description }),
|
|
119
|
+
subject: subjectFor(pending, toolCallId, cwd),
|
|
120
|
+
options: [...ACP_PERMISSION_OPTIONS],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The gate decision an ACP permission outcome means.
|
|
125
|
+
*
|
|
126
|
+
* **Every path that is not an explicit allow is a reject**, including an option id we do not
|
|
127
|
+
* recognise and an outcome variant that postdates this code. That polarity is the whole safety
|
|
128
|
+
* property: a client that answers with something unexpected must not be able to run a command,
|
|
129
|
+
* and a future ACP outcome must fail toward refusing rather than toward executing.
|
|
130
|
+
*
|
|
131
|
+
* `allow-always` maps to the `always` scope, which persists to the project allow-list — the same
|
|
132
|
+
* thing the terminal menu's *always approve* does. `reject-always` maps to the `session` scope
|
|
133
|
+
* because that is the strongest refusal the gate actually persists; ACP's `reject_always` is a UI
|
|
134
|
+
* hint, and promising a persistence nothing implements would be worse than serving it with the
|
|
135
|
+
* refusal that does exist. The option's own label says "for this session" so the user is told.
|
|
136
|
+
*/
|
|
137
|
+
export function decisionForOutcome(outcome) {
|
|
138
|
+
if (outcome.outcome !== 'selected') {
|
|
139
|
+
return { type: 'reject', message: 'The client cancelled the permission request.' };
|
|
140
|
+
}
|
|
141
|
+
const optionId = outcome.optionId;
|
|
142
|
+
switch (optionId) {
|
|
143
|
+
case 'allow-once':
|
|
144
|
+
return { type: 'approve' };
|
|
145
|
+
case 'allow-always':
|
|
146
|
+
return { type: 'approve', scope: 'always' };
|
|
147
|
+
case 'reject-always':
|
|
148
|
+
return { type: 'reject', scope: 'session', message: 'The user rejected this tool call.' };
|
|
149
|
+
case 'reject-once':
|
|
150
|
+
return { type: 'reject', message: 'The user rejected this tool call.' };
|
|
151
|
+
default:
|
|
152
|
+
return {
|
|
153
|
+
type: 'reject',
|
|
154
|
+
message: `The client selected an unknown permission option (${String(optionId)}).`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=acpPermissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acpPermissions.js","sourceRoot":"","sources":["../../../src/modules/acp/acpPermissions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
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`.
|
|
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/acpAgentApp.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 v2 over stdio until the client disconnects.
|
|
37
|
+
*
|
|
38
|
+
* Resolves when the connection closes, so a bin can `await` it and exit cleanly rather than
|
|
39
|
+
* holding the event loop open on a socket nobody is reading.
|
|
40
|
+
*/
|
|
41
|
+
export declare function startAcpServer(options?: AcpStdioOptions): Promise<void>;
|
|
@@ -0,0 +1,65 @@
|
|
|
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`.
|
|
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, createAcpAgentApp } from '#src/modules/acp/acpAgentApp.js';
|
|
27
|
+
/**
|
|
28
|
+
* Sends everything written to `process.stdout` to stderr instead, and returns a sink holding the
|
|
29
|
+
* ORIGINAL writer.
|
|
30
|
+
*
|
|
31
|
+
* The capture must happen before the redirect, and the sink must hold the captured function rather
|
|
32
|
+
* than reach for `process.stdout.write` at write time — otherwise the transport's own frames go
|
|
33
|
+
* through the redirect too and the agent talks to itself.
|
|
34
|
+
*/
|
|
35
|
+
function captureStdoutForProtocol() {
|
|
36
|
+
const writeToRealStdout = process.stdout.write.bind(process.stdout);
|
|
37
|
+
const writeToStderr = process.stderr.write.bind(process.stderr);
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
process.stdout.write = ((chunk, ...rest) =>
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
writeToStderr(chunk, ...rest));
|
|
42
|
+
return new WritableStream({
|
|
43
|
+
write(chunk) {
|
|
44
|
+
writeToRealStdout(chunk);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Serves ACP v2 over stdio until the client disconnects.
|
|
50
|
+
*
|
|
51
|
+
* Resolves when the connection closes, so a bin can `await` it and exit cleanly rather than
|
|
52
|
+
* holding the event loop open on a socket nobody is reading.
|
|
53
|
+
*/
|
|
54
|
+
export async function startAcpServer(options = {}) {
|
|
55
|
+
const { input, output, ...appOptions } = options;
|
|
56
|
+
// Before anything can print. `announceAcpStart` below is the first thing that tries, and it is
|
|
57
|
+
// there so the guarantee is exercised on every real start rather than only under test.
|
|
58
|
+
const protocolOut = output ?? captureStdoutForProtocol();
|
|
59
|
+
const protocolIn = input ?? Readable.toWeb(process.stdin);
|
|
60
|
+
const app = createAcpAgentApp(appOptions);
|
|
61
|
+
const connection = app.connect(acp.ndJsonStream(protocolOut, protocolIn));
|
|
62
|
+
announceAcpStart();
|
|
63
|
+
await connection.closed;
|
|
64
|
+
}
|
|
65
|
+
//# 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,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AActF;;;;;;;GAOG;AACH,SAAS,wBAAwB;IAC/B,MAAM,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,8DAA8D;IAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAU,EAAE,GAAG,IAAW,EAAE,EAAE;IACrD,8DAA8D;IAC7D,aAAqB,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAgC,CAAC;IACzE,OAAO,IAAI,cAAc,CAAa;QACpC,KAAK,CAAC,KAAK;YACT,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAO,GAAoB,EAAE;IAChE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,GAAG,OAAO,CAAC;IACjD,+FAA+F;IAC/F,uFAAuF;IACvF,MAAM,WAAW,GAAG,MAAM,IAAI,wBAAwB,EAAE,CAAC;IACzD,MAAM,UAAU,GAAG,KAAK,IAAK,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAgC,CAAC;IAE1F,MAAM,GAAG,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;IAC1E,gBAAgB,EAAE,CAAC;IACnB,MAAM,UAAU,CAAC,MAAM,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Translates the agent runtime's typed {@link AgentStreamEvent} stream into ACP v2
|
|
4
|
+
* `session/update` payloads.
|
|
5
|
+
*
|
|
6
|
+
* Kept as a pure, stateful-but-transport-free mapper rather than inlined in the request handlers
|
|
7
|
+
* for two reasons. It is the half of the ACP surface with real logic — message identity, tool-call
|
|
8
|
+
* upserts, which events open and close a run — so it is the half worth testing without a
|
|
9
|
+
* connection. And the `session/update` **upsert semantics** live here and nowhere else: what the
|
|
10
|
+
* mapper omits is what a client must leave unchanged, so a mapper that re-sent a full replacement
|
|
11
|
+
* on every update would silently erase fields a client had already rendered.
|
|
12
|
+
*
|
|
13
|
+
* ## The upsert contract this mapper is written against
|
|
14
|
+
*
|
|
15
|
+
* A client applies updates per id, in arrival order: an omitted field leaves the stored value
|
|
16
|
+
* unchanged, `null` clears it, a concrete value replaces it, and a chunk appends. The first
|
|
17
|
+
* `tool_call_update` a client sees for a `toolCallId` CREATES the tool call. So the mapper sends
|
|
18
|
+
* the descriptive fields **once**, on the creating update, and every later update for that call
|
|
19
|
+
* carries only what actually changed — which is what makes a client's rendering of a running tool
|
|
20
|
+
* call correct rather than flickering back to a bare id.
|
|
21
|
+
*/
|
|
22
|
+
import type { SessionUpdate, ToolKind } from '@agentclientprotocol/sdk/experimental/v2';
|
|
23
|
+
import type { AgentStreamEvent } from '@gaunt-sloth/core/core/types.js';
|
|
24
|
+
/** The ACP tool kind for a gth tool name; `other` for anything not built in. */
|
|
25
|
+
export declare function toolKindFor(name: string): ToolKind;
|
|
26
|
+
/**
|
|
27
|
+
* Turns one agent run's event stream into ACP `session/update` payloads.
|
|
28
|
+
*
|
|
29
|
+
* One instance per prompt turn: it holds the message identity of the assistant text run and of the
|
|
30
|
+
* reasoning run, plus the accumulated argument text per tool call. A fresh instance per turn is
|
|
31
|
+
* what makes a new turn a new `messageId`, which is how a client tells two messages apart.
|
|
32
|
+
*/
|
|
33
|
+
export declare class AcpUpdateMapper {
|
|
34
|
+
/**
|
|
35
|
+
* `messageId` of the assistant text message currently being streamed, or `null` when no text run
|
|
36
|
+
* is open. Cleared whenever something else interrupts the text (a tool call, a reasoning block),
|
|
37
|
+
* so the text that resumes afterwards is a NEW message rather than an append to the one the
|
|
38
|
+
* client already considers finished.
|
|
39
|
+
*/
|
|
40
|
+
private assistantMessageId;
|
|
41
|
+
/** `messageId` of the reasoning message currently being streamed, or `null` outside one. */
|
|
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
|
+
/**
|
|
97
|
+
* The `session/update` payloads one runtime event produces — usually one, sometimes none
|
|
98
|
+
* (`tool_args`, which only accumulates; the reasoning boundaries, which only move state).
|
|
99
|
+
*/
|
|
100
|
+
map(event: AgentStreamEvent): SessionUpdate[];
|
|
101
|
+
}
|