salmon-loop 0.3.2 → 0.5.0

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 (227) hide show
  1. package/dist/cli/authorization/non-interactive.js +9 -13
  2. package/dist/cli/authorization/provider.js +2 -10
  3. package/dist/cli/chat.js +12 -6
  4. package/dist/cli/commands/allowlist.js +1 -1
  5. package/dist/cli/commands/chat.js +13 -13
  6. package/dist/cli/commands/config.js +2 -2
  7. package/dist/cli/commands/mode.js +2 -2
  8. package/dist/cli/commands/parallel.js +1 -1
  9. package/dist/cli/commands/run/handler.js +9 -4
  10. package/dist/cli/commands/run/loop-params.js +2 -0
  11. package/dist/cli/commands/run/parse-options.js +14 -26
  12. package/dist/cli/commands/run/runtime-llm.js +15 -12
  13. package/dist/cli/commands/run/runtime-options.js +3 -1
  14. package/dist/cli/config.js +0 -8
  15. package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
  16. package/dist/cli/locales/en.js +2 -2
  17. package/dist/cli/reporters/standard.js +12 -3
  18. package/dist/cli/reporters/stream-json.js +2 -1
  19. package/dist/cli/slash/runtime.js +2 -2
  20. package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
  21. package/dist/cli/ui/hooks/useLoopState.js +1 -1
  22. package/dist/core/adapters/fs/file-adapter.js +3 -1
  23. package/dist/core/adapters/git/git-adapter.js +6 -3
  24. package/dist/core/adapters/git/git-runner.js +5 -2
  25. package/dist/core/adapters/git/lock-manager.js +7 -4
  26. package/dist/core/ast/parser.js +18 -9
  27. package/dist/core/checkpoint-domain/manifest-store.js +21 -13
  28. package/dist/core/checkpoint-domain/service.js +3 -1
  29. package/dist/core/config/limits.js +1 -1
  30. package/dist/core/config/model-pricing.js +61 -0
  31. package/dist/core/config/schema.js +738 -0
  32. package/dist/core/config/validate.js +11 -922
  33. package/dist/core/context/ast/skeleton-extractor.js +225 -0
  34. package/dist/core/context/ast/source-outline.js +24 -1
  35. package/dist/core/context/budget/dynamic-adjuster.js +20 -5
  36. package/dist/core/context/builder.js +7 -3
  37. package/dist/core/context/cache/store-factory.js +3 -1
  38. package/dist/core/context/dependencies.js +2 -1
  39. package/dist/core/context/effectiveness/persistence.js +50 -0
  40. package/dist/core/context/effectiveness/tracker.js +24 -0
  41. package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
  42. package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
  43. package/dist/core/context/gatherers/ast-gatherer.js +34 -40
  44. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
  45. package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
  46. package/dist/core/context/gatherers/knowledge-gatherer.js +21 -2
  47. package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
  48. package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
  49. package/dist/core/context/service.js +12 -2
  50. package/dist/core/context/steps/context-gather.js +14 -3
  51. package/dist/core/context/steps/context-targets.js +1 -0
  52. package/dist/core/context/targeting/target-resolver.js +29 -11
  53. package/dist/core/context/token/cache.js +5 -2
  54. package/dist/core/context/token/encoding-registry.js +7 -6
  55. package/dist/core/context/truncation/strategies/json.js +5 -2
  56. package/dist/core/context/truncation/type-detector.js +3 -1
  57. package/dist/core/extensions/index.js +48 -3
  58. package/dist/core/extensions/load.js +3 -2
  59. package/dist/core/extensions/merge.js +5 -1
  60. package/dist/core/extensions/paths.js +8 -2
  61. package/dist/core/extensions/schemas.js +21 -0
  62. package/dist/core/facades/cli-authorization-provider.js +1 -0
  63. package/dist/core/facades/cli-command-chat.js +2 -0
  64. package/dist/core/facades/cli-run-handler.js +1 -0
  65. package/dist/core/facades/cli-utils-serialize.js +2 -0
  66. package/dist/core/feedback/parsers.js +290 -1
  67. package/dist/core/grizzco/dsl/llm-strategy.js +4 -3
  68. package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
  69. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +30 -13
  70. package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
  71. package/dist/core/grizzco/engine/transaction/attempt-failure.js +49 -24
  72. package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
  73. package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
  74. package/dist/core/grizzco/execution/RejectionManager.js +7 -5
  75. package/dist/core/grizzco/runtime/apply-back-runtime.js +5 -2
  76. package/dist/core/grizzco/services/implementations/default/GitConfigService.js +2 -1
  77. package/dist/core/grizzco/services/registry.js +18 -0
  78. package/dist/core/grizzco/steps/audit.js +20 -10
  79. package/dist/core/grizzco/steps/autopilot.js +21 -32
  80. package/dist/core/grizzco/steps/display-report.js +4 -11
  81. package/dist/core/grizzco/steps/explore.js +14 -4
  82. package/dist/core/grizzco/steps/generateReview.js +3 -1
  83. package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
  84. package/dist/core/grizzco/steps/patch.js +1 -0
  85. package/dist/core/grizzco/steps/plan.js +58 -49
  86. package/dist/core/grizzco/steps/research.js +3 -1
  87. package/dist/core/grizzco/steps/tool-runtime.js +3 -0
  88. package/dist/core/grizzco/steps/verify.js +7 -1
  89. package/dist/core/grizzco/validation/AstValidationService.js +3 -1
  90. package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
  91. package/dist/core/history/input-history.js +3 -1
  92. package/dist/core/intent/chat-intent.js +3 -1
  93. package/dist/core/llm/ai-sdk/message-mapper.js +37 -26
  94. package/dist/core/llm/ai-sdk/request-params.js +2 -6
  95. package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
  96. package/dist/core/llm/ai-sdk/retry-classifier.js +17 -7
  97. package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
  98. package/dist/core/llm/contracts/repair.js +16 -8
  99. package/dist/core/llm/errors.js +18 -14
  100. package/dist/core/llm/output-policy.js +8 -0
  101. package/dist/core/llm/redact.js +1 -3
  102. package/dist/core/llm/retry-utils.js +8 -2
  103. package/dist/core/llm/stream-utils.js +5 -3
  104. package/dist/core/llm/sub-agent-factory.js +51 -0
  105. package/dist/core/llm/tool-calling-stub.js +48 -0
  106. package/dist/core/llm/utils.js +17 -6
  107. package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
  108. package/dist/core/mcp/bridge/resource-context-provider.js +3 -1
  109. package/dist/core/mcp/bridge/tool-bridge.js +5 -14
  110. package/dist/core/mcp/catalog/discovery.js +3 -1
  111. package/dist/core/mcp/client/connection-manager.js +7 -4
  112. package/dist/core/mcp/client/transport-factory.js +7 -3
  113. package/dist/core/mcp/host/sampling-provider.js +1 -1
  114. package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
  115. package/dist/core/memory/relevant-retrieval.js +6 -4
  116. package/dist/core/observability/audit-file.js +2 -1
  117. package/dist/core/observability/audit-trail.js +3 -1
  118. package/dist/core/observability/authorization-decisions.js +13 -12
  119. package/dist/core/observability/error-mapping.js +2 -1
  120. package/dist/core/observability/logger.js +2 -1
  121. package/dist/core/observability/monitor.js +24 -0
  122. package/dist/core/observability/run-outcome-reporter.js +1 -0
  123. package/dist/core/observability/token-usage.js +5 -4
  124. package/dist/core/permission-gate/default-gate.js +5 -8
  125. package/dist/core/plan/storage.js +7 -4
  126. package/dist/core/plugin/loader.js +8 -5
  127. package/dist/core/prompts/registry.js +12 -30
  128. package/dist/core/prompts/runtime.js +3 -1
  129. package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
  130. package/dist/core/protocols/a2a/sdk/executor.js +3 -1
  131. package/dist/core/protocols/a2a/sdk/server.js +5 -4
  132. package/dist/core/protocols/acp/acp-command-runner.js +7 -6
  133. package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
  134. package/dist/core/protocols/acp/formal-agent.js +13 -6
  135. package/dist/core/protocols/acp/permission-provider.js +3 -2
  136. package/dist/core/protocols/acp/stdio-server.js +6 -6
  137. package/dist/core/reflection/engine.js +114 -14
  138. package/dist/core/runtime/agent-server-runtime.js +3 -2
  139. package/dist/core/runtime/batch-runner.js +81 -0
  140. package/dist/core/runtime/initialize.js +71 -6
  141. package/dist/core/runtime/loop-finalize.js +3 -0
  142. package/dist/core/runtime/loop-session-runner.js +5 -0
  143. package/dist/core/runtime/loop.js +4 -0
  144. package/dist/core/runtime/paths.js +9 -6
  145. package/dist/core/runtime/spawn-interactive.js +5 -4
  146. package/dist/core/security/redaction.js +3 -2
  147. package/dist/core/session/compaction/index.js +4 -3
  148. package/dist/core/session/compression.js +3 -1
  149. package/dist/core/session/manager.js +26 -38
  150. package/dist/core/session/pruning-strategy.js +2 -1
  151. package/dist/core/session/token-tracker.js +27 -9
  152. package/dist/core/skills/parser.js +3 -2
  153. package/dist/core/skills/permissions.js +2 -2
  154. package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
  155. package/dist/core/skills/runtime/SkillRunner.js +5 -2
  156. package/dist/core/slash/steps/slash-execute.js +7 -5
  157. package/dist/core/slash/strategy.js +1 -1
  158. package/dist/core/strata/checkpoint/manager.js +16 -10
  159. package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
  160. package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
  161. package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
  162. package/dist/core/strata/interaction/file-system-provider.js +2 -1
  163. package/dist/core/strata/layers/file-state-resolver.js +9 -7
  164. package/dist/core/strata/layers/immutable-git-layer.js +3 -1
  165. package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
  166. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
  167. package/dist/core/strata/layers/worktree.js +9 -10
  168. package/dist/core/strata/runtime/environment.js +2 -1
  169. package/dist/core/strata/runtime/synchronizer.js +28 -26
  170. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
  171. package/dist/core/structured-output/json-extract.js +3 -1
  172. package/dist/core/structured-output/json-schema-validator.js +1 -13
  173. package/dist/core/sub-agent/artifacts/store.js +2 -1
  174. package/dist/core/sub-agent/context-snapshot.js +12 -6
  175. package/dist/core/sub-agent/controller.js +70 -1
  176. package/dist/core/sub-agent/core/loop.js +25 -3
  177. package/dist/core/sub-agent/core/manager.js +343 -117
  178. package/dist/core/sub-agent/registry-defaults.js +12 -0
  179. package/dist/core/sub-agent/registry.js +8 -0
  180. package/dist/core/sub-agent/summary.js +96 -0
  181. package/dist/core/sub-agent/team.js +98 -0
  182. package/dist/core/sub-agent/tools/task-await.js +109 -0
  183. package/dist/core/sub-agent/tools/task-spawn.js +52 -7
  184. package/dist/core/sub-agent/tools/team.js +92 -0
  185. package/dist/core/sub-agent/types.js +11 -2
  186. package/dist/core/target-runtime/profile.js +3 -1
  187. package/dist/core/tools/audit.js +3 -2
  188. package/dist/core/tools/budget.js +7 -12
  189. package/dist/core/tools/builtin/ast.js +144 -0
  190. package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
  191. package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
  192. package/dist/core/tools/builtin/code-search/executor.js +46 -43
  193. package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
  194. package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
  195. package/dist/core/tools/builtin/fs.js +90 -7
  196. package/dist/core/tools/builtin/git.js +242 -0
  197. package/dist/core/tools/builtin/glob.js +79 -0
  198. package/dist/core/tools/builtin/index.js +53 -111
  199. package/dist/core/tools/builtin/interaction.js +13 -15
  200. package/dist/core/tools/builtin/knowledge.js +146 -4
  201. package/dist/core/tools/builtin/proposal.js +14 -3
  202. package/dist/core/tools/builtin/verify.js +35 -3
  203. package/dist/core/tools/capability/executor.js +5 -5
  204. package/dist/core/tools/headless-payload.js +1 -3
  205. package/dist/core/tools/mapper.js +8 -42
  206. package/dist/core/tools/parallel/persistence.js +17 -5
  207. package/dist/core/tools/parallel/scheduler.js +23 -21
  208. package/dist/core/tools/permissions/permission-rules.js +69 -115
  209. package/dist/core/tools/plugins/loader.js +4 -3
  210. package/dist/core/tools/router.js +112 -58
  211. package/dist/core/tools/session.js +64 -102
  212. package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
  213. package/dist/core/tools/tool-visibility.js +2 -1
  214. package/dist/core/tools/types.js +10 -0
  215. package/dist/core/types/batch.js +2 -0
  216. package/dist/core/utils/error.js +79 -0
  217. package/dist/core/utils/sanitizer.js +5 -2
  218. package/dist/core/utils/serialize.js +66 -0
  219. package/dist/core/utils/zod.js +29 -0
  220. package/dist/core/verification/detect-runner.js +86 -0
  221. package/dist/core/verification/runner.js +76 -0
  222. package/dist/core/version.js +3 -1
  223. package/dist/core/workspace/capabilities.js +3 -2
  224. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
  225. package/dist/languages/python/index.js +154 -0
  226. package/dist/locales/en.js +8 -1
  227. package/package.json +2 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Sub-agent result synthesis.
