@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Gaunt Sloth's ACP (Agent Client Protocol) **v1** agent, built on the official SDK's stable root
|
|
4
|
+
* export. (`acpAgentApp.ts` is the v2 half; `acpRouter.ts` picks between them.)
|
|
5
|
+
*
|
|
6
|
+
* ## Why v1 is served at all
|
|
7
|
+
*
|
|
8
|
+
* v1 is ACP's stable protocol; v2 is a draft. Every shipping editor that speaks ACP today speaks
|
|
9
|
+
* v1 — measured against Zed 1.15.0 stable, whose `initialize` carries `protocolVersion: 1` as a
|
|
10
|
+
* literal with no setting to change it, and whose build does not compile the draft-v2 API at all.
|
|
11
|
+
* An agent that serves only v2 cannot open a session with any of them.
|
|
12
|
+
*
|
|
13
|
+
* ## The shape is the same; the semantics are not
|
|
14
|
+
*
|
|
15
|
+
* The SDK's v1 entry point offers the same fluent app builder as v2 — `acp.agent()` returns an
|
|
16
|
+
* `AgentApp`, handlers are registered by ACP method name, and `connect()` serves either a transport
|
|
17
|
+
* or, in tests, a `ClientApp` directly. That similarity is a trap if it is mistaken for sameness.
|
|
18
|
+
* Four differences decide what this file does, and none of them can be reached by renaming a v2
|
|
19
|
+
* handler:
|
|
20
|
+
*
|
|
21
|
+
* 1. **`session/prompt` answers with the stop reason.** The turn runs INSIDE the request and the
|
|
22
|
+
* response is what tells the client it is over. v2 acknowledges immediately and reports the stop
|
|
23
|
+
* reason later on an idle `state_update`, which does not exist here.
|
|
24
|
+
* 2. **A cancelled turn is a `cancelled` stop reason, never an error.** The v1 spec is explicit
|
|
25
|
+
* that agents MUST catch the abort their libraries throw and answer with the stop reason,
|
|
26
|
+
* because clients show unrecognized errors to the user and a cancellation is not one.
|
|
27
|
+
* 3. **A FAILED turn is a JSON-RPC error**, because v1's `StopReason` union is closed —
|
|
28
|
+
* `end_turn | max_tokens | max_turn_requests | refusal | cancelled` — and none of them means
|
|
29
|
+
* "crashed". (This is exactly why v2 needed a custom `_error` reason: there, the response was
|
|
30
|
+
* already sent and the notification stream was the only channel left. Here the request is still
|
|
31
|
+
* open, so the error belongs in its response, and answering `end_turn` would be a lie.)
|
|
32
|
+
* 4. **No `session/load`, and no `loadSession` capability.** In v1 that capability means sessions
|
|
33
|
+
* survive restarts and can be picked up by another client instance; these sessions live in one
|
|
34
|
+
* process and die with it. Clients MUST NOT call `session/load` unless it is advertised, so
|
|
35
|
+
* declining to advertise it is the conforming way to say so — and the same goes for
|
|
36
|
+
* `sessionCapabilities.resume`. Claiming either would be a promise this agent cannot keep.
|
|
37
|
+
*
|
|
38
|
+
* ## What carries over unchanged, because it must
|
|
39
|
+
*
|
|
40
|
+
* `session/request_permission` is wired to the approval gate, so [[EXT-54]]'s hole does not reopen
|
|
41
|
+
* on a second surface; and the process serves ONE workspace, refusing a `session/new` that names a
|
|
42
|
+
* different directory rather than silently re-rooting a live session's file tools. Both are
|
|
43
|
+
* properties of this agent rather than of a protocol version, so both hold here exactly as they do
|
|
44
|
+
* on v2.
|
|
45
|
+
*/
|
|
46
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
47
|
+
import type { AcpAgentAppOptions } from '#src/modules/acp/acpCommon.js';
|
|
48
|
+
/**
|
|
49
|
+
* Builds the ACP v1 agent app. Register-and-return only: nothing is connected and no config is
|
|
50
|
+
* read until a client connects and creates a session.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createAcpV1AgentApp(options?: AcpAgentAppOptions): acp.AgentApp;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Gaunt Sloth's ACP (Agent Client Protocol) **v1** agent, built on the official SDK's stable root
|
|
4
|
+
* export. (`acpAgentApp.ts` is the v2 half; `acpRouter.ts` picks between them.)
|
|
5
|
+
*
|
|
6
|
+
* ## Why v1 is served at all
|
|
7
|
+
*
|
|
8
|
+
* v1 is ACP's stable protocol; v2 is a draft. Every shipping editor that speaks ACP today speaks
|
|
9
|
+
* v1 — measured against Zed 1.15.0 stable, whose `initialize` carries `protocolVersion: 1` as a
|
|
10
|
+
* literal with no setting to change it, and whose build does not compile the draft-v2 API at all.
|
|
11
|
+
* An agent that serves only v2 cannot open a session with any of them.
|
|
12
|
+
*
|
|
13
|
+
* ## The shape is the same; the semantics are not
|
|
14
|
+
*
|
|
15
|
+
* The SDK's v1 entry point offers the same fluent app builder as v2 — `acp.agent()` returns an
|
|
16
|
+
* `AgentApp`, handlers are registered by ACP method name, and `connect()` serves either a transport
|
|
17
|
+
* or, in tests, a `ClientApp` directly. That similarity is a trap if it is mistaken for sameness.
|
|
18
|
+
* Four differences decide what this file does, and none of them can be reached by renaming a v2
|
|
19
|
+
* handler:
|
|
20
|
+
*
|
|
21
|
+
* 1. **`session/prompt` answers with the stop reason.** The turn runs INSIDE the request and the
|
|
22
|
+
* response is what tells the client it is over. v2 acknowledges immediately and reports the stop
|
|
23
|
+
* reason later on an idle `state_update`, which does not exist here.
|
|
24
|
+
* 2. **A cancelled turn is a `cancelled` stop reason, never an error.** The v1 spec is explicit
|
|
25
|
+
* that agents MUST catch the abort their libraries throw and answer with the stop reason,
|
|
26
|
+
* because clients show unrecognized errors to the user and a cancellation is not one.
|
|
27
|
+
* 3. **A FAILED turn is a JSON-RPC error**, because v1's `StopReason` union is closed —
|
|
28
|
+
* `end_turn | max_tokens | max_turn_requests | refusal | cancelled` — and none of them means
|
|
29
|
+
* "crashed". (This is exactly why v2 needed a custom `_error` reason: there, the response was
|
|
30
|
+
* already sent and the notification stream was the only channel left. Here the request is still
|
|
31
|
+
* open, so the error belongs in its response, and answering `end_turn` would be a lie.)
|
|
32
|
+
* 4. **No `session/load`, and no `loadSession` capability.** In v1 that capability means sessions
|
|
33
|
+
* survive restarts and can be picked up by another client instance; these sessions live in one
|
|
34
|
+
* process and die with it. Clients MUST NOT call `session/load` unless it is advertised, so
|
|
35
|
+
* declining to advertise it is the conforming way to say so — and the same goes for
|
|
36
|
+
* `sessionCapabilities.resume`. Claiming either would be a promise this agent cannot keep.
|
|
37
|
+
*
|
|
38
|
+
* ## What carries over unchanged, because it must
|
|
39
|
+
*
|
|
40
|
+
* `session/request_permission` is wired to the approval gate, so [[EXT-54]]'s hole does not reopen
|
|
41
|
+
* on a second surface; and the process serves ONE workspace, refusing a `session/new` that names a
|
|
42
|
+
* different directory rather than silently re-rooting a live session's file tools. Both are
|
|
43
|
+
* properties of this agent rather than of a protocol version, so both hold here exactly as they do
|
|
44
|
+
* on v2.
|
|
45
|
+
*/
|
|
46
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
47
|
+
import { randomUUID } from 'node:crypto';
|
|
48
|
+
import { resolve as resolvePath } from 'node:path';
|
|
49
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
50
|
+
import { MemorySaver } from '@langchain/langgraph';
|
|
51
|
+
import { GthAgentRunner } from '@gaunt-sloth/core/core/GthAgentRunner.js';
|
|
52
|
+
import { displayWarning } from '@gaunt-sloth/core/utils/consoleUtils.js';
|
|
53
|
+
import { createResolvers } from '#src/resolvers.js';
|
|
54
|
+
import { resolveAgentFactory } from '#src/core/resolveAgentFactory.js';
|
|
55
|
+
import { AcpV1UpdateMapper } from '#src/modules/acp/acpUpdatesV1.js';
|
|
56
|
+
import { decisionForOutcome } from '#src/modules/acp/acpPermissions.js';
|
|
57
|
+
import { permissionRequestForV1 } from '#src/modules/acp/acpPermissionsV1.js';
|
|
58
|
+
import { ACP_AGENT_NAME, ACP_AGENT_TITLE, CLOSE_TURN_DRAIN_MS, acpStatusCallback, agentVersion, drainWithDeadline, isSameWorkspace, loadConfigForCwd, promptText, resolveAcpSessionCommand, } from '#src/modules/acp/acpCommon.js';
|
|
59
|
+
/**
|
|
60
|
+
* Resolves with the `cancelled` permission outcome once `signal` aborts, and never otherwise.
|
|
61
|
+
*
|
|
62
|
+
* Used to race a permission request that the client may never answer. Resolving rather than
|
|
63
|
+
* rejecting is deliberate: the caller turns this into a gate DECISION, and a rejection there would
|
|
64
|
+
* be recorded as the gate failing rather than as the user's turn being stopped.
|
|
65
|
+
*/
|
|
66
|
+
function cancelledWhenAborted(signal) {
|
|
67
|
+
const cancelled = { outcome: 'cancelled' };
|
|
68
|
+
if (signal.aborted)
|
|
69
|
+
return Promise.resolve(cancelled);
|
|
70
|
+
return new Promise((resolveCancelled) => {
|
|
71
|
+
signal.addEventListener('abort', () => resolveCancelled(cancelled), { once: true });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Builds the ACP v1 agent app. Register-and-return only: nothing is connected and no config is
|
|
76
|
+
* read until a client connects and creates a session.
|
|
77
|
+
*/
|
|
78
|
+
export function createAcpV1AgentApp(options = {}) {
|
|
79
|
+
const loadConfig = options.loadConfig ?? loadConfigForCwd;
|
|
80
|
+
const agentFactoryFor = options.agentFactory ?? ((config) => resolveAgentFactory(config, 'lean'));
|
|
81
|
+
const sessions = new Map();
|
|
82
|
+
/** The workspace this process is serving, bound by the first `session/new`. */
|
|
83
|
+
let workspaceRoot;
|
|
84
|
+
/**
|
|
85
|
+
* `session/new` requests that have claimed the workspace and are still building their session.
|
|
86
|
+
*
|
|
87
|
+
* Counted so a release can tell "nothing depends on this binding" from "the session that depends
|
|
88
|
+
* on it does not exist yet". Without it, one `session/new` failing while a concurrent one is still
|
|
89
|
+
* loading would clear the root out from under the survivor, and a third request could then bind a
|
|
90
|
+
* different directory — the same race the claim closes, reopened by the cleanup.
|
|
91
|
+
*/
|
|
92
|
+
let pendingSessions = 0;
|
|
93
|
+
const releaseWorkspaceIfIdle = () => {
|
|
94
|
+
if (sessions.size === 0 && pendingSessions === 0)
|
|
95
|
+
workspaceRoot = undefined;
|
|
96
|
+
};
|
|
97
|
+
const sessionOrThrow = (sessionId) => {
|
|
98
|
+
const session = sessions.get(sessionId);
|
|
99
|
+
if (!session) {
|
|
100
|
+
throw acp.RequestError.invalidParams({ sessionId }, `No such session: ${sessionId}. Create one with session/new.`);
|
|
101
|
+
}
|
|
102
|
+
return session;
|
|
103
|
+
};
|
|
104
|
+
/** Send one `session/update`. Notifications, so nothing here waits on the client. */
|
|
105
|
+
const sendUpdate = async (session, update) => {
|
|
106
|
+
await session.client.notify(acp.CLIENT_METHODS.session_update, {
|
|
107
|
+
sessionId: session.sessionId,
|
|
108
|
+
update,
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Runs one prompt turn to completion and reports how it ended.
|
|
113
|
+
*
|
|
114
|
+
* Never rejects: the caller needs to answer the still-open `session/prompt` request, and it has
|
|
115
|
+
* to be able to tell a cancellation from a crash to do that. Progress goes out as
|
|
116
|
+
* `session/update` notifications; the outcome comes back here.
|
|
117
|
+
*/
|
|
118
|
+
const runTurn = async (session, prompt) => {
|
|
119
|
+
const mapper = new AcpV1UpdateMapper();
|
|
120
|
+
const abort = new AbortController();
|
|
121
|
+
session.abort = abort;
|
|
122
|
+
session.cancelled = false;
|
|
123
|
+
try {
|
|
124
|
+
// Inside the try, so a throw here is reported as a failed turn rather than leaving
|
|
125
|
+
// `session.abort` set by the prologue above and every later prompt refused as concurrent.
|
|
126
|
+
const text = promptText(prompt);
|
|
127
|
+
// No `user_message` echo. v1 replays a conversation only on `session/load`; during a turn the
|
|
128
|
+
// client already has the message it just sent, and echoing it would draw it twice.
|
|
129
|
+
//
|
|
130
|
+
// The gate's last hop. Registered per turn so it closes over THIS turn's mapper, which is
|
|
131
|
+
// what knows the id of the tool call a permission request is about.
|
|
132
|
+
session.runner.setToolApprovalCallback(async (pending) => {
|
|
133
|
+
const asked = session.client.request(acp.CLIENT_METHODS.session_request_permission, permissionRequestForV1({
|
|
134
|
+
sessionId: session.sessionId,
|
|
135
|
+
pending,
|
|
136
|
+
toolCallId: mapper.claimToolCallId(pending.name, pending.args),
|
|
137
|
+
}),
|
|
138
|
+
// The spec's cancellation cascade: when the turn is cancelled, outstanding permission
|
|
139
|
+
// requests are cancelled too, so the client can take its prompt off the screen instead of
|
|
140
|
+
// asking about work that has already stopped.
|
|
141
|
+
{ cancellationSignal: abort.signal });
|
|
142
|
+
// **The abort has to be able to end this wait on its own.** `cancellationSignal` sends the
|
|
143
|
+
// peer a `$/cancel_request` and then keeps waiting for its answer — cooperative by design.
|
|
144
|
+
// The spec says a client MUST answer an outstanding permission request with the `cancelled`
|
|
145
|
+
// outcome, but on v1 a client that does not would wedge the whole connection rather than
|
|
146
|
+
// just this turn: the `session/prompt` request is still open, so it would never be answered
|
|
147
|
+
// and the client would hold a pending request forever. (On v2 the prompt was acknowledged
|
|
148
|
+
// long before, which is why bounding the CLOSE was enough there and is not here.)
|
|
149
|
+
//
|
|
150
|
+
// Racing the signal ends the wait locally. `cancelled` is what the spec says the answer
|
|
151
|
+
// would have been, and {@link decisionForOutcome} turns it into a rejection — the same
|
|
152
|
+
// fail-closed polarity every other unrecognised answer gets.
|
|
153
|
+
const outcome = await Promise.race([
|
|
154
|
+
asked.then((response) => response.outcome),
|
|
155
|
+
cancelledWhenAborted(abort.signal),
|
|
156
|
+
]);
|
|
157
|
+
return decisionForOutcome(outcome);
|
|
158
|
+
});
|
|
159
|
+
for await (const event of session.runner.processMessagesWithEvents([new HumanMessage(text)], abort.signal)) {
|
|
160
|
+
for (const update of mapper.map(event))
|
|
161
|
+
await sendUpdate(session, update);
|
|
162
|
+
}
|
|
163
|
+
if (session.cancelled || abort.signal.aborted)
|
|
164
|
+
return { ok: true, stopReason: 'cancelled' };
|
|
165
|
+
return { ok: true, stopReason: 'end_turn' };
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
// A cancelled run surfaces as a thrown abort from whatever library noticed the signal first.
|
|
169
|
+
// The spec is explicit that this MUST NOT reach the client as an error: clients render
|
|
170
|
+
// unrecognized errors, and a user who pressed stop would be shown a failure for it.
|
|
171
|
+
if (session.cancelled || abort.signal.aborted)
|
|
172
|
+
return { ok: true, stopReason: 'cancelled' };
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
displayWarning(`ACP session ${session.sessionId}: turn failed — ${message}`);
|
|
175
|
+
return { ok: false, message };
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
session.runner.setToolApprovalCallback(null);
|
|
179
|
+
session.abort = null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const closeSession = async (session) => {
|
|
183
|
+
session.cancelled = true;
|
|
184
|
+
session.abort?.abort();
|
|
185
|
+
sessions.delete(session.sessionId);
|
|
186
|
+
// Wait for the aborted turn to finish unwinding before anything else. `cleanup()` does not do
|
|
187
|
+
// this — it drops the agent and returns — so releasing the workspace below without the wait
|
|
188
|
+
// would let a `session/new` for another directory reassign INIT_CWD out from under a turn still
|
|
189
|
+
// running its last tool call.
|
|
190
|
+
//
|
|
191
|
+
// BOUNDED, for the reason CLOSE_TURN_DRAIN_MS gives: a turn parked on a permission request is
|
|
192
|
+
// waiting on the client, and no abort of ours can force that to settle.
|
|
193
|
+
await drainWithDeadline(session.turn, CLOSE_TURN_DRAIN_MS);
|
|
194
|
+
try {
|
|
195
|
+
await session.runner.cleanup();
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
displayWarning(`ACP session ${session.sessionId}: cleanup failed — ${error instanceof Error ? error.message : String(error)}`);
|
|
199
|
+
}
|
|
200
|
+
releaseWorkspaceIfIdle();
|
|
201
|
+
};
|
|
202
|
+
return acp
|
|
203
|
+
.agent({ name: ACP_AGENT_NAME })
|
|
204
|
+
.onRequest(acp.AGENT_METHODS.initialize, () => ({
|
|
205
|
+
// The version the CLIENT asked for, which the router has already checked this app can serve.
|
|
206
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
207
|
+
agentInfo: { name: ACP_AGENT_NAME, title: ACP_AGENT_TITLE, version: agentVersion() },
|
|
208
|
+
agentCapabilities: {
|
|
209
|
+
// `loadSession` and `sessionCapabilities.resume` are deliberately absent: both promise a
|
|
210
|
+
// session that outlives the process, and these do not. `promptCapabilities` is absent for
|
|
211
|
+
// the same reason — omitted means the v1 baseline of text and resource links, which is
|
|
212
|
+
// exactly what this agent reads; claiming `image`, `audio` or `embeddedContext` would
|
|
213
|
+
// invite content it can only describe back.
|
|
214
|
+
sessionCapabilities: { list: {}, close: {} },
|
|
215
|
+
},
|
|
216
|
+
}))
|
|
217
|
+
.onRequest(acp.AGENT_METHODS.session_new, async ({ params, client }) => {
|
|
218
|
+
const cwd = resolvePath(params.cwd);
|
|
219
|
+
if (workspaceRoot !== undefined && !isSameWorkspace(workspaceRoot, cwd)) {
|
|
220
|
+
throw acp.RequestError.invalidParams({ cwd: params.cwd, workspaceRoot }, `This agent process is serving ${workspaceRoot}. Start a separate agent process for ${cwd}.`);
|
|
221
|
+
}
|
|
222
|
+
// **Claim the workspace BEFORE the first await, or the check above decides nothing.** Nothing
|
|
223
|
+
// serialises inbound requests on a stdio connection, so two `session/new` calls naming
|
|
224
|
+
// different directories would both see an unset root, both suspend on the config load, and
|
|
225
|
+
// both assign. Testing and assigning either side of an await is not a check.
|
|
226
|
+
workspaceRoot = cwd;
|
|
227
|
+
pendingSessions += 1;
|
|
228
|
+
try {
|
|
229
|
+
const config = await loadConfig(cwd);
|
|
230
|
+
const runner = new GthAgentRunner(acpStatusCallback, options.resolvers ?? createResolvers(), agentFactoryFor(config));
|
|
231
|
+
// The command a run is resolved under decides its toolset, its mode prompt and its
|
|
232
|
+
// approvals posture. For an editor session that is `acp.mode`, defaulting to `code`.
|
|
233
|
+
//
|
|
234
|
+
// **The checkpoint saver is not optional here, even though `init` accepts none.** A gated
|
|
235
|
+
// tool call suspends the graph on a LangGraph interrupt, and an interrupt with nowhere to
|
|
236
|
+
// checkpoint throws `MISSING_CHECKPOINTER` instead of asking the client for permission — so
|
|
237
|
+
// without one every shell command in an editor session fails while the ungated built-ins
|
|
238
|
+
// keep working. `MemorySaver` is the right lifetime as well as the right shape: these
|
|
239
|
+
// sessions live in one process and die with it, which is also why `loadSession` is not
|
|
240
|
+
// advertised.
|
|
241
|
+
//
|
|
242
|
+
// **Per session, not per process.** Each session owns its runner, its workspace and the
|
|
243
|
+
// graphs suspended in it, and `sessions` holds several at once; one shared saver would pool
|
|
244
|
+
// every session's checkpoint threads, so a `session/close` could clear state another
|
|
245
|
+
// session is still parked on.
|
|
246
|
+
await runner.init(resolveAcpSessionCommand(config), config, new MemorySaver());
|
|
247
|
+
const sessionId = randomUUID();
|
|
248
|
+
sessions.set(sessionId, {
|
|
249
|
+
sessionId,
|
|
250
|
+
cwd,
|
|
251
|
+
runner,
|
|
252
|
+
client,
|
|
253
|
+
abort: null,
|
|
254
|
+
turn: null,
|
|
255
|
+
cancelled: false,
|
|
256
|
+
});
|
|
257
|
+
return { sessionId };
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
pendingSessions -= 1;
|
|
261
|
+
// A `session/new` that never produced a session must not leave the process bound to its
|
|
262
|
+
// directory — one bad config would otherwise refuse every later session for the life of the
|
|
263
|
+
// agent.
|
|
264
|
+
releaseWorkspaceIfIdle();
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
.onRequest(acp.AGENT_METHODS.session_list, ({ params }) => {
|
|
268
|
+
const filter = params.cwd === undefined || params.cwd === null ? undefined : params.cwd;
|
|
269
|
+
return {
|
|
270
|
+
sessions: [...sessions.values()]
|
|
271
|
+
.filter((session) => filter === undefined || session.cwd === resolvePath(filter))
|
|
272
|
+
.map((session) => ({ sessionId: session.sessionId, cwd: session.cwd })),
|
|
273
|
+
};
|
|
274
|
+
})
|
|
275
|
+
.onRequest(acp.AGENT_METHODS.session_close, async ({ params }) => {
|
|
276
|
+
// Closing MUST cancel whatever is running, exactly as session/cancel would, before the
|
|
277
|
+
// session's resources go away.
|
|
278
|
+
await closeSession(sessionOrThrow(params.sessionId));
|
|
279
|
+
return {};
|
|
280
|
+
})
|
|
281
|
+
.onRequest(acp.AGENT_METHODS.session_prompt, async ({ params }) => {
|
|
282
|
+
const session = sessionOrThrow(params.sessionId);
|
|
283
|
+
if (session.abort) {
|
|
284
|
+
throw acp.RequestError.invalidRequest({ sessionId: params.sessionId }, 'A prompt is already running in this session. Wait for it to answer.');
|
|
285
|
+
}
|
|
286
|
+
// Started synchronously and recorded before anything can suspend: `runTurn` assigns
|
|
287
|
+
// `session.abort` before its first await, and `session.turn` is what a concurrent
|
|
288
|
+
// `session/close` waits on. A gap here is a close that skips the drain.
|
|
289
|
+
const turn = runTurn(session, params.prompt);
|
|
290
|
+
session.turn = turn.then(() => undefined);
|
|
291
|
+
const outcome = await turn;
|
|
292
|
+
if (!outcome.ok) {
|
|
293
|
+
throw acp.RequestError.internalError({ sessionId: session.sessionId }, `The agent could not complete this turn: ${outcome.message}`);
|
|
294
|
+
}
|
|
295
|
+
return { stopReason: outcome.stopReason };
|
|
296
|
+
})
|
|
297
|
+
.onNotification(acp.AGENT_METHODS.session_cancel, ({ params }) => {
|
|
298
|
+
const session = sessions.get(params.sessionId);
|
|
299
|
+
if (!session)
|
|
300
|
+
return;
|
|
301
|
+
// Recorded as well as aborted: the abort makes the run stop, and the flag is what makes the
|
|
302
|
+
// turn answer `cancelled` instead of reading its own truncated stream as a normal finish.
|
|
303
|
+
session.cancelled = true;
|
|
304
|
+
session.abort?.abort();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=acpAgentAppV1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acpAgentAppV1.js","sourceRoot":"","sources":["../../../src/modules/acp/acpAgentAppV1.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* The parts of Gaunt Sloth's ACP surface that are the same in **both** protocol dialects.
|
|
4
|
+
*
|
|
5
|
+
* The agent serves ACP v1 and ACP v2 from one process (`acpRouter.ts` picks between them on the
|
|
6
|
+
* first message). Two dialects mean two sets of handlers, and everything that is genuinely about
|
|
7
|
+
* the protocol version belongs in those. Everything else lives here — the workspace binding's
|
|
8
|
+
* comparison rule, the config load that roots a session, the version reported in a handshake, the
|
|
9
|
+
* bounded drain a close waits on, and, most importantly, the **untrusted-attachment fencing** a
|
|
10
|
+
* prompt goes through.
|
|
11
|
+
*
|
|
12
|
+
* **The fencing is here rather than duplicated because a second copy is how one of them ends up
|
|
13
|
+
* missing an arm.** It is the one piece on this surface that is a security boundary rather than a
|
|
14
|
+
* convenience: client-supplied names, URIs, descriptions and block types are interpolated into the
|
|
15
|
+
* same message as the user's own words, and the defang → cap → collapse → fence pipeline is what
|
|
16
|
+
* keeps them from impersonating the agent's instructions. A dialect that grew its own copy would
|
|
17
|
+
* be one review away from diverging.
|
|
18
|
+
*/
|
|
19
|
+
import type { GthConfig } from '@gaunt-sloth/core/config.js';
|
|
20
|
+
import type { AgentResolvers, GthAgentFactory, GthCommand, StatusUpdateCallback } from '@gaunt-sloth/core/core/types.js';
|
|
21
|
+
/** How this agent identifies itself in the `initialize` response. */
|
|
22
|
+
export declare const ACP_AGENT_NAME = "gaunt-sloth";
|
|
23
|
+
/** Human-facing name for the same, used where a client shows a title rather than an id. */
|
|
24
|
+
export declare const ACP_AGENT_TITLE = "Gaunt Sloth";
|
|
25
|
+
/**
|
|
26
|
+
* How long `session/close` waits for an aborted turn to finish unwinding before proceeding anyway.
|
|
27
|
+
*
|
|
28
|
+
* **The wait is bounded because it cannot be trusted to end.** A turn parked on
|
|
29
|
+
* `session/request_permission` is waiting on the CLIENT, and ACP cancellation is cooperative: the
|
|
30
|
+
* `$/cancel_request` we send settles the promise only when the peer answers it. A client that closes
|
|
31
|
+
* a session while its own permission prompt is on screen and then never answers would otherwise
|
|
32
|
+
* leave this handler suspended forever — and `session/close` would never get a response, which is a
|
|
33
|
+
* worse failure than the narrow re-rooting window the wait exists to close.
|
|
34
|
+
*
|
|
35
|
+
* On expiry the close proceeds exactly as it did before the wait existed. So the bound degrades to
|
|
36
|
+
* the previously accepted behaviour rather than to a hang, and an aborted turn — which unwinds in
|
|
37
|
+
* milliseconds — is unaffected.
|
|
38
|
+
*/
|
|
39
|
+
export declare const CLOSE_TURN_DRAIN_MS = 2000;
|
|
40
|
+
/** Seams the tests replace; production leaves every one of them at its default. */
|
|
41
|
+
export interface AcpAgentAppOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Loads the effective gth config for a session rooted at `cwd`. Defaults to pointing config
|
|
44
|
+
* discovery at the session workspace and running the normal loader.
|
|
45
|
+
*/
|
|
46
|
+
loadConfig?: (cwd: string) => Promise<GthConfig>;
|
|
47
|
+
/**
|
|
48
|
+
* The agent backend for a session. Defaults to `resolveAgentFactory`, i.e. the lean agent
|
|
49
|
+
* — the same backend every other command resolves, reached through the same seam rather than
|
|
50
|
+
* named here, so a future backend choice stays a single edit.
|
|
51
|
+
*/
|
|
52
|
+
agentFactory?: (config: GthConfig) => GthAgentFactory;
|
|
53
|
+
/** Tool/content resolvers handed to the runner. Defaults to `createResolvers`. */
|
|
54
|
+
resolvers?: AgentResolvers;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether two already-resolved absolute paths name the same workspace.
|
|
58
|
+
*
|
|
59
|
+
* **Case-insensitive on win32 and nowhere else**, because that is where the answer differs: NTFS is
|
|
60
|
+
* case-insensitive and case-preserving, so `C:\Foo` and `c:\foo` are one directory and an exact
|
|
61
|
+
* string compare calls them two — a client that re-sends its own `cwd` with different casing would
|
|
62
|
+
* be told to start a second agent process for the project it is already in. On POSIX the two really
|
|
63
|
+
* are different directories and must keep comparing unequal, so the platform is the whole
|
|
64
|
+
* distinction rather than a workaround for one.
|
|
65
|
+
*
|
|
66
|
+
* `platform` is a parameter with the live value as its default so both arms are testable on any
|
|
67
|
+
* host. Every other bug of this shape in this repo (OPS-27, EXT-38, GS2-42, EXT-16) was a POSIX-only
|
|
68
|
+
* assertion that passed everywhere except the Windows cell, and a test that can only run on win32
|
|
69
|
+
* would have the same blind spot pointed the other way.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isSameWorkspace(a: string, b: string, platform?: NodeJS.Platform): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Points config discovery — and with it the filesystem toolkit's allowed root, grep's boundary and
|
|
74
|
+
* the shell tool's spawn directory — at the ACP session's workspace.
|
|
75
|
+
*
|
|
76
|
+
* All of them read `getCurrentWorkDir()`, which prefers `INIT_CWD`. In a normal CLI run that is
|
|
77
|
+
* npm's, and correct. In an editor-spawned agent it is whatever the install shell left behind, and
|
|
78
|
+
* pointing an agent's file tools at a stale directory is worse than any error: it reads and writes
|
|
79
|
+
* real files in the wrong project. Setting it to the workspace the CLIENT named replaces a guess
|
|
80
|
+
* with the one authoritative value, and does it without `process.chdir()`, which in a process
|
|
81
|
+
* serving several sessions would be a race rather than a fix.
|
|
82
|
+
*/
|
|
83
|
+
export declare function loadConfigForCwd(cwd: string): Promise<GthConfig>;
|
|
84
|
+
/**
|
|
85
|
+
* The command an ACP session is resolved under — `acp.mode`, defaulting to **`code`**.
|
|
86
|
+
*
|
|
87
|
+
* An editor connects to get an agent that can do the job. Resolving its sessions under `chat`
|
|
88
|
+
* hands it `filesystem: 'read'` and, because `filterDevTools` only builds the toolkit for `code`
|
|
89
|
+
* and `exec`, no shell and no dev tools at all: a read-only agent in a place nobody asked for one.
|
|
90
|
+
* `code` is the same default the bare `gth` CLI already resolves to.
|
|
91
|
+
*
|
|
92
|
+
* **Why `acp.mode` naming an existing command, and not a `commands.acp` block.** `runner.init`
|
|
93
|
+
* takes a {@link GthCommand}, and two pieces of tool gating branch on that union — `filterDevTools`
|
|
94
|
+
* (`command !== 'code' && command !== 'exec'`) and `GthDevToolkit`, which resolves the shell
|
|
95
|
+
* default for the active mode. An `acp` command would have to join the union and every one of
|
|
96
|
+
* those branches would have to learn the new member, with a silently wrong default wherever one was
|
|
97
|
+
* missed. A `mode` whose value is an existing command passes something those branches already
|
|
98
|
+
* handle. `ask --write` is mapped the same way, to `'code'` at the call boundary, rather than
|
|
99
|
+
* becoming a command of its own. The return type is what enforces it: a mode that is not a
|
|
100
|
+
* `GthCommand` does not compile.
|
|
101
|
+
*
|
|
102
|
+
* **Both dialects call this**, so the v1 and v2 apps cannot drift on the answer.
|
|
103
|
+
*/
|
|
104
|
+
export declare function resolveAcpSessionCommand(config: GthConfig): GthCommand;
|
|
105
|
+
/**
|
|
106
|
+
* Status output from a session goes to the console utilities, which this surface has already
|
|
107
|
+
* routed away from stdout (see `acpStdio.ts`) — stdout belongs to the JSON-RPC framing.
|
|
108
|
+
*
|
|
109
|
+
* Only warnings and errors are forwarded. The rest of a run's status chatter is already reported
|
|
110
|
+
* to the client as `session/update` notifications, where the editor can render it; duplicating it
|
|
111
|
+
* into the agent's stderr would make the log a second, worse copy of the transcript.
|
|
112
|
+
*/
|
|
113
|
+
export declare const acpStatusCallback: StatusUpdateCallback;
|
|
114
|
+
/**
|
|
115
|
+
* This build's version, for the `initialize` handshake, or `unknown` when it cannot be read.
|
|
116
|
+
*
|
|
117
|
+
* `getSlothVersion()` reads the package manifest under the install dir, which an entry point has
|
|
118
|
+
* to have registered. Both ACP doors do. It is caught anyway because failing the HANDSHAKE over a
|
|
119
|
+
* display string would take the whole agent down for a piece of metadata — a host that cannot
|
|
120
|
+
* connect is a far worse outcome than one that shows an unknown version. The bins' own spec pins
|
|
121
|
+
* that the real path reports a real version, so this fallback cannot become the normal answer
|
|
122
|
+
* without something going red.
|
|
123
|
+
*/
|
|
124
|
+
export declare function agentVersion(): string;
|
|
125
|
+
/**
|
|
126
|
+
* Waits for `work` to settle, giving up after `ms`. Never rejects — the caller is tearing a session
|
|
127
|
+
* down, and a failure in what it is waiting on changes nothing about that.
|
|
128
|
+
*
|
|
129
|
+
* The timer is cleared on the winning path and unreferenced regardless, so a close cannot leave a
|
|
130
|
+
* pending timer holding the process open.
|
|
131
|
+
*/
|
|
132
|
+
export declare function drainWithDeadline(work: Promise<void> | null, ms: number): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* The least a prompt content block is guaranteed to be in either dialect.
|
|
135
|
+
*
|
|
136
|
+
* v1 and v2 declare their own `ContentBlock` unions and they are not the same type, but every
|
|
137
|
+
* member of both carries a `type` discriminator and nothing below reads a field without checking
|
|
138
|
+
* it first. Typing the seam this way is what lets one implementation of the fencing serve both,
|
|
139
|
+
* which is the point of keeping it here.
|
|
140
|
+
*/
|
|
141
|
+
export interface AcpContentBlockLike {
|
|
142
|
+
readonly type: string;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The prompt as the text handed to the model: the user's own words, then — when the client attached
|
|
146
|
+
* anything else — ONE fenced section describing what arrived.
|
|
147
|
+
*
|
|
148
|
+
* **The user's `text` blocks stay unfenced and everything else is fenced.** That split is the whole
|
|
149
|
+
* design: the text IS the user's instruction and always was, while a resource link's metadata comes
|
|
150
|
+
* from the editor, the filesystem or an MCP server, and a block's `type` is whatever the client put
|
|
151
|
+
* on the wire. Interpolating those into the same prose would put attacker-influenceable bytes in
|
|
152
|
+
* the model's context with no marker of provenance and a structural marker they could forge — the
|
|
153
|
+
* thing `acpPermissions.ts` says this surface must not do.
|
|
154
|
+
*
|
|
155
|
+
* The framing line and the closing reassertion are first-party and sit OUTSIDE the fence, so the
|
|
156
|
+
* last thing the model reads about the attachments is this agent's own authority rather than the
|
|
157
|
+
* client's text. That is the same shape `appendMcpServerInstructionsNote` uses, deliberately: a
|
|
158
|
+
* second shape for the same problem is how one of them ends up missing an arm.
|
|
159
|
+
*/
|
|
160
|
+
export declare function promptText(prompt: readonly AcpContentBlockLike[]): string;
|
|
161
|
+
/**
|
|
162
|
+
* Kept for the entry points, which announce themselves on stderr before serving.
|
|
163
|
+
*
|
|
164
|
+
* Both protocol versions are named because both are served, and the version a host gets is the
|
|
165
|
+
* one it asks for. A notice claiming a single dialect is how the wrong one ends up believed.
|
|
166
|
+
*/
|
|
167
|
+
export declare function announceAcpStart(): void;
|