salmon-loop 0.3.0 → 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 +109 -153
- package/dist/cli/headless/json-protocol.js +1 -1
- package/dist/cli/headless/stream-json-protocol.js +3 -2
- package/dist/cli/slash/runtime.js +5 -1
- package/dist/core/adapters/fs/node-fs.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/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-serve.js +0 -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 +10 -0
- 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 +36 -11
- 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 +271 -603
- 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/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 -309
- 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
|
];
|
|
@@ -130,17 +158,17 @@ function extractTextFromPrompt(prompt, capabilities) {
|
|
|
130
158
|
break;
|
|
131
159
|
case 'image':
|
|
132
160
|
if (!capabilities.image) {
|
|
133
|
-
throw new RequestError(-
|
|
161
|
+
throw new RequestError(-32602, 'Prompt content type image is not supported');
|
|
134
162
|
}
|
|
135
163
|
break;
|
|
136
164
|
case 'audio':
|
|
137
165
|
if (!capabilities.audio) {
|
|
138
|
-
throw new RequestError(-
|
|
166
|
+
throw new RequestError(-32602, 'Prompt content type audio is not supported');
|
|
139
167
|
}
|
|
140
168
|
break;
|
|
141
169
|
case 'resource':
|
|
142
170
|
if (!capabilities.embeddedContext) {
|
|
143
|
-
throw new RequestError(-
|
|
171
|
+
throw new RequestError(-32602, 'Prompt content type resource is not supported');
|
|
144
172
|
}
|
|
145
173
|
parts.push(formatEmbeddedResource(block));
|
|
146
174
|
break;
|
|
@@ -150,46 +178,6 @@ function extractTextFromPrompt(prompt, capabilities) {
|
|
|
150
178
|
}
|
|
151
179
|
return parts.join('\n');
|
|
152
180
|
}
|
|
153
|
-
function mapToolKind(toolName, intent) {
|
|
154
|
-
if (intent) {
|
|
155
|
-
switch (intent.toUpperCase()) {
|
|
156
|
-
case 'READ':
|
|
157
|
-
return 'read';
|
|
158
|
-
case 'LIST':
|
|
159
|
-
return 'read';
|
|
160
|
-
case 'SEARCH':
|
|
161
|
-
return 'search';
|
|
162
|
-
case 'WRITE':
|
|
163
|
-
return 'edit';
|
|
164
|
-
case 'INFRA':
|
|
165
|
-
return 'execute';
|
|
166
|
-
case 'AGENT':
|
|
167
|
-
return 'think';
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const name = toolName.toLowerCase();
|
|
171
|
-
if (name.includes('read') ||
|
|
172
|
-
name.includes('get') ||
|
|
173
|
-
name.includes('view') ||
|
|
174
|
-
name.includes('ls') ||
|
|
175
|
-
name.includes('list'))
|
|
176
|
-
return 'read';
|
|
177
|
-
if (name.includes('write') || name.includes('edit') || name.includes('patch'))
|
|
178
|
-
return 'edit';
|
|
179
|
-
if (name.includes('delete') || name.includes('remove') || name.includes('rm'))
|
|
180
|
-
return 'delete';
|
|
181
|
-
if (name.includes('move') || name.includes('rename') || name.includes('mv'))
|
|
182
|
-
return 'move';
|
|
183
|
-
if (name.includes('grep') || name.includes('search') || name.includes('find'))
|
|
184
|
-
return 'search';
|
|
185
|
-
if (name.includes('run') || name.includes('exec') || name.includes('spawn'))
|
|
186
|
-
return 'execute';
|
|
187
|
-
if (name.includes('plan') || name.includes('think') || name.includes('reason'))
|
|
188
|
-
return 'think';
|
|
189
|
-
if (name.includes('fetch') || name.includes('curl') || name.includes('http'))
|
|
190
|
-
return 'fetch';
|
|
191
|
-
return 'other';
|
|
192
|
-
}
|
|
193
181
|
function buildToolCallContent(textValue) {
|
|
194
182
|
return [{ type: 'content', content: buildTextContentBlock(textValue) }];
|
|
195
183
|
}
|
|
@@ -236,7 +224,7 @@ function loopEventToSessionUpdate(event) {
|
|
|
236
224
|
toolCallId: event.callId,
|
|
237
225
|
status: 'pending',
|
|
238
226
|
title: event.toolName,
|
|
239
|
-
kind: mapToolKind(event.toolName, event.toolIntent),
|
|
227
|
+
kind: mapToolKind(event.toolName, { intent: event.toolIntent }),
|
|
240
228
|
content: [],
|
|
241
229
|
rawInput: event.input,
|
|
242
230
|
locations: extractLocationFromInput(event.input),
|
|
@@ -259,11 +247,6 @@ function loopEventToSessionUpdate(event) {
|
|
|
259
247
|
return null;
|
|
260
248
|
}
|
|
261
249
|
}
|
|
262
|
-
function isPermissionPolicyValue(value) {
|
|
263
|
-
return (value === ACP_PERMISSION_POLICY_ASK ||
|
|
264
|
-
value === ACP_PERMISSION_POLICY_DENY_ALL ||
|
|
265
|
-
value === ACP_PERMISSION_POLICY_ALLOW_ALL);
|
|
266
|
-
}
|
|
267
250
|
function buildConfigOptions(state) {
|
|
268
251
|
return [
|
|
269
252
|
{
|
|
@@ -293,8 +276,8 @@ function buildConfigOptions(state) {
|
|
|
293
276
|
{
|
|
294
277
|
type: 'select',
|
|
295
278
|
id: ACP_MODE_CONFIG_ID,
|
|
296
|
-
name:
|
|
297
|
-
description:
|
|
279
|
+
name: text.acp.executionFlowName,
|
|
280
|
+
description: text.acp.executionFlowDescription,
|
|
298
281
|
currentValue: state.modeId,
|
|
299
282
|
options: ACP_PUBLIC_MODES.map((mode) => ({
|
|
300
283
|
value: mode.id,
|
|
@@ -355,16 +338,6 @@ function buildCurrentModeUpdateIfChanged(state) {
|
|
|
355
338
|
state.lastModeDigest = digest;
|
|
356
339
|
return buildCurrentModeUpdate(state.modeId);
|
|
357
340
|
}
|
|
358
|
-
function getLegacyPermissionPolicyForModeValue(value) {
|
|
359
|
-
const normalized = String(value ?? '')
|
|
360
|
-
.trim()
|
|
361
|
-
.toLowerCase();
|
|
362
|
-
if (normalized === 'interactive')
|
|
363
|
-
return ACP_PERMISSION_POLICY_ASK;
|
|
364
|
-
if (normalized === 'yolo')
|
|
365
|
-
return ACP_PERMISSION_POLICY_ALLOW_ALL;
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
341
|
function getPermissionPolicyForAuthorization(state) {
|
|
369
342
|
return state.permissionPolicy;
|
|
370
343
|
}
|
|
@@ -466,19 +439,36 @@ function buildPlanUpdateFromCoreIfChanged(read, state) {
|
|
|
466
439
|
entries,
|
|
467
440
|
};
|
|
468
441
|
}
|
|
469
|
-
function
|
|
442
|
+
function namedPairsToRecord(pairs) {
|
|
470
443
|
const record = {};
|
|
471
|
-
for (const entry of
|
|
444
|
+
for (const entry of pairs) {
|
|
472
445
|
record[entry.name] = entry.value;
|
|
473
446
|
}
|
|
474
447
|
return record;
|
|
475
448
|
}
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
};
|
|
482
472
|
}
|
|
483
473
|
function acpMcpServersToResolved(mcpServers) {
|
|
484
474
|
if (!Array.isArray(mcpServers))
|
|
@@ -494,11 +484,14 @@ function acpMcpServersToResolved(mcpServers) {
|
|
|
494
484
|
resolved.push({
|
|
495
485
|
name: httpServer.name,
|
|
496
486
|
enabled: true,
|
|
497
|
-
transport:
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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(),
|
|
502
495
|
scope: 'repo',
|
|
503
496
|
});
|
|
504
497
|
continue;
|
|
@@ -510,12 +503,15 @@ function acpMcpServersToResolved(mcpServers) {
|
|
|
510
503
|
resolved.push({
|
|
511
504
|
name: stdioServer.name,
|
|
512
505
|
enabled: true,
|
|
513
|
-
transport:
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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(),
|
|
519
515
|
scope: 'repo',
|
|
520
516
|
});
|
|
521
517
|
}
|
|
@@ -534,6 +530,17 @@ function acpMcpServersToExtensions(mcpServers) {
|
|
|
534
530
|
function validateAcpMcpServers(mcpServers) {
|
|
535
531
|
void acpMcpServersToResolved(mcpServers);
|
|
536
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
|
+
}
|
|
537
544
|
function extractSlashInput(prompt) {
|
|
538
545
|
if (prompt.length !== 1)
|
|
539
546
|
return null;
|
|
@@ -556,6 +563,9 @@ function isKnownSlashCommand(commandName) {
|
|
|
556
563
|
const normalized = normalizeSlashName(commandName);
|
|
557
564
|
return ACP_AVAILABLE_COMMANDS.some((cmd) => cmd.name.toLowerCase() === normalized);
|
|
558
565
|
}
|
|
566
|
+
function isPersistableSession(session) {
|
|
567
|
+
return session.materialized || session.history.length > 0 || typeof session.taskId === 'string';
|
|
568
|
+
}
|
|
559
569
|
async function awaitTerminalEvent(params) {
|
|
560
570
|
if (!params.eventBus)
|
|
561
571
|
return null;
|
|
@@ -603,368 +613,17 @@ export function createAcpFormalAgent(deps) {
|
|
|
603
613
|
lockAcquireTimeoutMs: deps.sessionStorePolicy?.lockAcquireTimeoutMs ?? ACP_SESSION_STORE_LOCK_ACQUIRE_TIMEOUT_MS,
|
|
604
614
|
};
|
|
605
615
|
const executionBinding = deps.executionBinding ?? 'local';
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
return [...records]
|
|
618
|
-
.filter((record) => parseTimestamp(record.updatedAt) >= cutoff)
|
|
619
|
-
.sort((a, b) => parseTimestamp(b.updatedAt) - parseTimestamp(a.updatedAt))
|
|
620
|
-
.slice(0, sessionStorePolicy.maxEntries);
|
|
621
|
-
}
|
|
622
|
-
function normalizeDeletedSessionRecords(input) {
|
|
623
|
-
if (!Array.isArray(input))
|
|
624
|
-
return [];
|
|
625
|
-
const byId = new Map();
|
|
626
|
-
for (const entry of input) {
|
|
627
|
-
if (!entry || typeof entry !== 'object')
|
|
628
|
-
continue;
|
|
629
|
-
const record = entry;
|
|
630
|
-
if (typeof record.id !== 'string' || !record.id)
|
|
631
|
-
continue;
|
|
632
|
-
if (typeof record.deletedAt !== 'string' || !record.deletedAt)
|
|
633
|
-
continue;
|
|
634
|
-
const current = byId.get(record.id);
|
|
635
|
-
if (!current || parseTimestamp(record.deletedAt) > parseTimestamp(current.deletedAt)) {
|
|
636
|
-
byId.set(record.id, { id: record.id, deletedAt: record.deletedAt });
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return Array.from(byId.values());
|
|
640
|
-
}
|
|
641
|
-
function pruneDeletedSessionRecords(records) {
|
|
642
|
-
const cutoff = Date.now() - sessionStorePolicy.maxAgeMs;
|
|
643
|
-
return normalizeDeletedSessionRecords(records)
|
|
644
|
-
.filter((record) => parseTimestamp(record.deletedAt) >= cutoff)
|
|
645
|
-
.sort((a, b) => parseTimestamp(b.deletedAt) - parseTimestamp(a.deletedAt));
|
|
646
|
-
}
|
|
647
|
-
function normalizePersistedSessionStore(input) {
|
|
648
|
-
if (!input || typeof input !== 'object') {
|
|
649
|
-
return { schemaVersion: 2, sessions: [] };
|
|
650
|
-
}
|
|
651
|
-
const raw = input;
|
|
652
|
-
if (!Array.isArray(raw.sessions))
|
|
653
|
-
return { schemaVersion: 2, sessions: [] };
|
|
654
|
-
if (raw.schemaVersion === 1) {
|
|
655
|
-
return {
|
|
656
|
-
schemaVersion: 2,
|
|
657
|
-
sessions: raw.sessions.map((entry) => ({
|
|
658
|
-
id: entry.id,
|
|
659
|
-
cwd: entry.cwd,
|
|
660
|
-
mcpServers: entry.mcpServers,
|
|
661
|
-
createdAt: entry.createdAt,
|
|
662
|
-
updatedAt: entry.updatedAt,
|
|
663
|
-
title: entry.title,
|
|
664
|
-
taskId: undefined,
|
|
665
|
-
history: [],
|
|
666
|
-
permissionPolicy: isPermissionPolicyValue(String(deps.defaultPermissionPolicy))
|
|
667
|
-
? deps.defaultPermissionPolicy
|
|
668
|
-
: ACP_PERMISSION_POLICY_ASK,
|
|
669
|
-
modeId: resolveExposedAcpModeId(deps.defaultModeId),
|
|
670
|
-
})),
|
|
671
|
-
deletedSessions: [],
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
if (raw.schemaVersion === 2) {
|
|
675
|
-
return {
|
|
676
|
-
schemaVersion: 2,
|
|
677
|
-
sessions: raw.sessions,
|
|
678
|
-
deletedSessions: pruneDeletedSessionRecords(raw.deletedSessions),
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
return { schemaVersion: 2, sessions: [] };
|
|
682
|
-
}
|
|
683
|
-
function isPidAlive(pid) {
|
|
684
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
685
|
-
return false;
|
|
686
|
-
try {
|
|
687
|
-
process.kill(pid, 0);
|
|
688
|
-
return true;
|
|
689
|
-
}
|
|
690
|
-
catch (error) {
|
|
691
|
-
if (error &&
|
|
692
|
-
typeof error === 'object' &&
|
|
693
|
-
'code' in error &&
|
|
694
|
-
error.code === 'EPERM') {
|
|
695
|
-
return true;
|
|
696
|
-
}
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
function isFileMissing(error) {
|
|
701
|
-
return Boolean(error &&
|
|
702
|
-
typeof error === 'object' &&
|
|
703
|
-
'code' in error &&
|
|
704
|
-
(error.code === 'ENOENT' ||
|
|
705
|
-
error.code === 'ENOTDIR'));
|
|
706
|
-
}
|
|
707
|
-
async function persistSessionsBestEffort() {
|
|
708
|
-
if (!sessionPersistencePath)
|
|
709
|
-
return;
|
|
710
|
-
const dir = defaultPathAdapter.dirname(sessionPersistencePath);
|
|
711
|
-
const lockPath = `${sessionPersistencePath}.lock`;
|
|
712
|
-
const baseRecords = sessions.list().map((session) => {
|
|
713
|
-
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
714
|
-
return {
|
|
715
|
-
id: session.id,
|
|
716
|
-
cwd: session.cwd,
|
|
717
|
-
mcpServers: session.mcpServers,
|
|
718
|
-
createdAt: session.createdAt,
|
|
719
|
-
updatedAt: session.updatedAt,
|
|
720
|
-
title: session.title,
|
|
721
|
-
taskId: session.taskId,
|
|
722
|
-
history: session.history.slice(-sessionStorePolicy.historyMaxEntries),
|
|
723
|
-
permissionPolicy: runtimeState.permissionPolicy,
|
|
724
|
-
modeId: runtimeState.modeId,
|
|
725
|
-
};
|
|
726
|
-
});
|
|
727
|
-
const prunedRecords = pruneSessionRecords(baseRecords);
|
|
728
|
-
const keepIds = new Set(prunedRecords.map((record) => record.id));
|
|
729
|
-
for (const record of sessions.list()) {
|
|
730
|
-
if (!keepIds.has(record.id)) {
|
|
731
|
-
sessions.delete(record.id);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
const payload = { schemaVersion: 2, sessions: prunedRecords };
|
|
735
|
-
const payloadDeletedSessions = pruneDeletedSessionRecords(Array.from(deletedSessionIds, ([id, deletedAt]) => ({ id, deletedAt })));
|
|
736
|
-
const primaryRepoPath = prunedRecords[0]?.cwd;
|
|
737
|
-
const lockAuditDetails = {
|
|
738
|
-
lockPath,
|
|
739
|
-
lockPathHash: createHash('sha256').update(lockPath).digest('hex').slice(0, 16),
|
|
740
|
-
repoPathHash: primaryRepoPath ? hashRepoPath(primaryRepoPath) : undefined,
|
|
741
|
-
};
|
|
742
|
-
const tryClearStaleLock = async () => {
|
|
743
|
-
try {
|
|
744
|
-
const raw = await readFile(lockPath, 'utf8');
|
|
745
|
-
const parsed = JSON.parse(raw);
|
|
746
|
-
const createdAtMs = typeof parsed.createdAtMs === 'number' && Number.isFinite(parsed.createdAtMs)
|
|
747
|
-
? parsed.createdAtMs
|
|
748
|
-
: null;
|
|
749
|
-
if (createdAtMs === null)
|
|
750
|
-
return;
|
|
751
|
-
if (Date.now() - createdAtMs <= sessionStorePolicy.lockStaleMs)
|
|
752
|
-
return;
|
|
753
|
-
if (typeof parsed.pid === 'number' && isPidAlive(parsed.pid))
|
|
754
|
-
return;
|
|
755
|
-
await unlink(lockPath);
|
|
756
|
-
recordAuditEvent('acp.session.lock.stale_reclaimed', lockAuditDetails, {
|
|
757
|
-
source: 'acp',
|
|
758
|
-
severity: 'low',
|
|
759
|
-
scope: 'session',
|
|
760
|
-
phase: 'PREFLIGHT',
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
catch {
|
|
764
|
-
try {
|
|
765
|
-
const lockStat = await stat(lockPath);
|
|
766
|
-
const ageMs = Date.now() - lockStat.mtimeMs;
|
|
767
|
-
if (Number.isFinite(ageMs) && ageMs > sessionStorePolicy.lockStaleMs * 2) {
|
|
768
|
-
await unlink(lockPath);
|
|
769
|
-
recordAuditEvent('acp.session.lock.corrupted_reclaimed', {
|
|
770
|
-
...lockAuditDetails,
|
|
771
|
-
ageMs: Math.max(0, Math.floor(ageMs)),
|
|
772
|
-
}, { source: 'acp', severity: 'medium', scope: 'session', phase: 'PREFLIGHT' });
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
catch {
|
|
776
|
-
// ignore
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
};
|
|
780
|
-
let lockHandle;
|
|
781
|
-
try {
|
|
782
|
-
await mkdir(dir, { recursive: true });
|
|
783
|
-
const acquireDeadlineMs = Date.now() + Math.max(250, sessionStorePolicy.lockAcquireTimeoutMs);
|
|
784
|
-
for (let attempt = 0; Date.now() < acquireDeadlineMs; attempt += 1) {
|
|
785
|
-
try {
|
|
786
|
-
lockHandle = await open(lockPath, 'wx');
|
|
787
|
-
await lockHandle.writeFile(JSON.stringify({ pid: process.pid, createdAtMs: Date.now() }), 'utf8');
|
|
788
|
-
break;
|
|
789
|
-
}
|
|
790
|
-
catch {
|
|
791
|
-
await tryClearStaleLock();
|
|
792
|
-
const delayMs = Math.min(250, 20 * (attempt + 1));
|
|
793
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (!lockHandle) {
|
|
797
|
-
recordAuditEvent('acp.session.lock.acquire_timeout', lockAuditDetails, {
|
|
798
|
-
source: 'acp',
|
|
799
|
-
severity: 'medium',
|
|
800
|
-
scope: 'session',
|
|
801
|
-
phase: 'PREFLIGHT',
|
|
802
|
-
});
|
|
803
|
-
throw new Error('ACP_SESSION_PERSIST_LOCK_TIMEOUT');
|
|
804
|
-
}
|
|
805
|
-
const heartbeat = setInterval(() => {
|
|
806
|
-
void writeFile(lockPath, JSON.stringify({ pid: process.pid, createdAtMs: Date.now() }), 'utf8');
|
|
807
|
-
}, Math.max(1000, sessionStorePolicy.lockHeartbeatMs));
|
|
808
|
-
const tempPath = defaultPathAdapter.join(dir, `.sessions.v1.json.tmp-${process.pid}-${Date.now()}`);
|
|
809
|
-
try {
|
|
810
|
-
let existing = { schemaVersion: 2, sessions: [] };
|
|
811
|
-
try {
|
|
812
|
-
const existingRaw = await readFile(sessionPersistencePath, 'utf8');
|
|
813
|
-
existing = normalizePersistedSessionStore(JSON.parse(existingRaw));
|
|
814
|
-
}
|
|
815
|
-
catch {
|
|
816
|
-
// ignore read failure; writing fresh payload is acceptable
|
|
817
|
-
}
|
|
818
|
-
const merged = new Map();
|
|
819
|
-
const mergedDeletedSessions = pruneDeletedSessionRecords([
|
|
820
|
-
...(existing.deletedSessions ?? []),
|
|
821
|
-
...payloadDeletedSessions,
|
|
822
|
-
]);
|
|
823
|
-
const mergedDeletedIds = new Set(mergedDeletedSessions.map((record) => record.id));
|
|
824
|
-
for (const record of mergedDeletedSessions) {
|
|
825
|
-
deletedSessionIds.set(record.id, record.deletedAt);
|
|
826
|
-
}
|
|
827
|
-
for (const entry of existing.sessions)
|
|
828
|
-
merged.set(entry.id, entry);
|
|
829
|
-
for (const entry of payload.sessions)
|
|
830
|
-
merged.set(entry.id, entry);
|
|
831
|
-
for (const id of mergedDeletedIds)
|
|
832
|
-
merged.delete(id);
|
|
833
|
-
const mergedPayload = {
|
|
834
|
-
schemaVersion: 2,
|
|
835
|
-
sessions: pruneSessionRecords(Array.from(merged.values())),
|
|
836
|
-
deletedSessions: mergedDeletedSessions,
|
|
837
|
-
};
|
|
838
|
-
await writeFile(tempPath, JSON.stringify(mergedPayload, null, 2), 'utf8');
|
|
839
|
-
await rename(tempPath, sessionPersistencePath);
|
|
840
|
-
}
|
|
841
|
-
finally {
|
|
842
|
-
clearInterval(heartbeat);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
recordAuditEvent('acp.session.persist.failed', {
|
|
847
|
-
errorName: error instanceof Error ? error.name : typeof error,
|
|
848
|
-
}, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
|
|
849
|
-
}
|
|
850
|
-
finally {
|
|
851
|
-
if (lockHandle) {
|
|
852
|
-
try {
|
|
853
|
-
await lockHandle.close();
|
|
854
|
-
}
|
|
855
|
-
catch {
|
|
856
|
-
// ignore
|
|
857
|
-
}
|
|
858
|
-
try {
|
|
859
|
-
await unlink(lockPath);
|
|
860
|
-
}
|
|
861
|
-
catch {
|
|
862
|
-
// ignore
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
async function hydrateSessionsOnce() {
|
|
868
|
-
if (sessionsHydrated)
|
|
869
|
-
return;
|
|
870
|
-
if (hydratePromise)
|
|
871
|
-
return hydratePromise;
|
|
872
|
-
hydratePromise = (async () => {
|
|
873
|
-
sessionsHydrated = true;
|
|
874
|
-
if (!sessionPersistencePath)
|
|
875
|
-
return;
|
|
876
|
-
try {
|
|
877
|
-
const raw = await readFile(sessionPersistencePath, 'utf8');
|
|
878
|
-
const parsed = normalizePersistedSessionStore(JSON.parse(raw));
|
|
879
|
-
const deletedIds = new Set(parsed.deletedSessions?.map((record) => record.id) ?? []);
|
|
880
|
-
for (const record of parsed.deletedSessions ?? []) {
|
|
881
|
-
deletedSessionIds.set(record.id, record.deletedAt);
|
|
882
|
-
}
|
|
883
|
-
for (const stored of pruneSessionRecords(parsed.sessions)) {
|
|
884
|
-
if (deletedIds.has(stored.id))
|
|
885
|
-
continue;
|
|
886
|
-
sessions.upsert({
|
|
887
|
-
id: stored.id,
|
|
888
|
-
cwd: stored.cwd,
|
|
889
|
-
mcpServers: Array.isArray(stored.mcpServers) ? stored.mcpServers : [],
|
|
890
|
-
createdAt: stored.createdAt,
|
|
891
|
-
updatedAt: stored.updatedAt,
|
|
892
|
-
title: stored.title,
|
|
893
|
-
taskId: stored.taskId,
|
|
894
|
-
history: Array.isArray(stored.history)
|
|
895
|
-
? stored.history.slice(-sessionStorePolicy.historyMaxEntries)
|
|
896
|
-
: [],
|
|
897
|
-
cancelRequested: false,
|
|
898
|
-
});
|
|
899
|
-
if (!sessionRuntime.has(stored.id)) {
|
|
900
|
-
sessionRuntime.set(stored.id, createSessionRuntimeStateFromPersisted({
|
|
901
|
-
permissionPolicy: stored.permissionPolicy,
|
|
902
|
-
defaultPermissionPolicy: deps.defaultPermissionPolicy,
|
|
903
|
-
modeId: stored.modeId,
|
|
904
|
-
defaultModeId: deps.defaultModeId,
|
|
905
|
-
}));
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
catch (error) {
|
|
910
|
-
if (isFileMissing(error))
|
|
911
|
-
return;
|
|
912
|
-
recordAuditEvent('acp.session.hydrate.failed', {
|
|
913
|
-
errorName: error instanceof Error ? error.name : typeof error,
|
|
914
|
-
}, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
|
|
915
|
-
}
|
|
916
|
-
})();
|
|
917
|
-
return hydratePromise;
|
|
918
|
-
}
|
|
919
|
-
function hashRepoPath(repoPath) {
|
|
920
|
-
return createHash('sha256').update(repoPath).digest('hex').slice(0, 16);
|
|
921
|
-
}
|
|
922
|
-
function toCheckpointMeta(input) {
|
|
923
|
-
if (!input)
|
|
924
|
-
return null;
|
|
925
|
-
return {
|
|
926
|
-
id: input.id,
|
|
927
|
-
createdAt: input.createdAt ?? null,
|
|
928
|
-
strategy: input.strategy ?? null,
|
|
929
|
-
backend: input.backend ?? null,
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
function toResumeHint(probe) {
|
|
933
|
-
if (!probe || probe.valid)
|
|
934
|
-
return null;
|
|
935
|
-
switch (probe.reason) {
|
|
936
|
-
case 'not_found':
|
|
937
|
-
return {
|
|
938
|
-
code: 'CHECKPOINT_NOT_FOUND',
|
|
939
|
-
message: 'Checkpoint not found. Start a new session.',
|
|
940
|
-
};
|
|
941
|
-
case 'manifest_parse_error':
|
|
942
|
-
return {
|
|
943
|
-
code: 'CHECKPOINT_MANIFEST_PARSE_ERROR',
|
|
944
|
-
message: 'Checkpoint metadata is corrupted. Recreate checkpoint metadata and retry.',
|
|
945
|
-
};
|
|
946
|
-
case 'manifest_io_error':
|
|
947
|
-
return {
|
|
948
|
-
code: 'CHECKPOINT_MANIFEST_IO_ERROR',
|
|
949
|
-
message: 'Checkpoint metadata is unreadable due to filesystem I/O issues.',
|
|
950
|
-
};
|
|
951
|
-
case 'manifest_lock_timeout':
|
|
952
|
-
return {
|
|
953
|
-
code: 'CHECKPOINT_MANIFEST_LOCK_TIMEOUT',
|
|
954
|
-
message: 'Checkpoint metadata is busy (lock timeout). Retry shortly.',
|
|
955
|
-
};
|
|
956
|
-
case 'manifest_unavailable':
|
|
957
|
-
return {
|
|
958
|
-
code: 'CHECKPOINT_MANIFEST_UNAVAILABLE',
|
|
959
|
-
message: 'Checkpoint metadata is unavailable in current runtime.',
|
|
960
|
-
};
|
|
961
|
-
default:
|
|
962
|
-
return {
|
|
963
|
-
code: 'CHECKPOINT_RESUME_UNAVAILABLE',
|
|
964
|
-
message: 'Checkpoint resume is unavailable. Start a new session or retry.',
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
}
|
|
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
|
+
});
|
|
968
627
|
async function emitSessionUpdate(sessionId, update) {
|
|
969
628
|
await deps.conn.sessionUpdate({ sessionId, update });
|
|
970
629
|
}
|
|
@@ -987,15 +646,13 @@ export function createAcpFormalAgent(deps) {
|
|
|
987
646
|
if (!shouldRefreshPlanForEvent(params.event))
|
|
988
647
|
return;
|
|
989
648
|
const { state, event } = params;
|
|
990
|
-
const planReader = deps.planReader
|
|
991
|
-
readBySession: async ({ repoPath, sessionId }) => await readPlan({ persistenceRoot: repoPath, sessionId }),
|
|
992
|
-
};
|
|
649
|
+
const planReader = deps.planReader;
|
|
993
650
|
if (event?.type === 'plan.runtime.ready') {
|
|
994
651
|
state.runtimePlanSessionId = event.sessionId;
|
|
995
652
|
state.runtimePlanPathHint = event.planPathHint;
|
|
996
653
|
state.lastPlanDigest = null;
|
|
997
654
|
}
|
|
998
|
-
if (!state.runtimePlanSessionId)
|
|
655
|
+
if (!state.runtimePlanSessionId || !planReader)
|
|
999
656
|
return;
|
|
1000
657
|
try {
|
|
1001
658
|
const read = await planReader.readBySession({
|
|
@@ -1017,8 +674,9 @@ export function createAcpFormalAgent(deps) {
|
|
|
1017
674
|
}, { source: 'acp', severity: 'low', scope: 'session', phase: 'PLAN' });
|
|
1018
675
|
}
|
|
1019
676
|
}
|
|
1020
|
-
async function
|
|
1021
|
-
await
|
|
677
|
+
async function resolveExistingSession(params) {
|
|
678
|
+
await sessionPersistence.hydrate();
|
|
679
|
+
validateUnsupportedAdditionalDirectories(params.additionalDirectories);
|
|
1022
680
|
validateAcpMcpServers(params.mcpServers);
|
|
1023
681
|
const session = sessions.get(params.sessionId);
|
|
1024
682
|
if (!session) {
|
|
@@ -1028,30 +686,12 @@ export function createAcpFormalAgent(deps) {
|
|
|
1028
686
|
throw new RequestError(-32602, 'Invalid params: cwd does not match session cwd');
|
|
1029
687
|
}
|
|
1030
688
|
if (params.mcpServers) {
|
|
689
|
+
const mcpServers = params.mcpServers;
|
|
1031
690
|
sessions.update(params.sessionId, (current) => ({
|
|
1032
691
|
...current,
|
|
1033
|
-
mcpServers
|
|
692
|
+
mcpServers,
|
|
1034
693
|
}));
|
|
1035
|
-
await
|
|
1036
|
-
}
|
|
1037
|
-
return session;
|
|
1038
|
-
}
|
|
1039
|
-
async function resumeSessionInternal(params) {
|
|
1040
|
-
await hydrateSessionsOnce();
|
|
1041
|
-
validateAcpMcpServers(params.mcpServers);
|
|
1042
|
-
const session = sessions.get(params.sessionId);
|
|
1043
|
-
if (!session) {
|
|
1044
|
-
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
1045
|
-
}
|
|
1046
|
-
if (session.cwd !== params.cwd) {
|
|
1047
|
-
throw new RequestError(-32602, 'Invalid params: cwd does not match session cwd');
|
|
1048
|
-
}
|
|
1049
|
-
if (params.mcpServers) {
|
|
1050
|
-
sessions.update(params.sessionId, (current) => ({
|
|
1051
|
-
...current,
|
|
1052
|
-
mcpServers: params.mcpServers ?? [],
|
|
1053
|
-
}));
|
|
1054
|
-
await persistSessionsBestEffort();
|
|
694
|
+
await sessionPersistence.persist();
|
|
1055
695
|
}
|
|
1056
696
|
return session;
|
|
1057
697
|
}
|
|
@@ -1067,29 +707,62 @@ export function createAcpFormalAgent(deps) {
|
|
|
1067
707
|
const existing = sessionRuntime.get(sessionId);
|
|
1068
708
|
if (existing)
|
|
1069
709
|
return existing;
|
|
710
|
+
const session = sessions.get(sessionId);
|
|
1070
711
|
const created = createSessionRuntimeStateFromPersisted({
|
|
712
|
+
permissionPolicy: session?.permissionPolicy,
|
|
1071
713
|
defaultPermissionPolicy: deps.defaultPermissionPolicy,
|
|
714
|
+
modeId: session?.modeId,
|
|
1072
715
|
defaultModeId: deps.defaultModeId,
|
|
1073
716
|
});
|
|
1074
717
|
sessionRuntime.set(sessionId, created);
|
|
1075
718
|
return created;
|
|
1076
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
|
+
}
|
|
1077
758
|
return {
|
|
1078
759
|
async initialize(params) {
|
|
1079
760
|
if (typeof params.protocolVersion !== 'number' || !Number.isFinite(params.protocolVersion)) {
|
|
1080
761
|
throw new RequestError(-32602, 'Invalid params: protocolVersion is required');
|
|
1081
762
|
}
|
|
1082
763
|
clientCapabilities = params.clientCapabilities;
|
|
1083
|
-
// Protocol version negotiation:
|
|
1084
|
-
// - If the client's requested version is supported, return the same version
|
|
1085
|
-
// - Otherwise, return the latest version the agent supports
|
|
1086
|
-
// Currently, the agent only supports protocol version 1
|
|
1087
|
-
const supportedProtocolVersion = PROTOCOL_VERSION;
|
|
1088
|
-
const negotiatedVersion = params.protocolVersion <= supportedProtocolVersion
|
|
1089
|
-
? params.protocolVersion
|
|
1090
|
-
: supportedProtocolVersion;
|
|
1091
764
|
return {
|
|
1092
|
-
protocolVersion:
|
|
765
|
+
protocolVersion: negotiateProtocolVersion(params.protocolVersion),
|
|
1093
766
|
agentInfo: deps.agentInfo,
|
|
1094
767
|
authMethods: [],
|
|
1095
768
|
agentCapabilities: {
|
|
@@ -1100,25 +773,30 @@ export function createAcpFormalAgent(deps) {
|
|
|
1100
773
|
list: {},
|
|
1101
774
|
resume: {},
|
|
1102
775
|
close: {},
|
|
776
|
+
delete: {},
|
|
1103
777
|
},
|
|
1104
778
|
},
|
|
1105
779
|
};
|
|
1106
780
|
},
|
|
1107
|
-
async authenticate() {
|
|
1108
|
-
|
|
781
|
+
async authenticate(params) {
|
|
782
|
+
throw new RequestError(-32602, `Invalid params: unsupported auth methodId "${params.methodId}"`);
|
|
1109
783
|
},
|
|
1110
784
|
async newSession(params) {
|
|
1111
|
-
await
|
|
785
|
+
await sessionPersistence.hydrate();
|
|
1112
786
|
if (!isAbsolutePath(params.cwd)) {
|
|
1113
787
|
throw new RequestError(-32602, 'Invalid params: cwd must be an absolute path');
|
|
1114
788
|
}
|
|
789
|
+
validateUnsupportedAdditionalDirectories(params.additionalDirectories);
|
|
1115
790
|
validateAcpMcpServers(params.mcpServers);
|
|
1116
791
|
const session = sessions.create({
|
|
1117
792
|
cwd: params.cwd,
|
|
1118
793
|
mcpServers: params.mcpServers ?? [],
|
|
1119
794
|
title: deriveSessionTitleFromCwd(params.cwd),
|
|
795
|
+
permissionPolicy: isPermissionPolicyValue(String(deps.defaultPermissionPolicy))
|
|
796
|
+
? deps.defaultPermissionPolicy
|
|
797
|
+
: ACP_PERMISSION_POLICY_ASK,
|
|
798
|
+
modeId: resolveExposedAcpModeId(deps.defaultModeId),
|
|
1120
799
|
});
|
|
1121
|
-
await persistSessionsBestEffort();
|
|
1122
800
|
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
1123
801
|
await emitSessionInfoUpdateBestEffort(session.id);
|
|
1124
802
|
// Restore session state on creation
|
|
@@ -1130,18 +808,11 @@ export function createAcpFormalAgent(deps) {
|
|
|
1130
808
|
await emitSessionUpdate(session.id, modeUpdate);
|
|
1131
809
|
let sessionMeta;
|
|
1132
810
|
if (deps.checkpointReader) {
|
|
1133
|
-
const
|
|
811
|
+
const result = await probeCheckpointForNewSession(deps.checkpointReader, {
|
|
1134
812
|
repoPath: params.cwd,
|
|
1135
813
|
sessionId: session.id,
|
|
1136
|
-
limit: 1,
|
|
1137
814
|
});
|
|
1138
|
-
|
|
1139
|
-
sessionMeta = {
|
|
1140
|
-
salmonloop: {
|
|
1141
|
-
latestCheckpointId: latest?.id ?? null,
|
|
1142
|
-
checkpoint: toCheckpointMeta(latest),
|
|
1143
|
-
},
|
|
1144
|
-
};
|
|
815
|
+
sessionMeta = result._meta;
|
|
1145
816
|
}
|
|
1146
817
|
return {
|
|
1147
818
|
sessionId: session.id,
|
|
@@ -1152,9 +823,9 @@ export function createAcpFormalAgent(deps) {
|
|
|
1152
823
|
},
|
|
1153
824
|
async loadSession(params) {
|
|
1154
825
|
if (!loadSessionCapability) {
|
|
1155
|
-
throw new RequestError(-32601, '
|
|
826
|
+
throw new RequestError(-32601, 'Method not found: session/load');
|
|
1156
827
|
}
|
|
1157
|
-
await
|
|
828
|
+
await resolveExistingSession(params);
|
|
1158
829
|
let session = sessions.get(params.sessionId);
|
|
1159
830
|
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
1160
831
|
if (typeof session.title !== 'string' || !session.title.trim()) {
|
|
@@ -1163,7 +834,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1163
834
|
...current,
|
|
1164
835
|
title: deriveSessionTitleFromCwd(current.cwd),
|
|
1165
836
|
})) ?? session;
|
|
1166
|
-
await
|
|
837
|
+
await sessionPersistence.persist();
|
|
1167
838
|
}
|
|
1168
839
|
runtimeState.lastSessionInfoDigest = null;
|
|
1169
840
|
await emitSessionInfoUpdateBestEffort(session.id);
|
|
@@ -1183,10 +854,10 @@ export function createAcpFormalAgent(deps) {
|
|
|
1183
854
|
await emitSessionUpdate(session.id, modeUpdate);
|
|
1184
855
|
for (const entry of session.history) {
|
|
1185
856
|
for (const block of entry.content) {
|
|
1186
|
-
if (block
|
|
857
|
+
if (isReplayableSessionContentBlock(block)) {
|
|
1187
858
|
await emitSessionUpdate(session.id, {
|
|
1188
859
|
sessionUpdate: entry.role === 'assistant' ? 'agent_message_chunk' : 'user_message_chunk',
|
|
1189
|
-
content:
|
|
860
|
+
content: block,
|
|
1190
861
|
});
|
|
1191
862
|
}
|
|
1192
863
|
}
|
|
@@ -1196,56 +867,12 @@ export function createAcpFormalAgent(deps) {
|
|
|
1196
867
|
modes: buildModesState(runtimeState.modeId),
|
|
1197
868
|
};
|
|
1198
869
|
if (deps.checkpointReader) {
|
|
1199
|
-
const
|
|
1200
|
-
const checkpoints = await deps.checkpointReader.listBySession({
|
|
870
|
+
const result = await probeCheckpoint(deps.checkpointReader, {
|
|
1201
871
|
repoPath: params.cwd,
|
|
1202
|
-
sessionId: params.sessionId,
|
|
1203
|
-
limit: 1,
|
|
1204
|
-
});
|
|
1205
|
-
const latest = checkpoints.at(-1);
|
|
1206
|
-
let resumeProbe = null;
|
|
1207
|
-
if (latest?.id && deps.checkpointReader.probeById) {
|
|
1208
|
-
const probed = await deps.checkpointReader.probeById({
|
|
1209
|
-
repoPath: params.cwd,
|
|
1210
|
-
checkpointId: latest.id,
|
|
1211
|
-
});
|
|
1212
|
-
resumeProbe = {
|
|
1213
|
-
checkpointId: latest.id,
|
|
1214
|
-
valid: probed.valid,
|
|
1215
|
-
reason: probed.reason,
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
|
-
else if (latest?.id && deps.checkpointReader.getById) {
|
|
1219
|
-
const found = await deps.checkpointReader.getById({
|
|
1220
|
-
repoPath: params.cwd,
|
|
1221
|
-
checkpointId: latest.id,
|
|
1222
|
-
});
|
|
1223
|
-
resumeProbe = {
|
|
1224
|
-
checkpointId: latest.id,
|
|
1225
|
-
valid: Boolean(found),
|
|
1226
|
-
reason: found ? 'ok' : 'not_found',
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
const resumeReady = resumeProbe?.valid ?? Boolean(latest);
|
|
1230
|
-
recordAuditEvent('acp.checkpoint.read', {
|
|
1231
872
|
sessionId: params.sessionId,
|
|
1232
873
|
repoPathHash: hashRepoPath(params.cwd),
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
latencyMs: Date.now() - startedAt,
|
|
1236
|
-
resumeProbe: resumeProbe ?? undefined,
|
|
1237
|
-
}, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
|
|
1238
|
-
const resumeHint = toResumeHint(resumeProbe);
|
|
1239
|
-
response._meta = {
|
|
1240
|
-
salmonloop: {
|
|
1241
|
-
latestCheckpointId: latest?.id ?? null,
|
|
1242
|
-
checkpoint: toCheckpointMeta(latest),
|
|
1243
|
-
resumeReady,
|
|
1244
|
-
resumeProbe,
|
|
1245
|
-
resumeHint: resumeHint?.message ?? null,
|
|
1246
|
-
resumeHintCode: resumeHint?.code ?? null,
|
|
1247
|
-
},
|
|
1248
|
-
};
|
|
874
|
+
});
|
|
875
|
+
response._meta = result._meta;
|
|
1249
876
|
}
|
|
1250
877
|
else {
|
|
1251
878
|
recordAuditEvent('acp.checkpoint.read', {
|
|
@@ -1259,23 +886,39 @@ export function createAcpFormalAgent(deps) {
|
|
|
1259
886
|
return response;
|
|
1260
887
|
},
|
|
1261
888
|
async listSessions(params) {
|
|
1262
|
-
await
|
|
889
|
+
await sessionPersistence.hydrate();
|
|
1263
890
|
if (typeof params.cwd === 'string' && params.cwd && !isAbsolutePath(params.cwd)) {
|
|
1264
891
|
throw new RequestError(-32602, 'Invalid params: cwd must be an absolute path');
|
|
1265
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
|
+
}
|
|
1266
902
|
const filtered = sessions
|
|
1267
903
|
.list()
|
|
1268
|
-
.filter((session) => !
|
|
904
|
+
.filter((session) => !cwdFilter || session.cwd === cwdFilter)
|
|
1269
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;
|
|
1270
911
|
return {
|
|
1271
|
-
sessions:
|
|
912
|
+
sessions: page.map(toSessionInfo),
|
|
913
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
1272
914
|
};
|
|
1273
915
|
},
|
|
1274
916
|
async resumeSession(params) {
|
|
1275
|
-
const session = await
|
|
917
|
+
const session = await resolveExistingSession({
|
|
1276
918
|
sessionId: params.sessionId,
|
|
1277
919
|
cwd: params.cwd,
|
|
1278
920
|
mcpServers: params.mcpServers,
|
|
921
|
+
additionalDirectories: params.additionalDirectories,
|
|
1279
922
|
});
|
|
1280
923
|
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
1281
924
|
runtimeState.lastSessionInfoDigest = null;
|
|
@@ -1286,57 +929,64 @@ export function createAcpFormalAgent(deps) {
|
|
|
1286
929
|
const modeUpdate = buildCurrentModeUpdateIfChanged(runtimeState);
|
|
1287
930
|
if (modeUpdate)
|
|
1288
931
|
await emitSessionUpdate(session.id, modeUpdate);
|
|
1289
|
-
|
|
932
|
+
const response = {
|
|
1290
933
|
configOptions: buildConfigOptions(runtimeState),
|
|
1291
934
|
modes: buildModesState(runtimeState.modeId),
|
|
1292
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;
|
|
1293
945
|
},
|
|
1294
946
|
async closeSession(params) {
|
|
1295
|
-
await
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
}
|
|
1303
|
-
deletedSessionIds.set(params.sessionId, new Date().toISOString());
|
|
1304
|
-
sessionRuntime.delete(params.sessionId);
|
|
1305
|
-
sessions.delete(params.sessionId);
|
|
1306
|
-
await persistSessionsBestEffort();
|
|
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 });
|
|
1307
954
|
return {};
|
|
1308
955
|
},
|
|
1309
956
|
async setSessionConfigOption(params) {
|
|
1310
|
-
await
|
|
957
|
+
await sessionPersistence.hydrate();
|
|
1311
958
|
if (!sessions.get(params.sessionId)) {
|
|
1312
959
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
1313
960
|
}
|
|
1314
961
|
const runtimeState = ensureSessionRuntimeState(params.sessionId);
|
|
1315
|
-
if (typeof params.value !== 'string') {
|
|
1316
|
-
throw new RequestError(-32602, `Invalid params: unsupported non-string value for "${params.configId}"`);
|
|
1317
|
-
}
|
|
1318
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
|
+
}
|
|
1319
966
|
if (!isPermissionPolicyValue(params.value)) {
|
|
1320
967
|
throw new RequestError(-32602, `Invalid params: unsupported value "${params.value}" for "${params.configId}"`);
|
|
1321
968
|
}
|
|
1322
969
|
runtimeState.permissionPolicy = params.value;
|
|
1323
970
|
}
|
|
1324
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
|
+
}
|
|
1325
975
|
const parsedModeId = parseAcpFlowMode(params.value);
|
|
1326
976
|
if (!parsedModeId || !ACP_PUBLIC_MODE_IDS.has(parsedModeId)) {
|
|
1327
977
|
throw new RequestError(-32602, `Invalid params: unsupported value "${params.value}" for "${params.configId}"`);
|
|
1328
978
|
}
|
|
1329
979
|
runtimeState.modeId = parsedModeId;
|
|
1330
|
-
const legacyPermissionPolicy = getLegacyPermissionPolicyForModeValue(params.value);
|
|
1331
|
-
if (legacyPermissionPolicy) {
|
|
1332
|
-
runtimeState.permissionPolicy = legacyPermissionPolicy;
|
|
1333
|
-
}
|
|
1334
980
|
}
|
|
1335
981
|
else {
|
|
1336
982
|
throw new RequestError(-32602, `Invalid params: unsupported configId "${params.configId}"`);
|
|
1337
983
|
}
|
|
1338
|
-
sessions.update(params.sessionId, (current) => ({
|
|
1339
|
-
|
|
984
|
+
sessions.update(params.sessionId, (current) => ({
|
|
985
|
+
...current,
|
|
986
|
+
permissionPolicy: runtimeState.permissionPolicy,
|
|
987
|
+
modeId: runtimeState.modeId,
|
|
988
|
+
}));
|
|
989
|
+
await sessionPersistence.persist();
|
|
1340
990
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1341
991
|
const update = buildConfigOptionUpdateIfChanged(runtimeState);
|
|
1342
992
|
if (update) {
|
|
@@ -1349,7 +999,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1349
999
|
return { configOptions: buildConfigOptions(runtimeState) };
|
|
1350
1000
|
},
|
|
1351
1001
|
async setSessionMode(params) {
|
|
1352
|
-
await
|
|
1002
|
+
await sessionPersistence.hydrate();
|
|
1353
1003
|
if (!sessions.get(params.sessionId)) {
|
|
1354
1004
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
1355
1005
|
}
|
|
@@ -1359,12 +1009,12 @@ export function createAcpFormalAgent(deps) {
|
|
|
1359
1009
|
throw new RequestError(-32602, `Invalid params: unsupported modeId "${params.modeId}"`);
|
|
1360
1010
|
}
|
|
1361
1011
|
runtimeState.modeId = resolvedModeId;
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
runtimeState.permissionPolicy
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
await
|
|
1012
|
+
sessions.update(params.sessionId, (current) => ({
|
|
1013
|
+
...current,
|
|
1014
|
+
permissionPolicy: runtimeState.permissionPolicy,
|
|
1015
|
+
modeId: runtimeState.modeId,
|
|
1016
|
+
}));
|
|
1017
|
+
await sessionPersistence.persist();
|
|
1368
1018
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1369
1019
|
const configUpdate = buildConfigOptionUpdateIfChanged(runtimeState);
|
|
1370
1020
|
if (configUpdate) {
|
|
@@ -1378,7 +1028,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1378
1028
|
return {};
|
|
1379
1029
|
},
|
|
1380
1030
|
async prompt(params) {
|
|
1381
|
-
await
|
|
1031
|
+
await sessionPersistence.hydrate();
|
|
1382
1032
|
const session = sessions.get(params.sessionId);
|
|
1383
1033
|
if (!session) {
|
|
1384
1034
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
@@ -1391,7 +1041,10 @@ export function createAcpFormalAgent(deps) {
|
|
|
1391
1041
|
const runtimeState = ensureSessionRuntimeState(params.sessionId);
|
|
1392
1042
|
// Check for cancellation before starting processing
|
|
1393
1043
|
if (sessions.get(params.sessionId)?.cancelRequested === true) {
|
|
1394
|
-
return {
|
|
1044
|
+
return {
|
|
1045
|
+
stopReason: 'cancelled',
|
|
1046
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1047
|
+
};
|
|
1395
1048
|
}
|
|
1396
1049
|
sessions.update(params.sessionId, (current) => {
|
|
1397
1050
|
const title = typeof current.title === 'string' && current.title.trim()
|
|
@@ -1401,13 +1054,16 @@ export function createAcpFormalAgent(deps) {
|
|
|
1401
1054
|
...current,
|
|
1402
1055
|
cancelRequested: false,
|
|
1403
1056
|
title,
|
|
1057
|
+
materialized: true,
|
|
1058
|
+
permissionPolicy: runtimeState.permissionPolicy,
|
|
1059
|
+
modeId: runtimeState.modeId,
|
|
1404
1060
|
history: [
|
|
1405
1061
|
...current.history,
|
|
1406
1062
|
{ role: 'user', content: params.prompt },
|
|
1407
1063
|
],
|
|
1408
1064
|
};
|
|
1409
1065
|
});
|
|
1410
|
-
await
|
|
1066
|
+
await sessionPersistence.persist();
|
|
1411
1067
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1412
1068
|
const configUpdate = buildConfigOptionUpdateIfChanged(runtimeState);
|
|
1413
1069
|
if (configUpdate) {
|
|
@@ -1439,14 +1095,20 @@ export function createAcpFormalAgent(deps) {
|
|
|
1439
1095
|
{ role: 'assistant', content: [buildTextContentBlock(responseText)] },
|
|
1440
1096
|
],
|
|
1441
1097
|
}));
|
|
1442
|
-
await
|
|
1098
|
+
await sessionPersistence.persist();
|
|
1443
1099
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1444
|
-
return {
|
|
1100
|
+
return {
|
|
1101
|
+
stopReason: 'end_turn',
|
|
1102
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1103
|
+
};
|
|
1445
1104
|
}
|
|
1446
1105
|
}
|
|
1447
1106
|
// Check for cancellation again before creating task
|
|
1448
1107
|
if (sessions.get(params.sessionId)?.cancelRequested === true) {
|
|
1449
|
-
return {
|
|
1108
|
+
return {
|
|
1109
|
+
stopReason: 'cancelled',
|
|
1110
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1111
|
+
};
|
|
1450
1112
|
}
|
|
1451
1113
|
const pendingUpdates = [];
|
|
1452
1114
|
const executionRequest = buildCanonicalExecutionRequest({
|
|
@@ -1489,34 +1151,39 @@ export function createAcpFormalAgent(deps) {
|
|
|
1489
1151
|
},
|
|
1490
1152
|
});
|
|
1491
1153
|
sessions.update(params.sessionId, (current) => ({ ...current, taskId: task.id }));
|
|
1492
|
-
await
|
|
1154
|
+
await sessionPersistence.persist();
|
|
1493
1155
|
if (signal.aborted) {
|
|
1494
1156
|
await emitSessionUpdate(params.sessionId, {
|
|
1495
1157
|
sessionUpdate: 'agent_message_chunk',
|
|
1496
|
-
content: buildTextContentBlock(ensureMarkdownParagraphBreak(
|
|
1158
|
+
content: buildTextContentBlock(ensureMarkdownParagraphBreak(text.acp.taskCancelled)),
|
|
1497
1159
|
});
|
|
1498
|
-
return {
|
|
1160
|
+
return {
|
|
1161
|
+
stopReason: 'cancelled',
|
|
1162
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1163
|
+
};
|
|
1499
1164
|
}
|
|
1500
1165
|
const terminalEvent = await awaitTerminalEvent({ taskId: task.id, eventBus: deps.eventBus });
|
|
1501
1166
|
let stopReason = 'end_turn';
|
|
1502
|
-
let assistantText =
|
|
1167
|
+
let assistantText = text.acp.taskCompleted;
|
|
1503
1168
|
let assistantMeta;
|
|
1504
1169
|
let latest;
|
|
1505
1170
|
const cancelRequested = sessions.get(params.sessionId)?.cancelRequested === true;
|
|
1506
1171
|
if (cancelRequested) {
|
|
1507
|
-
assistantText =
|
|
1172
|
+
assistantText = text.acp.taskCancelled;
|
|
1508
1173
|
stopReason = 'cancelled';
|
|
1509
1174
|
}
|
|
1510
1175
|
else if (terminalEvent?.type === 'task.failed') {
|
|
1511
1176
|
latest = await deps.facade.getTask(task.id);
|
|
1512
1177
|
const failureMessage = typeof latest?.failure?.message === 'string' ? latest.failure.message : undefined;
|
|
1513
|
-
assistantText = failureMessage
|
|
1178
|
+
assistantText = failureMessage
|
|
1179
|
+
? text.acp.taskFailedWithReason(failureMessage)
|
|
1180
|
+
: text.acp.taskFailed;
|
|
1514
1181
|
const inferred = inferTurnStopReasonFromFailure(latest?.failure);
|
|
1515
1182
|
if (inferred)
|
|
1516
1183
|
stopReason = inferred;
|
|
1517
1184
|
}
|
|
1518
1185
|
else if (terminalEvent?.type === 'task.awaiting_input') {
|
|
1519
|
-
assistantText =
|
|
1186
|
+
assistantText = text.acp.taskAwaitingInput;
|
|
1520
1187
|
latest = await deps.facade.getTask(task.id);
|
|
1521
1188
|
const formatted = latest?.inputRequired
|
|
1522
1189
|
? formatInputRequiredMessage(latest.inputRequired)
|
|
@@ -1528,7 +1195,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1528
1195
|
}
|
|
1529
1196
|
}
|
|
1530
1197
|
else if (terminalEvent?.type === 'task.cancelled') {
|
|
1531
|
-
assistantText =
|
|
1198
|
+
assistantText = text.acp.taskCancelled;
|
|
1532
1199
|
stopReason = 'cancelled';
|
|
1533
1200
|
}
|
|
1534
1201
|
await emitSessionUpdate(params.sessionId, {
|
|
@@ -1549,7 +1216,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1549
1216
|
{ role: 'assistant', content: [buildTextContentBlock(assistantText)] },
|
|
1550
1217
|
],
|
|
1551
1218
|
}));
|
|
1552
|
-
await
|
|
1219
|
+
await sessionPersistence.persist();
|
|
1553
1220
|
const latestSession = sessions.get(params.sessionId);
|
|
1554
1221
|
if (latestSession) {
|
|
1555
1222
|
const sessionInfoUpdate = buildSessionInfoUpdateIfChanged(latestSession, runtimeState);
|
|
@@ -1561,16 +1228,19 @@ export function createAcpFormalAgent(deps) {
|
|
|
1561
1228
|
}
|
|
1562
1229
|
// Wait for all pending session updates to be sent before responding
|
|
1563
1230
|
await Promise.all(pendingUpdates);
|
|
1564
|
-
return {
|
|
1231
|
+
return {
|
|
1232
|
+
stopReason,
|
|
1233
|
+
...(params.messageId ? { userMessageId: params.messageId } : {}),
|
|
1234
|
+
};
|
|
1565
1235
|
},
|
|
1566
1236
|
async cancel(params) {
|
|
1567
|
-
await
|
|
1237
|
+
await sessionPersistence.hydrate();
|
|
1568
1238
|
const session = sessions.get(params.sessionId);
|
|
1569
1239
|
if (!session)
|
|
1570
1240
|
return;
|
|
1571
1241
|
// Mark the session as cancelled
|
|
1572
1242
|
sessions.update(params.sessionId, (current) => ({ ...current, cancelRequested: true }));
|
|
1573
|
-
await
|
|
1243
|
+
await sessionPersistence.persist();
|
|
1574
1244
|
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1575
1245
|
// If a task is running, cancel it
|
|
1576
1246
|
if (session.taskId) {
|
|
@@ -1579,8 +1249,6 @@ export function createAcpFormalAgent(deps) {
|
|
|
1579
1249
|
// Note: The prompt method will check the cancelRequested flag and return
|
|
1580
1250
|
// StopReason::Cancelled as required by the protocol
|
|
1581
1251
|
},
|
|
1582
|
-
extMethod: async () => ({}),
|
|
1583
|
-
extNotification: async () => { },
|
|
1584
1252
|
};
|
|
1585
1253
|
}
|
|
1586
1254
|
//# sourceMappingURL=formal-agent.js.map
|