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.
- package/dist/cli/authorization/non-interactive.js +9 -13
- package/dist/cli/chat.js +12 -6
- package/dist/cli/commands/allowlist.js +1 -1
- package/dist/cli/commands/chat.js +13 -13
- package/dist/cli/commands/parallel.js +1 -1
- package/dist/cli/commands/run/handler.js +6 -3
- package/dist/cli/commands/run/loop-params.js +1 -0
- package/dist/cli/commands/run/parse-options.js +14 -26
- package/dist/cli/commands/run/runtime-llm.js +15 -12
- package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
- package/dist/cli/reporters/standard.js +2 -3
- package/dist/cli/reporters/stream-json.js +2 -1
- package/dist/cli/slash/runtime.js +2 -2
- package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
- package/dist/cli/ui/hooks/useLoopState.js +1 -1
- package/dist/core/ast/parser.js +18 -9
- package/dist/core/config/schema.js +738 -0
- package/dist/core/config/validate.js +11 -922
- package/dist/core/context/gatherers/ast-gatherer.js +4 -12
- package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
- package/dist/core/context/gatherers/knowledge-gatherer.js +3 -0
- package/dist/core/context/service.js +8 -0
- package/dist/core/context/token/encoding-registry.js +7 -6
- package/dist/core/extensions/index.js +48 -3
- package/dist/core/extensions/load.js +3 -2
- package/dist/core/extensions/merge.js +5 -1
- package/dist/core/extensions/paths.js +6 -0
- package/dist/core/extensions/schemas.js +21 -0
- package/dist/core/facades/cli-command-chat.js +2 -0
- package/dist/core/facades/cli-run-handler.js +1 -0
- package/dist/core/facades/cli-utils-serialize.js +2 -0
- package/dist/core/grizzco/dsl/llm-strategy.js +3 -2
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -10
- package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +5 -4
- package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
- package/dist/core/grizzco/runtime/apply-back-runtime.js +2 -1
- package/dist/core/grizzco/services/registry.js +18 -0
- package/dist/core/grizzco/steps/audit.js +20 -10
- package/dist/core/grizzco/steps/display-report.js +4 -11
- package/dist/core/grizzco/steps/explore.js +9 -2
- package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
- package/dist/core/grizzco/steps/patch.js +1 -0
- package/dist/core/grizzco/steps/plan.js +58 -49
- package/dist/core/grizzco/steps/tool-runtime.js +3 -0
- package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
- package/dist/core/llm/ai-sdk/message-mapper.js +24 -18
- package/dist/core/llm/ai-sdk/request-params.js +1 -3
- package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
- package/dist/core/llm/ai-sdk/retry-classifier.js +6 -4
- package/dist/core/llm/contracts/repair.js +16 -8
- package/dist/core/llm/errors.js +13 -10
- package/dist/core/llm/output-policy.js +8 -0
- package/dist/core/llm/redact.js +1 -3
- package/dist/core/llm/sub-agent-factory.js +48 -0
- package/dist/core/llm/tool-calling-stub.js +48 -0
- package/dist/core/llm/utils.js +17 -6
- package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
- package/dist/core/mcp/bridge/tool-bridge.js +5 -14
- package/dist/core/mcp/client/connection-manager.js +3 -2
- package/dist/core/mcp/host/sampling-provider.js +1 -1
- package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
- package/dist/core/memory/relevant-retrieval.js +6 -4
- package/dist/core/observability/authorization-decisions.js +13 -12
- package/dist/core/observability/error-mapping.js +2 -1
- package/dist/core/observability/token-usage.js +5 -4
- package/dist/core/plugin/loader.js +5 -4
- package/dist/core/prompts/registry.js +11 -29
- package/dist/core/protocols/a2a/sdk/server.js +2 -3
- package/dist/core/protocols/acp/formal-agent.js +10 -4
- package/dist/core/protocols/acp/stdio-server.js +6 -6
- package/dist/core/runtime/agent-server-runtime.js +3 -2
- package/dist/core/runtime/initialize.js +70 -6
- package/dist/core/session/compaction/index.js +4 -3
- package/dist/core/session/manager.js +24 -37
- package/dist/core/session/token-tracker.js +18 -7
- package/dist/core/skills/parser.js +3 -2
- package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
- package/dist/core/skills/runtime/SkillRunner.js +5 -2
- package/dist/core/slash/steps/slash-execute.js +7 -5
- package/dist/core/slash/strategy.js +1 -1
- package/dist/core/strata/layers/worktree.js +7 -9
- package/dist/core/strata/runtime/synchronizer.js +10 -9
- package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
- package/dist/core/structured-output/json-schema-validator.js +1 -13
- package/dist/core/sub-agent/context-snapshot.js +12 -6
- package/dist/core/sub-agent/controller.js +70 -1
- package/dist/core/sub-agent/core/loop.js +25 -3
- package/dist/core/sub-agent/core/manager.js +319 -116
- package/dist/core/sub-agent/registry-defaults.js +12 -0
- package/dist/core/sub-agent/registry.js +8 -0
- package/dist/core/sub-agent/team.js +98 -0
- package/dist/core/sub-agent/tools/task-await.js +109 -0
- package/dist/core/sub-agent/tools/task-spawn.js +49 -7
- package/dist/core/sub-agent/tools/team.js +92 -0
- package/dist/core/sub-agent/types.js +11 -2
- package/dist/core/tools/budget.js +4 -11
- package/dist/core/tools/builtin/code-search/executor.js +46 -43
- package/dist/core/tools/builtin/fs.js +14 -6
- package/dist/core/tools/builtin/index.js +41 -107
- package/dist/core/tools/builtin/interaction.js +13 -15
- package/dist/core/tools/builtin/proposal.js +11 -2
- package/dist/core/tools/capability/executor.js +5 -5
- package/dist/core/tools/headless-payload.js +1 -3
- package/dist/core/tools/mapper.js +8 -42
- package/dist/core/tools/parallel/persistence.js +17 -5
- package/dist/core/tools/parallel/scheduler.js +23 -21
- package/dist/core/tools/permissions/permission-rules.js +66 -114
- package/dist/core/tools/plugins/loader.js +4 -3
- package/dist/core/tools/router.js +24 -53
- package/dist/core/tools/session.js +54 -97
- package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
- package/dist/core/tools/tool-visibility.js +2 -1
- package/dist/core/tools/types.js +10 -0
- package/dist/core/utils/error.js +79 -0
- package/dist/core/utils/serialize.js +63 -0
- package/dist/core/utils/zod.js +29 -0
- package/dist/core/workspace/capabilities.js +3 -2
- package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
- package/dist/locales/en.js +2 -1
- 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
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
28
|
-
runner: ctx.runner
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
62
|
-
exitCode: 1,
|
|
63
|
-
timedOut:
|
|
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
|
|
342
|
-
const
|
|
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:
|
|
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) =>
|
|
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) =>
|
|
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 === '
|
|
450
|
+
const code = isRecord(e) && typeof e.code === 'string' ? e.code : undefined;
|
|
443
451
|
if (code === 'ENOENT')
|
|
444
452
|
exists = false;
|
|
445
453
|
else
|