3
+ *
4
+ * Detects file-level conflicts between sub-agent patches
5
+ * and generates structured summaries for parent injection.
6
+ */
7
+ /**
8
+ * Extract file paths touched by a unified diff.
9
+ * Parses diff headers (lines starting with "--- a/" and "+++ b/").
10
+ */
11
+ export function extractDiffFiles(patch) {
12
+ const files = new Set();
13
+ const lines = patch.split('\n');
14
+ for (const line of lines) {
15
+ if (line.startsWith('+++ b/')) {
16
+ files.add(line.slice(6));
17
+ }
18
+ }
19
+ return Array.from(files);
20
+ }
21
+ /**
22
+ * Detect file-level conflicts across multiple sub-agent results.
23
+ * Two agents conflict if they both modify the same file.
24
+ */
25
+ export function detectConflicts(results) {
26
+ const fileToAgents = new Map();
27
+ for (const { agentId, result } of results) {
28
+ if (!result.finalPatch || typeof result.finalPatch !== 'string')
29
+ continue;
30
+ const files = extractDiffFiles(result.finalPatch);
31
+ for (const file of files) {
32
+ const agents = fileToAgents.get(file) ?? [];
33
+ agents.push(agentId);
34
+ fileToAgents.set(file, agents);
35
+ }
36
+ }
37
+ const conflicts = [];
38
+ for (const [file, agents] of fileToAgents) {
39
+ if (agents.length > 1) {
40
+ conflicts.push({ file, agents });
41
+ }
42
+ }
43
+ return conflicts;
44
+ }
45
+ /**
46
+ * Generate a structured summary of all sub-agent results.
47
+ */
48
+ export function generateSubAgentSummary(results) {
49
+ const conflicts = detectConflicts(results);
50
+ const changedFiles = new Set();
51
+ let succeeded = 0;
52
+ let failed = 0;
53
+ let totalTokens = 0;
54
+ for (const { result } of results) {
55
+ if (result.success)
56
+ succeeded++;
57
+ else
58
+ failed++;
59
+ totalTokens += result.tokenUsage ?? 0;
60
+ if (result.finalPatch && typeof result.finalPatch === 'string') {
61
+ for (const file of extractDiffFiles(result.finalPatch)) {
62
+ changedFiles.add(file);
63
+ }
64
+ }
65
+ }
66
+ return {
67
+ totalAgents: results.length,
68
+ succeeded,
69
+ failed,
70
+ conflicts,
71
+ totalTokens,
72
+ changedFiles: Array.from(changedFiles),
73
+ };
74
+ }
75
+ /**
76
+ * Format a sub-agent summary as a human-readable string for LLM context injection.
77
+ */
78
+ export function formatSubAgentSummary(summary) {
79
+ const lines = [
80
+ `## Sub-Agent Summary`,
81
+ `- Total: ${summary.totalAgents} | Succeeded: ${summary.succeeded} | Failed: ${summary.failed}`,
82
+ `- Total tokens: ${summary.totalTokens.toLocaleString()}`,
83
+ `- Changed files: ${summary.changedFiles.length}`,
84
+ ];
85
+ if (summary.changedFiles.length > 0) {
86
+ lines.push(`- Files: ${summary.changedFiles.join(', ')}`);
87
+ }
88
+ if (summary.conflicts.length > 0) {
89
+ lines.push(`\n### Conflicts Detected`);
90
+ for (const conflict of summary.conflicts) {
91
+ lines.push(`- \`${conflict.file}\`: modified by ${conflict.agents.join(', ')}`);
92
+ }
93
+ }
94
+ return lines.join('\n');
95
+ }
96
+ //# sourceMappingURL=summary.js.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * SubAgentTeam — lightweight task board for parallel sub-agent coordination.
3
+ *
4
+ * Multiple sub-agents can claim tasks/files to avoid duplicate work.
5
+ * Claims are advisory (not enforced) — the team board is a shared
6
+ * declaration board, not a mutex.
7
+ */
8
+ export class SubAgentTeam {
9
+ board = new Map();
10
+ /**
11
+ * Attempt to claim a task key. Returns true if the claim succeeded
12
+ * (key was unclaimed), false if already claimed by another agent.
13
+ * Re-claiming by the same agent is idempotent (returns true).
14
+ */
15
+ claim(taskKey, agentId) {
16
+ const existing = this.board.get(taskKey);
17
+ if (existing && existing.claimedBy !== agentId) {
18
+ return false;
19
+ }
20
+ this.board.set(taskKey, { claimedBy: agentId, claimedAt: Date.now() });
21
+ return true;
22
+ }
23
+ /**
24
+ * Release a claim (e.g., on completion or failure).
25
+ */
26
+ release(taskKey, agentId) {
27
+ const existing = this.board.get(taskKey);
28
+ if (!existing || existing.claimedBy !== agentId) {
29
+ return false;
30
+ }
31
+ this.board.delete(taskKey);
32
+ return true;
33
+ }
34
+ /**
35
+ * Check if a task key is already claimed by someone else.
36
+ */
37
+ isClaimed(taskKey, excludeAgent) {
38
+ const existing = this.board.get(taskKey);
39
+ if (!existing)
40
+ return false;
41
+ return existing.claimedBy !== excludeAgent;
42
+ }
43
+ /**
44
+ * List all current claims.
45
+ */
46
+ listClaims() {
47
+ return Array.from(this.board.entries()).map(([taskKey, entry]) => ({
48
+ taskKey,
49
+ claimedBy: entry.claimedBy,
50
+ claimedAt: entry.claimedAt,
51
+ }));
52
+ }
53
+ /**
54
+ * Get all claims for a specific agent.
55
+ */
56
+ getAgentClaims(agentId) {
57
+ const claims = [];
58
+ for (const [taskKey, entry] of this.board) {
59
+ if (entry.claimedBy === agentId) {
60
+ claims.push({ taskKey, claimedBy: entry.claimedBy, claimedAt: entry.claimedAt });
61
+ }
62
+ }
63
+ return claims;
64
+ }
65
+ /**
66
+ * Clear all claims for a specific agent (e.g., on termination).
67
+ */
68
+ releaseAll(agentId) {
69
+ let released = 0;
70
+ for (const [key, entry] of this.board) {
71
+ if (entry.claimedBy === agentId) {
72
+ this.board.delete(key);
73
+ released++;
74
+ }
75
+ }
76
+ return released;
77
+ }
78
+ }
79
+ // Global team registry — teams live for the duration of the parent session
80
+ const teams = new Map();
81
+ /** Get an existing team or create a new one. Teams are keyed by teamId. */
82
+ export function getOrCreateTeam(teamId) {
83
+ let team = teams.get(teamId);
84
+ if (!team) {
85
+ team = new SubAgentTeam();
86
+ teams.set(teamId, team);
87
+ }
88
+ return team;
89
+ }
90
+ /** Remove a team from the global registry. Returns false if not found. */
91
+ export function removeTeam(teamId) {
92
+ return teams.delete(teamId);
93
+ }
94
+ /** Remove all teams from the global registry. */
95
+ export function clearAllTeams() {
96
+ teams.clear();
97
+ }
98
+ //# sourceMappingURL=team.js.map
@@ -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,45 @@ 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 controller = ctx.subAgentController ?? createSubAgentController();
127
+ const manager = ctx.subAgentManagerFactory
128
+ ? ctx.subAgentManagerFactory(ctx, controller)
129
+ : new SubAgentManager(ctx, controller, {
130
+ llmFactory: ctx.llmFactory,
131
+ onSubAgentComplete: ctx.onSubAgentComplete,
132
+ });
133
+ const request = normalizeDispatchRequest(parsed, ctx);
134
+ try {
135
+ return await manager.execute(request);
136
+ }
137
+ catch (error) {
138
+ return {
139
+ success: false,
140
+ agent_ref: request.agent_ref,
141
+ summary: error instanceof Error ? error.message : String(error),
142
+ reason: error instanceof Error ? error.message : String(error),
143
+ reasonCode: 'DISPATCH_FAILED',
144
+ tokenUsage: 0,
145
+ attempts: 1,
146
+ logs: [],
147
+ };
148
+ }
104
149
  },
