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
@@ -0,0 +1,109 @@
1
+ import { z } from 'zod';
2
+ import { text } from '../../../locales/index.js';
3
+ import { Phase } from '../../types/runtime.js';
4
+ import { createSubAgentController } from '../controller.js';
5
+ const AgentAwaitInputSchema = z.object({
6
+ agentId: z.string().min(1).describe('The agent ID returned by agent_dispatch in async mode.'),
7
+ timeout_seconds: z
8
+ .number()
9
+ .positive()
10
+ .optional()
11
+ .describe('Maximum time to wait for the result. Defaults to the agent profile timeout.'),
12
+ });
13
+ /**
14
+ * agent_await (Internal: Smallfry Result Collector)
15
+ * Waits for an asynchronously dispatched sub-agent to complete and returns its result.
16
+ * Polls the shared controller (same instance used by agent_dispatch) for agent status.
17
+ */
18
+ export const agentAwaitTaskSpec = {
19
+ name: 'agent_await',
20
+ source: 'builtin',
21
+ intent: 'AGENT',
22
+ description: text.smallfry.ui.awaitToolDescription,
23
+ riskLevel: 'low',
24
+ defaultTimeoutMs: 300_000,
25
+ sideEffects: ['none'],
26
+ concurrency: 'parallel_ok',
27
+ allowedPhases: [Phase.PLAN, Phase.CONTEXT, Phase.AUTOPILOT],
28
+ inputSchema: AgentAwaitInputSchema,
29
+ outputSchema: z.any(), // Maps to SubAgentResult
30
+ examples: [
31
+ {
32
+ description: 'Await the result of an async sub-agent',
33
+ input: {
34
+ agentId: 'smallfry-a1b2c3d4',
35
+ },
36
+ output: {
37
+ success: true,
38
+ agent_ref: 'explorer',
39
+ summary: '<diagnosis and findings>',
40
+ },
41
+ },
42
+ ],
43
+ executor: async (input, ctx) => {
44
+ const parsed = AgentAwaitInputSchema.parse(input);
45
+ const controller = ctx.subAgentController ?? createSubAgentController();
46
+ const timeoutMs = parsed.timeout_seconds ? parsed.timeout_seconds * 1000 : 300_000;
47
+ try {
48
+ // Try awaiting the specific agent ID first (short timeout — if the ID is a
49
+ // placeholder like '{{handle}}' that no one will resolve, we must fall through
50
+ // to the scanning fallback quickly).
51
+ let result = await controller.awaitResult(parsed.agentId, Math.min(timeoutMs, 200));
52
+ // If not found (e.g., LLM used a placeholder), wait for any pending agent.
53
+ if (!result) {
54
+ const agents = controller.listAgents();
55
+ for (const agent of agents) {
56
+ if (agent.status === 'terminated') {
57
+ // Already completed — try to get the stored result
58
+ result = await controller.awaitResult(agent.id, 0);
59
+ if (result)
60
+ break;
61
+ }
62
+ }
63
+ }
64
+ // If still no result, wait for the first agent to complete
65
+ if (!result) {
66
+ const deadline = Date.now() + timeoutMs;
67
+ while (Date.now() < deadline) {
68
+ const agents = controller.listAgents();
69
+ for (const agent of agents) {
70
+ if (agent.status === 'terminated') {
71
+ result = await controller.awaitResult(agent.id, 0);
72
+ if (result)
73
+ break;
74
+ }
75
+ }
76
+ if (result)
77
+ break;
78
+ await new Promise((resolve) => setTimeout(resolve, 50));
79
+ }
80
+ }
81
+ if (result)
82
+ return result;
83
+ // Timeout
84
+ return {
85
+ success: false,
86
+ agent_ref: parsed.agentId,
87
+ summary: `Timed out waiting for agent ${parsed.agentId}`,
88
+ reason: `Timed out after ${timeoutMs}ms`,
89
+ reasonCode: 'AWAIT_FAILED',
90
+ tokenUsage: 0,
91
+ attempts: 1,
92
+ logs: [],
93
+ };
94
+ }
95
+ catch (error) {
96
+ return {
97
+ success: false,
98
+ agent_ref: parsed.agentId,
99
+ summary: error instanceof Error ? error.message : String(error),
100
+ reason: error instanceof Error ? error.message : String(error),
101
+ reasonCode: 'AWAIT_FAILED',
102
+ tokenUsage: 0,
103
+ attempts: 1,
104
+ logs: [],
105
+ };
106
+ }
107
+ },
108
+ };
109
+ //# sourceMappingURL=task-await.js.map
@@ -6,8 +6,8 @@ import { mergeSubAgentContextSnapshot } from '../context-snapshot.js';
6
6
  import { createSubAgentController } from '../controller.js';
