salmon-loop 0.2.16 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/cli/authorization/non-interactive.js +7 -21
  2. package/dist/cli/commands/chat.js +1 -1
  3. package/dist/cli/commands/parallel.js +46 -41
  4. package/dist/cli/commands/run/assistant-message.js +3 -0
  5. package/dist/cli/commands/run/handler.js +2 -1
  6. package/dist/cli/commands/serve.js +112 -156
  7. package/dist/cli/headless/json-protocol.js +1 -1
  8. package/dist/cli/headless/stream-json-protocol.js +3 -2
  9. package/dist/cli/program-bootstrap.js +2 -2
  10. package/dist/cli/slash/runtime.js +5 -1
  11. package/dist/core/adapters/fs/node-fs.js +1 -0
  12. package/dist/core/backends/salmon-loop/task-executor.js +1 -0
  13. package/dist/core/benchmark/patch-artifact.js +1 -1
  14. package/dist/core/context/service.js +5 -2
  15. package/dist/core/extensions/index.js +2 -35
  16. package/dist/core/extensions/merge.js +14 -0
  17. package/dist/core/extensions/redact.js +9 -3
  18. package/dist/core/extensions/schemas.js +2 -51
  19. package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
  20. package/dist/core/facades/cli-program-bootstrap.js +1 -0
  21. package/dist/core/facades/cli-serve.js +2 -1
  22. package/dist/core/grizzco/dsl/strategies.js +1 -3
  23. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
  24. package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
  25. package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
  26. package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
  27. package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
  28. package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
  29. package/dist/core/grizzco/steps/apply.js +0 -7
  30. package/dist/core/grizzco/steps/autopilot.js +108 -6
  31. package/dist/core/grizzco/steps/preflight.js +10 -0
  32. package/dist/core/grizzco/steps/tool-runtime.js +1 -0
  33. package/dist/core/interaction/events/bus.js +14 -0
  34. package/dist/core/interaction/orchestration/facade.js +11 -1
  35. package/dist/core/llm/ai-sdk/request-params.js +40 -1
  36. package/dist/core/mcp/bridge/index.js +4 -0
  37. package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
  38. package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
  39. package/dist/core/mcp/bridge/tool-bridge.js +303 -0
  40. package/dist/core/mcp/cache/resource-cache.js +41 -0
  41. package/dist/core/mcp/catalog/discovery.js +51 -0
  42. package/dist/core/mcp/catalog/notification-router.js +28 -0
  43. package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
  44. package/dist/core/mcp/catalog/resource-catalog.js +7 -0
  45. package/dist/core/mcp/catalog/tool-catalog.js +4 -0
  46. package/dist/core/mcp/client/connection-manager.js +239 -0
  47. package/dist/core/mcp/client/lifecycle.js +13 -0
  48. package/dist/core/mcp/client/transport-factory.js +168 -0
  49. package/dist/core/mcp/config/index.js +32 -0
  50. package/dist/core/mcp/config/schema-v2.js +129 -0
  51. package/dist/core/mcp/host/elicitation-provider.js +209 -0
  52. package/dist/core/mcp/host/roots-provider.js +70 -0
  53. package/dist/core/mcp/host/sampling-provider.js +170 -0
  54. package/dist/core/mcp/index.js +4 -0
  55. package/dist/core/mcp/observability/events.js +19 -0
  56. package/dist/core/mcp/policy/approval-policy.js +2 -0
  57. package/dist/core/mcp/policy/classifier.js +172 -0
  58. package/dist/core/mcp/policy/grants.js +356 -0
  59. package/dist/core/mcp/policy/uri-policy.js +60 -0
  60. package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
  61. package/dist/core/mcp/types.js +2 -0
  62. package/dist/core/protocols/a2a/agent-card.js +38 -12
  63. package/dist/core/protocols/a2a/sdk/executor.js +105 -36
  64. package/dist/core/protocols/a2a/sdk/server.js +1311 -3
  65. package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
  66. package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
  67. package/dist/core/protocols/acp/acp-types.js +17 -0
  68. package/dist/core/protocols/acp/formal-agent.js +389 -502
  69. package/dist/core/protocols/acp/handlers.js +3 -0
  70. package/dist/core/protocols/acp/permission-provider.js +11 -39
  71. package/dist/core/protocols/acp/stdio-server.js +20 -1
  72. package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
  73. package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
  74. package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
  75. package/dist/core/public-capabilities/projections.js +1 -0
  76. package/dist/core/runtime/agent-server-runtime.js +2 -3
  77. package/dist/core/runtime/spawn-command.js +8 -2
  78. package/dist/core/runtime/spawn-interactive.js +26 -0
  79. package/dist/core/session/manager.js +48 -25
  80. package/dist/core/tools/builtin/index.js +6 -1
  81. package/dist/core/tools/builtin/proposal.js +0 -7
  82. package/dist/core/tools/builtin/workspace.js +76 -0
  83. package/dist/core/tools/dispatcher.js +1 -0
  84. package/dist/core/tools/loader.js +92 -46
  85. package/dist/core/verification/runner.js +60 -31
  86. package/dist/core/version.js +17 -0
  87. package/dist/core/workspace/capabilities.js +80 -0
  88. package/dist/locales/en.js +17 -3
  89. package/package.json +4 -2
  90. package/dist/core/protocols/a2a/mapper.js +0 -14
  91. package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
  92. package/dist/core/protocols/a2a/task-projection.js +0 -45
  93. package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
  94. package/dist/core/tools/mcp/client.js +0 -308
  95. package/dist/core/tools/mcp/loader.js +0 -110
  96. package/dist/core/tools/mcp/schema.js +0 -54
  97. package/dist/core/tools/mcp/streamable-http.js +0 -101
  98. package/dist/core/tools/mcp/types.js +0 -26
