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.
@@ -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: '0.2.0' },
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: '0.2.0' },
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('0.2.0')
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
@@ -1,2 +1,3 @@
1
1
  export { initializeRuntime } from '../runtime/initialize.js';
2
+ export { PACKAGE_VERSION } from '../version.js';
2
3
  //# sourceMappingURL=cli-program-bootstrap.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
- messages: params.messages,
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 ?? '0.2.0',
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 ?? false,
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 { schemaVersion: 2, sessions: raw.sessions };
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: '0.2.0' },
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.2.16",
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": "^0.14.1",
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",