105
150
  };
106
151
  //# 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),
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from '../adapters/fs/node-fs.js';
2
2
  import { readFile } from '../adapters/fs/node-fs.js';
3
+ import { getLogger } from '../observability/logger.js';
3
4
  import { ensureInSandbox, safeJoin } from '../utils/path.js';
4
5
  const LOCKFILE_HINTS = [
5
6
  { file: 'bun.lock', manager: 'bun' },
@@ -59,7 +60,8 @@ export async function detectNodeRuntimeProfile(repoPath) {
59
60
  try {
60
61
  parsed = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
61
62
  }
62
- catch {
63
+ catch (error) {
64
+ getLogger().debug(`[TargetRuntime] Failed to parse package.json: ${error instanceof Error ? error.message : String(error)}`);
63
65
  return undefined;
64
66
  }
65
67
  const { packageManager, source } = detectPackageManager(repoPath, parsed);
@@ -1,5 +1,5 @@
1
1
  import { text } from '../../locales/index.js';
2
- import { tryGetLogger } from '../observability/logger.js';
2
+ import { getLogger, tryGetLogger } from '../observability/logger.js';
3
3
  import { Phase } from '../types/runtime.js';
4
4
  import { sanitizeErrorMessage } from '../utils/sanitizer.js';
5
5
  export class ToolAuditLogger {
@@ -112,7 +112,8 @@ export class ToolAuditLogger {
112
112
  const str = JSON.stringify(data);
113
113
  return str.length > 200 ? str.substring(0, 200) + '...' : str;
114
114
  }
115
- catch {
115
+ catch (error) {
116
+ getLogger().debug(`[ToolAudit] Failed to summarize data: ${error instanceof Error ? error.message : String(error)}`);
116
117
  return '[Circular/Unserializable]';
117
118
  }
118
119
  }
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../observability/logger.js';
1
2
  /**
2
3
  * Default budget limits to ensure system stability.
3
4
  */
@@ -50,18 +51,14 @@ export class BudgetGuard {
50
51
  const currentRiskActive = this.activeCallsByRisk[params.riskLevel];
51
52
  const maxRiskAllowed = this.config.maxConcurrentByRisk[params.riskLevel];
52
53
  if (currentRiskActive >= maxRiskAllowed) {
53
- throw {
54
- code: 'BUDGET_CONCURRENCY',
55
- message: `Too many concurrent ${params.riskLevel}-risk tool calls (limit: ${maxRiskAllowed})`,
56
- };
54
+ throw Object.assign(new Error(`Too many concurrent ${params.riskLevel}-risk tool calls (limit: ${maxRiskAllowed})`), { code: 'BUDGET_CONCURRENCY' });
57
55
  }
58
56
  // 2. Rate Limit / Count Check per Phase
59
57
  const currentCount = this.callCounts.get(params.phase) || 0;
60
58
  if (currentCount >= this.config.maxCallsPerPhase) {
61
- throw {
59
+ throw Object.assign(new Error(`Too many tool calls in phase ${params.phase}`), {
62
60
  code: 'BUDGET_RATE_LIMIT',
63
- message: `Too many tool calls in phase ${params.phase}`,
64
- };
61
+ });
65
62
  }
66
63
  this.activeCallsByRisk[params.riskLevel]++;
67
64
  this.callCounts.set(params.phase, currentCount + 1);
@@ -71,10 +68,7 @@ export class BudgetGuard {
71
68
  // 4. Output Size Check (Preliminary)
72
69
  const size = this.estimateSize(result);
73
70
  if (size > params.maxOutputBytes) {
74
- throw {
75
- code: 'OUTPUT_TOO_LARGE',
76
- message: `Output size ${size} bytes exceeds limit of ${params.maxOutputBytes}`,
77
- };
71
+ throw Object.assign(new Error(`Output size ${size} bytes exceeds limit of ${params.maxOutputBytes}`), { code: 'OUTPUT_TOO_LARGE' });
78
72
  }
79
73
  return result;
80
74
  }
@@ -110,7 +104,8 @@ export class BudgetGuard {
110
104
  // This is expensive for large objects, but safe for checking limits
111
105
  return JSON.stringify(obj).length;
112
106
  }
113
- catch {
107
+ catch (error) {
108
+ getLogger().debug(`[Budget] Failed to estimate size: ${error instanceof Error ? error.message : String(error)}`);
114
109
  return 0; // Circular structure or otherwise un-stringifiable
115
110
  }
116
111
  }