@@ -12,7 +12,10 @@ export function createAcpSessionStore() {
12
12
  createdAt: now,
13
13
  updatedAt: now,
14
14
  title: input.title,
15
+ permissionPolicy: input.permissionPolicy,
16
+ modeId: input.modeId,
15
17
  history: [],
18
+ materialized: false,
16
19
  cancelRequested: false,
17
20
  };
18
21
  sessions.set(id, session);
@@ -1,43 +1,15 @@
1
- function toToolKind(request) {
2
- const name = request.toolName.toLowerCase();
3
- if (name.includes('read') ||
4
- name.includes('get') ||
5
- name.includes('view') ||
6
- name.includes('ls') ||
7
- name.includes('list') ||
8
- (request.sideEffects.length > 0 && request.sideEffects.every((e) => e === 'fs_read')))
9
- return 'read';
10
- if (name.includes('write') ||
11
- name.includes('edit') ||
12
- name.includes('patch') ||
13
- request.sideEffects.includes('fs_write'))
14
- return 'edit';
15
- if (name.includes('delete') ||
16
- name.includes('remove') ||
17
- name.includes('rm') ||
18
- request.sideEffects.includes('fs_delete'))
19
- return 'delete';
20
- if (name.includes('move') || name.includes('rename') || name.includes('mv'))
21
- return 'move';
22
- if (name.includes('grep') || name.includes('search') || name.includes('find'))
23
- return 'search';
24
- if (name.includes('run') ||
25
- name.includes('exec') ||
26
- name.includes('spawn') ||
27
- request.sideEffects.includes('process'))
28
- return 'execute';
29
- if (name.includes('plan') || name.includes('think') || name.includes('reason'))
30
- return 'think';
31
- if (name.includes('fetch') || name.includes('curl') || name.includes('http'))
32
- return 'fetch';
33
- return 'other';
34
- }
1
+ import { text } from '../../../locales/index.js';
2
+ import { mapToolKind } from './tool-kind-mapping.js';
35
3
  function buildPermissionOptions() {
36
4
  return [
37
- { kind: 'allow_once', name: 'Allow once', optionId: 'allow_once' },
38
- { kind: 'allow_always', name: 'Allow for session', optionId: 'allow_always' },
39
- { kind: 'reject_once', name: 'Reject once', optionId: 'reject_once' },
40
- { kind: 'reject_always', name: 'Reject for session', optionId: 'reject_always' },
5
+ { kind: 'allow_once', name: text.acp.permissionOptionAllowOnce, optionId: 'allow_once' },
6
+ { kind: 'allow_always', name: text.acp.permissionOptionAllowSession, optionId: 'allow_always' },
7
+ { kind: 'reject_once', name: text.acp.permissionOptionRejectOnce, optionId: 'reject_once' },
8
+ {
9
+ kind: 'reject_always',
10
+ name: text.acp.permissionOptionRejectSession,
11
+ optionId: 'reject_always',
12
+ },
41
13
  ];
42
14
  }
