salmon-loop 0.2.16 → 0.3.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/dist/cli/authorization/non-interactive.js +7 -21
- package/dist/cli/commands/chat.js +1 -1
- package/dist/cli/commands/parallel.js +46 -41
- package/dist/cli/commands/run/assistant-message.js +3 -0
- package/dist/cli/commands/run/handler.js +2 -1
- package/dist/cli/commands/serve.js +112 -156
- package/dist/cli/headless/json-protocol.js +1 -1
- package/dist/cli/headless/stream-json-protocol.js +3 -2
- package/dist/cli/program-bootstrap.js +2 -2
- package/dist/cli/slash/runtime.js +5 -1
- package/dist/core/adapters/fs/node-fs.js +1 -0
- package/dist/core/backends/salmon-loop/task-executor.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +1 -1
- package/dist/core/context/service.js +5 -2
- package/dist/core/extensions/index.js +2 -35
- package/dist/core/extensions/merge.js +14 -0
- package/dist/core/extensions/redact.js +9 -3
- package/dist/core/extensions/schemas.js +2 -51
- package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
- package/dist/core/facades/cli-program-bootstrap.js +1 -0
- package/dist/core/facades/cli-serve.js +2 -1
- package/dist/core/grizzco/dsl/strategies.js +1 -3
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
- package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
- package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
- package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
- package/dist/core/grizzco/steps/apply.js +0 -7
- package/dist/core/grizzco/steps/autopilot.js +108 -6
- package/dist/core/grizzco/steps/preflight.js +10 -0
- package/dist/core/grizzco/steps/tool-runtime.js +1 -0
- package/dist/core/interaction/events/bus.js +14 -0
- package/dist/core/interaction/orchestration/facade.js +11 -1
- package/dist/core/llm/ai-sdk/request-params.js +40 -1
- package/dist/core/mcp/bridge/index.js +4 -0
- package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
- package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
- package/dist/core/mcp/bridge/tool-bridge.js +303 -0
- package/dist/core/mcp/cache/resource-cache.js +41 -0
- package/dist/core/mcp/catalog/discovery.js +51 -0
- package/dist/core/mcp/catalog/notification-router.js +28 -0
- package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
- package/dist/core/mcp/catalog/resource-catalog.js +7 -0
- package/dist/core/mcp/catalog/tool-catalog.js +4 -0
- package/dist/core/mcp/client/connection-manager.js +239 -0
- package/dist/core/mcp/client/lifecycle.js +13 -0
- package/dist/core/mcp/client/transport-factory.js +168 -0
- package/dist/core/mcp/config/index.js +32 -0
- package/dist/core/mcp/config/schema-v2.js +129 -0
- package/dist/core/mcp/host/elicitation-provider.js +209 -0
- package/dist/core/mcp/host/roots-provider.js +70 -0
- package/dist/core/mcp/host/sampling-provider.js +170 -0
- package/dist/core/mcp/index.js +4 -0
- package/dist/core/mcp/observability/events.js +19 -0
- package/dist/core/mcp/policy/approval-policy.js +2 -0
- package/dist/core/mcp/policy/classifier.js +172 -0
- package/dist/core/mcp/policy/grants.js +356 -0
- package/dist/core/mcp/policy/uri-policy.js +60 -0
- package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
- package/dist/core/mcp/types.js +2 -0
- package/dist/core/protocols/a2a/agent-card.js +38 -12
- package/dist/core/protocols/a2a/sdk/executor.js +105 -36
- package/dist/core/protocols/a2a/sdk/server.js +1311 -3
- package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
- package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
- package/dist/core/protocols/acp/acp-types.js +17 -0
- package/dist/core/protocols/acp/formal-agent.js +389 -502
- package/dist/core/protocols/acp/handlers.js +3 -0
- package/dist/core/protocols/acp/permission-provider.js +11 -39
- package/dist/core/protocols/acp/stdio-server.js +20 -1
- package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
- package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
- package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
- package/dist/core/public-capabilities/projections.js +1 -0
- package/dist/core/runtime/agent-server-runtime.js +2 -3
- package/dist/core/runtime/spawn-command.js +8 -2
- package/dist/core/runtime/spawn-interactive.js +26 -0
- package/dist/core/session/manager.js +48 -25
- package/dist/core/tools/builtin/index.js +6 -1
- package/dist/core/tools/builtin/proposal.js +0 -7
- package/dist/core/tools/builtin/workspace.js +76 -0
- package/dist/core/tools/dispatcher.js +1 -0
- package/dist/core/tools/loader.js +92 -46
- package/dist/core/verification/runner.js +60 -31
- package/dist/core/version.js +17 -0
- package/dist/core/workspace/capabilities.js +80 -0
- package/dist/locales/en.js +17 -3
- package/package.json +4 -2
- package/dist/core/protocols/a2a/mapper.js +0 -14
- package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
- package/dist/core/protocols/a2a/task-projection.js +0 -45
- package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
- package/dist/core/tools/mcp/client.js +0 -308
- package/dist/core/tools/mcp/loader.js +0 -110
- package/dist/core/tools/mcp/schema.js +0 -54
- package/dist/core/tools/mcp/streamable-http.js +0 -101
- package/dist/core/tools/mcp/types.js +0 -26
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
1
|
import { PROTOCOL_VERSION, RequestError, } from '@agentclientprotocol/sdk';
|
|
3
2
|
import { text } from '../../../locales/index.js';
|
|
4
|
-
import { mkdir, open, readFile, rename, stat, unlink, writeFile, } from '../../adapters/fs/node-fs.js';
|
|
5
3
|
import { defaultPathAdapter } from '../../adapters/path/path-adapter.js';
|
|
6
4
|
import { inferTurnStopReasonFromFailure } from '../../interaction/turn-stop-reason.js';
|
|
7
5
|
import { recordAuditEvent } from '../../observability/audit-trail.js';
|
|
8
|
-
import { readPlan } from '../../plan/index.js';
|
|
9
6
|
import { toAcpPublicModes } from '../../public-capabilities/projections.js';
|
|
10
7
|
import { buildPublicCapabilityRegistry } from '../../public-capabilities/registry.js';
|
|
11
8
|
import { parseSlashInput } from '../../slash/parser.js';
|
|
9
|
+
import { Phase } from '../../types/runtime.js';
|
|
12
10
|
import { buildCanonicalExecutionRequest } from '../shared/execution-request.js';
|
|
13
11
|
import { parseAcpFlowMode } from '../shared/flow-mode-mapping.js';
|
|
12
|
+
import { probeCheckpoint, probeCheckpointForNewSession, } from './acp-checkpoint-probe.js';
|
|
14
13
|
import { createAcpCommandRunner } from './acp-command-runner.js';
|
|
15
14
|
import { createAcpFileSystem } from './acp-filesystem.js';
|
|
15
|
+
import { createAcpSessionPersistence } from './acp-session-persistence.js';
|
|
16
|
+
import { ACP_PERMISSION_POLICY_ASK, ACP_PERMISSION_POLICY_ALLOW_ALL, ACP_PERMISSION_POLICY_DENY_ALL, hashRepoPath, isPermissionPolicyValue, parseTimestamp, } from './acp-types.js';
|
|
16
17
|
import { createAcpSessionStore, isTerminalTaskEvent } from './handlers.js';
|
|
17
18
|
import { createAcpToolAuthorizationProvider } from './permission-provider.js';
|
|
19
|
+
import { mapToolKind } from './tool-kind-mapping.js';
|
|
18
20
|
function formatInputRequiredMessage(inputRequired) {
|
|
19
21
|
if (!inputRequired || !Array.isArray(inputRequired.questions))
|
|
20
22
|
return null;
|
|
@@ -36,9 +38,6 @@ function formatInputRequiredMessage(inputRequired) {
|
|
|
36
38
|
}
|
|
37
39
|
const ACP_PERMISSION_POLICY_CONFIG_ID = '_salmonloop_permission_policy';
|
|
38
40
|
const ACP_MODE_CONFIG_ID = '_salmonloop_mode';
|
|
39
|
-
const ACP_PERMISSION_POLICY_ASK = 'ask';
|
|
40
|
-
const ACP_PERMISSION_POLICY_DENY_ALL = 'deny_all';
|
|
41
|
-
const ACP_PERMISSION_POLICY_ALLOW_ALL = 'allow_all';
|
|
42
41
|
const ACP_DEFAULT_MODE_ID = 'autopilot';
|
|
43
42
|
const ACP_SESSION_STORE_MAX_ENTRIES = 200;
|
|
44
43
|
const ACP_SESSION_STORE_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 30;
|
|
@@ -46,6 +45,8 @@ const ACP_SESSION_STORE_LOCK_STALE_MS = 1000 * 30;
|
|
|
46
45
|
const ACP_SESSION_STORE_LOCK_HEARTBEAT_MS = 1000 * 5;
|
|
47
46
|
const ACP_SESSION_STORE_LOCK_ACQUIRE_TIMEOUT_MS = 1000 * 5;
|
|
48
47
|
const ACP_SESSION_HISTORY_MAX_ENTRIES = 40;
|
|
48
|
+
const ACP_SESSION_LIST_PAGE_SIZE = 50;
|
|
49
|
+
const ACP_SUPPORTED_PROTOCOL_VERSIONS = new Set([PROTOCOL_VERSION]);
|
|
49
50
|
function isAbsolutePath(filePath) {
|
|
50
51
|
if (defaultPathAdapter.isAbsolute(filePath))
|
|
51
52
|
return true;
|
|
@@ -86,6 +87,33 @@ function buildJsonResourceContentBlock(data) {
|
|
|
86
87
|
},
|
|
87
88
|
};
|
|
88
89
|
}
|
|
90
|
+
function encodeSessionListCursor(input) {
|
|
91
|
+
return `${input.offset}:${input.cwd ?? ''}`;
|
|
92
|
+
}
|
|
93
|
+
function decodeSessionListCursor(cursor) {
|
|
94
|
+
const colon = cursor.indexOf(':');
|
|
95
|
+
if (colon < 0) {
|
|
96
|
+
throw new RequestError(-32602, 'Invalid params: invalid session/list cursor');
|
|
97
|
+
}
|
|
98
|
+
const offset = Number(cursor.slice(0, colon));
|
|
99
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
100
|
+
throw new RequestError(-32602, 'Invalid params: invalid session/list cursor');
|
|
101
|
+
}
|
|
102
|
+
const cwd = cursor.slice(colon + 1) || null;
|
|
103
|
+
return { offset, cwd };
|
|
104
|
+
}
|
|
105
|
+
function isReplayableSessionContentBlock(block) {
|
|
106
|
+
if (!block || typeof block !== 'object')
|
|
107
|
+
return false;
|
|
108
|
+
switch (block.type) {
|
|
109
|
+
case 'text':
|
|
110
|
+
return typeof block.text === 'string';
|
|
111
|
+
case 'resource_link':
|
|
112
|
+
return typeof block.name === 'string' && typeof block.uri === 'string';
|
|
113
|
+
default:
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
89
117
|
const ACP_AVAILABLE_COMMANDS = [
|
|
90
118
|
{ name: 'help', description: text.acp.slashHelpDescription },
|
|
91
119
|
];
|
|
@@ -103,6 +131,21 @@ function formatResourceLink(block) {
|
|
|
103
131
|
const description = block.description ? ` - ${block.description}` : '';
|
|
104
132
|
return `Resource: ${title} (${block.uri})${description}`;
|
|
105
133
|
}
|
|
134
|
+
function formatEmbeddedResource(block) {
|
|
135
|
+
const resource = block.resource;
|
|
136
|
+
const uri = typeof resource.uri === 'string' ? resource.uri : 'embedded-resource';
|
|
137
|
+
const mimeType = typeof resource.mimeType === 'string' ? resource.mimeType : undefined;
|
|
138
|
+
if (typeof resource.text === 'string') {
|
|
139
|
+
const header = mimeType
|
|
140
|
+
? `Embedded resource: ${uri} (${mimeType})`
|
|
141
|
+
: `Embedded resource: ${uri}`;
|
|
142
|
+
return `${header}\n${resource.text}`;
|
|
143
|
+
}
|
|
144
|
+
const header = mimeType
|
|
145
|
+
? `Embedded binary resource: ${uri} (${mimeType})`
|
|
146
|
+
: `Embedded binary resource: ${uri}`;
|
|
147
|
+
return header;
|
|
148
|
+
}
|
|
106
149
|
function extractTextFromPrompt(prompt, capabilities) {
|
|
107
150
|
const parts = [];
|
|
108
151
|
for (const block of prompt) {
|
|
@@ -115,18 +158,19 @@ function extractTextFromPrompt(prompt, capabilities) {
|
|
|
115
158
|
break;
|
|
116
159
|
case 'image':
|
|
117
160
|
if (!capabilities.image) {
|
|
118
|
-
throw new RequestError(-
|
|
161
|
+
throw new RequestError(-32602, 'Prompt content type image is not supported');
|
|
119
162
|
}
|
|
120
163
|
break;
|
|
121
164
|
case 'audio':
|
|
122
165
|
if (!capabilities.audio) {
|
|
123
|
-
throw new RequestError(-
|
|
166
|
+
throw new RequestError(-32602, 'Prompt content type audio is not supported');
|
|
124
167
|
}
|
|
125
168
|
break;
|
|
126
169
|
case 'resource':
|
|
127
170
|
if (!capabilities.embeddedContext) {
|
|
128
|
-
throw new RequestError(-
|
|
171
|
+
throw new RequestError(-32602, 'Prompt content type resource is not supported');
|
|
129
172
|
}
|
|
173
|
+
parts.push(formatEmbeddedResource(block));
|
|
130
174
|
break;
|
|
131
175
|
default:
|
|
132
176
|
throw new RequestError(-32602, 'Invalid params: unsupported content block type');
|
|
@@ -134,46 +178,6 @@ function extractTextFromPrompt(prompt, capabilities) {
|
|
|
134
178
|
}
|
|
135
179
|
return parts.join('\n');
|
|
136
180
|
}
|
|
137
|
-
function mapToolKind(toolName, intent) {
|
|
138
|
-
if (intent) {
|
|
139
|
-
switch (intent.toUpperCase()) {
|
|
140
|
-
case 'READ':
|
|
141
|
-
return 'read';
|
|
142
|
-
case 'LIST':
|
|
143
|
-
return 'read';
|
|
144
|
-
case 'SEARCH':
|
|
145
|
-
return 'search';
|
|
146
|
-
case 'WRITE':
|
|
147
|
-
return 'edit';
|
|
148
|
-
case 'INFRA':
|
|
149
|
-
return 'execute';
|
|
150
|
-
case 'AGENT':
|
|
151
|
-
return 'think';
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const name = toolName.toLowerCase();
|
|
155
|
-
if (name.includes('read') ||
|
|
156
|
-
name.includes('get') ||
|
|
157
|
-
name.includes('view') ||
|
|
158
|
-
name.includes('ls') ||
|
|
159
|
-
name.includes('list'))
|
|
160
|
-
return 'read';
|
|
161
|
-
if (name.includes('write') || name.includes('edit') || name.includes('patch'))
|
|
162
|
-
return 'edit';
|
|
163
|
-
if (name.includes('delete') || name.includes('remove') || name.includes('rm'))
|
|
164
|
-
return 'delete';
|
|
165
|
-
if (name.includes('move') || name.includes('rename') || name.includes('mv'))
|
|
166
|
-
return 'move';
|
|
167
|
-
if (name.includes('grep') || name.includes('search') || name.includes('find'))
|
|
168
|
-
return 'search';
|
|
169
|
-
if (name.includes('run') || name.includes('exec') || name.includes('spawn'))
|
|
170
|
-
return 'execute';
|
|
171
|
-
if (name.includes('plan') || name.includes('think') || name.includes('reason'))
|
|
172
|
-
return 'think';
|
|
173
|
-
if (name.includes('fetch') || name.includes('curl') || name.includes('http'))
|
|
174
|
-
return 'fetch';
|
|
175
|
-
return 'other';
|
|
176
|
-
}
|
|
177
181
|
function buildToolCallContent(textValue) {
|
|
178
182
|
return [{ type: 'content', content: buildTextContentBlock(textValue) }];
|
|
179
183
|
}
|
|
@@ -220,7 +224,7 @@ function loopEventToSessionUpdate(event) {
|
|
|
220
224
|
toolCallId: event.callId,
|
|
221
225
|
status: 'pending',
|
|
222
226
|
title: event.toolName,
|
|
223
|
-
kind: mapToolKind(event.toolName, event.toolIntent),
|
|
227
|
+
kind: mapToolKind(event.toolName, { intent: event.toolIntent }),
|
|
224
228
|
content: [],
|
|
225
229
|
rawInput: event.input,
|
|
226
230
|
locations: extractLocationFromInput(event.input),
|
|
@@ -243,11 +247,6 @@ function loopEventToSessionUpdate(event) {
|
|
|
243
247
|
return null;
|
|
244
248
|
}
|
|
245
249
|
}
|
|
246
|
-
function isPermissionPolicyValue(value) {
|
|
247
|
-
return (value === ACP_PERMISSION_POLICY_ASK ||
|
|
248
|
-
value === ACP_PERMISSION_POLICY_DENY_ALL ||
|
|
249
|
-
value === ACP_PERMISSION_POLICY_ALLOW_ALL);
|
|
250
|
-
}
|
|
251
250
|
function buildConfigOptions(state) {
|
|
252
251
|
return [
|
|
253
252
|
{
|
|
@@ -277,8 +276,8 @@ function buildConfigOptions(state) {
|
|
|
277
276
|
{
|
|
278
277
|
type: 'select',
|
|
279
278
|
id: ACP_MODE_CONFIG_ID,
|
|
280
|
-
name:
|
|
281
|
-
description:
|
|
279
|
+
name: text.acp.executionFlowName,
|
|
280
|
+
description: text.acp.executionFlowDescription,
|
|
282
281
|
currentValue: state.modeId,
|
|
283
282
|
options: ACP_PUBLIC_MODES.map((mode) => ({
|
|
284
283
|
value: mode.id,
|
|
@@ -339,16 +338,6 @@ function buildCurrentModeUpdateIfChanged(state) {
|
|
|
339
338
|
state.lastModeDigest = digest;
|
|
340
339
|
return buildCurrentModeUpdate(state.modeId);
|
|
341
340
|
}
|
|
342
|
-
function getLegacyPermissionPolicyForModeValue(value) {
|
|
343
|
-
const normalized = String(value ?? '')
|
|
344
|
-
.trim()
|
|
345
|
-
.toLowerCase();
|
|
346
|
-
if (normalized === 'interactive')
|
|
347
|
-
return ACP_PERMISSION_POLICY_ASK;
|
|
348
|
-
if (normalized === 'yolo')
|
|
349
|
-
return ACP_PERMISSION_POLICY_ALLOW_ALL;
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
341
|
function getPermissionPolicyForAuthorization(state) {
|
|
353
342
|
return state.permissionPolicy;
|
|
354
343
|
}
|
|
@@ -450,6 +439,108 @@ function buildPlanUpdateFromCoreIfChanged(read, state) {
|
|
|
450
439
|
entries,
|
|
451
440
|
};
|
|
452
441
|
}
|
|
442
|
+
function namedPairsToRecord(pairs) {
|
|
443
|
+
const record = {};
|
|
444
|
+
for (const entry of pairs) {
|
|
445
|
+
record[entry.name] = entry.value;
|
|
446
|
+
}
|
|
447
|
+
return record;
|
|
448
|
+
}
|
|
449
|
+
function defaultAcpMcpCapabilities() {
|
|
450
|
+
return {
|
|
451
|
+
tools: {
|
|
452
|
+
exposeToModel: true,
|
|
453
|
+
allow: ['*'],
|
|
454
|
+
phases: [Phase.VERIFY],
|
|
455
|
+
approval: 'ask',
|
|
456
|
+
},
|
|
457
|
+
resources: {
|
|
458
|
+
allowUris: [],
|
|
459
|
+
autoInclude: false,
|
|
460
|
+
subscribe: false,
|
|
461
|
+
maxBytes: 64_000,
|
|
462
|
+
ttlMs: 30_000,
|
|
463
|
+
},
|
|
464
|
+
prompts: {
|
|
465
|
+
exposeAs: 'none',
|
|
466
|
+
allow: [],
|
|
467
|
+
},
|
|
468
|
+
roots: { mode: 'none' },
|
|
469
|
+
sampling: { enabled: false, maxTokens: 0, maxDepth: 0 },
|
|
470
|
+
elicitation: { enabled: false },
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function acpMcpServersToResolved(mcpServers) {
|
|
474
|
+
if (!Array.isArray(mcpServers))
|
|
475
|
+
return [];
|
|
476
|
+
const resolved = [];
|
|
477
|
+
for (const server of mcpServers) {
|
|
478
|
+
const transportType = 'type' in server ? server.type : 'stdio';
|
|
479
|
+
if (transportType === 'sse' || transportType === 'acp') {
|
|
480
|
+
throw new RequestError(-32602, `Invalid params: unsupported MCP server transport "${transportType}"`);
|
|
481
|
+
}
|
|
482
|
+
if ('type' in server && server.type === 'http') {
|
|
483
|
+
const httpServer = server;
|
|
484
|
+
resolved.push({
|
|
485
|
+
name: httpServer.name,
|
|
486
|
+
enabled: true,
|
|
487
|
+
transport: {
|
|
488
|
+
type: 'http',
|
|
489
|
+
url: httpServer.url,
|
|
490
|
+
headers: namedPairsToRecord(httpServer.headers),
|
|
491
|
+
},
|
|
492
|
+
auth: { type: 'none', scopes: [] },
|
|
493
|
+
trust: 'remote',
|
|
494
|
+
capabilities: defaultAcpMcpCapabilities(),
|
|
495
|
+
scope: 'repo',
|
|
496
|
+
});
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (transportType !== 'stdio') {
|
|
500
|
+
throw new RequestError(-32602, `Invalid params: unsupported MCP server transport "${transportType}"`);
|
|
501
|
+
}
|
|
502
|
+
const stdioServer = server;
|
|
503
|
+
resolved.push({
|
|
504
|
+
name: stdioServer.name,
|
|
505
|
+
enabled: true,
|
|
506
|
+
transport: {
|
|
507
|
+
type: 'stdio',
|
|
508
|
+
command: stdioServer.command,
|
|
509
|
+
args: stdioServer.args,
|
|
510
|
+
env: namedPairsToRecord(stdioServer.env),
|
|
511
|
+
},
|
|
512
|
+
auth: { type: 'none', scopes: [] },
|
|
513
|
+
trust: 'local',
|
|
514
|
+
capabilities: defaultAcpMcpCapabilities(),
|
|
515
|
+
scope: 'repo',
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return resolved;
|
|
519
|
+
}
|
|
520
|
+
function acpMcpServersToExtensions(mcpServers) {
|
|
521
|
+
const resolvedServers = acpMcpServersToResolved(mcpServers);
|
|
522
|
+
if (resolvedServers.length === 0)
|
|
523
|
+
return undefined;
|
|
524
|
+
return {
|
|
525
|
+
mcpServers: resolvedServers,
|
|
526
|
+
toolPlugins: [],
|
|
527
|
+
skillDiscovery: { paths: [], scope: 'repo' },
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function validateAcpMcpServers(mcpServers) {
|
|
531
|
+
void acpMcpServersToResolved(mcpServers);
|
|
532
|
+
}
|
|
533
|
+
function validateUnsupportedAdditionalDirectories(additionalDirectories) {
|
|
534
|
+
if (Array.isArray(additionalDirectories) && additionalDirectories.length > 0) {
|
|
535
|
+
throw new RequestError(-32602, 'Invalid params: additionalDirectories is not supported by this agent');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function negotiateProtocolVersion(clientProtocolVersion) {
|
|
539
|
+
if (ACP_SUPPORTED_PROTOCOL_VERSIONS.has(clientProtocolVersion)) {
|
|
540
|
+
return clientProtocolVersion;
|
|
541
|
+
}
|
|
542
|
+
return PROTOCOL_VERSION;
|
|
543
|
+
}
|
|
453
544
|
function extractSlashInput(prompt) {
|
|
454
545
|
if (prompt.length !== 1)
|
|
455
546
|
return null;
|
|
@@ -472,6 +563,9 @@ function isKnownSlashCommand(commandName) {
|
|
|
472
563
|
const normalized = normalizeSlashName(commandName);
|
|
473
564
|
return ACP_AVAILABLE_COMMANDS.some((cmd) => cmd.name.toLowerCase() === normalized);
|
|
474
565
|
}
|
|
566
|
+
function isPersistableSession(session) {
|
|
567
|
+
return session.materialized || session.history.length > 0 || typeof session.taskId === 'string';
|
|
568
|
+
}
|
|
475
569
|
async function awaitTerminalEvent(params) {
|
|
476
570
|
if (!params.eventBus)
|
|
477
571
|
return null;
|
|
@@ -505,8 +599,9 @@ export function createAcpFormalAgent(deps) {
|
|
|
505
599
|
embeddedContext: deps.capabilityPolicy?.promptCapabilities?.embeddedContext ?? false,
|
|
506
600
|
};
|
|
507
601
|
const mcpCapabilities = {
|
|
508
|
-
http: deps.capabilityPolicy?.mcpCapabilities?.http ??
|
|
602
|
+
http: deps.capabilityPolicy?.mcpCapabilities?.http ?? true,
|
|
509
603
|
sse: deps.capabilityPolicy?.mcpCapabilities?.sse ?? false,
|
|
604
|
+
acp: deps.capabilityPolicy?.mcpCapabilities?.acp ?? false,
|
|
510
605
|
};
|
|
511
606
|
const sessionPersistencePath = deps.sessionPersistencePath;
|
|
512
607
|
const sessionStorePolicy = {
|
|
@@ -518,319 +613,17 @@ export function createAcpFormalAgent(deps) {
|
|
|
518
613
|
lockAcquireTimeoutMs: deps.sessionStorePolicy?.lockAcquireTimeoutMs ?? ACP_SESSION_STORE_LOCK_ACQUIRE_TIMEOUT_MS,
|
|
519
614
|
};
|
|
520
615
|
const executionBinding = deps.executionBinding ?? 'local';
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
.filter((record) => parseTimestamp(record.updatedAt) >= cutoff)
|
|
533
|
-
.sort((a, b) => parseTimestamp(b.updatedAt) - parseTimestamp(a.updatedAt))
|
|
534
|
-
.slice(0, sessionStorePolicy.maxEntries);
|
|
535
|
-
}
|
|
536
|
-
function normalizePersistedSessionStore(input) {
|
|
537
|
-
if (!input || typeof input !== 'object') {
|
|
538
|
-
return { schemaVersion: 2, sessions: [] };
|
|
539
|
-
}
|
|
540
|
-
const raw = input;
|
|
541
|
-
if (!Array.isArray(raw.sessions))
|
|
542
|
-
return { schemaVersion: 2, sessions: [] };
|
|
543
|
-
if (raw.schemaVersion === 1) {
|
|
544
|
-
return {
|
|
545
|
-
schemaVersion: 2,
|
|
546
|
-
sessions: raw.sessions.map((entry) => ({
|
|
547
|
-
id: entry.id,
|
|
548
|
-
cwd: entry.cwd,
|
|
549
|
-
mcpServers: entry.mcpServers,
|
|
550
|
-
createdAt: entry.createdAt,
|
|
551
|
-
updatedAt: entry.updatedAt,
|
|
552
|
-
title: entry.title,
|
|
553
|
-
taskId: undefined,
|
|
554
|
-
history: [],
|
|
555
|
-
permissionPolicy: isPermissionPolicyValue(String(deps.defaultPermissionPolicy))
|
|
556
|
-
? deps.defaultPermissionPolicy
|
|
557
|
-
: ACP_PERMISSION_POLICY_ASK,
|
|
558
|
-
modeId: resolveExposedAcpModeId(deps.defaultModeId),
|
|
559
|
-
})),
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
if (raw.schemaVersion === 2) {
|
|
563
|
-
return { schemaVersion: 2, sessions: raw.sessions };
|
|
564
|
-
}
|
|
565
|
-
return { schemaVersion: 2, sessions: [] };
|
|
566
|
-
}
|
|
567
|
-
function isPidAlive(pid) {
|
|
568
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
569
|
-
return false;
|
|
570
|
-
try {
|
|
571
|
-
process.kill(pid, 0);
|
|
572
|
-
return true;
|
|
573
|
-
}
|
|
574
|
-
catch (error) {
|
|
575
|
-
if (error &&
|
|
576
|
-
typeof error === 'object' &&
|
|
577
|
-
'code' in error &&
|
|
578
|
-
error.code === 'EPERM') {
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
return false;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
function isFileMissing(error) {
|
|
585
|
-
return Boolean(error &&
|
|
586
|
-
typeof error === 'object' &&
|
|
587
|
-
'code' in error &&
|
|
588
|
-
(error.code === 'ENOENT' ||
|
|
589
|
-
error.code === 'ENOTDIR'));
|
|
590
|
-
}
|
|
591
|
-
async function persistSessionsBestEffort() {
|
|
592
|
-
if (!sessionPersistencePath)
|
|
593
|
-
return;
|
|
594
|
-
const dir = defaultPathAdapter.dirname(sessionPersistencePath);
|
|
595
|
-
const lockPath = `${sessionPersistencePath}.lock`;
|
|
596
|
-
const baseRecords = sessions.list().map((session) => {
|
|
597
|
-
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
598
|
-
return {
|
|
599
|
-
id: session.id,
|
|
600
|
-
cwd: session.cwd,
|
|
601
|
-
mcpServers: session.mcpServers,
|
|
602
|
-
createdAt: session.createdAt,
|
|
603
|
-
updatedAt: session.updatedAt,
|
|
604
|
-
title: session.title,
|
|
605
|
-
taskId: session.taskId,
|
|
606
|
-
history: session.history.slice(-sessionStorePolicy.historyMaxEntries),
|
|
607
|
-
permissionPolicy: runtimeState.permissionPolicy,
|
|
608
|
-
modeId: runtimeState.modeId,
|
|
609
|
-
};
|
|
610
|
-
});
|
|
611
|
-
const prunedRecords = pruneSessionRecords(baseRecords);
|
|
612
|
-
const keepIds = new Set(prunedRecords.map((record) => record.id));
|
|
613
|
-
for (const record of sessions.list()) {
|
|
614
|
-
if (!keepIds.has(record.id)) {
|
|
615
|
-
sessions.delete(record.id);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
const payload = { schemaVersion: 2, sessions: prunedRecords };
|
|
619
|
-
const primaryRepoPath = prunedRecords[0]?.cwd;
|
|
620
|
-
const lockAuditDetails = {
|
|
621
|
-
lockPath,
|
|
622
|
-
lockPathHash: createHash('sha256').update(lockPath).digest('hex').slice(0, 16),
|
|
623
|
-
repoPathHash: primaryRepoPath ? hashRepoPath(primaryRepoPath) : undefined,
|
|
624
|
-
};
|
|
625
|
-
const tryClearStaleLock = async () => {
|
|
626
|
-
try {
|
|
627
|
-
const raw = await readFile(lockPath, 'utf8');
|
|
628
|
-
const parsed = JSON.parse(raw);
|
|
629
|
-
const createdAtMs = typeof parsed.createdAtMs === 'number' && Number.isFinite(parsed.createdAtMs)
|
|
630
|
-
? parsed.createdAtMs
|
|
631
|
-
: null;
|
|
632
|
-
if (createdAtMs === null)
|
|
633
|
-
return;
|
|
634
|
-
if (Date.now() - createdAtMs <= sessionStorePolicy.lockStaleMs)
|
|
635
|
-
return;
|
|
636
|
-
if (typeof parsed.pid === 'number' && isPidAlive(parsed.pid))
|
|
637
|
-
return;
|
|
638
|
-
await unlink(lockPath);
|
|
639
|
-
recordAuditEvent('acp.session.lock.stale_reclaimed', lockAuditDetails, {
|
|
640
|
-
source: 'acp',
|
|
641
|
-
severity: 'low',
|
|
642
|
-
scope: 'session',
|
|
643
|
-
phase: 'PREFLIGHT',
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
catch {
|
|
647
|
-
try {
|
|
648
|
-
const lockStat = await stat(lockPath);
|
|
649
|
-
const ageMs = Date.now() - lockStat.mtimeMs;
|
|
650
|
-
if (Number.isFinite(ageMs) && ageMs > sessionStorePolicy.lockStaleMs * 2) {
|
|
651
|
-
await unlink(lockPath);
|
|
652
|
-
recordAuditEvent('acp.session.lock.corrupted_reclaimed', {
|
|
653
|
-
...lockAuditDetails,
|
|
654
|
-
ageMs: Math.max(0, Math.floor(ageMs)),
|
|
655
|
-
}, { source: 'acp', severity: 'medium', scope: 'session', phase: 'PREFLIGHT' });
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
catch {
|
|
659
|
-
// ignore
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
};
|
|
663
|
-
let lockHandle;
|
|
664
|
-
try {
|
|
665
|
-
await mkdir(dir, { recursive: true });
|
|
666
|
-
const acquireDeadlineMs = Date.now() + Math.max(250, sessionStorePolicy.lockAcquireTimeoutMs);
|
|
667
|
-
for (let attempt = 0; Date.now() < acquireDeadlineMs; attempt += 1) {
|
|
668
|
-
try {
|
|
669
|
-
lockHandle = await open(lockPath, 'wx');
|
|
670
|
-
await lockHandle.writeFile(JSON.stringify({ pid: process.pid, createdAtMs: Date.now() }), 'utf8');
|
|
671
|
-
break;
|
|
672
|
-
}
|
|
673
|
-
catch {
|
|
674
|
-
await tryClearStaleLock();
|
|
675
|
-
const delayMs = Math.min(250, 20 * (attempt + 1));
|
|
676
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
if (!lockHandle) {
|
|
680
|
-
recordAuditEvent('acp.session.lock.acquire_timeout', lockAuditDetails, {
|
|
681
|
-
source: 'acp',
|
|
682
|
-
severity: 'medium',
|
|
683
|
-
scope: 'session',
|
|
684
|
-
phase: 'PREFLIGHT',
|
|
685
|
-
});
|
|
686
|
-
throw new Error('ACP_SESSION_PERSIST_LOCK_TIMEOUT');
|
|
687
|
-
}
|
|
688
|
-
const heartbeat = setInterval(() => {
|
|
689
|
-
void writeFile(lockPath, JSON.stringify({ pid: process.pid, createdAtMs: Date.now() }), 'utf8');
|
|
690
|
-
}, Math.max(1000, sessionStorePolicy.lockHeartbeatMs));
|
|
691
|
-
const tempPath = defaultPathAdapter.join(dir, `.sessions.v1.json.tmp-${process.pid}-${Date.now()}`);
|
|
692
|
-
try {
|
|
693
|
-
let existing = { schemaVersion: 2, sessions: [] };
|
|
694
|
-
try {
|
|
695
|
-
const existingRaw = await readFile(sessionPersistencePath, 'utf8');
|
|
696
|
-
existing = normalizePersistedSessionStore(JSON.parse(existingRaw));
|
|
697
|
-
}
|
|
698
|
-
catch {
|
|
699
|
-
// ignore read failure; writing fresh payload is acceptable
|
|
700
|
-
}
|
|
701
|
-
const merged = new Map();
|
|
702
|
-
for (const entry of existing.sessions)
|
|
703
|
-
merged.set(entry.id, entry);
|
|
704
|
-
for (const entry of payload.sessions)
|
|
705
|
-
merged.set(entry.id, entry);
|
|
706
|
-
const mergedPayload = {
|
|
707
|
-
schemaVersion: 2,
|
|
708
|
-
sessions: pruneSessionRecords(Array.from(merged.values())),
|
|
709
|
-
};
|
|
710
|
-
await writeFile(tempPath, JSON.stringify(mergedPayload, null, 2), 'utf8');
|
|
711
|
-
await rename(tempPath, sessionPersistencePath);
|
|
712
|
-
}
|
|
713
|
-
finally {
|
|
714
|
-
clearInterval(heartbeat);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
catch (error) {
|
|
718
|
-
recordAuditEvent('acp.session.persist.failed', {
|
|
719
|
-
errorName: error instanceof Error ? error.name : typeof error,
|
|
720
|
-
}, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
|
|
721
|
-
}
|
|
722
|
-
finally {
|
|
723
|
-
if (lockHandle) {
|
|
724
|
-
try {
|
|
725
|
-
await lockHandle.close();
|
|
726
|
-
}
|
|
727
|
-
catch {
|
|
728
|
-
// ignore
|
|
729
|
-
}
|
|
730
|
-
try {
|
|
731
|
-
await unlink(lockPath);
|
|
732
|
-
}
|
|
733
|
-
catch {
|
|
734
|
-
// ignore
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
async function hydrateSessionsOnce() {
|
|
740
|
-
if (sessionsHydrated)
|
|
741
|
-
return;
|
|
742
|
-
if (hydratePromise)
|
|
743
|
-
return hydratePromise;
|
|
744
|
-
hydratePromise = (async () => {
|
|
745
|
-
sessionsHydrated = true;
|
|
746
|
-
if (!sessionPersistencePath)
|
|
747
|
-
return;
|
|
748
|
-
try {
|
|
749
|
-
const raw = await readFile(sessionPersistencePath, 'utf8');
|
|
750
|
-
const parsed = normalizePersistedSessionStore(JSON.parse(raw));
|
|
751
|
-
for (const stored of pruneSessionRecords(parsed.sessions)) {
|
|
752
|
-
sessions.upsert({
|
|
753
|
-
id: stored.id,
|
|
754
|
-
cwd: stored.cwd,
|
|
755
|
-
mcpServers: Array.isArray(stored.mcpServers) ? stored.mcpServers : [],
|
|
756
|
-
createdAt: stored.createdAt,
|
|
757
|
-
updatedAt: stored.updatedAt,
|
|
758
|
-
title: stored.title,
|
|
759
|
-
taskId: stored.taskId,
|
|
760
|
-
history: Array.isArray(stored.history)
|
|
761
|
-
? stored.history.slice(-sessionStorePolicy.historyMaxEntries)
|
|
762
|
-
: [],
|
|
763
|
-
cancelRequested: false,
|
|
764
|
-
});
|
|
765
|
-
if (!sessionRuntime.has(stored.id)) {
|
|
766
|
-
sessionRuntime.set(stored.id, createSessionRuntimeStateFromPersisted({
|
|
767
|
-
permissionPolicy: stored.permissionPolicy,
|
|
768
|
-
defaultPermissionPolicy: deps.defaultPermissionPolicy,
|
|
769
|
-
modeId: stored.modeId,
|
|
770
|
-
defaultModeId: deps.defaultModeId,
|
|
771
|
-
}));
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
catch (error) {
|
|
776
|
-
if (isFileMissing(error))
|
|
777
|
-
return;
|
|
778
|
-
recordAuditEvent('acp.session.hydrate.failed', {
|
|
779
|
-
errorName: error instanceof Error ? error.name : typeof error,
|
|
780
|
-
}, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
|
|
781
|
-
}
|
|
782
|
-
})();
|
|
783
|
-
return hydratePromise;
|
|
784
|
-
}
|
|
785
|
-
function hashRepoPath(repoPath) {
|
|
786
|
-
return createHash('sha256').update(repoPath).digest('hex').slice(0, 16);
|
|
787
|
-
}
|
|
788
|
-
function toCheckpointMeta(input) {
|
|
789
|
-
if (!input)
|
|
790
|
-
return null;
|
|
791
|
-
return {
|
|
792
|
-
id: input.id,
|
|
793
|
-
createdAt: input.createdAt ?? null,
|
|
794
|
-
strategy: input.strategy ?? null,
|
|
795
|
-
backend: input.backend ?? null,
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
function toResumeHint(probe) {
|
|
799
|
-
if (!probe || probe.valid)
|
|
800
|
-
return null;
|
|
801
|
-
switch (probe.reason) {
|
|
802
|
-
case 'not_found':
|
|
803
|
-
return {
|
|
804
|
-
code: 'CHECKPOINT_NOT_FOUND',
|
|
805
|
-
message: 'Checkpoint not found. Start a new session.',
|
|
806
|
-
};
|
|
807
|
-
case 'manifest_parse_error':
|
|
808
|
-
return {
|
|
809
|
-
code: 'CHECKPOINT_MANIFEST_PARSE_ERROR',
|
|
810
|
-
message: 'Checkpoint metadata is corrupted. Recreate checkpoint metadata and retry.',
|
|
811
|
-
};
|
|
812
|
-
case 'manifest_io_error':
|
|
813
|
-
return {
|
|
814
|
-
code: 'CHECKPOINT_MANIFEST_IO_ERROR',
|
|
815
|
-
message: 'Checkpoint metadata is unreadable due to filesystem I/O issues.',
|
|
816
|
-
};
|
|
817
|
-
case 'manifest_lock_timeout':
|
|
818
|
-
return {
|
|
819
|
-
code: 'CHECKPOINT_MANIFEST_LOCK_TIMEOUT',
|
|
820
|
-
message: 'Checkpoint metadata is busy (lock timeout). Retry shortly.',
|
|
821
|
-
};
|
|
822
|
-
case 'manifest_unavailable':
|
|
823
|
-
return {
|
|
824
|
-
code: 'CHECKPOINT_MANIFEST_UNAVAILABLE',
|
|
825
|
-
message: 'Checkpoint metadata is unavailable in current runtime.',
|
|
826
|
-
};
|
|
827
|
-
default:
|
|
828
|
-
return {
|
|
829
|
-
code: 'CHECKPOINT_RESUME_UNAVAILABLE',
|
|
830
|
-
message: 'Checkpoint resume is unavailable. Start a new session or retry.',
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
}
|
|
616
|
+
const sessionPersistence = createAcpSessionPersistence({
|
|
617
|
+
path: sessionPersistencePath ?? '',
|
|
618
|
+
storePolicy: sessionStorePolicy,
|
|
619
|
+
defaultPermissionPolicy: deps.defaultPermissionPolicy ?? ACP_PERMISSION_POLICY_ASK,
|
|
620
|
+
defaultModeId: deps.defaultModeId,
|
|
621
|
+
sessions,
|
|
622
|
+
sessionRuntime,
|
|
623
|
+
isPersistableSession,
|
|
624
|
+
ensureSessionRuntimeState,
|
|
625
|
+
resolveExposedAcpModeId,
|
|
626
|
+
});
|
|
834
627
|
async function emitSessionUpdate(sessionId, update) {
|
|
835
628
|
await deps.conn.sessionUpdate({ sessionId, update });
|
|
836
629
|
}
|
|
@@ -853,15 +646,13 @@ export function createAcpFormalAgent(deps) {
|
|
|
853
646
|
if (!shouldRefreshPlanForEvent(params.event))
|
|
854
647
|
return;
|
|
855
648
|
const { state, event } = params;
|
|
856
|
-
const planReader = deps.planReader
|
|
857
|
-
readBySession: async ({ repoPath, sessionId }) => await readPlan({ persistenceRoot: repoPath, sessionId }),
|
|
858
|
-
};
|
|
649
|
+
const planReader = deps.planReader;
|
|
859
650
|
if (event?.type === 'plan.runtime.ready') {
|
|
860
651
|
state.runtimePlanSessionId = event.sessionId;
|
|
861
652
|
state.runtimePlanPathHint = event.planPathHint;
|
|
862
653
|
state.lastPlanDigest = null;
|
|
863
654
|
}
|
|
864
|
-
if (!state.runtimePlanSessionId)
|
|
655
|
+
if (!state.runtimePlanSessionId || !planReader)
|
|
865
656
|
return;
|
|
866
657
|
try {
|
|
867
658
|
const read = await planReader.readBySession({
|
|
@@ -883,73 +674,129 @@ export function createAcpFormalAgent(deps) {
|
|
|
883
674
|
}, { source: 'acp', severity: 'low', scope: 'session', phase: 'PLAN' });
|
|
884
675
|
}
|
|
885
676
|
}
|
|
886
|
-
async function
|
|
887
|
-
await
|
|
677
|
+
async function resolveExistingSession(params) {
|
|
678
|
+
await sessionPersistence.hydrate();
|
|
679
|
+
validateUnsupportedAdditionalDirectories(params.additionalDirectories);
|
|
680
|
+
validateAcpMcpServers(params.mcpServers);
|
|
888
681
|
const session = sessions.get(params.sessionId);
|
|
889
682
|
if (!session) {
|
|
890
683
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
891
684
|
}
|
|
892
685
|
if (session.cwd !== params.cwd) {
|
|
686
|
+
throw new RequestError(-32602, 'Invalid params: cwd does not match session cwd');
|
|
687
|
+
}
|
|
688
|
+
if (params.mcpServers) {
|
|
689
|
+
const mcpServers = params.mcpServers;
|
|
893
690
|
sessions.update(params.sessionId, (current) => ({
|
|
894
691
|
...current,
|
|
895
|
-
|
|
896
|
-
mcpServers: params.mcpServers ?? [],
|
|
692
|
+
mcpServers,
|
|
897
693
|
}));
|
|
898
|
-
await
|
|
694
|
+
await sessionPersistence.persist();
|
|
899
695
|
}
|
|
900
696
|
return session;
|
|
901
697
|
}
|
|
698
|
+
function toSessionInfo(session) {
|
|
699
|
+
return {
|
|
700
|
+
sessionId: session.id,
|
|
701
|
+
cwd: session.cwd,
|
|
702
|
+
title: typeof session.title === 'string' && session.title.trim() ? session.title : null,
|
|
703
|
+
updatedAt: session.updatedAt,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
902
706
|
function ensureSessionRuntimeState(sessionId) {
|
|
903
707
|
const existing = sessionRuntime.get(sessionId);
|
|
904
708
|
if (existing)
|
|
905
709
|
return existing;
|
|
710
|
+
const session = sessions.get(sessionId);
|
|
906
711
|
const created = createSessionRuntimeStateFromPersisted({
|
|
712
|
+
permissionPolicy: session?.permissionPolicy,
|
|
907
713
|
defaultPermissionPolicy: deps.defaultPermissionPolicy,
|
|
714
|
+
modeId: session?.modeId,
|
|
908
715
|
defaultModeId: deps.defaultModeId,
|
|
909
716
|
});
|
|
910
717
|
sessionRuntime.set(sessionId, created);
|
|
911
718
|
return created;
|
|
912
719
|
}
|
|
720
|
+
async function cancelSessionTaskBestEffort(session) {
|
|
721
|
+
sessions.update(session.id, (current) => ({ ...current, cancelRequested: true }));
|
|
722
|
+
if (session.taskId) {
|
|
723
|
+
await deps.facade.cancelTask(session.taskId);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function closeSessionRecord(params) {
|
|
727
|
+
const session = sessions.get(params.sessionId);
|
|
728
|
+
if (!session)
|
|
729
|
+
return;
|
|
730
|
+
const runtimeState = ensureSessionRuntimeState(params.sessionId);
|
|
731
|
+
sessions.update(params.sessionId, (current) => ({
|
|
732
|
+
...current,
|
|
733
|
+
permissionPolicy: runtimeState.permissionPolicy,
|
|
734
|
+
modeId: runtimeState.modeId,
|
|
735
|
+
}));
|
|
736
|
+
await cancelSessionTaskBestEffort(session);
|
|
737
|
+
const latestSession = sessions.get(params.sessionId) ?? session;
|
|
738
|
+
if (!isPersistableSession(latestSession)) {
|
|
739
|
+
sessionRuntime.delete(params.sessionId);
|
|
740
|
+
sessions.delete(params.sessionId);
|
|
741
|
+
await sessionPersistence.persist();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
sessions.update(params.sessionId, (current) => ({ ...current, cancelRequested: false }));
|
|
745
|
+
await sessionPersistence.persist();
|
|
746
|
+
sessionRuntime.delete(params.sessionId);
|
|
747
|
+
}
|
|
748
|
+
async function deleteSessionRecord(params) {
|
|
749
|
+
const session = sessions.get(params.sessionId);
|
|
750
|
+
if (!session)
|
|
751
|
+
return;
|
|
752
|
+
await cancelSessionTaskBestEffort(session);
|
|
753
|
+
sessionPersistence.markDeleted(params.sessionId);
|
|
754
|
+
sessionRuntime.delete(params.sessionId);
|
|
755
|
+
sessions.delete(params.sessionId);
|
|
756
|
+
await sessionPersistence.persist();
|
|
757
|
+
}
|
|
913
758
|
return {
|
|
914
759
|
async initialize(params) {
|
|
915
760
|
if (typeof params.protocolVersion !== 'number' || !Number.isFinite(params.protocolVersion)) {
|
|
916
761
|
throw new RequestError(-32602, 'Invalid params: protocolVersion is required');
|
|
917
762
|
}
|
|
918
763
|
clientCapabilities = params.clientCapabilities;
|
|
919
|
-
// Protocol version negotiation:
|
|
920
|
-
// - If the client's requested version is supported, return the same version
|
|
921
|
-
// - Otherwise, return the latest version the agent supports
|
|
922
|
-
// Currently, the agent only supports protocol version 1
|
|
923
|
-
const supportedProtocolVersion = PROTOCOL_VERSION;
|
|
924
|
-
const negotiatedVersion = params.protocolVersion <= supportedProtocolVersion
|
|
925
|
-
? params.protocolVersion
|
|
926
|
-
: supportedProtocolVersion;
|
|
927
764
|
return {
|
|
928
|
-
protocolVersion:
|
|
765
|
+
protocolVersion: negotiateProtocolVersion(params.protocolVersion),
|
|
929
766
|
agentInfo: deps.agentInfo,
|
|
930
767
|
authMethods: [],
|
|
931
768
|
agentCapabilities: {
|
|
932
769
|
loadSession: loadSessionCapability,
|
|
933
770
|
promptCapabilities: promptCapabilities,
|
|
934
771
|
mcpCapabilities: mcpCapabilities,
|
|
935
|
-
sessionCapabilities: {
|
|
772
|
+
sessionCapabilities: {
|
|
773
|
+
list: {},
|
|
774
|
+
resume: {},
|
|
775
|
+
close: {},
|
|
776
|
+
delete: {},
|
|
777
|
+
},
|
|
936
778
|
},
|
|
937
779
|
};
|
|
938
780
|
},
|
|
939
|
-
async authenticate() {
|
|
940
|
-
|
|
781
|
+
async authenticate(params) {
|
|
782
|
+
throw new RequestError(-32602, `Invalid params: unsupported auth methodId "${params.methodId}"`);
|
|
941
783
|
},
|
|
942
784
|
async newSession(params) {
|
|
943
|
-
await
|
|
785
|
+
await sessionPersistence.hydrate();
|
|
944
786
|
if (!isAbsolutePath(params.cwd)) {
|
|
945
787
|
throw new RequestError(-32602, 'Invalid params: cwd must be an absolute path');
|
|
946
788
|
}
|
|
789
|
+
validateUnsupportedAdditionalDirectories(params.additionalDirectories);
|
|
790
|
+
validateAcpMcpServers(params.mcpServers);
|
|
947
791
|
const session = sessions.create({
|
|
948
792
|
cwd: params.cwd,
|
|
949
793
|
mcpServers: params.mcpServers ?? [],
|
|
950
794
|
title: deriveSessionTitleFromCwd(params.cwd),
|
|
795
|
+
permissionPolicy: isPermissionPolicyValue(String(deps.defaultPermissionPolicy))
|
|
796
|
+
? deps.defaultPermissionPolicy
|
|
797
|
+
: ACP_PERMISSION_POLICY_ASK,
|
|
798
|
+
modeId: resolveExposedAcpModeId(deps.defaultModeId),
|
|
951
799
|
});
|
|
952
|
-
await persistSessionsBestEffort();
|
|
953
800
|
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
954
801
|
await emitSessionInfoUpdateBestEffort(session.id);
|
|
955
802
|
// Restore session state on creation
|
|
@@ -961,18 +808,11 @@ export function createAcpFormalAgent(deps) {
|
|
|
961
808
|
await emitSessionUpdate(session.id, modeUpdate);
|
|
962
809
|
let sessionMeta;
|
|
963
810
|
if (deps.checkpointReader) {
|
|
964
|
-
const
|
|
811
|
+
const result = await probeCheckpointForNewSession(deps.checkpointReader, {
|
|
965
812
|
repoPath: params.cwd,
|
|
966
813
|
sessionId: session.id,
|
|
967
|
-
limit: 1,
|
|
968
814
|
});
|
|
969
|
-
|
|
970
|
-
sessionMeta = {
|
|
971
|
-
salmonloop: {
|
|
972
|
-
latestCheckpointId: latest?.id ?? null,
|
|
973
|
-
checkpoint: toCheckpointMeta(latest),
|
|
974
|
-
},
|
|
975
|
-
};
|
|
815
|
+
sessionMeta = result._meta;
|
|
976
816
|
}
|
|
977
817
|
return {
|
|
978
818
|
sessionId: session.id,
|
|
@@ -983,9 +823,9 @@ export function createAcpFormalAgent(deps) {
|
|
|
983
823
|
},
|
|
984
824
|
async loadSession(params) {
|
|
985
825
|
if (!loadSessionCapability) {
|
|
986
|
-
throw new RequestError(-32601, '
|
|
826
|
+
throw new RequestError(-32601, 'Method not found: session/load');
|
|
987
827
|
}
|
|
988
|
-
await
|
|
828
|
+
await resolveExistingSession(params);
|
|
989
829
|
let session = sessions.get(params.sessionId);
|
|
990
830
|
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
991
831
|
if (typeof session.title !== 'string' || !session.title.trim()) {
|
|
@@ -994,7 +834,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
994
834
|
...current,
|
|
995
835
|
title: deriveSessionTitleFromCwd(current.cwd),
|
|
996
836
|
})) ?? session;
|
|
997
|
-
await
|
|
837
|
+
await sessionPersistence.persist();
|
|
998
838
|
}
|
|
999
839
|
runtimeState.lastSessionInfoDigest = null;
|
|
1000
840
|
await emitSessionInfoUpdateBestEffort(session.id);
|
|
@@ -1013,13 +853,11 @@ export function createAcpFormalAgent(deps) {
|
|
|
1013
853
|
if (modeUpdate)
|
|
1014
854
|
await emitSessionUpdate(session.id, modeUpdate);
|
|
1015
855
|
for (const entry of session.history) {
|
|
1016
|
-
if (entry.role !== 'assistant')
|
|
1017
|
-
continue;
|
|
1018
856
|
for (const block of entry.content) {
|
|
1019
|
-
if (block
|
|
857
|
+
if (isReplayableSessionContentBlock(block)) {
|
|
1020
858
|
await emitSessionUpdate(session.id, {
|
|
1021
|
-
sessionUpdate: 'agent_message_chunk',
|
|
1022
|
-
content:
|
|
859
|
+
sessionUpdate: entry.role === 'assistant' ? 'agent_message_chunk' : 'user_message_chunk',
|
|
860
|
+
content: block,
|
|
1023
861
|
});
|
|
1024
862
|
}
|
|
1025
863
|
}
|
|
@@ -1029,56 +867,12 @@ export function createAcpFormalAgent(deps) {
|
|
|
1029
867
|
modes: buildModesState(runtimeState.modeId),
|
|
1030
868
|
};
|
|
1031
869
|
if (deps.checkpointReader) {
|
|
1032
|
-
const
|
|
1033
|
-
const checkpoints = await deps.checkpointReader.listBySession({
|
|
870
|
+
const result = await probeCheckpoint(deps.checkpointReader, {
|
|
1034
871
|
repoPath: params.cwd,
|
|
1035
|
-
sessionId: params.sessionId,
|
|
1036
|
-
limit: 1,
|
|
1037
|
-
});
|
|
1038
|
-
const latest = checkpoints.at(-1);
|
|
1039
|
-
let resumeProbe = null;
|
|
1040
|
-
if (latest?.id && deps.checkpointReader.probeById) {
|
|
1041
|
-
const probed = await deps.checkpointReader.probeById({
|
|
1042
|
-
repoPath: params.cwd,
|
|
1043
|
-
checkpointId: latest.id,
|
|
1044
|
-
});
|
|
1045
|
-
resumeProbe = {
|
|
1046
|
-
checkpointId: latest.id,
|
|
1047
|
-
valid: probed.valid,
|
|
1048
|
-
reason: probed.reason,
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
else if (latest?.id && deps.checkpointReader.getById) {
|
|
1052
|
-
const found = await deps.checkpointReader.getById({
|
|
1053
|
-
repoPath: params.cwd,
|
|
1054
|
-
checkpointId: latest.id,
|
|
1055
|
-
});
|
|
1056
|
-
resumeProbe = {
|
|
1057
|
-
checkpointId: latest.id,
|
|
1058
|
-
valid: Boolean(found),
|
|
1059
|
-
reason: found ? 'ok' : 'not_found',
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
const resumeReady = resumeProbe?.valid ?? Boolean(latest);
|
|
1063
|
-
recordAuditEvent('acp.checkpoint.read', {
|
|
1064
872
|
sessionId: params.sessionId,
|
|
1065
873
|
repoPathHash: hashRepoPath(params.cwd),
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
latencyMs: Date.now() - startedAt,
|
|
1069
|
-
resumeProbe: resumeProbe ?? undefined,
|
|
1070
|
-
}, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
|
|
1071
|
-
const resumeHint = toResumeHint(resumeProbe);
|
|
1072
|
-
response._meta = {
|
|
1073
|
-
salmonloop: {
|
|
1074
|
-
latestCheckpointId: latest?.id ?? null,
|
|
1075
|
-
checkpoint: toCheckpointMeta(latest),
|
|
1076
|
-
resumeReady,
|
|
1077
|
-
resumeProbe,
|
|
1078
|
-
resumeHint: resumeHint?.message ?? null,
|
|
1079
|
-
resumeHintCode: resumeHint?.code ?? null,
|
|
1080
|
-
},
|
|
1081
|
-
};
|
|
874
|
+
});
|
|
875
|
+
response._meta = result._meta;
|
|
1082
876
|
}
|
|
1083
877
|
else {
|
|
1084
878
|
recordAuditEvent('acp.checkpoint.read', {
|
|
@@ -1091,34 +885,108 @@ export function createAcpFormalAgent(deps) {
|
|
|
1091
885
|
}
|
|
1092
886
|
return response;
|
|
1093
887
|
},
|
|
888
|
+
async listSessions(params) {
|
|
889
|
+
await sessionPersistence.hydrate();
|
|
890
|
+
if (typeof params.cwd === 'string' && params.cwd && !isAbsolutePath(params.cwd)) {
|
|
891
|
+
throw new RequestError(-32602, 'Invalid params: cwd must be an absolute path');
|
|
892
|
+
}
|
|
893
|
+
const cwdFilter = typeof params.cwd === 'string' && params.cwd ? params.cwd : null;
|
|
894
|
+
let offset = 0;
|
|
895
|
+
if (typeof params.cursor === 'string' && params.cursor) {
|
|
896
|
+
const decodedCursor = decodeSessionListCursor(params.cursor);
|
|
897
|
+
if (decodedCursor.cwd !== cwdFilter) {
|
|
898
|
+
throw new RequestError(-32602, 'Invalid params: cursor does not match cwd filter');
|
|
899
|
+
}
|
|
900
|
+
offset = decodedCursor.offset;
|
|
901
|
+
}
|
|
902
|
+
const filtered = sessions
|
|
903
|
+
.list()
|
|
904
|
+
.filter((session) => !cwdFilter || session.cwd === cwdFilter)
|
|
905
|
+
.sort((a, b) => parseTimestamp(b.updatedAt) - parseTimestamp(a.updatedAt));
|
|
906
|
+
const page = filtered.slice(offset, offset + ACP_SESSION_LIST_PAGE_SIZE);
|
|
907
|
+
const nextOffset = offset + page.length;
|
|
908
|
+
const nextCursor = nextOffset < filtered.length
|
|
909
|
+
? encodeSessionListCursor({ offset: nextOffset, cwd: cwdFilter })
|
|
910
|
+
: undefined;
|
|
911
|
+
return {
|
|
912
|
+
sessions: page.map(toSessionInfo),
|
|
913
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
914
|
+
};
|
|
915
|
+
},
|
|
916
|
+
async resumeSession(params) {
|
|
917
|
+
const session = await resolveExistingSession({
|
|
918
|
+
sessionId: params.sessionId,
|
|
919
|
+
cwd: params.cwd,
|
|
920
|
+
mcpServers: params.mcpServers,
|
|
921
|
+
additionalDirectories: params.additionalDirectories,
|
|
922
|
+
});
|
|
923
|
+
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
924
|
+
runtimeState.lastSessionInfoDigest = null;
|
|
925
|
+
await emitSessionInfoUpdateBestEffort(session.id);
|
|
926
|
+
const commandsUpdate = buildAvailableCommandsUpdateIfChanged(runtimeState);
|
|
927
|
+
if (commandsUpdate)
|
|
928
|
+
await emitSessionUpdate(session.id, commandsUpdate);
|
|
929
|
+
const modeUpdate = buildCurrentModeUpdateIfChanged(runtimeState);
|
|
930
|
+
if (modeUpdate)
|
|
931
|
+
await emitSessionUpdate(session.id, modeUpdate);
|
|
932
|
+
const response = {
|
|
933
|
+
configOptions: buildConfigOptions(runtimeState),
|
|
934
|
+
modes: buildModesState(runtimeState.modeId),
|
|
935
|
+
};
|
|
936
|
+
if (deps.checkpointReader) {
|
|
937
|
+
const result = await probeCheckpoint(deps.checkpointReader, {
|
|
938
|
+
repoPath: session.cwd,
|
|
939
|
+
sessionId: params.sessionId,
|
|
940
|
+
repoPathHash: hashRepoPath(session.cwd),
|
|
941
|
+
});
|
|
942
|
+
response._meta = result._meta;
|
|
943
|
+
}
|
|
944
|
+
return response;
|
|
945
|
+
},
|
|
946
|
+
async closeSession(params) {
|
|
947
|
+
await sessionPersistence.hydrate();
|
|
948
|
+
await closeSessionRecord({ sessionId: params.sessionId });
|
|
949
|
+
return {};
|
|
950
|
+
},
|
|
951
|
+
async unstable_deleteSession(params) {
|
|
952
|
+
await sessionPersistence.hydrate();
|
|
953
|
+
await deleteSessionRecord({ sessionId: params.sessionId });
|
|
954
|
+
return {};
|
|
955
|
+
},
|
|
1094
956
|
async setSessionConfigOption(params) {
|
|
1095
|
-
await
|
|
957
|
+
await sessionPersistence.hydrate();
|
|
1096
958
|
if (!sessions.get(params.sessionId)) {
|
|
1097
959
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
1098
960
|
}
|
|
1099
961
|
const runtimeState = ensureSessionRuntimeState(params.sessionId);
|
|
1100
962
|
if (params.configId === ACP_PERMISSION_POLICY_CONFIG_ID) {
|
|
963
|
+
if (typeof params.value !== 'string') {
|
|
964
|
+
throw new RequestError(-32602, `Invalid params: "${params.configId}" does not support boolean values`);
|
|
965
|
+
}
|
|
1101
966
|
if (!isPermissionPolicyValue(params.value)) {
|
|
1102
967
|
throw new RequestError(-32602, `Invalid params: unsupported value "${params.value}" for "${params.configId}"`);
|
|
1103
968
|
}
|
|
1104
969
|
runtimeState.permissionPolicy = params.value;
|
|
1105
970
|
}
|
|
1106
971
|
else if (params.configId === ACP_MODE_CONFIG_ID) {
|
|
972
|
+
if (typeof params.value !== 'string') {
|
|
973
|
+
throw new RequestError(-32602, `Invalid params: "${params.configId}" does not support boolean values`);
|
|
974
|
+
}
|
|
1107
975
|
const parsedModeId = parseAcpFlowMode(params.value);
|
|
1108
976
|
if (!parsedModeId || !ACP_PUBLIC_MODE_IDS.has(parsedModeId)) {
|
|
1109
977
|
throw new RequestError(-32602, `Invalid params: unsupported value "${params.value}" for "${params.configId}"`);
|
|
1110
978
|
}
|
|
1111
979
|
runtimeState.modeId = parsedModeId;
|
|
1112
|
-
const legacyPermissionPolicy = getLegacyPermissionPolicyForModeValue(params.value);
|
|
1113
|
-
if (legacyPermissionPolicy) {
|
|
1114
|
-
runtimeState.permissionPolicy = legacyPermissionPolicy;
|
|
1115
|
-
}
|
|
1116
980
|
}
|
|
1117
981
|
else {
|
|
1118
982
|
throw new RequestError(-32602, `Invalid params: unsupported configId "${params.configId}"`);
|
|
1119
983
|
}
|
|
1120
|
-
sessions.update(params.sessionId, (current) => ({
|
|
1121
|
-
|
|
984
|
+
sessions.update(params.sessionId, (current) => ({
|
|
985
|
+
...current,
|
|
986
|
+
permissionPolicy: runtimeState.permissionPolicy,
|
|
987
|
+
modeId: runtimeState.modeId,
|
|
988
|
+
}));
|
|
989
|
+
await sessionPersistence.persist();
|
|
1122
990
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1123
991
|
const update = buildConfigOptionUpdateIfChanged(runtimeState);
|
|
1124
992
|
if (update) {
|
|
@@ -1131,7 +999,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1131
999
|
return { configOptions: buildConfigOptions(runtimeState) };
|
|
1132
1000
|
},
|
|
1133
1001
|
async setSessionMode(params) {
|
|
1134
|
-
await
|
|
1002
|
+
await sessionPersistence.hydrate();
|
|
1135
1003
|
if (!sessions.get(params.sessionId)) {
|
|
1136
1004
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
1137
1005
|
}
|
|
@@ -1141,12 +1009,12 @@ export function createAcpFormalAgent(deps) {
|
|
|
1141
1009
|
throw new RequestError(-32602, `Invalid params: unsupported modeId "${params.modeId}"`);
|
|
1142
1010
|
}
|
|
1143
1011
|
runtimeState.modeId = resolvedModeId;
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
runtimeState.permissionPolicy
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
await
|
|
1012
|
+
sessions.update(params.sessionId, (current) => ({
|
|
1013
|
+
...current,
|
|
1014
|
+
permissionPolicy: runtimeState.permissionPolicy,
|
|
1015
|
+
modeId: runtimeState.modeId,
|
|
1016
|
+
}));
|
|
1017
|
+
await sessionPersistence.persist();
|
|
1150
1018
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1151
1019
|
const configUpdate = buildConfigOptionUpdateIfChanged(runtimeState);
|
|
1152
1020
|
if (configUpdate) {
|
|
@@ -1160,7 +1028,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1160
1028
|
return {};
|
|
1161
1029
|
},
|
|
1162
1030
|
async prompt(params) {
|
|
1163
|
-
await
|
|
1031
|
+
await sessionPersistence.hydrate();
|
|
1164
1032
|
const session = sessions.get(params.sessionId);
|
|
1165
1033
|
if (!session) {
|
|
1166
1034
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
@@ -1173,7 +1041,10 @@ export function createAcpFormalAgent(deps) {
|
|
|
1173
1041
|
const runtimeState = ensureSessionRuntimeState(params.sessionId);
|
|
1174
1042
|
// Check for cancellation before starting processing
|
|
1175
1043
|
if (sessions.get(params.sessionId)?.cancelRequested === true) {
|
|
1176
|
-
return {
|
|
1044
|
+
return {
|
|
1045
|
+
stopReason: 'cancelled',
|
|
1046
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1047
|
+
};
|
|
1177
1048
|
}
|
|
1178
1049
|
sessions.update(params.sessionId, (current) => {
|
|
1179
1050
|
const title = typeof current.title === 'string' && current.title.trim()
|
|
@@ -1183,13 +1054,16 @@ export function createAcpFormalAgent(deps) {
|
|
|
1183
1054
|
...current,
|
|
1184
1055
|
cancelRequested: false,
|
|
1185
1056
|
title,
|
|
1057
|
+
materialized: true,
|
|
1058
|
+
permissionPolicy: runtimeState.permissionPolicy,
|
|
1059
|
+
modeId: runtimeState.modeId,
|
|
1186
1060
|
history: [
|
|
1187
1061
|
...current.history,
|
|
1188
1062
|
{ role: 'user', content: params.prompt },
|
|
1189
1063
|
],
|
|
1190
1064
|
};
|
|
1191
1065
|
});
|
|
1192
|
-
await
|
|
1066
|
+
await sessionPersistence.persist();
|
|
1193
1067
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1194
1068
|
const configUpdate = buildConfigOptionUpdateIfChanged(runtimeState);
|
|
1195
1069
|
if (configUpdate) {
|
|
@@ -1221,14 +1095,20 @@ export function createAcpFormalAgent(deps) {
|
|
|
1221
1095
|
{ role: 'assistant', content: [buildTextContentBlock(responseText)] },
|
|
1222
1096
|
],
|
|
1223
1097
|
}));
|
|
1224
|
-
await
|
|
1098
|
+
await sessionPersistence.persist();
|
|
1225
1099
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1226
|
-
return {
|
|
1100
|
+
return {
|
|
1101
|
+
stopReason: 'end_turn',
|
|
1102
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1103
|
+
};
|
|
1227
1104
|
}
|
|
1228
1105
|
}
|
|
1229
1106
|
// Check for cancellation again before creating task
|
|
1230
1107
|
if (sessions.get(params.sessionId)?.cancelRequested === true) {
|
|
1231
|
-
return {
|
|
1108
|
+
return {
|
|
1109
|
+
stopReason: 'cancelled',
|
|
1110
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1111
|
+
};
|
|
1232
1112
|
}
|
|
1233
1113
|
const pendingUpdates = [];
|
|
1234
1114
|
const executionRequest = buildCanonicalExecutionRequest({
|
|
@@ -1245,6 +1125,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1245
1125
|
fileSystemOverride: effectiveExecutionBinding === 'client'
|
|
1246
1126
|
? createAcpFileSystem({ conn: deps.conn, sessionId: params.sessionId })
|
|
1247
1127
|
: undefined,
|
|
1128
|
+
extensions: acpMcpServersToExtensions(session.mcpServers),
|
|
1248
1129
|
authorizationProvider: createAcpToolAuthorizationProvider({
|
|
1249
1130
|
conn: deps.conn,
|
|
1250
1131
|
sessionId: params.sessionId,
|
|
@@ -1270,34 +1151,39 @@ export function createAcpFormalAgent(deps) {
|
|
|
1270
1151
|
},
|
|
1271
1152
|
});
|
|
1272
1153
|
sessions.update(params.sessionId, (current) => ({ ...current, taskId: task.id }));
|
|
1273
|
-
await
|
|
1154
|
+
await sessionPersistence.persist();
|
|
1274
1155
|
if (signal.aborted) {
|
|
1275
1156
|
await emitSessionUpdate(params.sessionId, {
|
|
1276
1157
|
sessionUpdate: 'agent_message_chunk',
|
|
1277
|
-
content: buildTextContentBlock(ensureMarkdownParagraphBreak(
|
|
1158
|
+
content: buildTextContentBlock(ensureMarkdownParagraphBreak(text.acp.taskCancelled)),
|
|
1278
1159
|
});
|
|
1279
|
-
return {
|
|
1160
|
+
return {
|
|
1161
|
+
stopReason: 'cancelled',
|
|
1162
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1163
|
+
};
|
|
1280
1164
|
}
|
|
1281
1165
|
const terminalEvent = await awaitTerminalEvent({ taskId: task.id, eventBus: deps.eventBus });
|
|
1282
1166
|
let stopReason = 'end_turn';
|
|
1283
|
-
let assistantText =
|
|
1167
|
+
let assistantText = text.acp.taskCompleted;
|
|
1284
1168
|
let assistantMeta;
|
|
1285
1169
|
let latest;
|
|
1286
1170
|
const cancelRequested = sessions.get(params.sessionId)?.cancelRequested === true;
|
|
1287
1171
|
if (cancelRequested) {
|
|
1288
|
-
assistantText =
|
|
1172
|
+
assistantText = text.acp.taskCancelled;
|
|
1289
1173
|
stopReason = 'cancelled';
|
|
1290
1174
|
}
|
|
1291
1175
|
else if (terminalEvent?.type === 'task.failed') {
|
|
1292
1176
|
latest = await deps.facade.getTask(task.id);
|
|
1293
1177
|
const failureMessage = typeof latest?.failure?.message === 'string' ? latest.failure.message : undefined;
|
|
1294
|
-
assistantText = failureMessage
|
|
1178
|
+
assistantText = failureMessage
|
|
1179
|
+
? text.acp.taskFailedWithReason(failureMessage)
|
|
1180
|
+
: text.acp.taskFailed;
|
|
1295
1181
|
const inferred = inferTurnStopReasonFromFailure(latest?.failure);
|
|
1296
1182
|
if (inferred)
|
|
1297
1183
|
stopReason = inferred;
|
|
1298
1184
|
}
|
|
1299
1185
|
else if (terminalEvent?.type === 'task.awaiting_input') {
|
|
1300
|
-
assistantText =
|
|
1186
|
+
assistantText = text.acp.taskAwaitingInput;
|
|
1301
1187
|
latest = await deps.facade.getTask(task.id);
|
|
1302
1188
|
const formatted = latest?.inputRequired
|
|
1303
1189
|
? formatInputRequiredMessage(latest.inputRequired)
|
|
@@ -1309,7 +1195,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1309
1195
|
}
|
|
1310
1196
|
}
|
|
1311
1197
|
else if (terminalEvent?.type === 'task.cancelled') {
|
|
1312
|
-
assistantText =
|
|
1198
|
+
assistantText = text.acp.taskCancelled;
|
|
1313
1199
|
stopReason = 'cancelled';
|
|
1314
1200
|
}
|
|
1315
1201
|
await emitSessionUpdate(params.sessionId, {
|
|
@@ -1330,7 +1216,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1330
1216
|
{ role: 'assistant', content: [buildTextContentBlock(assistantText)] },
|
|
1331
1217
|
],
|
|
1332
1218
|
}));
|
|
1333
|
-
await
|
|
1219
|
+
await sessionPersistence.persist();
|
|
1334
1220
|
const latestSession = sessions.get(params.sessionId);
|
|
1335
1221
|
if (latestSession) {
|
|
1336
1222
|
const sessionInfoUpdate = buildSessionInfoUpdateIfChanged(latestSession, runtimeState);
|
|
@@ -1342,16 +1228,19 @@ export function createAcpFormalAgent(deps) {
|
|
|
1342
1228
|
}
|
|
1343
1229
|
// Wait for all pending session updates to be sent before responding
|
|
1344
1230
|
await Promise.all(pendingUpdates);
|
|
1345
|
-
return {
|
|
1231
|
+
return {
|
|
1232
|
+
stopReason,
|
|
1233
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1234
|
+
};
|
|
1346
1235
|
},
|
|
1347
1236
|
async cancel(params) {
|
|
1348
|
-
await
|
|
1237
|
+
await sessionPersistence.hydrate();
|
|
1349
1238
|
const session = sessions.get(params.sessionId);
|
|
1350
1239
|
if (!session)
|
|
1351
1240
|
return;
|
|
1352
1241
|
// Mark the session as cancelled
|
|
1353
1242
|
sessions.update(params.sessionId, (current) => ({ ...current, cancelRequested: true }));
|
|
1354
|
-
await
|
|
1243
|
+
await sessionPersistence.persist();
|
|
1355
1244
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1356
1245
|
// If a task is running, cancel it
|
|
1357
1246
|
if (session.taskId) {
|
|
@@ -1360,8 +1249,6 @@ export function createAcpFormalAgent(deps) {
|
|
|
1360
1249
|
// Note: The prompt method will check the cancelRequested flag and return
|
|
1361
1250
|
// StopReason::Cancelled as required by the protocol
|
|
1362
1251
|
},
|
|
1363
|
-
extMethod: async () => ({}),
|
|
1364
|
-
extNotification: async () => { },
|
|
1365
1252
|
};
|
|
1366
1253
|
}
|
|
1367
1254
|
//# sourceMappingURL=formal-agent.js.map
|