salmon-loop 0.3.2 → 0.4.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 (121) hide show
  1. package/dist/cli/authorization/non-interactive.js +9 -13
  2. package/dist/cli/chat.js +12 -6
  3. package/dist/cli/commands/allowlist.js +1 -1
  4. package/dist/cli/commands/chat.js +13 -13
  5. package/dist/cli/commands/parallel.js +1 -1
  6. package/dist/cli/commands/run/handler.js +6 -3
  7. package/dist/cli/commands/run/loop-params.js +1 -0
  8. package/dist/cli/commands/run/parse-options.js +14 -26
  9. package/dist/cli/commands/run/runtime-llm.js +15 -12
  10. package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
  11. package/dist/cli/reporters/standard.js +2 -3
  12. package/dist/cli/reporters/stream-json.js +2 -1
  13. package/dist/cli/slash/runtime.js +2 -2
  14. package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
  15. package/dist/cli/ui/hooks/useLoopState.js +1 -1
  16. package/dist/core/ast/parser.js +18 -9
  17. package/dist/core/config/schema.js +738 -0
  18. package/dist/core/config/validate.js +11 -922
  19. package/dist/core/context/gatherers/ast-gatherer.js +4 -12
  20. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
  21. package/dist/core/context/gatherers/knowledge-gatherer.js +3 -0
  22. package/dist/core/context/service.js +8 -0
  23. package/dist/core/context/token/encoding-registry.js +7 -6
  24. package/dist/core/extensions/index.js +48 -3
  25. package/dist/core/extensions/load.js +3 -2
  26. package/dist/core/extensions/merge.js +5 -1
  27. package/dist/core/extensions/paths.js +6 -0
  28. package/dist/core/extensions/schemas.js +21 -0
  29. package/dist/core/facades/cli-command-chat.js +2 -0
  30. package/dist/core/facades/cli-run-handler.js +1 -0
  31. package/dist/core/facades/cli-utils-serialize.js +2 -0
  32. package/dist/core/grizzco/dsl/llm-strategy.js +3 -2
  33. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -10
  34. package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
  35. package/dist/core/grizzco/engine/transaction/attempt-failure.js +5 -4
  36. package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
  37. package/dist/core/grizzco/runtime/apply-back-runtime.js +2 -1
  38. package/dist/core/grizzco/services/registry.js +18 -0
  39. package/dist/core/grizzco/steps/audit.js +20 -10
  40. package/dist/core/grizzco/steps/display-report.js +4 -11
  41. package/dist/core/grizzco/steps/explore.js +9 -2
  42. package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
  43. package/dist/core/grizzco/steps/patch.js +1 -0
  44. package/dist/core/grizzco/steps/plan.js +58 -49
  45. package/dist/core/grizzco/steps/tool-runtime.js +3 -0
  46. package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
  47. package/dist/core/llm/ai-sdk/message-mapper.js +24 -18
  48. package/dist/core/llm/ai-sdk/request-params.js +1 -3
  49. package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
  50. package/dist/core/llm/ai-sdk/retry-classifier.js +6 -4
  51. package/dist/core/llm/contracts/repair.js +16 -8
  52. package/dist/core/llm/errors.js +13 -10
  53. package/dist/core/llm/output-policy.js +8 -0
  54. package/dist/core/llm/redact.js +1 -3
  55. package/dist/core/llm/sub-agent-factory.js +48 -0
  56. package/dist/core/llm/tool-calling-stub.js +48 -0
  57. package/dist/core/llm/utils.js +17 -6
  58. package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
  59. package/dist/core/mcp/bridge/tool-bridge.js +5 -14
  60. package/dist/core/mcp/client/connection-manager.js +3 -2
  61. package/dist/core/mcp/host/sampling-provider.js +1 -1
  62. package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
  63. package/dist/core/memory/relevant-retrieval.js +6 -4
  64. package/dist/core/observability/authorization-decisions.js +13 -12
  65. package/dist/core/observability/error-mapping.js +2 -1
  66. package/dist/core/observability/token-usage.js +5 -4
  67. package/dist/core/plugin/loader.js +5 -4
  68. package/dist/core/prompts/registry.js +11 -29
  69. package/dist/core/protocols/a2a/sdk/server.js +2 -3
  70. package/dist/core/protocols/acp/formal-agent.js +10 -4
  71. package/dist/core/protocols/acp/stdio-server.js +6 -6
  72. package/dist/core/runtime/agent-server-runtime.js +3 -2
  73. package/dist/core/runtime/initialize.js +70 -6
  74. package/dist/core/session/compaction/index.js +4 -3
  75. package/dist/core/session/manager.js +24 -37
  76. package/dist/core/session/token-tracker.js +18 -7
  77. package/dist/core/skills/parser.js +3 -2
  78. package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
  79. package/dist/core/skills/runtime/SkillRunner.js +5 -2
  80. package/dist/core/slash/steps/slash-execute.js +7 -5
  81. package/dist/core/slash/strategy.js +1 -1
  82. package/dist/core/strata/layers/worktree.js +7 -9
  83. package/dist/core/strata/runtime/synchronizer.js +10 -9
  84. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
  85. package/dist/core/structured-output/json-schema-validator.js +1 -13
  86. package/dist/core/sub-agent/context-snapshot.js +12 -6
  87. package/dist/core/sub-agent/controller.js +70 -1
  88. package/dist/core/sub-agent/core/loop.js +25 -3
  89. package/dist/core/sub-agent/core/manager.js +319 -116
  90. package/dist/core/sub-agent/registry-defaults.js +12 -0
  91. package/dist/core/sub-agent/registry.js +8 -0
  92. package/dist/core/sub-agent/team.js +98 -0
  93. package/dist/core/sub-agent/tools/task-await.js +109 -0
  94. package/dist/core/sub-agent/tools/task-spawn.js +49 -7
  95. package/dist/core/sub-agent/tools/team.js +92 -0
  96. package/dist/core/sub-agent/types.js +11 -2
  97. package/dist/core/tools/budget.js +4 -11
  98. package/dist/core/tools/builtin/code-search/executor.js +46 -43
  99. package/dist/core/tools/builtin/fs.js +14 -6
  100. package/dist/core/tools/builtin/index.js +41 -107
  101. package/dist/core/tools/builtin/interaction.js +13 -15
  102. package/dist/core/tools/builtin/proposal.js +11 -2
  103. package/dist/core/tools/capability/executor.js +5 -5
  104. package/dist/core/tools/headless-payload.js +1 -3
  105. package/dist/core/tools/mapper.js +8 -42
  106. package/dist/core/tools/parallel/persistence.js +17 -5
  107. package/dist/core/tools/parallel/scheduler.js +23 -21
  108. package/dist/core/tools/permissions/permission-rules.js +66 -114
  109. package/dist/core/tools/plugins/loader.js +4 -3
  110. package/dist/core/tools/router.js +24 -53
  111. package/dist/core/tools/session.js +54 -97
  112. package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
  113. package/dist/core/tools/tool-visibility.js +2 -1
  114. package/dist/core/tools/types.js +10 -0
  115. package/dist/core/utils/error.js +79 -0
  116. package/dist/core/utils/serialize.js +63 -0
  117. package/dist/core/utils/zod.js +29 -0
  118. package/dist/core/workspace/capabilities.js +3 -2
  119. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
  120. package/dist/locales/en.js +2 -1
  121. package/package.json +1 -1
