salmon-loop 0.2.16 → 0.3.0
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/commands/serve.js +7 -7
- package/dist/cli/program-bootstrap.js +2 -2
- package/dist/core/backends/salmon-loop/task-executor.js +1 -0
- package/dist/core/extensions/merge.js +14 -0
- package/dist/core/facades/cli-program-bootstrap.js +1 -0
- package/dist/core/facades/cli-serve.js +2 -0
- package/dist/core/interaction/orchestration/facade.js +1 -1
- package/dist/core/llm/ai-sdk/request-params.js +40 -1
- package/dist/core/protocols/a2a/agent-card.js +2 -1
- package/dist/core/protocols/acp/formal-agent.js +226 -7
- package/dist/core/tools/mcp/client.js +2 -1
- package/dist/core/version.js +17 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizePermissionMode } from '../../core/config/index.js';
|
|
2
|
-
import { buildA2AAgentCard, createAcpFormalAgent, createAgentServerRuntime, createInteractionFacade, createSalmonTaskExecutor, createTaskEventBus, createPluginRegistry, createPromptRegistry, getUserAcpSessionStorePath, GitSnapshotCheckpointService, getLogger, PlainReporter, PluginLoader, resolveExtensions, resolveExecutionProfile, runSalmonLoop, setPluginRegistry, setPromptRegistry, startAcpStdioServer, StderrReporter, } from '../../core/facades/cli-serve.js';
|
|
2
|
+
import { buildA2AAgentCard, createAcpFormalAgent, createAgentServerRuntime, createInteractionFacade, createSalmonTaskExecutor, createTaskEventBus, createPluginRegistry, createPromptRegistry, getUserAcpSessionStorePath, GitSnapshotCheckpointService, getLogger, mergeResolvedExtensions, PACKAGE_VERSION, PlainReporter, PluginLoader, resolveExtensions, resolveExecutionProfile, runSalmonLoop, setPluginRegistry, setPromptRegistry, startAcpStdioServer, StderrReporter, } from '../../core/facades/cli-serve.js';
|
|
3
3
|
import { selectPublicCapabilitiesForSurface, toA2APublicSkills, } from '../../core/public-capabilities/projections.js';
|
|
4
4
|
import { buildPublicCapabilityRegistry } from '../../core/public-capabilities/registry.js';
|
|
5
5
|
import { createTerminalAuthorizationProvider } from '../authorization/provider.js';
|
|
@@ -124,7 +124,7 @@ export async function handleServeCommand(_options, command) {
|
|
|
124
124
|
forceNonInteractive: true,
|
|
125
125
|
});
|
|
126
126
|
const executor = createSalmonTaskExecutor({
|
|
127
|
-
runLoop: async ({ instruction, mode, repoPath, onEvent, signal, authorizationProvider, authorizationMode, fileSystemOverride, }) => {
|
|
127
|
+
runLoop: async ({ instruction, mode, repoPath, onEvent, signal, authorizationProvider, authorizationMode, fileSystemOverride, extensions: taskExtensions, }) => {
|
|
128
128
|
const effectiveRepoPath = repoPath ?? defaultRepoPath;
|
|
129
129
|
const flowMode = mode;
|
|
130
130
|
const executionDefaults = buildServeLoopExecutionDefaults(flowMode);
|
|
@@ -151,7 +151,7 @@ export async function handleServeCommand(_options, command) {
|
|
|
151
151
|
fileSystemOverride,
|
|
152
152
|
authorizationProvider: authorizationProvider ?? defaultAuthorizationProvider,
|
|
153
153
|
authorizationMode,
|
|
154
|
-
extensions: extensions.resolved,
|
|
154
|
+
extensions: mergeResolvedExtensions(extensions.resolved, taskExtensions),
|
|
155
155
|
onEvent,
|
|
156
156
|
signal,
|
|
157
157
|
});
|
|
@@ -199,7 +199,7 @@ export async function handleServeCommand(_options, command) {
|
|
|
199
199
|
if (acpStdioEnabled) {
|
|
200
200
|
startAcpStdioServer((conn) => createAcpFormalAgent({
|
|
201
201
|
conn,
|
|
202
|
-
agentInfo: { name: 'salmon-loop', version:
|
|
202
|
+
agentInfo: { name: 'salmon-loop', version: PACKAGE_VERSION },
|
|
203
203
|
defaultModeId: 'autopilot',
|
|
204
204
|
defaultPermissionPolicy: resolveDefaultAcpPermissionPolicy(defaultPermissionMode),
|
|
205
205
|
checkpointReader: {
|
|
@@ -280,7 +280,7 @@ export async function handleServeAcpCommand(_options, command) {
|
|
|
280
280
|
forceNonInteractive: true,
|
|
281
281
|
});
|
|
282
282
|
const executor = createSalmonTaskExecutor({
|
|
283
|
-
runLoop: async ({ instruction, mode, repoPath, onEvent, signal, authorizationProvider, authorizationMode, }) => {
|
|
283
|
+
runLoop: async ({ instruction, mode, repoPath, onEvent, signal, authorizationProvider, authorizationMode, extensions: taskExtensions, }) => {
|
|
284
284
|
const effectiveRepoPath = repoPath ?? defaultRepoPath;
|
|
285
285
|
const flowMode = mode;
|
|
286
286
|
const executionDefaults = buildServeLoopExecutionDefaults(flowMode);
|
|
@@ -306,7 +306,7 @@ export async function handleServeAcpCommand(_options, command) {
|
|
|
306
306
|
languagePlugins,
|
|
307
307
|
authorizationProvider: authorizationProvider ?? defaultAuthorizationProvider,
|
|
308
308
|
authorizationMode,
|
|
309
|
-
extensions: extensions.resolved,
|
|
309
|
+
extensions: mergeResolvedExtensions(extensions.resolved, taskExtensions),
|
|
310
310
|
onEvent,
|
|
311
311
|
signal,
|
|
312
312
|
});
|
|
@@ -325,7 +325,7 @@ export async function handleServeAcpCommand(_options, command) {
|
|
|
325
325
|
});
|
|
326
326
|
startAcpStdioServer((conn) => createAcpFormalAgent({
|
|
327
327
|
conn,
|
|
328
|
-
agentInfo: { name: 'salmon-loop', version:
|
|
328
|
+
agentInfo: { name: 'salmon-loop', version: PACKAGE_VERSION },
|
|
329
329
|
defaultModeId: 'autopilot',
|
|
330
330
|
defaultPermissionPolicy: resolveDefaultAcpPermissionPolicy(defaultPermissionMode),
|
|
331
331
|
checkpointReader: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { initializeRuntime } from '../core/facades/cli-program-bootstrap.js';
|
|
3
|
+
import { initializeRuntime, PACKAGE_VERSION } from '../core/facades/cli-program-bootstrap.js';
|
|
4
4
|
import { text } from './locales/index.js';
|
|
5
5
|
export function bootstrapProgram(options = {}) {
|
|
6
6
|
initializeRuntime();
|
|
@@ -17,7 +17,7 @@ export function bootstrapProgram(options = {}) {
|
|
|
17
17
|
.name('s8p')
|
|
18
18
|
.alias('salmonloop')
|
|
19
19
|
.description(text.cli.programDescription)
|
|
20
|
-
.version(
|
|
20
|
+
.version(PACKAGE_VERSION)
|
|
21
21
|
.addHelpText('after', text.cli.programHelpFooter);
|
|
22
22
|
return program;
|
|
23
23
|
}
|
|
@@ -31,6 +31,7 @@ export function createSalmonTaskExecutor(deps) {
|
|
|
31
31
|
authorizationProvider: options?.authorizationProvider,
|
|
32
32
|
authorizationMode: options?.authorizationMode,
|
|
33
33
|
fileSystemOverride: options?.fileSystemOverride,
|
|
34
|
+
extensions: task.request.extensions,
|
|
34
35
|
});
|
|
35
36
|
});
|
|
36
37
|
if (result.reasonCode === 'AWAITING_INPUT' && result.inputRequired) {
|
|
@@ -26,4 +26,18 @@ export function mergeScopedEntries(user, repo) {
|
|
|
26
26
|
}
|
|
27
27
|
return Array.from(merged.values());
|
|
28
28
|
}
|
|
29
|
+
export function mergeResolvedExtensions(base, overlay) {
|
|
30
|
+
if (!overlay)
|
|
31
|
+
return base;
|
|
32
|
+
return {
|
|
33
|
+
mcpServers: [...base.mcpServers, ...overlay.mcpServers],
|
|
34
|
+
toolPlugins: [...base.toolPlugins, ...overlay.toolPlugins],
|
|
35
|
+
skillDiscovery: {
|
|
36
|
+
scope: overlay.skillDiscovery.paths.length > 0
|
|
37
|
+
? overlay.skillDiscovery.scope
|
|
38
|
+
: base.skillDiscovery.scope,
|
|
39
|
+
paths: [...base.skillDiscovery.paths, ...overlay.skillDiscovery.paths],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
29
43
|
//# sourceMappingURL=merge.js.map
|
|
@@ -2,6 +2,7 @@ export { createSalmonTaskExecutor } from '../backends/salmon-loop/task-executor.
|
|
|
2
2
|
export { GitSnapshotCheckpointService } from '../checkpoint-domain/service.js';
|
|
3
3
|
export { resolveConfig } from '../config/resolve.js';
|
|
4
4
|
export { resolveExtensions } from '../extensions/index.js';
|
|
5
|
+
export { mergeResolvedExtensions } from '../extensions/merge.js';
|
|
5
6
|
export { createTaskEventBus } from '../interaction/events/bus.js';
|
|
6
7
|
export { createInteractionFacade } from '../interaction/orchestration/facade.js';
|
|
7
8
|
export { getLogger, PlainReporter, StderrReporter } from '../observability/logger.js';
|
|
@@ -16,4 +17,5 @@ export { createAgentServerRuntime } from '../runtime/agent-server-runtime.js';
|
|
|
16
17
|
export { resolveExecutionProfile } from '../runtime/execution-profile.js';
|
|
17
18
|
export { runSalmonLoop } from '../runtime/loop.js';
|
|
18
19
|
export { getUserAcpSessionStorePath } from '../runtime/paths.js';
|
|
20
|
+
export { PACKAGE_VERSION } from '../version.js';
|
|
19
21
|
//# sourceMappingURL=cli-serve.js.map
|
|
@@ -20,7 +20,7 @@ export function createInteractionFacade(deps) {
|
|
|
20
20
|
id: input.taskId ?? `task_${Date.now()}`,
|
|
21
21
|
capability: input.capability,
|
|
22
22
|
state: 'accepted',
|
|
23
|
-
request: input.request,
|
|
23
|
+
request: { ...input.request, extensions: input.extensions },
|
|
24
24
|
createdAt: new Date().toISOString(),
|
|
25
25
|
attempt: 1,
|
|
26
26
|
};
|
|
@@ -65,10 +65,49 @@ function resolveResponseFormat(options) {
|
|
|
65
65
|
}
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
|
+
function stringifySystemContent(content) {
|
|
69
|
+
if (typeof content === 'string')
|
|
70
|
+
return content;
|
|
71
|
+
if (content === undefined || content === null)
|
|
72
|
+
return '';
|
|
73
|
+
if (Array.isArray(content)) {
|
|
74
|
+
return content
|
|
75
|
+
.map((part) => {
|
|
76
|
+
if (typeof part === 'string')
|
|
77
|
+
return part;
|
|
78
|
+
if (isRecord(part) && typeof part.text === 'string')
|
|
79
|
+
return part.text;
|
|
80
|
+
return '';
|
|
81
|
+
})
|
|
82
|
+
.filter((part) => part.length > 0)
|
|
83
|
+
.join('\n');
|
|
84
|
+
}
|
|
85
|
+
return String(content);
|
|
86
|
+
}
|
|
87
|
+
function splitSystemMessages(messages) {
|
|
88
|
+
const systemParts = [];
|
|
89
|
+
const conversationMessages = [];
|
|
90
|
+
for (const message of messages) {
|
|
91
|
+
if (isRecord(message) && message.role === 'system') {
|
|
92
|
+
const content = stringifySystemContent(message.content).trim();
|
|
93
|
+
if (content) {
|
|
94
|
+
systemParts.push(content);
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
conversationMessages.push(message);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
system: systemParts.length > 0 ? systemParts.join('\n\n') : undefined,
|
|
102
|
+
messages: conversationMessages,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
68
105
|
export function buildAiSdkRequestParams(params) {
|
|
106
|
+
const splitMessages = splitSystemMessages(params.messages);
|
|
69
107
|
return {
|
|
70
108
|
model: params.model,
|
|
71
|
-
|
|
109
|
+
system: splitMessages.system,
|
|
110
|
+
messages: splitMessages.messages,
|
|
72
111
|
tools: params.tools,
|
|
73
112
|
temperature: params.options.temperature,
|
|
74
113
|
maxOutputTokens: params.options.maxTokens != null ? Number(params.options.maxTokens) : undefined,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from '../../version.js';
|
|
1
2
|
export function buildA2AAgentCard(input) {
|
|
2
3
|
const capabilities = input.capabilities ?? [];
|
|
3
4
|
const securitySchemes = input.security.length > 0
|
|
@@ -10,7 +11,7 @@ export function buildA2AAgentCard(input) {
|
|
|
10
11
|
name: input.name,
|
|
11
12
|
url: input.url,
|
|
12
13
|
description: input.description ?? 'Salmon Loop agent',
|
|
13
|
-
version: input.version ??
|
|
14
|
+
version: input.version ?? PACKAGE_VERSION,
|
|
14
15
|
protocolVersion: input.protocolVersion ?? '1.0.0',
|
|
15
16
|
defaultInputModes: ['text/plain'],
|
|
16
17
|
defaultOutputModes: ['text/plain'],
|
|
@@ -103,6 +103,21 @@ function formatResourceLink(block) {
|
|
|
103
103
|
const description = block.description ? ` - ${block.description}` : '';
|
|
104
104
|
return `Resource: ${title} (${block.uri})${description}`;
|
|
105
105
|
}
|
|
106
|
+
function formatEmbeddedResource(block) {
|
|
107
|
+
const resource = block.resource;
|
|
108
|
+
const uri = typeof resource.uri === 'string' ? resource.uri : 'embedded-resource';
|
|
109
|
+
const mimeType = typeof resource.mimeType === 'string' ? resource.mimeType : undefined;
|
|
110
|
+
if (typeof resource.text === 'string') {
|
|
111
|
+
const header = mimeType
|
|
112
|
+
? `Embedded resource: ${uri} (${mimeType})`
|
|
113
|
+
: `Embedded resource: ${uri}`;
|
|
114
|
+
return `${header}\n${resource.text}`;
|
|
115
|
+
}
|
|
116
|
+
const header = mimeType
|
|
117
|
+
? `Embedded binary resource: ${uri} (${mimeType})`
|
|
118
|
+
: `Embedded binary resource: ${uri}`;
|
|
119
|
+
return header;
|
|
120
|
+
}
|
|
106
121
|
function extractTextFromPrompt(prompt, capabilities) {
|
|
107
122
|
const parts = [];
|
|
108
123
|
for (const block of prompt) {
|
|
@@ -127,6 +142,7 @@ function extractTextFromPrompt(prompt, capabilities) {
|
|
|
127
142
|
if (!capabilities.embeddedContext) {
|
|
128
143
|
throw new RequestError(-32000, 'Prompt content type resource is not supported');
|
|
129
144
|
}
|
|
145
|
+
parts.push(formatEmbeddedResource(block));
|
|
130
146
|
break;
|
|
131
147
|
default:
|
|
132
148
|
throw new RequestError(-32602, 'Invalid params: unsupported content block type');
|
|
@@ -450,6 +466,74 @@ function buildPlanUpdateFromCoreIfChanged(read, state) {
|
|
|
450
466
|
entries,
|
|
451
467
|
};
|
|
452
468
|
}
|
|
469
|
+
function envListToRecord(env) {
|
|
470
|
+
const record = {};
|
|
471
|
+
for (const entry of env) {
|
|
472
|
+
record[entry.name] = entry.value;
|
|
473
|
+
}
|
|
474
|
+
return record;
|
|
475
|
+
}
|
|
476
|
+
function headersListToRecord(headers) {
|
|
477
|
+
const record = {};
|
|
478
|
+
for (const entry of headers) {
|
|
479
|
+
record[entry.name] = entry.value;
|
|
480
|
+
}
|
|
481
|
+
return record;
|
|
482
|
+
}
|
|
483
|
+
function acpMcpServersToResolved(mcpServers) {
|
|
484
|
+
if (!Array.isArray(mcpServers))
|
|
485
|
+
return [];
|
|
486
|
+
const resolved = [];
|
|
487
|
+
for (const server of mcpServers) {
|
|
488
|
+
const transportType = 'type' in server ? server.type : 'stdio';
|
|
489
|
+
if (transportType === 'sse' || transportType === 'acp') {
|
|
490
|
+
throw new RequestError(-32602, `Invalid params: unsupported MCP server transport "${transportType}"`);
|
|
491
|
+
}
|
|
492
|
+
if ('type' in server && server.type === 'http') {
|
|
493
|
+
const httpServer = server;
|
|
494
|
+
resolved.push({
|
|
495
|
+
name: httpServer.name,
|
|
496
|
+
enabled: true,
|
|
497
|
+
transport: 'http',
|
|
498
|
+
url: httpServer.url,
|
|
499
|
+
headers: headersListToRecord(httpServer.headers),
|
|
500
|
+
allowTools: ['*'],
|
|
501
|
+
allowResources: [],
|
|
502
|
+
scope: 'repo',
|
|
503
|
+
});
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (transportType !== 'stdio') {
|
|
507
|
+
throw new RequestError(-32602, `Invalid params: unsupported MCP server transport "${transportType}"`);
|
|
508
|
+
}
|
|
509
|
+
const stdioServer = server;
|
|
510
|
+
resolved.push({
|
|
511
|
+
name: stdioServer.name,
|
|
512
|
+
enabled: true,
|
|
513
|
+
transport: 'stdio',
|
|
514
|
+
command: stdioServer.command,
|
|
515
|
+
args: stdioServer.args,
|
|
516
|
+
env: envListToRecord(stdioServer.env),
|
|
517
|
+
allowTools: ['*'],
|
|
518
|
+
allowResources: [],
|
|
519
|
+
scope: 'repo',
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return resolved;
|
|
523
|
+
}
|
|
524
|
+
function acpMcpServersToExtensions(mcpServers) {
|
|
525
|
+
const resolvedServers = acpMcpServersToResolved(mcpServers);
|
|
526
|
+
if (resolvedServers.length === 0)
|
|
527
|
+
return undefined;
|
|
528
|
+
return {
|
|
529
|
+
mcpServers: resolvedServers,
|
|
530
|
+
toolPlugins: [],
|
|
531
|
+
skillDiscovery: { paths: [], scope: 'repo' },
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function validateAcpMcpServers(mcpServers) {
|
|
535
|
+
void acpMcpServersToResolved(mcpServers);
|
|
536
|
+
}
|
|
453
537
|
function extractSlashInput(prompt) {
|
|
454
538
|
if (prompt.length !== 1)
|
|
455
539
|
return null;
|
|
@@ -505,8 +589,9 @@ export function createAcpFormalAgent(deps) {
|
|
|
505
589
|
embeddedContext: deps.capabilityPolicy?.promptCapabilities?.embeddedContext ?? false,
|
|
506
590
|
};
|
|
507
591
|
const mcpCapabilities = {
|
|
508
|
-
http: deps.capabilityPolicy?.mcpCapabilities?.http ??
|
|
592
|
+
http: deps.capabilityPolicy?.mcpCapabilities?.http ?? true,
|
|
509
593
|
sse: deps.capabilityPolicy?.mcpCapabilities?.sse ?? false,
|
|
594
|
+
acp: deps.capabilityPolicy?.mcpCapabilities?.acp ?? false,
|
|
510
595
|
};
|
|
511
596
|
const sessionPersistencePath = deps.sessionPersistencePath;
|
|
512
597
|
const sessionStorePolicy = {
|
|
@@ -520,6 +605,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
520
605
|
const executionBinding = deps.executionBinding ?? 'local';
|
|
521
606
|
let sessionsHydrated = false;
|
|
522
607
|
let hydratePromise = null;
|
|
608
|
+
const deletedSessionIds = new Map();
|
|
523
609
|
function parseTimestamp(value) {
|
|
524
610
|
if (typeof value !== 'string' || value.length === 0)
|
|
525
611
|
return 0;
|
|
@@ -533,6 +619,31 @@ export function createAcpFormalAgent(deps) {
|
|
|
533
619
|
.sort((a, b) => parseTimestamp(b.updatedAt) - parseTimestamp(a.updatedAt))
|
|
534
620
|
.slice(0, sessionStorePolicy.maxEntries);
|
|
535
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
|
+
}
|
|
536
647
|
function normalizePersistedSessionStore(input) {
|
|
537
648
|
if (!input || typeof input !== 'object') {
|
|
538
649
|
return { schemaVersion: 2, sessions: [] };
|
|
@@ -557,10 +668,15 @@ export function createAcpFormalAgent(deps) {
|
|
|
557
668
|
: ACP_PERMISSION_POLICY_ASK,
|
|
558
669
|
modeId: resolveExposedAcpModeId(deps.defaultModeId),
|
|
559
670
|
})),
|
|
671
|
+
deletedSessions: [],
|
|
560
672
|
};
|
|
561
673
|
}
|
|
562
674
|
if (raw.schemaVersion === 2) {
|
|
563
|
-
return {
|
|
675
|
+
return {
|
|
676
|
+
schemaVersion: 2,
|
|
677
|
+
sessions: raw.sessions,
|
|
678
|
+
deletedSessions: pruneDeletedSessionRecords(raw.deletedSessions),
|
|
679
|
+
};
|
|
564
680
|
}
|
|
565
681
|
return { schemaVersion: 2, sessions: [] };
|
|
566
682
|
}
|
|
@@ -616,6 +732,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
616
732
|
}
|
|
617
733
|
}
|
|
618
734
|
const payload = { schemaVersion: 2, sessions: prunedRecords };
|
|
735
|
+
const payloadDeletedSessions = pruneDeletedSessionRecords(Array.from(deletedSessionIds, ([id, deletedAt]) => ({ id, deletedAt })));
|
|
619
736
|
const primaryRepoPath = prunedRecords[0]?.cwd;
|
|
620
737
|
const lockAuditDetails = {
|
|
621
738
|
lockPath,
|
|
@@ -699,13 +816,24 @@ export function createAcpFormalAgent(deps) {
|
|
|
699
816
|
// ignore read failure; writing fresh payload is acceptable
|
|
700
817
|
}
|
|
701
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
|
+
}
|
|
702
827
|
for (const entry of existing.sessions)
|
|
703
828
|
merged.set(entry.id, entry);
|
|
704
829
|
for (const entry of payload.sessions)
|
|
705
830
|
merged.set(entry.id, entry);
|
|
831
|
+
for (const id of mergedDeletedIds)
|
|
832
|
+
merged.delete(id);
|
|
706
833
|
const mergedPayload = {
|
|
707
834
|
schemaVersion: 2,
|
|
708
835
|
sessions: pruneSessionRecords(Array.from(merged.values())),
|
|
836
|
+
deletedSessions: mergedDeletedSessions,
|
|
709
837
|
};
|
|
710
838
|
await writeFile(tempPath, JSON.stringify(mergedPayload, null, 2), 'utf8');
|
|
711
839
|
await rename(tempPath, sessionPersistencePath);
|
|
@@ -748,7 +876,13 @@ export function createAcpFormalAgent(deps) {
|
|
|
748
876
|
try {
|
|
749
877
|
const raw = await readFile(sessionPersistencePath, 'utf8');
|
|
750
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
|
+
}
|
|
751
883
|
for (const stored of pruneSessionRecords(parsed.sessions)) {
|
|
884
|
+
if (deletedIds.has(stored.id))
|
|
885
|
+
continue;
|
|
752
886
|
sessions.upsert({
|
|
753
887
|
id: stored.id,
|
|
754
888
|
cwd: stored.cwd,
|
|
@@ -885,20 +1019,50 @@ export function createAcpFormalAgent(deps) {
|
|
|
885
1019
|
}
|
|
886
1020
|
async function loadSessionInternal(params) {
|
|
887
1021
|
await hydrateSessionsOnce();
|
|
1022
|
+
validateAcpMcpServers(params.mcpServers);
|
|
888
1023
|
const session = sessions.get(params.sessionId);
|
|
889
1024
|
if (!session) {
|
|
890
1025
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
891
1026
|
}
|
|
892
1027
|
if (session.cwd !== params.cwd) {
|
|
1028
|
+
throw new RequestError(-32602, 'Invalid params: cwd does not match session cwd');
|
|
1029
|
+
}
|
|
1030
|
+
if (params.mcpServers) {
|
|
1031
|
+
sessions.update(params.sessionId, (current) => ({
|
|
1032
|
+
...current,
|
|
1033
|
+
mcpServers: params.mcpServers,
|
|
1034
|
+
}));
|
|
1035
|
+
await persistSessionsBestEffort();
|
|
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) {
|
|
893
1050
|
sessions.update(params.sessionId, (current) => ({
|
|
894
1051
|
...current,
|
|
895
|
-
cwd: params.cwd,
|
|
896
1052
|
mcpServers: params.mcpServers ?? [],
|
|
897
1053
|
}));
|
|
898
1054
|
await persistSessionsBestEffort();
|
|
899
1055
|
}
|
|
900
1056
|
return session;
|
|
901
1057
|
}
|
|
1058
|
+
function toSessionInfo(session) {
|
|
1059
|
+
return {
|
|
1060
|
+
sessionId: session.id,
|
|
1061
|
+
cwd: session.cwd,
|
|
1062
|
+
title: typeof session.title === 'string' && session.title.trim() ? session.title : null,
|
|
1063
|
+
updatedAt: session.updatedAt,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
902
1066
|
function ensureSessionRuntimeState(sessionId) {
|
|
903
1067
|
const existing = sessionRuntime.get(sessionId);
|
|
904
1068
|
if (existing)
|
|
@@ -932,7 +1096,11 @@ export function createAcpFormalAgent(deps) {
|
|
|
932
1096
|
loadSession: loadSessionCapability,
|
|
933
1097
|
promptCapabilities: promptCapabilities,
|
|
934
1098
|
mcpCapabilities: mcpCapabilities,
|
|
935
|
-
sessionCapabilities: {
|
|
1099
|
+
sessionCapabilities: {
|
|
1100
|
+
list: {},
|
|
1101
|
+
resume: {},
|
|
1102
|
+
close: {},
|
|
1103
|
+
},
|
|
936
1104
|
},
|
|
937
1105
|
};
|
|
938
1106
|
},
|
|
@@ -944,6 +1112,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
944
1112
|
if (!isAbsolutePath(params.cwd)) {
|
|
945
1113
|
throw new RequestError(-32602, 'Invalid params: cwd must be an absolute path');
|
|
946
1114
|
}
|
|
1115
|
+
validateAcpMcpServers(params.mcpServers);
|
|
947
1116
|
const session = sessions.create({
|
|
948
1117
|
cwd: params.cwd,
|
|
949
1118
|
mcpServers: params.mcpServers ?? [],
|
|
@@ -1013,12 +1182,10 @@ export function createAcpFormalAgent(deps) {
|
|
|
1013
1182
|
if (modeUpdate)
|
|
1014
1183
|
await emitSessionUpdate(session.id, modeUpdate);
|
|
1015
1184
|
for (const entry of session.history) {
|
|
1016
|
-
if (entry.role !== 'assistant')
|
|
1017
|
-
continue;
|
|
1018
1185
|
for (const block of entry.content) {
|
|
1019
1186
|
if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
|
|
1020
1187
|
await emitSessionUpdate(session.id, {
|
|
1021
|
-
sessionUpdate: 'agent_message_chunk',
|
|
1188
|
+
sessionUpdate: entry.role === 'assistant' ? 'agent_message_chunk' : 'user_message_chunk',
|
|
1022
1189
|
content: buildTextContentBlock(block.text),
|
|
1023
1190
|
});
|
|
1024
1191
|
}
|
|
@@ -1091,12 +1258,63 @@ export function createAcpFormalAgent(deps) {
|
|
|
1091
1258
|
}
|
|
1092
1259
|
return response;
|
|
1093
1260
|
},
|
|
1261
|
+
async listSessions(params) {
|
|
1262
|
+
await hydrateSessionsOnce();
|
|
1263
|
+
if (typeof params.cwd === 'string' && params.cwd && !isAbsolutePath(params.cwd)) {
|
|
1264
|
+
throw new RequestError(-32602, 'Invalid params: cwd must be an absolute path');
|
|
1265
|
+
}
|
|
1266
|
+
const filtered = sessions
|
|
1267
|
+
.list()
|
|
1268
|
+
.filter((session) => !params.cwd || session.cwd === params.cwd)
|
|
1269
|
+
.sort((a, b) => parseTimestamp(b.updatedAt) - parseTimestamp(a.updatedAt));
|
|
1270
|
+
return {
|
|
1271
|
+
sessions: filtered.map(toSessionInfo),
|
|
1272
|
+
};
|
|
1273
|
+
},
|
|
1274
|
+
async resumeSession(params) {
|
|
1275
|
+
const session = await resumeSessionInternal({
|
|
1276
|
+
sessionId: params.sessionId,
|
|
1277
|
+
cwd: params.cwd,
|
|
1278
|
+
mcpServers: params.mcpServers,
|
|
1279
|
+
});
|
|
1280
|
+
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
1281
|
+
runtimeState.lastSessionInfoDigest = null;
|
|
1282
|
+
await emitSessionInfoUpdateBestEffort(session.id);
|
|
1283
|
+
const commandsUpdate = buildAvailableCommandsUpdateIfChanged(runtimeState);
|
|
1284
|
+
if (commandsUpdate)
|
|
1285
|
+
await emitSessionUpdate(session.id, commandsUpdate);
|
|
1286
|
+
const modeUpdate = buildCurrentModeUpdateIfChanged(runtimeState);
|
|
1287
|
+
if (modeUpdate)
|
|
1288
|
+
await emitSessionUpdate(session.id, modeUpdate);
|
|
1289
|
+
return {
|
|
1290
|
+
configOptions: buildConfigOptions(runtimeState),
|
|
1291
|
+
modes: buildModesState(runtimeState.modeId),
|
|
1292
|
+
};
|
|
1293
|
+
},
|
|
1294
|
+
async closeSession(params) {
|
|
1295
|
+
await hydrateSessionsOnce();
|
|
1296
|
+
const session = sessions.get(params.sessionId);
|
|
1297
|
+
if (!session)
|
|
1298
|
+
return {};
|
|
1299
|
+
sessions.update(params.sessionId, (current) => ({ ...current, cancelRequested: true }));
|
|
1300
|
+
if (session.taskId) {
|
|
1301
|
+
await deps.facade.cancelTask(session.taskId);
|
|
1302
|
+
}
|
|
1303
|
+
deletedSessionIds.set(params.sessionId, new Date().toISOString());
|
|
1304
|
+
sessionRuntime.delete(params.sessionId);
|
|
1305
|
+
sessions.delete(params.sessionId);
|
|
1306
|
+
await persistSessionsBestEffort();
|
|
1307
|
+
return {};
|
|
1308
|
+
},
|
|
1094
1309
|
async setSessionConfigOption(params) {
|
|
1095
1310
|
await hydrateSessionsOnce();
|
|
1096
1311
|
if (!sessions.get(params.sessionId)) {
|
|
1097
1312
|
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
1098
1313
|
}
|
|
1099
1314
|
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
|
+
}
|
|
1100
1318
|
if (params.configId === ACP_PERMISSION_POLICY_CONFIG_ID) {
|
|
1101
1319
|
if (!isPermissionPolicyValue(params.value)) {
|
|
1102
1320
|
throw new RequestError(-32602, `Invalid params: unsupported value "${params.value}" for "${params.configId}"`);
|
|
@@ -1245,6 +1463,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1245
1463
|
fileSystemOverride: effectiveExecutionBinding === 'client'
|
|
1246
1464
|
? createAcpFileSystem({ conn: deps.conn, sessionId: params.sessionId })
|
|
1247
1465
|
: undefined,
|
|
1466
|
+
extensions: acpMcpServersToExtensions(session.mcpServers),
|
|
1248
1467
|
authorizationProvider: createAcpToolAuthorizationProvider({
|
|
1249
1468
|
conn: deps.conn,
|
|
1250
1469
|
sessionId: params.sessionId,
|
|
@@ -2,6 +2,7 @@ import { createInterface } from 'readline';
|
|
|
2
2
|
import { LIMITS } from '../../config/limits.js';
|
|
3
3
|
import { getLogger } from '../../observability/logger.js';
|
|
4
4
|
import { spawnInteractiveProcess } from '../../runtime/process-runner.js';
|
|
5
|
+
import { PACKAGE_VERSION } from '../../version.js';
|
|
5
6
|
import { assertOk, createMcpHeaders, decodeSseEvents, delayMs, isEventStreamResponse, safeDrainResponse, } from './streamable-http.js';
|
|
6
7
|
/**
|
|
7
8
|
* MCP Client handling JSON-RPC communication over stdio with an external server.
|
|
@@ -77,7 +78,7 @@ export class McpClient {
|
|
|
77
78
|
await this.request('initialize', {
|
|
78
79
|
protocolVersion: '2025-11-25',
|
|
79
80
|
capabilities: {},
|
|
80
|
-
clientInfo: { name: 'salmon-loop', version:
|
|
81
|
+
clientInfo: { name: 'salmon-loop', version: PACKAGE_VERSION },
|
|
81
82
|
});
|
|
82
83
|
// Step 2: Signal initialized
|
|
83
84
|
await this.notification('notifications/initialized', {});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
function readPackageVersion() {
|
|
4
|
+
try {
|
|
5
|
+
const pkg = require('../../package.json');
|
|
6
|
+
if (typeof pkg.version === 'string' && pkg.version.trim()) {
|
|
7
|
+
return pkg.version;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// Fall back for non-package runtime embeddings.
|
|
12
|
+
}
|
|
13
|
+
return '0.0.0';
|
|
14
|
+
}
|
|
15
|
+
export const PACKAGE_NAME = 'salmon-loop';
|
|
16
|
+
export const PACKAGE_VERSION = readPackageVersion();
|
|
17
|
+
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "salmon-loop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A chat-first coding agent CLI for safe, reviewable repository changes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
},
|
|
127
127
|
"dependencies": {
|
|
128
128
|
"@a2a-js/sdk": "0.3.10",
|
|
129
|
-
"@agentclientprotocol/sdk": "
|
|
129
|
+
"@agentclientprotocol/sdk": "0.22.0",
|
|
130
130
|
"@ai-sdk/openai": "^3.0.23",
|
|
131
131
|
"@ai-sdk/openai-compatible": "^2.0.24",
|
|
132
132
|
"@inquirer/prompts": "^8.2.0",
|