43
15
  function toToolCallUpdate(request) {
@@ -48,7 +20,7 @@ function toToolCallUpdate(request) {
48
20
  return {
49
21
  toolCallId: request.id,
50
22
  title: request.toolName,
51
- kind: toToolKind(request),
23
+ kind: mapToolKind(request.toolName, { sideEffects: request.sideEffects }),
52
24
  status: 'pending',
53
25
  rawInput: {
54
26
  toolName: request.toolName,
@@ -14,11 +14,21 @@ const PARSE_ERROR = {
14
14
  function isJsonObject(value) {
15
15
  return typeof value === 'object' && value !== null && !Array.isArray(value);
16
16
  }
17
+ function hasOwn(value, key) {
18
+ return Object.prototype.hasOwnProperty.call(value, key);
19
+ }
17
20
  function formatLineSnippet(line, maxLength = 160) {
18
21
  if (line.length <= maxLength)
19
22
  return line;
20
23
  return `${line.slice(0, maxLength - 3)}...`;
21
24
  }
25
+ function normalizeJsonRpcParams(message) {
26
+ if (typeof message.method !== 'string')
27
+ return;
28
+ if (!hasOwn(message, 'params') || message.params === null) {
29
+ message.params = {};
30
+ }
31
+ }
22
32
  function safeWarn(message) {
23
33
  const logger = tryGetLogger();
24
34
  if (logger)
@@ -28,8 +38,12 @@ function createNdjsonWriter(output) {
28
38
  const writer = output.getWriter();
29
39
  const encoder = new TextEncoder();
30
40
  let tail = Promise.resolve();
41
+ let lastError;
31
42
  return {
32
43
  async write(message) {
44
+ if (lastError) {
45
+ throw lastError;
46
+ }
33
47
  const content = JSON.stringify(message) + '\n';
34
48
  const data = encoder.encode(content);
35
49
  tail = tail
@@ -38,6 +52,7 @@ function createNdjsonWriter(output) {
38
52
  .catch((error) => {
39
53
  const detail = error instanceof Error ? error.message : String(error);
40
54
  safeWarn(`ACP stdio failed to write NDJSON line. reason="${detail}"`);
55
+ lastError = error instanceof Error ? new Error(detail) : new Error(detail);
41
56
  });
42
57
  await tail;
43
58
  },
@@ -58,6 +73,7 @@ async function processStdioLine(line, ndjson, controller) {
58
73
  await writeInvalidRequest(ndjson, trimmed);
59
74
  return;
60
75
  }
76
+ normalizeJsonRpcParams(parsed);
61
77
  if (parsed.params && typeof parsed.params === 'object' && !Array.isArray(parsed.params)) {
62
78
  const params = parsed.params;
63
79
  if (params.mcpServers === null) {
@@ -82,8 +98,10 @@ export function createAcpStdioStream(output, input) {
82
98
  try {
83
99
  while (true) {
84
100
  const { value, done } = await reader.read();
85
- if (done)
101
+ if (done) {
102
+ buffer += textDecoder.decode();
86
103
  break;
104
+ }
87
105
  if (!value)
88
106
  continue;
89
107
  buffer += textDecoder.decode(value, { stream: true });
@@ -93,6 +111,7 @@ export function createAcpStdioStream(output, input) {
93
111
  await processStdioLine(line, ndjson, controller);
94
112
  }
95
113
  }
114
+ await processStdioLine(buffer, ndjson, controller);
96
115
  }
97
116
  finally {
98
117
  reader.releaseLock();
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Maps a tool name and optional metadata to an ACP ToolKind.
3
+ *
4
+ * Priority: explicit intent > side-effect inference > name heuristics.
5
+ *
6
+ * Covers all 10 protocol-defined kinds:
7
+ * read | edit | delete | move | search | execute | think | fetch | switch_mode | other
8
+ */
9
+ export function mapToolKind(toolName, options) {
10
+ const intent = options?.intent;
11
+ if (intent) {
12
+ switch (intent.toUpperCase()) {
13
+ case 'READ':
14
+ case 'LIST':
15
+ return 'read';
16
+ case 'SEARCH':
17
+ return 'search';
18
+ case 'WRITE':
19
+ return 'edit';
20
+ case 'INFRA':
21
+ return 'execute';
22
+ case 'AGENT':
23
+ return 'think';
24
+ }
25
+ }
26
+ const sideEffects = options?.sideEffects;
27
+ if (sideEffects && sideEffects.length > 0) {
28
+ if (sideEffects.every((e) => e === 'fs_read'))
29
+ return 'read';
30
+ if (sideEffects.includes('fs_write'))
31
+ return 'edit';
32
+ if (sideEffects.includes('fs_delete'))
33
+ return 'delete';
34
+ if (sideEffects.includes('process'))
35
+ return 'execute';
36
+ }
37
+ const name = toolName.toLowerCase();
38
+ if (name.includes('read') ||
39
+ name.includes('get') ||
40
+ name.includes('view') ||
41
+ name.includes('ls') ||
42
+ name.includes('list'))
43
+ return 'read';
44
+ if (name.includes('write') || name.includes('edit') || name.includes('patch'))
45
+ return 'edit';
46
+ if (name.includes('delete') || name.includes('remove') || name.includes('rm'))
47
+ return 'delete';
48
+ if (name.includes('move') || name.includes('rename') || name.includes('mv'))
49
+ return 'move';
50
+ if (name.includes('grep') || name.includes('search') || name.includes('find'))
51
+ return 'search';
52
+ if (name.includes('run') || name.includes('exec') || name.includes('spawn'))
53
+ return 'execute';
54
+ if (name.includes('plan') || name.includes('think') || name.includes('reason'))
55
+ return 'think';
56
+ if (name.includes('fetch') || name.includes('curl') || name.includes('http'))
57
+ return 'fetch';
58
+ if (name.includes('mode') || name.includes('switch'))
59
+ return 'switch_mode';
60
+ return 'other';
61
+ }
62
+ //# sourceMappingURL=tool-kind-mapping.js.map
@@ -1,4 +1,3 @@
1
- import { FLOW_MODE_PUBLIC_METADATA } from '../../public-capabilities/flow-mode-metadata.js';
2
1
  import { FLOW_MODES, parseFlowMode } from '../../types/flow-mode.js';
3
2
  export const SUPPORTED_PROTOCOL_FLOW_MODES = FLOW_MODES;
4
3
  export function parseAcpFlowMode(value) {
@@ -13,11 +12,4 @@ export function parseAcpFlowMode(value) {
13
12
  export function parseA2ASkillFlowMode(value) {
14
13
  return parseFlowMode(value);
15
14
  }
16
- export function buildA2AFlowSkills() {
17
- return SUPPORTED_PROTOCOL_FLOW_MODES.map((mode) => ({
18
- id: mode,
19
- title: FLOW_MODE_PUBLIC_METADATA[mode].a2aTitle,
20
- description: FLOW_MODE_PUBLIC_METADATA[mode].description,
21
- }));
22
- }
23
15
  //# sourceMappingURL=flow-mode-mapping.js.map
@@ -1,37 +1,31 @@
1
1
  export const FLOW_MODE_PUBLIC_METADATA = {
2
2
  autopilot: {
3
3
  publicTitle: 'Autopilot',
4
- a2aTitle: 'Autopilot',
5
4
  acpName: 'Autopilot',
6
5
  description: 'Let the agent decide which actions and tools to use.',
7
6
  },
8
7
  patch: {
9
8
  publicTitle: 'Patch code',
10
- a2aTitle: 'Patch code',
11
9
  acpName: 'Patch',
12
10
  description: 'Apply code changes with verification.',
13
11
  },
14
12
  review: {
15
13
  publicTitle: 'Review code',
16
- a2aTitle: 'Review code',
17
14
  acpName: 'Review',
18
15
  description: 'Inspect code and report findings without mutating files.',
19
16
  },
20
17
  debug: {
21
18
  publicTitle: 'Debug issue',
22
- a2aTitle: 'Debug issue',
23
19
  acpName: 'Debug',
24
20
  description: 'Investigate issues and make targeted fixes when needed.',
25
21
  },
26
22
  research: {
27
23
  publicTitle: 'Research request',
28
- a2aTitle: 'Research request',
29
24
  acpName: 'Research',
30
25
  description: 'Explore the codebase and summarize relevant findings.',
31
26
  },
32
27
  answer: {
33
28
  publicTitle: 'Answer question',
34
- a2aTitle: 'Answer question',
35
29
  acpName: 'Answer',
36
30
  description: 'Answer questions directly without editing files.',
37
31
  },
@@ -24,6 +24,7 @@ export function toA2APublicSkills(entries = buildPublicCapabilityRegistry()) {
24
24
  examples: entry.examples,
25
25
  inputModes: entry.inputModes,
26
26
  outputModes: entry.outputModes,
27
+ security: entry.security,
27
28
  }));
28
29
  }
29
30
  //# sourceMappingURL=projections.js.map
@@ -1,11 +1,10 @@
1
- import { InMemoryTaskStore } from '@a2a-js/sdk/server';
2
1
  import { createTaskEventBus } from '../interaction/events/bus.js';
3
2
  import { createInteractionFacade } from '../interaction/orchestration/facade.js';
4
3
  import { createA2AInteractionExecutor } from '../protocols/a2a/sdk/executor.js';
5
- import { createA2ASdkExpressApp } from '../protocols/a2a/sdk/server.js';
4
+ import { createA2ASdkExpressApp, ProtocolAlignedInMemoryTaskStore, } from '../protocols/a2a/sdk/server.js';
6
5
  export function createAgentServerRuntime(deps) {
7
6
  const eventBus = deps.a2a.eventBus ?? createTaskEventBus();
8
- const taskStore = deps.a2a.taskStore ?? new InMemoryTaskStore();
7
+ const taskStore = deps.a2a.taskStore ?? new ProtocolAlignedInMemoryTaskStore();
9
8
  const facade = createInteractionFacade({
10
9
  executeTask: deps.a2a.executeTask,
11
10
  eventBus,
@@ -349,14 +349,20 @@ async function spawnWithNode(input) {
349
349
  timeoutTimer = setTimeout(() => {
350
350
  timedOut = true;
351
351
  killProcess('SIGTERM');
352
- killTimer = setTimeout(() => killProcess('SIGKILL'), killGraceMs);
352
+ killTimer = setTimeout(() => {
353
+ killProcess('SIGKILL');
354
+ settle(null, null);
355
+ }, killGraceMs);
353
356
  }, input.timeoutMs);
354
357
  }
355
358
  if (input.signal) {
356
359
  onAbort = () => {
357
360
  aborted = true;
358
361
  killProcess('SIGTERM');
359
- killTimer = setTimeout(() => killProcess('SIGKILL'), input.killGraceMs ?? 2000);
362
+ killTimer = setTimeout(() => {
363
+ killProcess('SIGKILL');
364
+ settle(null, null);
365
+ }, input.killGraceMs ?? 2000);
360
366
  };
361
367
  input.signal.addEventListener('abort', onAbort, { once: true });
362
368
  }
@@ -14,15 +14,22 @@ export function spawnInteractiveProcess(input) {
14
14
  windowsHide: input.windowsHide,
15
15
  });
16
16
  const events = new EventEmitter();
17
+ let exitCode;
18
+ queueMicrotask(() => events.emit('spawn'));
17
19
  void subprocess.exited
18
20
  .then((code) => {
21
+ exitCode = code;
19
22
  events.emit('exit', code, normalizeSignal(subprocess.signalCode));
23
+ events.emit('close', code, normalizeSignal(subprocess.signalCode));
20
24
  })
21
25
  .catch((error) => {
22
26
  events.emit('error', error);
23
27
  });
24
28
  const processRef = {
25
29
  pid: subprocess.pid,
30
+ get exitCode() {
31
+ return exitCode ?? null;
32
+ },
26
33
  stdin: subprocess.stdin ?? undefined,
27
34
  stdout: toNodeReadableStream(subprocess.stdout),
28
35
  stderr: toNodeReadableStream(subprocess.stderr),
@@ -38,6 +45,14 @@ export function spawnInteractiveProcess(input) {
38
45
  events.on(event, listener);
39
46
  return processRef;
40
47
  },
48
+ once: (event, listener) => {
49
+ events.once(event, listener);
50
+ return processRef;
51
+ },
52
+ off: (event, listener) => {
53
+ events.off(event, listener);
54
+ return processRef;
55
+ },
41
56
  };
42
57
  return processRef;
43
58
  }
@@ -50,6 +65,9 @@ export function spawnInteractiveProcess(input) {
50
65
  });
51
66
  const processRef = {
52
67
  pid: child.pid,
68
+ get exitCode() {
69
+ return child.exitCode;
70
+ },
53
71
  stdin: child.stdin ?? undefined,
54
72
  stdout: child.stdout ?? undefined,
55
73
  stderr: child.stderr ?? undefined,
@@ -65,6 +83,14 @@ export function spawnInteractiveProcess(input) {
65
83
  child.on(event, listener);
66
84
  return processRef;
67
85
  },
86
+ once: (event, listener) => {
87
+ child.once(event, listener);
88
+ return processRef;
89
+ },
90
+ off: (event, listener) => {
91
+ child.off(event, listener);
92
+ return processRef;
93
+ },
68
94
  };
69
95
  return processRef;
70
96
  }
@@ -45,6 +45,7 @@ function normalizeChatState(chatState) {
45
45
  * Features: Auto-pruning, compression, intelligent cleanup
46
46
  */
47
47
  export class ChatSessionManager {
48
+ static FILE_READ_CHUNK_SIZE = 10;
48
49
  repoPath;
49
50
  storageDir;
50
51
  currentSession = null;
@@ -268,18 +269,32 @@ export class ChatSessionManager {
268
269
  */
269
270
  async listSessions() {
270
271
  const files = await this.fileAdapter.readdir(this.storageDir).catch(() => []);
272
+ const jsonFiles = files.filter((f) => f.endsWith('.json'));
271
273
  const sessions = [];
272
- for (const file of files) {
273
- if (!file.endsWith('.json'))
274
- continue;
275
- const filePath = join(this.storageDir, file);
276
- const data = await this.fileAdapter.readFile(filePath);
277
- const session = JSON.parse(data);
278
- sessions.push({
279
- id: session.meta.id,
280
- name: session.meta.name,
281
- updatedAt: session.meta.updatedAt,
274
+ for (let i = 0; i < jsonFiles.length; i += ChatSessionManager.FILE_READ_CHUNK_SIZE) {
275
+ const chunk = jsonFiles.slice(i, i + ChatSessionManager.FILE_READ_CHUNK_SIZE);
276
+ const promises = chunk.map(async (file) => {
277
+ try {
278
+ const filePath = join(this.storageDir, file);
279
+ const data = await this.fileAdapter.readFile(filePath);
280
+ const session = JSON.parse(data);
281
+ return {
282
+ id: session.meta.id,
283
+ name: session.meta.name,
284
+ updatedAt: session.meta.updatedAt,
285
+ };
286
+ }
287
+ catch (error) {
288
+ getLogger().warn(`Failed to list session file ${file}: ${error}`);
289
+ return null;
290
+ }
282
291
  });
292
+ const results = await Promise.all(promises);
293
+ for (const result of results) {
294
+ if (result) {
295
+ sessions.push(result);
296
+ }
297
+ }
283
298
  }
284
299
  return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
285
300
  }
@@ -323,22 +338,30 @@ export class ChatSessionManager {
323
338
  */
324
339
  async loadAllSessions() {
325
340
  const files = await this.fileAdapter.readdir(this.storageDir).catch(() => []);
341
+ const jsonFiles = files.filter((f) => f.endsWith('.json'));
326
342
  const sessions = [];
327
- for (const file of files) {
328
- if (!file.endsWith('.json'))
329
- continue;
330
- try {
331
- const filePath = join(this.storageDir, file);
332
- const data = await this.fileAdapter.readFile(filePath);
333
- const session = JSON.parse(data);
334
- session.meta.chatState = normalizeChatState(session.meta.chatState);
335
- session.meta.artifactState = normalizeSessionArtifactState(session.meta.artifactState);
336
- session.meta.replacementState = normalizeToolResultReplacementState(session.meta.replacementState);
337
- sessions.push(session);
338
- }
339
- catch (error) {
340
- // Skip corrupted session files
341
- getLogger().warn(`Failed to load session file ${file}: ${error}`);
343
+ for (let i = 0; i < jsonFiles.length; i += ChatSessionManager.FILE_READ_CHUNK_SIZE) {
344
+ const chunk = jsonFiles.slice(i, i + ChatSessionManager.FILE_READ_CHUNK_SIZE);
345
+ const promises = chunk.map(async (file) => {
346
+ try {
347
+ const filePath = join(this.storageDir, file);
348
+ const data = await this.fileAdapter.readFile(filePath);
349
+ const session = JSON.parse(data);
350
+ session.meta.chatState = normalizeChatState(session.meta.chatState);
351
+ session.meta.artifactState = normalizeSessionArtifactState(session.meta.artifactState);
352
+ session.meta.replacementState = normalizeToolResultReplacementState(session.meta.replacementState);
353
+ return session;
354
+ }
355
+ catch (error) {
356
+ getLogger().warn(`Failed to load session file ${file}: ${error}`);
357
+ return null;
358
+ }
359
+ });
360
+ const results = await Promise.all(promises);
361
+ for (const result of results) {
362
+ if (result) {
363
+ sessions.push(result);
364
+ }
342
365
  }
343
366
  }
344
367
  return sessions;
@@ -13,6 +13,7 @@ import { planInitSpec, planReadSpec, planUpdateSpec } from './plan.js';
13
13
  import { proposalApplySpec, executeProposalApply } from './proposal.js';
14
14
  import { shellExecSpec, executeShellExec } from './shell.js';
15
15
  import { verifyRunSpec, executeVerifyRun } from './verify.js';
16
+ import { workspaceInfoSpec, executeWorkspaceInfo } from './workspace.js';
16
17
  /**
17
18
  * Registers all builtin tools into the provided registry
18
19
  */
@@ -27,6 +28,10 @@ export function registerAllBuiltins(registry) {
27
28
  ...updateKnowledgeSpec,
28
29
  executor: executeUpdateKnowledge,
29
30
  });
31
+ registry.register({
32
+ ...workspaceInfoSpec,
33
+ executor: executeWorkspaceInfo,
34
+ });
30
35
  registry.register({
31
36
  ...proposalApplySpec,
32
37
  executor: executeProposalApply,
@@ -125,5 +130,5 @@ export function registerAllBuiltins(registry) {
125
130
  registry.register(planUpdateSpec);
126
131
  registry.register(askUserSpec);
127
132
  }
128
- export { CodeSearchSpec, codeSearchExecutor, astDefsRefsSpec as codeAstSpec, executeAstDefsRefs as executeCodeAst, gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus, codeReadSpec, fsListSpec, executeFsList, fsReadFileSpec as fsReadSpec, executeFsReadFile as executeFsRead, updateKnowledgeSpec, executeUpdateKnowledge, astGrepSpec as codeSearchAstSpec, executeAstGrep as executeCodeSearchAst, verifyRunSpec as testRunSpec, executeVerifyRun as executeTestRun, gitDiffCheckSpec, executeGitDiffCheck, gitApplyCheckSpec, executeGitApplyCheck, benchmarkReportSpec, executeBenchmarkReport, sweBenchLoadInstanceSpec, executeSweBenchLoadInstance, sweBenchWritePredictionSpec, executeSweBenchWritePrediction, sweBenchSubmitPredictionsSpec, executeSweBenchSubmitPredictions, sweBenchGetReportSpec, executeSweBenchGetReport, };
133
+ export { CodeSearchSpec, codeSearchExecutor, astDefsRefsSpec as codeAstSpec, executeAstDefsRefs as executeCodeAst, gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus, codeReadSpec, fsListSpec, executeFsList, fsReadFileSpec as fsReadSpec, executeFsReadFile as executeFsRead, updateKnowledgeSpec, executeUpdateKnowledge, astGrepSpec as codeSearchAstSpec, executeAstGrep as executeCodeSearchAst, verifyRunSpec as testRunSpec, executeVerifyRun as executeTestRun, gitDiffCheckSpec, executeGitDiffCheck, gitApplyCheckSpec, executeGitApplyCheck, benchmarkReportSpec, executeBenchmarkReport, sweBenchLoadInstanceSpec, executeSweBenchLoadInstance, sweBenchWritePredictionSpec, executeSweBenchWritePrediction, sweBenchSubmitPredictionsSpec, executeSweBenchSubmitPredictions, sweBenchGetReportSpec, executeSweBenchGetReport, workspaceInfoSpec, executeWorkspaceInfo, };
129
134
  //# sourceMappingURL=index.js.map
@@ -7,10 +7,7 @@ import { DecisionEngine, PlanBuilder } from '../../grizzco/dsl/DecisionEngine.js
7
7
  import { StandardStrategy } from '../../grizzco/dsl/strategies.js';
8
8
  import { Executor } from '../../grizzco/execution/Executor.js';
9
9
  import { WorkerFactory } from '../../grizzco/execution/WorkerFactory.js';
10
- import { CachedService } from '../../grizzco/services/CachedService.js';
11
- import { GitConfigService } from '../../grizzco/services/implementations/default/GitConfigService.js';
12
10
  import { MockLockService } from '../../grizzco/services/implementations/mock/MockLockService.js';
13
- import { MockUserQuotaService } from '../../grizzco/services/implementations/mock/MockUserQuotaService.js';
14
11
  import { registry } from '../../grizzco/services/registry.js';
15
12
  import { normalizeDiff, validateDiff, convertDiffToShadowOperations } from '../../patch/diff.js';
16
13
  import { getRejectionsDir } from '../../runtime/paths.js';
@@ -20,10 +17,6 @@ import { Phase } from '../../types/runtime.js';
20
17
  function bootstrapRegistry() {
21
18
  if (!registry.has('remote_lock'))
22
19
  registry.register(new MockLockService());
23
- if (!registry.has('user_quota'))
24
- registry.register(new MockUserQuotaService());
25
- if (!registry.has('git_config'))
26
- registry.register(new CachedService(new GitConfigService()));
27
20
  }
28
21
  export const proposalApplySpec = {
29
22
  name: 'proposal.apply',
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod';
2
+ import { Phase } from '../../types/runtime.js';
3
+ import { detectWorkspaceCapabilities } from '../../workspace/capabilities.js';
4
+ const WorkspaceCapabilitiesOutputSchema = z.object({
5
+ root: z.string(),
6
+ capabilities: z.object({
7
+ git: z.object({
8
+ available: z.boolean(),
9
+ insideWorkTree: z.boolean(),
10
+ head: z.string().optional(),
11
+ reason: z.string().optional(),
12
+ }),
13
+ filesystem: z.object({
14
+ readable: z.boolean(),
15
+ writable: z.boolean(),
16
+ reason: z.string().optional(),
17
+ }),
18
+ }),
19
+ guidance: z.object({
20
+ useGitTools: z.boolean(),
21
+ useFilesystemReadTools: z.boolean(),
22
+ useFilesystemWriteTools: z.boolean(),
23
+ }),
24
+ });
25
+ export const workspaceInfoSpec = {
26
+ name: 'workspace.info',
27
+ source: 'builtin',
28
+ intent: 'READ',
29
+ description: 'Report the current workspace root and available capabilities, including whether git tools are usable.',
30
+ riskLevel: 'low',
31
+ sideEffects: ['none'],
32
+ concurrency: 'parallel_ok',
33
+ inputSchema: z.object({}),
34
+ outputSchema: WorkspaceCapabilitiesOutputSchema,
35
+ allowedPhases: [
36
+ Phase.SLASH,
37
+ Phase.CONTEXT,
38
+ Phase.EXPLORE,
39
+ Phase.PLAN,
40
+ Phase.AUTOPILOT,
41
+ Phase.PATCH,
42
+ Phase.VERIFY,
43
+ Phase.SHRINK,
44
+ ],
45
+ examples: [
46
+ {
47
+ description: 'Check whether git tools are available before calling git.status',
48
+ input: {},
49
+ output: {
50
+ root: '/workspace/project',
51
+ capabilities: {
52
+ git: { available: true, insideWorkTree: true, head: '<commit>' },
53
+ filesystem: { readable: true, writable: true },
54
+ },
55
+ guidance: {
56
+ useGitTools: true,
57
+ useFilesystemReadTools: true,
58
+ useFilesystemWriteTools: true,
59
+ },
60
+ },
61
+ },
62
+ ],
63
+ };
64
+ export async function executeWorkspaceInfo(_input, ctx) {
65
+ const capabilities = ctx.workspaceCapabilities ?? (await detectWorkspaceCapabilities(ctx.repoRoot));
66
+ return {
67
+ root: ctx.repoRoot,
68
+ capabilities,
69
+ guidance: {
70
+ useGitTools: capabilities.git.available && capabilities.git.insideWorkTree,
71
+ useFilesystemReadTools: capabilities.filesystem.readable,
72
+ useFilesystemWriteTools: capabilities.filesystem.writable,
73
+ },
74
+ };
75
+ }
76
+ //# sourceMappingURL=workspace.js.map
@@ -40,6 +40,7 @@ export class ToolDispatcher {
40
40
  repoRoot: this.options.repoRoot,
41
41
  persistenceRoot: this.options.persistenceRoot,
42
42
  worktreeRoot: this.options.worktreeRoot,
43
+ workspaceCapabilities: this.options.workspaceCapabilities,
43
44
  attemptId: this.options.attemptId,
44
45
  dryRun: this.options.dryRun,
45
46
  model: this.options.model,