@@ -1,4 +1,7 @@
1
+ import { agentAwaitTaskSpec } from '../../sub-agent/tools/task-await.js';
1
2
  import { subAgentTaskSpec } from '../../sub-agent/tools/task-spawn.js';
3
+ import { agentTeamSpec } from '../../sub-agent/tools/team.js';
4
+ import { defineTool } from '../types.js';
2
5
  import { artifactReadSpec, executeArtifactRead } from './artifact.js';
3
6
  import { astGrepSpec, executeAstGrep } from './ast-grep.js';
4
7
  import { astDefsRefsSpec, executeAstDefsRefs } from './ast.js';
@@ -15,116 +18,47 @@ import { shellExecSpec, executeShellExec } from './shell.js';
15
18
  import { verifyRunSpec, executeVerifyRun } from './verify.js';
16
19
  import { workspaceInfoSpec, executeWorkspaceInfo } from './workspace.js';
17
20
  /**
18
- * Registers all builtin tools into the provided registry
21
+ * Registers all builtin tools into the provided registry.
22
+ * Uses defineTool() to pair specs with executors type-safely.
19
23
  */
20
24
  export function registerAllBuiltins(registry) {
21
- // Register sub-agent tool
25
+ // Sub-agent tools (already self-contained)
22
26
  registry.register(subAgentTaskSpec);
23
- registry.register({
24
- ...artifactReadSpec,
25
- executor: executeArtifactRead,
26
- });
27
- registry.register({
28
- ...updateKnowledgeSpec,
29
- executor: executeUpdateKnowledge,
30
- });
31
- registry.register({
32
- ...workspaceInfoSpec,
33
- executor: executeWorkspaceInfo,
34
- });
35
- registry.register({
36
- ...proposalApplySpec,
37
- executor: executeProposalApply,
38
- });
39
- // Register unified code.search with its specific executor
40
- registry.register({
41
- ...CodeSearchSpec,
42
- executor: codeSearchExecutor,
43
- });
44
- registry.register({
45
- ...astDefsRefsSpec,
46
- executor: executeAstDefsRefs,
47
- });
48
- registry.register({
49
- ...gitCatSpec,
50
- executor: executeGitCat,
51
- });
52
- registry.register({
53
- ...gitStatusSpec,
54
- executor: executeGitStatus,
55
- });
56
- registry.register({
57
- ...gitDiffCheckSpec,
58
- executor: executeGitDiffCheck,
59
- });
60
- registry.register({
61
- ...gitApplyCheckSpec,
62
- executor: executeGitApplyCheck,
63
- });
64
- registry.register({
65
- ...benchmarkReportSpec,
66
- executor: executeBenchmarkReport,
67
- });
68
- registry.register({
69
- ...sweBenchLoadInstanceSpec,
70
- executor: executeSweBenchLoadInstance,
71
- });
72
- registry.register({
73
- ...sweBenchWritePredictionSpec,
74
- executor: executeSweBenchWritePrediction,
75
- });
76
- registry.register({
77
- ...sweBenchSubmitPredictionsSpec,
78
- executor: executeSweBenchSubmitPredictions,
79
- });
80
- registry.register({
81
- ...sweBenchGetReportSpec,
82
- executor: executeSweBenchGetReport,
83
- });
84
- registry.register({
85
- ...fsReadFileSpec,
86
- executor: executeFsReadFile,
87
- });
88
- registry.register({
89
- ...codeReadSpec,
90
- executor: executeFsReadFile,
91
- });
92
- registry.register({
93
- ...fsListSpec,
94
- executor: executeFsList,
95
- });
96
- registry.register({
97
- ...fsListDirectorySpec,
98
- executor: executeFsListDirectory,
99
- });
100
- registry.register({
101
- ...fsListFilesSpec,
102
- executor: executeFsListFiles,
103
- });
104
- registry.register({
105
- ...astGrepSpec,
106
- executor: executeAstGrep,
107
- });
108
- registry.register({
109
- ...verifyRunSpec,
110
- executor: executeVerifyRun,
111
- });
112
- registry.register({
113
- ...shellExecSpec,
114
- executor: executeShellExec,
115
- });
116
- registry.register({
117
- ...fsWriteFileSpec,
118
- executor: executeFsWriteFile,
119
- });
120
- registry.register({
121
- ...fsCreateDirectorySpec,
122
- executor: executeFsCreateDirectory,
123
- });
124
- registry.register({
125
- ...fsDeleteFileSpec,
126
- executor: executeFsDeleteFile,
127
- });
27
+ registry.register(agentAwaitTaskSpec);
28
+ registry.register(agentTeamSpec);
29
+ // Artifact & knowledge
30
+ registry.register(defineTool(artifactReadSpec, executeArtifactRead));
31
+ registry.register(defineTool(updateKnowledgeSpec, executeUpdateKnowledge));
32
+ registry.register(defineTool(workspaceInfoSpec, executeWorkspaceInfo));
33
+ registry.register(defineTool(proposalApplySpec, executeProposalApply));
34
+ // Code search & AST
35
+ registry.register(defineTool(CodeSearchSpec, codeSearchExecutor));
36
+ registry.register(defineTool(astDefsRefsSpec, executeAstDefsRefs));
37
+ registry.register(defineTool(astGrepSpec, executeAstGrep));
38
+ // Git
39
+ registry.register(defineTool(gitCatSpec, executeGitCat));
40
+ registry.register(defineTool(gitStatusSpec, executeGitStatus));
41
+ registry.register(defineTool(gitDiffCheckSpec, executeGitDiffCheck));
42
+ registry.register(defineTool(gitApplyCheckSpec, executeGitApplyCheck));
43
+ // Benchmark / SWE-bench
44
+ registry.register(defineTool(benchmarkReportSpec, executeBenchmarkReport));
45
+ registry.register(defineTool(sweBenchLoadInstanceSpec, executeSweBenchLoadInstance));
46
+ registry.register(defineTool(sweBenchWritePredictionSpec, executeSweBenchWritePrediction));
47
+ registry.register(defineTool(sweBenchSubmitPredictionsSpec, executeSweBenchSubmitPredictions));
48
+ registry.register(defineTool(sweBenchGetReportSpec, executeSweBenchGetReport));
49
+ // Filesystem
50
+ registry.register(defineTool(fsReadFileSpec, executeFsReadFile));
51
+ registry.register(defineTool(codeReadSpec, executeFsReadFile));
52
+ registry.register(defineTool(fsListSpec, executeFsList));
53
+ registry.register(defineTool(fsListDirectorySpec, executeFsListDirectory));
54
+ registry.register(defineTool(fsListFilesSpec, executeFsListFiles));
55
+ registry.register(defineTool(fsWriteFileSpec, executeFsWriteFile));
56
+ registry.register(defineTool(fsCreateDirectorySpec, executeFsCreateDirectory));
57
+ registry.register(defineTool(fsDeleteFileSpec, executeFsDeleteFile));
58
+ // Execution
59
+ registry.register(defineTool(verifyRunSpec, executeVerifyRun));
60
+ registry.register(defineTool(shellExecSpec, executeShellExec));
61
+ // Plan & interaction
128
62
  registry.register(planInitSpec);
129
63
  registry.register(planReadSpec);
130
64
  registry.register(planUpdateSpec);
@@ -75,28 +75,26 @@ export const askUserSpec = {
75
75
  outputSchema: askUserOutputSchema,
76
76
  executor: async (input, ctx) => {
77
77
  if (ctx.agentKind === 'subagent') {
78
- const err = new Error(text.tools.askUserSubagentBlocked);
79
- err.code = 'ASK_USER_SUBAGENT_BLOCKED';
80
- throw err;
78
+ throw Object.assign(new Error(text.tools.askUserSubagentBlocked), {
79
+ code: 'ASK_USER_SUBAGENT_BLOCKED',
80
+ });
81
81
  }
82
82
  if (!ctx.userInputProvider) {
83
- const err = new Error(text.tools.askUserRequired);
84
83
  const inputRequired = buildInputRequired(input);
85
- err.code = 'INTERRUPT_REQUIRED';
86
- err.interrupt = {
87
- type: 'awaiting_input',
88
- reason: inputRequired.reason ?? 'clarification',
89
- prompt: inputRequired.prompt,
90
- data: { inputRequired },
91
- };
92
- throw err;
84
+ throw Object.assign(new Error(text.tools.askUserRequired), {
85
+ code: 'INTERRUPT_REQUIRED',
86
+ interrupt: {
87
+ type: 'awaiting_input',
88
+ reason: inputRequired.reason ?? 'clarification',
89
+ prompt: inputRequired.prompt,
90
+ data: { inputRequired },
91
+ },
92
+ });
93
93
  }
94
94
  const output = await ctx.userInputProvider.askUser(input, { signal: ctx.signal });
95
95
  const validationError = validateAnswers(input, output.answers);
96
96
  if (validationError) {
97
- const err = new Error(validationError);
98
- err.code = 'INVALID_OUTPUT';
99
- throw err;
97
+ throw Object.assign(new Error(validationError), { code: 'INVALID_OUTPUT' });
100
98
  }
101
99
  return { questions: input.questions, answers: output.answers };
102
100
  },
@@ -14,6 +14,7 @@ import { getRejectionsDir } from '../../runtime/paths.js';
14
14
  import { FileStateResolver } from '../../strata/layers/file-state-resolver.js';
15
15
  import { ArtifactStore } from '../../sub-agent/artifacts/store.js';
16
16
  import { Phase } from '../../types/runtime.js';
17
+ import { isRecord } from '../../utils/serialize.js';
17
18
  function bootstrapRegistry() {
18
19
  if (!registry.has('remote_lock'))
19
20
  registry.register(new MockLockService());
@@ -48,7 +49,7 @@ export const proposalApplySpec = {
48
49
  // challenge-response authorization without violating the execution contract.
49
50
  allowedPhases: [Phase.VERIFY],
50
51
  summarizeArgsForAuthorization: async (args, _ctx) => {
51
- const handle = args?.handle;
52
+ const handle = isRecord(args) && typeof args.handle === 'string' ? args.handle : undefined;
52
53
  if (!handle)
53
54
  return undefined;
54
55
  const read = await ArtifactStore.readText(handle);
@@ -99,7 +100,15 @@ export async function executeProposalApply(input, ctx) {
99
100
  for (const op of operations) {
100
101
  const fileState = stateMap.get(op.path);
101
102
  const fileInfo = fileState
102
- ? { ...fileState, hasConflict: fileState.status === FileStatus.CONFLICT }
103
+ ? {
104
+ path: fileState.path,
105
+ status: fileState.status,
106
+ isBinary: fileState.isBinary,
107
+ isSymlink: fileState.isSymlink,
108
+ isIgnored: fileState.isIgnored,
109
+ hasConflict: fileState.status === FileStatus.CONFLICT,
110
+ size: fileState.size,
111
+ }
103
112
  : {
104
113
  path: op.path,
105
114
  status: FileStatus.CLEAN,
@@ -75,10 +75,10 @@ export async function runWithFallback(backends, input, ctx, opts) {
75
75
  throw new Error(`All backends failed for capability. Tried: ${JSON.stringify(meta.tried)}`);
76
76
  }
77
77
  function createBackendError(backendId, fail, meta) {
78
- const error = new Error(`Backend ${backendId} failed: [${fail.code}] ${fail.message}`);
79
- error.backendId = backendId;
80
- error.failCode = fail.code;
81
- error.meta = meta;
82
- return error;
78
+ return Object.assign(new Error(`Backend ${backendId} failed: [${fail.code}] ${fail.message}`), {
79
+ backendId,
80
+ failCode: fail.code,
81
+ meta,
82
+ });
83
83
  }
84
84
  //# sourceMappingURL=executor.js.map
@@ -1,7 +1,5 @@
1
1
  import { redactValue } from '../llm/redact.js';
2
- function isRecord(value) {
3
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
4
- }
2
+ import { isRecord } from '../utils/serialize.js';
5
3
  function limitValue(value, params) {
6
4
  if (params.depth >= params.maxDepth)
7
5
  return '[Truncated]';
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { unwrapZodSchema } from '../utils/zod.js';
2
3
  function formatToolExamplesForDescription(spec) {
3
4
  if (!Array.isArray(spec.examples) || spec.examples.length === 0)
4
5
  return '';
@@ -13,37 +14,8 @@ function formatToolExamplesForDescription(spec) {
13
14
  function toolDescriptionForModel(spec) {
14
15
  return `${spec.description}${formatToolExamplesForDescription(spec)}`;
15
16
  }
16
- function unwrapForSchemaGeneration(schema) {
17
- let current = schema;
18
- for (let depth = 0; depth < 20; depth++) {
19
- const ZodEffects = z.ZodEffects;
20
- if (typeof ZodEffects === 'function' && current instanceof ZodEffects) {
21
- current = current._def.schema;
22
- continue;
23
- }
24
- if (current instanceof z.ZodPipe) {
25
- // z.preprocess in Zod v4 produces a ZodPipe(in=ZodTransform, out=<schema>).
26
- current = current._def.out;
27
- continue;
28
- }
29
- if (current instanceof z.ZodOptional) {
30
- current = current._def.innerType;
31
- continue;
32
- }
33
- if (current instanceof z.ZodNullable) {
34
- current = current._def.innerType;
35
- continue;
36
- }
37
- if (current instanceof z.ZodDefault) {
38
- current = current._def.innerType;
39
- continue;
40
- }
41
- break;
42
- }
43
- return current;
44
- }
45
17
  function zodToOpenApi3(schema) {
46
- const unwrapped = unwrapForSchemaGeneration(schema);
18
+ const unwrapped = unwrapZodSchema(schema);
47
19
  const description = unwrapped.description;
48
20
  if (unwrapped instanceof z.ZodObject) {
49
21
  const shape = unwrapped.shape;
@@ -64,34 +36,28 @@ function zodToOpenApi3(schema) {
64
36
  return out;
65
37
  }
66
38
  if (unwrapped instanceof z.ZodArray) {
67
- const items = zodToOpenApi3(unwrapped._def.type);
39
+ const items = zodToOpenApi3(unwrapped.element);
68
40
  const out = { type: 'array', items };
69
41
  if (description)
70
42
  out.description = description;
71
43
  return out;
72
44
  }
73
45
  if (unwrapped instanceof z.ZodEnum) {
74
- const options = unwrapped.options ?? unwrapped._def?.values;
75
- let values = [];
76
- if (Array.isArray(options)) {
77
- values = options.map(String);
78
- }
79
- else if (options && typeof options === 'object') {
80
- values = Object.values(options).map(String);
81
- }
46
+ const options = unwrapped.options;
47
+ const values = options.map(String);
82
48
  const out = values.length > 0 ? { type: 'string', enum: values } : { type: 'string' };
83
49
  if (description)
84
50
  out.description = description;
85
51
  return out;
86
52
  }
87
53
  if (unwrapped instanceof z.ZodLiteral) {
88
- const out = { const: unwrapped._def.value };
54
+ const out = { const: unwrapped.value };
89
55
  if (description)
90
56
  out.description = description;
91
57
  return out;
92
58
  }
93
59
  if (unwrapped instanceof z.ZodUnion) {
94
- const options = unwrapped._def.options;
60
+ const options = unwrapped.options;
95
61
  const out = { oneOf: options.map((o) => zodToOpenApi3(o)) };
96
62
  if (description)
97
63
  out.description = description;
@@ -110,7 +76,7 @@ function zodToOpenApi3(schema) {
110
76
  return out;
111
77
  }
112
78
  if (unwrapped instanceof z.ZodNumber) {
113
- const isInt = Boolean(unwrapped._def.checks?.some((c) => c.kind === 'int'));
79
+ const isInt = unwrapped.isInt;
114
80
  const out = { type: isInt ? 'integer' : 'number' };
115
81
  if (description)
116
82
  out.description = description;
@@ -1,5 +1,12 @@
1
1
  import path from 'path';
2
2
  import { syncFs as fs } from '../../adapters/fs/node-fs.js';
3
+ import { isRecord } from '../../utils/serialize.js';
4
+ function isPersistedPlanState(value) {
5
+ return (isRecord(value) &&
6
+ isRecord(value.plan) &&
7
+ isRecord(value.result) &&
8
+ typeof value.updatedAt === 'string');
9
+ }
3
10
  /**
4
11
  * Persistence layer for parallel execution plans.
5
12
  * Supports saving, loading, and listing plans from the .salmonloop/parallel directory.
@@ -41,10 +48,13 @@ export class PlanPersistence {
41
48
  const filePath = path.join(this.getPersistenceDir(repoRoot), `${planId}.json`);
42
49
  try {
43
50
  const content = await fs.readFile(filePath, 'utf8');
44
- return JSON.parse(content);
51
+ const parsed = JSON.parse(content);
52
+ if (!isPersistedPlanState(parsed))
53
+ return null;
54
+ return parsed;
45
55
  }
46
56
  catch (_error) {
47
- if (_error.code === 'ENOENT') {
57
+ if (isRecord(_error) && _error.code === 'ENOENT') {
48
58
  return null;
49
59
  }
50
60
  throw _error;
@@ -63,7 +73,9 @@ export class PlanPersistence {
63
73
  for (const file of jsonFiles) {
64
74
  try {
65
75
  const content = await fs.readFile(path.join(dir, file), 'utf8');
66
- states.push(JSON.parse(content));
76
+ const parsed = JSON.parse(content);
77
+ if (isPersistedPlanState(parsed))
78
+ states.push(parsed);
67
79
  }
68
80
  catch (_error) {
69
81
  // Skip malformed or unreadable files
@@ -73,7 +85,7 @@ export class PlanPersistence {
73
85
  return states;
74
86
  }
75
87
  catch (_error) {
76
- if (_error.code === 'ENOENT') {
88
+ if (isRecord(_error) && _error.code === 'ENOENT') {
77
89
  return [];
78
90
  }
79
91
  throw _error;
@@ -117,7 +129,7 @@ export class PlanPersistence {
117
129
  await fs.unlink(filePath);
118
130
  }
119
131
  catch (_error) {
120
- if (_error.code !== 'ENOENT') {
132
+ if (isRecord(_error) && _error.code !== 'ENOENT') {
121
133
  throw _error;
122
134
  }
123
135
  }
@@ -1,3 +1,5 @@
1
+ import { Phase } from '../../types/runtime.js';
2
+ import { isRecord } from '../../utils/serialize.js';
1
3
  import { isRecoverableToolInputErrorCode } from '../recoverable-tool-errors.js';
2
4
  import { IsolationManager } from './isolation.js';
3
5
  import { resolveArgsWithResults } from './resolve-args.js';
@@ -14,8 +16,7 @@ export class ParallelScheduler {
14
16
  tryResolveSpec(node) {
15
17
  if (node.spec)
16
18
  return node.spec;
17
- const router = this.router;
18
- const spec = typeof router.getSpec === 'function' ? router.getSpec(node.toolName) : undefined;
19
+ const spec = this.router.getSpec?.(node.toolName);
19
20
  if (!spec)
20
21
  return undefined;
21
22
  node.spec = spec;
@@ -28,6 +29,8 @@ export class ParallelScheduler {
28
29
  return parsed.success ? parsed.data : args;
29
30
  }
30
31
  shouldFallbackFromComputeResources(spec, args, error) {
32
+ if (!spec.inputSchema || typeof spec.inputSchema.safeParse !== 'function')
33
+ return false;
31
34
  const parsed = spec.inputSchema.safeParse(args);
32
35
  if (parsed.success)
33
36
  return false;
@@ -35,9 +38,7 @@ export class ParallelScheduler {
35
38
  if (issueCode === 'invalid_type' || issueCode === 'invalid_union' || issueCode === 'custom') {
36
39
  return true;
37
40
  }
38
- const errorCode = typeof error === 'object' && error !== null && 'code' in error
39
- ? error.code
40
- : undefined;
41
+ const errorCode = isRecord(error) && typeof error.code === 'string' ? error.code : undefined;
41
42
  return isRecoverableToolInputErrorCode(errorCode);
42
43
  }
43
44
  deriveDefaultResources(spec, ctx) {
@@ -139,7 +140,7 @@ export class ParallelScheduler {
139
140
  try {
140
141
  const spec = this.tryResolveSpec(node);
141
142
  if (!spec) {
142
- const phase = typeof baseCtx.phase === 'string' ? baseCtx.phase : undefined;
143
+ const phase = isRecord(baseCtx) && typeof baseCtx.phase === 'string' ? baseCtx.phase : undefined;
143
144
  const toolResult = {
144
145
  id: nodeId,
145
146
  toolName: node.toolName,
@@ -185,15 +186,14 @@ export class ParallelScheduler {
185
186
  const resolvedArgs = resolveArgsWithResults(node.args, nodeResults);
186
187
  const normalizedArgs = this.normalizeArgsForSpec(spec, resolvedArgs);
187
188
  // 1.5 Deferred authorization preflight (avoid holding locks while waiting for user)
188
- const preflight = typeof this.router.preflightDeferredAuthorization === 'function'
189
- ? await this.router.preflightDeferredAuthorization({
190
- id: nodeId,
191
- phase: baseCtx.phase || 'execute',
192
- toolName: node.toolName,
193
- args: normalizedArgs,
194
- ctx: baseCtx,
195
- })
196
- : null;
189
+ const phase = isRecord(baseCtx) && typeof baseCtx.phase === 'string' ? baseCtx.phase : Phase.EXPLORE;
190
+ const preflight = (await this.router.preflightDeferredAuthorization?.({
191
+ id: nodeId,
192
+ phase,
193
+ toolName: node.toolName,
194
+ args: normalizedArgs,
195
+ ctx: baseCtx,
196
+ })) ?? null;
197
197
  if (preflight?.kind === 'pending') {
198
198
  nodeStates.set(nodeId, 'BLOCKED_APPROVAL');
199
199
  const approval = {
@@ -250,7 +250,7 @@ export class ParallelScheduler {
250
250
  const runStart = Date.now();
251
251
  const result = await this.router.call({
252
252
  id: nodeId,
253
- phase: baseCtx.phase || 'execute',
253
+ phase,
254
254
  toolName: node.toolName,
255
255
  args: normalizedArgs,
256
256
  ctx: isolatedEnv
@@ -275,7 +275,9 @@ export class ParallelScheduler {
275
275
  toolName: node.toolName,
276
276
  riskLevel: spec.riskLevel,
277
277
  message: result.error.message || 'Approval required',
278
- confirmToken: result.error.confirmToken,
278
+ confirmToken: isRecord(result.error)
279
+ ? result.error.confirmToken
280
+ : undefined,
279
281
  };
280
282
  nodeResults[nodeId] = {
281
283
  status: 'BLOCKED_APPROVAL',
@@ -307,18 +309,18 @@ export class ParallelScheduler {
307
309
  catch (e) {
308
310
  nodeStates.set(nodeId, 'FAILED');
309
311
  const error = e instanceof Error
310
- ? { code: 'EXECUTION_ERROR', message: e.message, stack: e.stack }
311
- : { code: 'EXECUTION_ERROR', message: String(e) };
312
+ ? { code: 'EXECUTION_ERROR', message: e.message, retryable: false }
313
+ : { code: 'EXECUTION_ERROR', message: String(e), retryable: false };
312
314
  const toolResult = {
313
315
  id: nodeId,
314
316
  toolName: node.toolName,
315
317
  source: 'builtin',
316
318
  status: 'error',
317
- error: error,
319
+ error,
318
320
  };
319
321
  nodeResults[nodeId] = {
320
322
  status: 'FAILED',
321
- error: error,
323
+ error,
322
324
  toolResult,
323
325
  timing: { lockWaitMs: 0, runMs: 0 },
324
326
  };