7
7
  import { SubAgentManager } from '../core/manager.js';
8
8
  import { validateSharedPrefixConsistency } from '../prefix-consistency.js';
9
- import { SubAgentRequestSchema } from '../types.js';
10
- function normalizeDispatchRequest(input, ctx) {
9
+ import { SubAgentRequestSchema, } from '../types.js';
10
+ export function normalizeDispatchRequest(input, ctx) {
11
11
  const requested = {
12
12
  ...input,
13
13
  session_target: input.session_target ?? 'isolated',
@@ -18,6 +18,17 @@ function normalizeDispatchRequest(input, ctx) {
18
18
  ? 'review'
19
19
  : 'diagnosis'),
20
20
  };
21
+ if (requested.session_target === 'fork') {
22
+ // Fork mode: inherit parent's conversation context with cache sharing
23
+ return {
24
+ ...requested,
25
+ contextSnapshot: {
26
+ ...requested.contextSnapshot,
27
+ conversationContext: ctx.contextSnapshot?.conversationContext,
28
+ cacheSharing: ctx.contextSnapshot?.cacheSharing,
29
+ },
30
+ };
31
+ }
21
32
  if (requested.session_target !== 'shared') {
22
33
  return requested;
23
34
  }
@@ -52,6 +63,7 @@ function normalizeDispatchRequest(input, ctx) {
52
63
  /**
53
64
  * agent_dispatch (Internal: Smallfry Dispatcher)
54
65
  * The primary tool for spawning autonomous sub-agents to handle specialized sub-tasks.
66
+ * Supports async mode: set async=true to get a handle immediately, then use agent_await.
55
67
  */
56
68
  export const subAgentTaskSpec = {
57
69
  name: 'agent_dispatch',
@@ -66,7 +78,7 @@ export const subAgentTaskSpec = {
66
78
  concurrency: 'parallel_ok', // Smallfrys handle their own isolation
67
79
  allowedPhases: [Phase.PLAN, Phase.CONTEXT, Phase.AUTOPILOT],
68
80
  inputSchema: SubAgentRequestSchema,
69
- outputSchema: z.any(), // Maps to SubAgentResult
81
+ outputSchema: z.any(), // Maps to SubAgentResult | SubAgentHandle
70
82
  examples: [
71
83
  {
72
84
  description: 'Ask a read-only explorer to inspect failing tests before editing',
@@ -95,12 +107,42 @@ export const subAgentTaskSpec = {
95
107
  summary: '<review findings>',
96
108
  },
97
109
  },
110
+ {
111
+ description: 'Async dispatch: spawn an explorer and continue working',
112
+ input: {
113
+ agent_ref: 'explorer',
114
+ task: 'Scan src/utils/ for unused exports.',
115
+ async: true,
116
+ },
117
+ output: {
118
+ agentId: 'smallfry-a1b2c3d4',
119
+ status: 'working',
120
+ taskId: 'smallfry-a1b2c3d4',
121
+ },
122
+ },
98
123
  ],
99
124
  executor: async (input, ctx) => {
100
- const manager = new SubAgentManager(ctx, ctx.subAgentController ?? createSubAgentController());
101
- const request = normalizeDispatchRequest(input, ctx);
102
- // Launch the Smallfry via the manager
103
- return await manager.execute(request);
125
+ const parsed = SubAgentRequestSchema.parse(input);
126
+ const manager = new SubAgentManager(ctx, ctx.subAgentController ?? createSubAgentController(), {
127
+ llmFactory: ctx.llmFactory,
128
+ onSubAgentComplete: ctx.onSubAgentComplete,
129
+ });
130
+ const request = normalizeDispatchRequest(parsed, ctx);
131
+ try {
132
+ return await manager.execute(request);
133
+ }
134
+ catch (error) {
135
+ return {
136
+ success: false,
137
+ agent_ref: request.agent_ref,
138
+ summary: error instanceof Error ? error.message : String(error),
139
+ reason: error instanceof Error ? error.message : String(error),
140
+ reasonCode: 'DISPATCH_FAILED',
141
+ tokenUsage: 0,
142
+ attempts: 1,
143
+ logs: [],
144
+ };
145
+ }
104
146
  },
105
147
  };
106
148
  //# sourceMappingURL=task-spawn.js.map
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod';
2
+ import { Phase } from '../../types/runtime.js';
3
+ import { getOrCreateTeam } from '../team.js';
4
+ const AgentTeamInputSchema = z.object({
5
+ action: z
6
+ .enum(['claim', 'release', 'list', 'is_claimed'])
7
+ .describe('Action: claim a task key, release a claim, list all claims, or check if claimed.'),
8
+ taskKey: z
9
+ .string()
10
+ .optional()
11
+ .describe('The task/file key to claim, release, or check. Required for claim/release/is_claimed.'),
12
+ teamId: z.string().min(1).describe('The team ID to operate on.'),
13
+ });
14
+ /**
15
+ * agent_team — Coordination tool for parallel sub-agents.
16
+ * Allows sub-agents to claim tasks/files and query the team board
17
+ * to avoid duplicate work.
18
+ */
19
+ export const agentTeamSpec = {
20
+ name: 'agent_team',
21
+ source: 'builtin',
22
+ intent: 'AGENT',
23
+ description: 'Coordinate with parallel sub-agents. Use "claim" to declare you are working on a task/file, "list" to see current claims, "is_claimed" to check availability, "release" to free a claim.',
24
+ riskLevel: 'low',
25
+ defaultTimeoutMs: 5_000,
26
+ sideEffects: ['none'],
27
+ concurrency: 'parallel_ok',
28
+ allowedPhases: [Phase.PLAN, Phase.CONTEXT, Phase.AUTOPILOT],
29
+ inputSchema: AgentTeamInputSchema,
30
+ outputSchema: z.any(),
31
+ examples: [
32
+ {
33
+ description: 'Claim a file for editing',
34
+ input: { action: 'claim', taskKey: 'src/utils/parser.ts', teamId: 'team-alpha' },
35
+ output: { success: true, claimed: true },
36
+ },
37
+ {
38
+ description: 'Check if a file is already claimed',
39
+ input: { action: 'is_claimed', taskKey: 'src/utils/parser.ts', teamId: 'team-alpha' },
40
+ output: { claimed: true, claimedBy: 'smallfry-a1b2c3d4' },
41
+ },
42
+ {
43
+ description: 'List all current claims',
44
+ input: { action: 'list', teamId: 'team-alpha' },
45
+ output: {
46
+ claims: [
47
+ {
48
+ taskKey: 'src/utils/parser.ts',
49
+ claimedBy: 'smallfry-a1b2c3d4',
50
+ claimedAt: 1717800000000,
51
+ },
52
+ ],
53
+ },
54
+ },
55
+ ],
56
+ executor: async (input, ctx) => {
57
+ const parsed = AgentTeamInputSchema.parse(input);
58
+ const agentId = ctx.agentId ?? 'unknown';
59
+ const team = getOrCreateTeam(parsed.teamId);
60
+ switch (parsed.action) {
61
+ case 'claim': {
62
+ if (!parsed.taskKey)
63
+ return { success: false, error: 'taskKey required for claim' };
64
+ const claimed = team.claim(parsed.taskKey, agentId);
65
+ return { success: true, claimed };
66
+ }
67
+ case 'release': {
68
+ if (!parsed.taskKey)
69
+ return { success: false, error: 'taskKey required for release' };
70
+ const released = team.release(parsed.taskKey, agentId);
71
+ return { success: true, released };
72
+ }
73
+ case 'is_claimed': {
74
+ if (!parsed.taskKey)
75
+ return { success: false, error: 'taskKey required for is_claimed' };
76
+ const existing = team.listClaims().find((c) => c.taskKey === parsed.taskKey);
77
+ return {
78
+ success: true,
79
+ claimed: team.isClaimed(parsed.taskKey),
80
+ claimedBy: existing?.claimedBy,
81
+ };
82
+ }
83
+ case 'list': {
84
+ const claims = team.listClaims();
85
+ return { success: true, claims };
86
+ }
87
+ default:
88
+ return { success: false, error: `Unknown action: ${parsed.action}` };
89
+ }
90
+ },
91
+ };
92
+ //# sourceMappingURL=team.js.map
@@ -40,9 +40,9 @@ export const SubAgentRequestSchema = z.object({
40
40
  .describe('Optional repo-relative files the sub-agent should inspect first.'),
41
41
  recursionDepth: z.number().optional().default(0),
42
42
  session_target: z
43
- .enum(['isolated', 'shared'])
43
+ .enum(['isolated', 'shared', 'fork'])
44
44
  .default('isolated')
45
- .describe('Optional runtime strategy. Omit unless shared context is explicitly needed.'),
45
+ .describe('Optional runtime strategy. "fork" inherits parent conversation context with cache sharing. "shared" merges context snapshots. "isolated" (default) uses a clean environment.'),
46
46
  timeout_seconds: z
47
47
  .preprocess((value) => {
48
48
  if (typeof value !== 'string')
@@ -58,6 +58,15 @@ export const SubAgentRequestSchema = z.object({
58
58
  .enum(['diagnosis', 'patch', 'review'])
59
59
  .optional()
60
60
  .describe('Expected deliverable. Use patch for coder-style implementation proposals.'),
61
+ async: z
62
+ .boolean()
63
+ .default(false)
64
+ .optional()
65
+ .describe('If true, return a handle immediately and let the caller await the result.'),
66
+ teamId: z
67
+ .string()
68
+ .optional()
69
+ .describe('Join a coordination team. Sub-agents sharing a teamId can avoid duplicate work via claim/list.'),
61
70
  contextSnapshot: z
62
71
  .object({
63
72
  version: z.literal(SUB_AGENT_CONTEXT_SNAPSHOT_VERSION).optional().default(1),
@@ -50,18 +50,14 @@ export class BudgetGuard {
50
50
  const currentRiskActive = this.activeCallsByRisk[params.riskLevel];
51
51
  const maxRiskAllowed = this.config.maxConcurrentByRisk[params.riskLevel];
52
52
  if (currentRiskActive >= maxRiskAllowed) {
53
- throw {
54
- code: 'BUDGET_CONCURRENCY',
55
- message: `Too many concurrent ${params.riskLevel}-risk tool calls (limit: ${maxRiskAllowed})`,
56
- };
53
+ throw Object.assign(new Error(`Too many concurrent ${params.riskLevel}-risk tool calls (limit: ${maxRiskAllowed})`), { code: 'BUDGET_CONCURRENCY' });
57
54
  }
58
55
  // 2. Rate Limit / Count Check per Phase
59
56
  const currentCount = this.callCounts.get(params.phase) || 0;
60
57
  if (currentCount >= this.config.maxCallsPerPhase) {
61
- throw {
58
+ throw Object.assign(new Error(`Too many tool calls in phase ${params.phase}`), {
62
59
  code: 'BUDGET_RATE_LIMIT',
63
- message: `Too many tool calls in phase ${params.phase}`,
64
- };
60
+ });
65
61
  }
66
62
  this.activeCallsByRisk[params.riskLevel]++;
67
63
  this.callCounts.set(params.phase, currentCount + 1);
@@ -71,10 +67,7 @@ export class BudgetGuard {
71
67
  // 4. Output Size Check (Preliminary)
72
68
  const size = this.estimateSize(result);
73
69
  if (size > params.maxOutputBytes) {
74
- throw {
75
- code: 'OUTPUT_TOO_LARGE',
76
- message: `Output size ${size} bytes exceeds limit of ${params.maxOutputBytes}`,
77
- };
70
+ throw Object.assign(new Error(`Output size ${size} bytes exceeds limit of ${params.maxOutputBytes}`), { code: 'OUTPUT_TOO_LARGE' });
78
71
  }
79
72
  return result;
80
73
  }
@@ -1,6 +1,7 @@
1
1
  import { LIMITS } from '../../../config/limits.js';
2
2
  import { getLogger } from '../../../observability/logger.js';
3
3
  import { spawnCommand } from '../../../runtime/process-runner.js';
4
+ import { isRecord } from '../../../utils/serialize.js';
4
5
  import { runWithFallback } from '../../capability/executor.js';
5
6
  import { psBackend } from './backends/powershell.js';
6
7
  import { rgBackend } from './backends/rg.js';
@@ -24,53 +25,55 @@ export async function codeSearchExecutor(input, ctx) {
24
25
  attemptId: ctx.attemptId,
25
26
  dryRun: ctx.dryRun,
26
27
  // Allow tests (and callers) to override platform; default to host platform.
27
- platform: ctx.platform ?? process.platform,
28
- runner: ctx.runner ?? {
29
- execFile: async (file, args, opts) => {
30
- const maxStdoutBytes = opts?.maxStdoutBytes ?? Number.POSITIVE_INFINITY;
31
- let stdout = '';
32
- let stderr = '';
33
- let stdoutBytes = 0;
34
- const result = await spawnCommand({
35
- command: file,
36
- args,
37
- cwd: opts?.cwd ?? ctx.repoRoot,
38
- timeoutMs: opts?.timeoutMs,
39
- signal: ctx.signal,
40
- env: { ...process.env, ...ctx.env, ...opts?.env },
41
- onStdoutChunk: (chunk) => {
42
- if (stdoutBytes >= maxStdoutBytes)
43
- return;
44
- const buffer = Buffer.from(chunk);
45
- const remaining = maxStdoutBytes - stdoutBytes;
46
- if (buffer.length <= remaining) {
47
- stdout += buffer.toString();
48
- stdoutBytes += buffer.length;
49
- return;
50
- }
51
- stdout += buffer.subarray(0, remaining).toString();
52
- stdoutBytes += remaining;
53
- },
54
- onStderrChunk: (chunk) => {
55
- stderr += Buffer.from(chunk).toString();
56
- },
57
- });
58
- if (result.error) {
28
+ platform: isRecord(ctx) && typeof ctx.platform === 'string' ? ctx.platform : process.platform,
29
+ runner: isRecord(ctx) && typeof ctx.runner === 'object' && ctx.runner !== null
30
+ ? ctx.runner
31
+ : {
32
+ execFile: async (file, args, opts) => {
33
+ const maxStdoutBytes = opts?.maxStdoutBytes ?? Number.POSITIVE_INFINITY;
34
+ let stdout = '';
35
+ let stderr = '';
36
+ let stdoutBytes = 0;
37
+ const result = await spawnCommand({
38
+ command: file,
39
+ args,
40
+ cwd: opts?.cwd ?? ctx.repoRoot,
41
+ timeoutMs: opts?.timeoutMs,
42
+ signal: ctx.signal,
43
+ env: { ...process.env, ...ctx.env, ...opts?.env },
44
+ onStdoutChunk: (chunk) => {
45
+ if (stdoutBytes >= maxStdoutBytes)
46
+ return;
47
+ const buffer = Buffer.from(chunk);
48
+ const remaining = maxStdoutBytes - stdoutBytes;
49
+ if (buffer.length <= remaining) {
50
+ stdout += buffer.toString();
51
+ stdoutBytes += buffer.length;
52
+ return;
53
+ }
54
+ stdout += buffer.subarray(0, remaining).toString();
55
+ stdoutBytes += remaining;
56
+ },
57
+ onStderrChunk: (chunk) => {
58
+ stderr += Buffer.from(chunk).toString();
59
+ },
60
+ });
61
+ if (result.error) {
62
+ return {
63
+ stdout,
64
+ stderr: stderr || result.error.message,
65
+ exitCode: 1,
66
+ timedOut: false,
67
+ };
68
+ }
59
69
  return {
60
70
  stdout,
61
- stderr: stderr || result.error.message,
62
- exitCode: 1,
63
- timedOut: false,
71
+ stderr,
72
+ exitCode: result.code ?? 1,
73
+ timedOut: result.timedOut,
64
74
  };
65
- }
66
- return {
67
- stdout,
68
- stderr,
69
- exitCode: result.code ?? 1,
70
- timedOut: result.timedOut,
71
- };
75
+ },
72
76
  },
73
- },
74
77
  limits: {
75
78
  timeoutMs: LIMITS.defaultToolTimeoutMs,
76
79
  maxOutputBytes: LIMITS.maxToolOutputBytes,
@@ -7,6 +7,7 @@ import { AtomicFileWriter } from '../../adapters/fs/atomic-file-writer.js';
7
7
  import { mkdir, readFile, readdir, stat } from '../../adapters/fs/node-fs.js';
8
8
  import { Phase } from '../../types/runtime.js';
9
9
  import { normalizeRepoRelativePath } from '../../utils/path.js';
10
+ import { isRecord } from '../../utils/serialize.js';
10
11
  import { pathPrefixResource } from '../parallel/resource-helpers.js';
11
12
  const FsListEntryType = z.enum(['file', 'dir', 'symlink', 'other']);
12
13
  const fsListInputSchema = z.preprocess((raw) => {
@@ -338,12 +339,13 @@ export const fsWriteFileSpec = {
338
339
  bytesWritten: z.number().int().nonnegative(),
339
340
  }),
340
341
  summarizeArgsForAuthorization: async (args) => {
341
- const encoding = args?.encoding || 'utf-8';
342
- const content = String(args?.content ?? '');
342
+ const a = isRecord(args) ? args : {};
343
+ const encoding = typeof a.encoding === 'string' ? a.encoding : 'utf-8';
344
+ const content = String(a.content ?? '');
343
345
  const bytes = Buffer.byteLength(content, 'utf8');
344
346
  const sha256 = createHash('sha256').update(content, 'utf8').digest('hex');
345
347
  return JSON.stringify({
346
- file: args?.file,
348
+ file: typeof a.file === 'string' ? a.file : undefined,
347
349
  encoding,
348
350
  bytes,
349
351
  sha256,
@@ -390,7 +392,10 @@ export const fsCreateDirectorySpec = {
390
392
  ok: z.boolean(),
391
393
  path: z.string(),
392
394
  }),
393
- summarizeArgsForAuthorization: async (args) => JSON.stringify({ path: args?.path, recursive: args?.recursive }),
395
+ summarizeArgsForAuthorization: async (args) => {
396
+ const a = isRecord(args) ? args : {};
397
+ return JSON.stringify({ path: a.path, recursive: a.recursive });
398
+ },
394
399
  };
395
400
  export async function executeFsCreateDirectory(input, ctx) {
396
401
  if (ctx.dryRun) {
@@ -427,7 +432,10 @@ export const fsDeleteFileSpec = {
427
432
  path: z.string(),
428
433
  deleted: z.boolean(),
429
434
  }),
430
- summarizeArgsForAuthorization: async (args) => JSON.stringify({ file: args?.file, missingOk: args?.missingOk }),
435
+ summarizeArgsForAuthorization: async (args) => {
436
+ const a = isRecord(args) ? args : {};
437
+ return JSON.stringify({ file: a.file, missingOk: a.missingOk });
438
+ },
431
439
  };
432
440
  export async function executeFsDeleteFile(input, ctx) {
433
441
  if (ctx.dryRun) {
@@ -439,7 +447,7 @@ export async function executeFsDeleteFile(input, ctx) {
439
447
  await stat(absolutePath);
440
448
  }
441
449
  catch (e) {
442
- const code = e && typeof e === 'object' && 'code' in e ? e.code : undefined;
450
+ const code = isRecord(e) && typeof e.code === 'string' ? e.code : undefined;
443
451
  if (code === 'ENOENT')
444
452
  exists = false;
445
453
  else