salmon-loop 0.3.1 → 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 (123) 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/commands/serve.js +14 -1
  11. package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
  12. package/dist/cli/reporters/standard.js +2 -3
  13. package/dist/cli/reporters/stream-json.js +2 -1
  14. package/dist/cli/slash/runtime.js +2 -2
  15. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  16. package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
  17. package/dist/cli/ui/hooks/useLoopState.js +1 -1
  18. package/dist/core/ast/parser.js +18 -9
  19. package/dist/core/config/schema.js +738 -0
  20. package/dist/core/config/validate.js +11 -922
  21. package/dist/core/context/gatherers/ast-gatherer.js +4 -12
  22. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
  23. package/dist/core/context/gatherers/knowledge-gatherer.js +3 -0
  24. package/dist/core/context/service.js +39 -8
  25. package/dist/core/context/token/encoding-registry.js +7 -6
  26. package/dist/core/extensions/index.js +48 -3
  27. package/dist/core/extensions/load.js +3 -2
  28. package/dist/core/extensions/merge.js +5 -1
  29. package/dist/core/extensions/paths.js +6 -0
  30. package/dist/core/extensions/schemas.js +21 -0
  31. package/dist/core/facades/cli-command-chat.js +2 -0
  32. package/dist/core/facades/cli-run-handler.js +1 -0
  33. package/dist/core/facades/cli-utils-serialize.js +2 -0
  34. package/dist/core/grizzco/dsl/llm-strategy.js +3 -2
  35. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -10
  36. package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
  37. package/dist/core/grizzco/engine/transaction/attempt-failure.js +5 -4
  38. package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
  39. package/dist/core/grizzco/runtime/apply-back-runtime.js +2 -1
  40. package/dist/core/grizzco/services/registry.js +18 -0
  41. package/dist/core/grizzco/steps/audit.js +20 -10
  42. package/dist/core/grizzco/steps/display-report.js +4 -11
  43. package/dist/core/grizzco/steps/explore.js +9 -2
  44. package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
  45. package/dist/core/grizzco/steps/patch.js +1 -0
  46. package/dist/core/grizzco/steps/plan.js +58 -49
  47. package/dist/core/grizzco/steps/tool-runtime.js +3 -0
  48. package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
  49. package/dist/core/llm/ai-sdk/message-mapper.js +24 -18
  50. package/dist/core/llm/ai-sdk/request-params.js +1 -3
  51. package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
  52. package/dist/core/llm/ai-sdk/retry-classifier.js +6 -4
  53. package/dist/core/llm/contracts/repair.js +16 -8
  54. package/dist/core/llm/errors.js +13 -10
  55. package/dist/core/llm/output-policy.js +8 -0
  56. package/dist/core/llm/redact.js +1 -3
  57. package/dist/core/llm/sub-agent-factory.js +48 -0
  58. package/dist/core/llm/tool-calling-stub.js +48 -0
  59. package/dist/core/llm/utils.js +17 -6
  60. package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
  61. package/dist/core/mcp/bridge/tool-bridge.js +5 -14
  62. package/dist/core/mcp/client/connection-manager.js +3 -2
  63. package/dist/core/mcp/host/sampling-provider.js +1 -1
  64. package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
  65. package/dist/core/memory/relevant-retrieval.js +6 -4
  66. package/dist/core/observability/authorization-decisions.js +13 -12
  67. package/dist/core/observability/error-mapping.js +2 -1
  68. package/dist/core/observability/token-usage.js +5 -4
  69. package/dist/core/plugin/loader.js +5 -4
  70. package/dist/core/prompts/registry.js +11 -29
  71. package/dist/core/protocols/a2a/sdk/server.js +2 -3
  72. package/dist/core/protocols/acp/formal-agent.js +10 -4
  73. package/dist/core/protocols/acp/stdio-server.js +6 -6
  74. package/dist/core/runtime/agent-server-runtime.js +3 -2
  75. package/dist/core/runtime/initialize.js +70 -6
  76. package/dist/core/session/compaction/index.js +4 -3
  77. package/dist/core/session/manager.js +41 -47
  78. package/dist/core/session/token-tracker.js +18 -7
  79. package/dist/core/skills/parser.js +3 -2
  80. package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
  81. package/dist/core/skills/runtime/SkillRunner.js +5 -2
  82. package/dist/core/slash/steps/slash-execute.js +7 -5
  83. package/dist/core/slash/strategy.js +1 -1
  84. package/dist/core/strata/layers/worktree.js +7 -9
  85. package/dist/core/strata/runtime/synchronizer.js +10 -9
  86. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
  87. package/dist/core/structured-output/json-schema-validator.js +1 -13
  88. package/dist/core/sub-agent/context-snapshot.js +12 -6
  89. package/dist/core/sub-agent/controller.js +70 -1
  90. package/dist/core/sub-agent/core/loop.js +25 -3
  91. package/dist/core/sub-agent/core/manager.js +319 -116
  92. package/dist/core/sub-agent/registry-defaults.js +12 -0
  93. package/dist/core/sub-agent/registry.js +8 -0
  94. package/dist/core/sub-agent/team.js +98 -0
  95. package/dist/core/sub-agent/tools/task-await.js +109 -0
  96. package/dist/core/sub-agent/tools/task-spawn.js +49 -7
  97. package/dist/core/sub-agent/tools/team.js +92 -0
  98. package/dist/core/sub-agent/types.js +11 -2
  99. package/dist/core/tools/budget.js +4 -11
  100. package/dist/core/tools/builtin/code-search/executor.js +46 -43
  101. package/dist/core/tools/builtin/fs.js +14 -6
  102. package/dist/core/tools/builtin/index.js +41 -107
  103. package/dist/core/tools/builtin/interaction.js +13 -15
  104. package/dist/core/tools/builtin/proposal.js +11 -2
  105. package/dist/core/tools/capability/executor.js +5 -5
  106. package/dist/core/tools/headless-payload.js +1 -3
  107. package/dist/core/tools/mapper.js +8 -42
  108. package/dist/core/tools/parallel/persistence.js +17 -5
  109. package/dist/core/tools/parallel/scheduler.js +23 -21
  110. package/dist/core/tools/permissions/permission-rules.js +66 -114
  111. package/dist/core/tools/plugins/loader.js +4 -3
  112. package/dist/core/tools/router.js +24 -53
  113. package/dist/core/tools/session.js +54 -97
  114. package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
  115. package/dist/core/tools/tool-visibility.js +2 -1
  116. package/dist/core/tools/types.js +10 -0
  117. package/dist/core/utils/error.js +79 -0
  118. package/dist/core/utils/serialize.js +63 -0
  119. package/dist/core/utils/zod.js +29 -0
  120. package/dist/core/workspace/capabilities.js +3 -2
  121. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
  122. package/dist/locales/en.js +2 -1
  123. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { text } from '../../../locales/index.js';
2
2
  import { normalizeDiff, validateDiff } from '../../patch/diff.js';
3
3
  import { ArtifactStore } from '../../sub-agent/artifacts/store.js';
4
4
  import { normalizeRepoRelativePath } from '../../utils/path.js';
5
+ import { isRecord } from '../../utils/serialize.js';
5
6
  const DEFAULT_TOOL_ALIASES = {
6
7
  bash: 'Bash',
7
8
  read: 'Read',
@@ -218,57 +219,66 @@ function compilePathMatcher(specifier) {
218
219
  matches: (repoRelativePath) => re.test(normalizeRepoRelativePath(repoRelativePath)),
219
220
  };
220
221
  }
222
+ const TOOL_CATEGORY = {
223
+ Bash: 'bash',
224
+ bash: 'bash',
225
+ 'shell.exec': 'bash',
226
+ 'test.run': 'bash',
227
+ Edit: 'edit',
228
+ edit: 'edit',
229
+ 'proposal.apply': 'edit',
230
+ Read: 'path',
231
+ read: 'path',
232
+ LS: 'path',
233
+ ls: 'path',
234
+ 'fs.read': 'path',
235
+ 'code.read': 'path',
236
+ 'git.cat': 'path',
237
+ 'fs.list': 'path',
238
+ 'fs.list_directory': 'path',
239
+ 'fs.list_files': 'path',
240
+ 'artifact.read': 'path',
241
+ };
242
+ function resolveToolCategory(tool) {
243
+ if (TOOL_CATEGORY[tool])
244
+ return TOOL_CATEGORY[tool];
245
+ if (isAliasToolName(tool)) {
246
+ const alias = DEFAULT_TOOL_ALIASES[tool.toLowerCase()];
247
+ return TOOL_CATEGORY[alias];
248
+ }
249
+ return undefined;
250
+ }
221
251
  function compileRule(effect, parsed) {
222
- const tool = parsed.tool;
223
- const specifier = parsed.specifier;
224
- const asAlias = typeof tool === 'string' && isAliasToolName(tool) ? tool : null;
225
- const shouldTreatAsBash = tool === 'Bash' ||
226
- tool === 'bash' ||
227
- tool === 'shell.exec' ||
228
- tool === 'test.run' ||
229
- asAlias === 'Bash';
230
- const shouldTreatAsEdit = tool === 'Edit' || tool === 'edit' || tool === 'proposal.apply' || asAlias === 'Edit';
231
- const shouldTreatAsPath = tool === 'Read' ||
232
- tool === 'read' ||
233
- tool === 'LS' ||
234
- tool === 'ls' ||
235
- tool === 'fs.read' ||
236
- tool === 'code.read' ||
237
- tool === 'git.cat' ||
238
- tool === 'fs.list' ||
239
- tool === 'fs.list_directory' ||
240
- tool === 'fs.list_files' ||
241
- tool === 'artifact.read' ||
242
- asAlias === 'Read' ||
243
- asAlias === 'LS';
244
- if (shouldTreatAsBash) {
252
+ const { tool, raw, specifier } = parsed;
253
+ const category = resolveToolCategory(tool);
254
+ if (category === 'bash') {
245
255
  return {
246
256
  effect,
247
257
  tool,
248
- raw: parsed.raw,
258
+ raw,
249
259
  specifier,
250
260
  compiled: { kind: 'bash', matcher: compileBashMatcher(specifier) },
251
261
  };
252
262
  }
253
- if (shouldTreatAsEdit) {
263
+ if (category === 'edit') {
254
264
  return {
255
265
  effect,
256
266
  tool,
257
- raw: parsed.raw,
267
+ raw,
258
268
  specifier,
259
269
  compiled: { kind: 'edit', matcher: compilePathMatcher(specifier) },
260
270
  };
261
271
  }
262
- if (shouldTreatAsPath) {
272
+ if (category === 'path') {
263
273
  return {
264
274
  effect,
265
275
  tool,
266
- raw: parsed.raw,
276
+ raw,
267
277
  specifier,
268
278
  compiled: { kind: 'path', matcher: compilePathMatcher(specifier) },
269
279
  };
270
280
  }
271
- return { effect, tool, raw: parsed.raw, specifier, compiled: { kind: 'tool_any' } };
281
+ return { effect, tool, raw, specifier, compiled: { kind: 'tool_any' } };
272
282
  }
273
283
  function buildVisibleToolNamesFromAllow(allowRules) {
274
284
  const visible = new Set();
@@ -357,81 +367,37 @@ async function loadProposalChangedFiles(handle) {
357
367
  return null;
358
368
  }
359
369
  }
360
- function matchAllowRule(rule, toolName, args) {
370
+ function matchRule(rule, toolName, args) {
361
371
  if (!toolMatchesRuleTool(toolName, rule.tool))
362
372
  return false;
363
- if (rule.compiled.kind === 'tool_any')
364
- return true;
365
- if (rule.compiled.kind === 'bash') {
366
- const cmd = extractCommandArg(toolName, args);
367
- if (!cmd)
368
- return false;
369
- return rule.compiled.matcher.matches(cmd);
370
- }
371
- if (rule.compiled.kind === 'path') {
372
- const p = extractPrimaryPathArg(toolName, args);
373
- if (!p)
374
- return false;
375
- return rule.compiled.matcher.matches(p);
376
- }
377
- if (rule.compiled.kind === 'edit') {
378
- // Edit allow rules are handled by an async path-aware matcher.
379
- return toolName === 'proposal.apply';
373
+ switch (rule.compiled.kind) {
374
+ case 'tool_any':
375
+ return true;
376
+ case 'bash': {
377
+ const cmd = extractCommandArg(toolName, args);
378
+ return cmd ? rule.compiled.matcher.matches(cmd) : false;
379
+ }
380
+ case 'path': {
381
+ const p = extractPrimaryPathArg(toolName, args);
382
+ return p ? rule.compiled.matcher.matches(p) : false;
383
+ }
384
+ case 'edit':
385
+ // Edit rules require async file-level matching; sync check is a gate.
386
+ return toolName === 'proposal.apply';
380
387
  }
381
- return false;
382
388
  }
383
- async function matchAllowEditRule(rule, args) {
389
+ async function matchEditRule(rule, args, filePredicate) {
384
390
  if (rule.compiled.kind !== 'edit')
385
391
  return false;
386
- const matcher = rule.compiled.matcher;
387
- if (!args || typeof args !== 'object' || Array.isArray(args))
392
+ if (!isRecord(args))
388
393
  return false;
389
394
  const handle = args.handle;
390
395
  if (typeof handle !== 'string' || !handle.trim())
391
396
  return false;
392
397
  const changedFiles = await loadProposalChangedFiles(handle);
393
- if (!changedFiles)
394
- return false;
395
- if (changedFiles.length === 0)
398
+ if (!changedFiles || changedFiles.length === 0)
396
399
  return false;
397
- return changedFiles.every((p) => matcher.matches(p));
398
- }
399
- async function matchDenyEditRule(rule, args) {
400
- if (rule.compiled.kind !== 'edit')
401
- return false;
402
- const matcher = rule.compiled.matcher;
403
- if (!args || typeof args !== 'object' || Array.isArray(args))
404
- return false;
405
- const handle = args.handle;
406
- if (typeof handle !== 'string' || !handle.trim())
407
- return false;
408
- const changedFiles = await loadProposalChangedFiles(handle);
409
- if (!changedFiles)
410
- return false;
411
- return changedFiles.some((p) => matcher.matches(p));
412
- }
413
- function matchDenyRule(rule, toolName, args) {
414
- if (!toolMatchesRuleTool(toolName, rule.tool))
415
- return false;
416
- if (rule.compiled.kind === 'tool_any')
417
- return true;
418
- if (rule.compiled.kind === 'bash') {
419
- const cmd = extractCommandArg(toolName, args);
420
- if (!cmd)
421
- return false;
422
- return rule.compiled.matcher.matches(cmd);
423
- }
424
- if (rule.compiled.kind === 'path') {
425
- const p = extractPrimaryPathArg(toolName, args);
426
- if (!p)
427
- return false;
428
- return rule.compiled.matcher.matches(p);
429
- }
430
- if (rule.compiled.kind === 'edit') {
431
- // Deny edit rules are handled by an async path-aware matcher.
432
- return toolName === 'proposal.apply';
433
- }
434
- return false;
400
+ return filePredicate(changedFiles, rule.compiled.matcher);
435
401
  }
436
402
  export async function decidePermissionForToolCall(options) {
437
403
  const rules = options.rules;
@@ -442,17 +408,10 @@ export async function decidePermissionForToolCall(options) {
442
408
  }
443
409
  // Deny rules win.
444
410
  for (const rule of rules.deny) {
445
- if (rule.compiled.kind === 'edit') {
446
- if (options.toolName === 'proposal.apply' && (await matchDenyEditRule(rule, options.args))) {
447
- return {
448
- kind: 'deny',
449
- reason: text.tools.permissionRuleDenied(rule.raw),
450
- rule: { effect: 'deny', raw: rule.raw, tool: rule.tool },
451
- };
452
- }
453
- continue;
454
- }
455
- if (matchDenyRule(rule, options.toolName, options.args)) {
411
+ const isDeny = rule.compiled.kind === 'edit'
412
+ ? await matchEditRule(rule, options.args, (files, matcher) => files.some((p) => matcher.matches(p)))
413
+ : matchRule(rule, options.toolName, options.args);
414
+ if (isDeny) {
456
415
  return {
457
416
  kind: 'deny',
458
417
  reason: text.tools.permissionRuleDenied(rule.raw),
@@ -461,17 +420,10 @@ export async function decidePermissionForToolCall(options) {
461
420
  }
462
421
  }
463
422
  for (const rule of rules.allow) {
464
- if (rule.compiled.kind === 'edit') {
465
- if (options.toolName === 'proposal.apply' && (await matchAllowEditRule(rule, options.args))) {
466
- return {
467
- kind: 'allow',
468
- reason: rule.raw,
469
- rule: { effect: 'allow', raw: rule.raw, tool: rule.tool },
470
- };
471
- }
472
- continue;
473
- }
474
- if (matchAllowRule(rule, options.toolName, options.args)) {
423
+ const isAllow = rule.compiled.kind === 'edit'
424
+ ? await matchEditRule(rule, options.args, (files, matcher) => files.every((p) => matcher.matches(p)))
425
+ : matchRule(rule, options.toolName, options.args);
426
+ if (isAllow) {
475
427
  return {
476
428
  kind: 'allow',
477
429
  reason: rule.raw,
@@ -3,6 +3,7 @@ import { pathToFileURL } from 'node:url';
3
3
  import { syncFs as fs } from '../../adapters/fs/node-fs.js';
4
4
  import { getLogger } from '../../observability/logger.js';
5
5
  import { Phase } from '../../types/runtime.js';
6
+ import { errorMessage } from '../../utils/error.js';
6
7
  const FORBIDDEN_PHASES = new Set([
7
8
  Phase.PLAN,
8
9
  Phase.PATCH,
@@ -25,7 +26,7 @@ export async function registerPluginTools(registry, plugins) {
25
26
  }
26
27
  }
27
28
  catch (error) {
28
- getLogger().warn(`Plugin ${plugin.id} path ${entryPoint} is not accessible: ${error instanceof Error ? error.message : String(error)}`);
29
+ getLogger().warn(`Plugin ${plugin.id} path ${entryPoint} is not accessible: ${errorMessage(error)}`);
29
30
  continue;
30
31
  }
31
32
  const moduleUrl = pathToFileURL(entryPoint).href;
@@ -35,7 +36,7 @@ export async function registerPluginTools(registry, plugins) {
35
36
  manifest = (mod.default ?? mod);
36
37
  }
37
38
  catch (error) {
38
- getLogger().error(`Failed to import plugin ${plugin.id} from ${entryPoint}: ${error instanceof Error ? error.message : String(error)}`);
39
+ getLogger().error(`Failed to import plugin ${plugin.id} from ${entryPoint}: ${errorMessage(error)}`);
39
40
  continue;
40
41
  }
41
42
  const registerFn = manifest?.register;
@@ -52,7 +53,7 @@ export async function registerPluginTools(registry, plugins) {
52
53
  tools = await registerFn();
53
54
  }
54
55
  catch (error) {
55
- getLogger().error(`Plugin ${pluginId} register() failed: ${error instanceof Error ? error.message : String(error)}`);
56
+ getLogger().error(`Plugin ${pluginId} register() failed: ${errorMessage(error)}`);
56
57
  continue;
57
58
  }
58
59
  if (!Array.isArray(tools)) {
@@ -2,6 +2,8 @@ import * as crypto from 'crypto';
2
2
  import { z } from 'zod';
3
3
  import { LIMITS } from '../config/limits.js';
4
4
  import { getLogger } from '../observability/logger.js';
5
+ import { isRecord } from '../utils/serialize.js';
6
+ import { unwrapZodSchema } from '../utils/zod.js';
5
7
  import { decidePermissionForToolCall } from './permissions/permission-rules.js';
6
8
  export class ToolRouter {
7
9
  registry;
@@ -14,32 +16,7 @@ export class ToolRouter {
14
16
  authorizationMode;
15
17
  permissionRules;
16
18
  unwrapForHint(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
- current = current._def.out;
26
- continue;
27
- }
28
- if (current instanceof z.ZodOptional) {
29
- current = current._def.innerType;
30
- continue;
31
- }
32
- if (current instanceof z.ZodNullable) {
33
- current = current._def.innerType;
34
- continue;
35
- }
36
- if (current instanceof z.ZodDefault) {
37
- current = current._def.innerType;
38
- continue;
39
- }
40
- break;
41
- }
42
- return current;
19
+ return unwrapZodSchema(schema);
43
20
  }
44
21
  buildInputHint(spec) {
45
22
  if (!spec.inputSchema)
@@ -115,7 +92,7 @@ export class ToolRouter {
115
92
  const message = hint
116
93
  ? `${inputCheck.message} (${hint})`
117
94
  : inputCheck.message || 'Invalid input';
118
- throw { code: 'INVALID_INPUT', message };
95
+ throw Object.assign(new Error(message), { code: 'INVALID_INPUT' });
119
96
  }
120
97
  const normalizedArgs = inputCheck.value ?? envelope.args;
121
98
  const normalizedEnvelope = normalizedArgs === envelope.args ? envelope : { ...envelope, args: normalizedArgs };
@@ -178,7 +155,9 @@ export class ToolRouter {
178
155
  },
179
156
  });
180
157
  // Provide a stable token for challenge-response UIs.
181
- result.error.confirmToken = auth.challenge;
158
+ if (isRecord(result.error)) {
159
+ result.error.confirmToken = auth.challenge;
160
+ }
182
161
  this.audit.onEnd(result);
183
162
  return result;
184
163
  }
@@ -199,7 +178,7 @@ export class ToolRouter {
199
178
  // 5. Output Validation & Sanitize: Result validation and sensitive summary
200
179
  const sanitized = this.sanitizer.sanitizeOutput(spec, rawOutput);
201
180
  if (!sanitized.ok) {
202
- throw { code: 'INVALID_OUTPUT', message: sanitized.message };
181
+ throw Object.assign(new Error(sanitized.message), { code: 'INVALID_OUTPUT' });
203
182
  }
204
183
  // 6. Return Standard Result (ok)
205
184
  const durationMs = Date.now() - startedAt;
@@ -222,28 +201,18 @@ export class ToolRouter {
222
201
  let errorMeta;
223
202
  if (e instanceof Error) {
224
203
  errorMessage = e.message;
225
- if ('code' in e && typeof e.code === 'string') {
226
- errorCode = e.code;
227
- }
228
- if ('interrupt' in e) {
229
- errorMeta = { ...(errorMeta ?? {}), interrupt: e.interrupt };
230
- }
231
- if ('inputRequired' in e) {
232
- errorMeta = { ...(errorMeta ?? {}), inputRequired: e.inputRequired };
233
- }
234
204
  }
235
- else if (e && typeof e === 'object') {
236
- if ('message' in e && typeof e.message === 'string') {
237
- errorMessage = e.message;
238
- }
239
- if ('code' in e && typeof e.code === 'string') {
240
- errorCode = e.code;
205
+ const errObj = isRecord(e) ? e : null;
206
+ if (errObj) {
207
+ if (typeof errObj.message === 'string')
208
+ errorMessage = errObj.message;
209
+ if (typeof errObj.code === 'string')
210
+ errorCode = errObj.code;
211
+ if ('interrupt' in errObj) {
212
+ errorMeta = { ...(errorMeta ?? {}), interrupt: errObj.interrupt };
241
213
  }
242
- if ('interrupt' in e) {
243
- errorMeta = { ...(errorMeta ?? {}), interrupt: e.interrupt };
244
- }
245
- if ('inputRequired' in e) {
246
- errorMeta = { ...(errorMeta ?? {}), inputRequired: e.inputRequired };
214
+ if ('inputRequired' in errObj) {
215
+ errorMeta = { ...(errorMeta ?? {}), inputRequired: errObj.inputRequired };
247
216
  }
248
217
  }
249
218
  const result = this.createErrorResult(envelope, startedAt, errorCode === 'TIMEOUT' ? 'timeout' : 'error', errorCode, errorMessage, errorMeta);
@@ -315,7 +284,7 @@ export class ToolRouter {
315
284
  source: spec.source,
316
285
  phase: normalizedEnvelope.phase,
317
286
  riskLevel: spec.riskLevel,
318
- sideEffects: (spec.sideEffects || []),
287
+ sideEffects: spec.sideEffects,
319
288
  argsSummary,
320
289
  argsHash,
321
290
  repoRoot: normalizedEnvelope.ctx.repoRoot,
@@ -333,7 +302,9 @@ export class ToolRouter {
333
302
  source: 'user',
334
303
  },
335
304
  });
336
- toolResult.error.confirmToken = deferred.challenge;
305
+ if (isRecord(toolResult.error)) {
306
+ toolResult.error.confirmToken = deferred.challenge;
307
+ }
337
308
  return {
338
309
  kind: 'pending',
339
310
  message: deferred.message,
@@ -450,7 +421,7 @@ export class ToolRouter {
450
421
  source: spec.source,
451
422
  phase: envelope.phase,
452
423
  riskLevel: spec.riskLevel,
453
- sideEffects: (spec.sideEffects || []),
424
+ sideEffects: (spec.sideEffects ?? []),
454
425
  argsSummary,
455
426
  argsHash,
456
427
  repoRoot: envelope.ctx.repoRoot,
@@ -511,7 +482,7 @@ export class ToolRouter {
511
482
  }
512
483
  async getAuthorizationArgsSummary(envelope, spec) {
513
484
  const fallback = this.summarizeArgs(envelope.args);
514
- const summarize = spec?.summarizeArgsForAuthorization;
485
+ const summarize = spec.summarizeArgsForAuthorization;
515
486
  if (typeof summarize !== 'function')
516
487
  return fallback;
517
488
  // Best-effort only. Avoid hanging authorization prompts on slow IO.