@nexrall/code-core 1.4.66 → 1.4.68

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 (91) hide show
  1. package/dist/agent/agentRegistry.d.ts +27 -0
  2. package/dist/agent/agentRegistry.d.ts.map +1 -1
  3. package/dist/agent/agentRegistry.js +45 -0
  4. package/dist/agent/agentTypes.d.ts +6 -2
  5. package/dist/agent/agentTypes.d.ts.map +1 -1
  6. package/dist/agent/agentTypes.js +13 -4
  7. package/dist/agent/askOnce.d.ts +12 -0
  8. package/dist/agent/askOnce.d.ts.map +1 -0
  9. package/dist/agent/askOnce.js +41 -0
  10. package/dist/agent/compaction.d.ts +244 -0
  11. package/dist/agent/compaction.d.ts.map +1 -0
  12. package/dist/agent/compaction.js +976 -0
  13. package/dist/agent/fileLocks.d.ts +34 -0
  14. package/dist/agent/fileLocks.d.ts.map +1 -0
  15. package/dist/agent/fileLocks.js +114 -0
  16. package/dist/agent/hooks.d.ts +331 -0
  17. package/dist/agent/hooks.d.ts.map +1 -0
  18. package/dist/agent/hooks.js +1239 -0
  19. package/dist/agent/iterationPolicy.d.ts +121 -0
  20. package/dist/agent/iterationPolicy.d.ts.map +1 -0
  21. package/dist/agent/iterationPolicy.js +297 -0
  22. package/dist/agent/lifecycleHost.d.ts +55 -0
  23. package/dist/agent/lifecycleHost.d.ts.map +1 -0
  24. package/dist/agent/lifecycleHost.js +294 -0
  25. package/dist/agent/loop.d.ts +39 -491
  26. package/dist/agent/loop.d.ts.map +1 -1
  27. package/dist/agent/loop.js +538 -3069
  28. package/dist/agent/planMode.d.ts.map +1 -1
  29. package/dist/agent/planMode.js +1 -0
  30. package/dist/agent/sharedTasks.d.ts +7 -0
  31. package/dist/agent/sharedTasks.d.ts.map +1 -1
  32. package/dist/agent/sharedTasks.js +16 -0
  33. package/dist/agent/subAgentBudget.d.ts +65 -0
  34. package/dist/agent/subAgentBudget.d.ts.map +1 -0
  35. package/dist/agent/subAgentBudget.js +269 -0
  36. package/dist/agent/subTask.d.ts +15 -0
  37. package/dist/agent/subTask.d.ts.map +1 -0
  38. package/dist/agent/subTask.js +730 -0
  39. package/dist/agent/subTaskSupport.d.ts +167 -0
  40. package/dist/agent/subTaskSupport.d.ts.map +1 -0
  41. package/dist/agent/subTaskSupport.js +425 -0
  42. package/dist/agent/toolDescriptions.d.ts +3 -0
  43. package/dist/agent/toolDescriptions.d.ts.map +1 -0
  44. package/dist/agent/toolDescriptions.js +116 -0
  45. package/dist/api/client.d.ts +41 -0
  46. package/dist/api/client.d.ts.map +1 -1
  47. package/dist/api/client.js +173 -1
  48. package/dist/index.d.ts +5 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +5 -0
  51. package/dist/mcp/client.d.ts +104 -0
  52. package/dist/mcp/client.d.ts.map +1 -1
  53. package/dist/mcp/client.js +136 -2
  54. package/dist/mcp/httpClient.d.ts +17 -1
  55. package/dist/mcp/httpClient.d.ts.map +1 -1
  56. package/dist/mcp/httpClient.js +120 -19
  57. package/dist/mcp/manager.d.ts +77 -2
  58. package/dist/mcp/manager.d.ts.map +1 -1
  59. package/dist/mcp/manager.js +275 -9
  60. package/dist/mcp/server.d.ts +39 -0
  61. package/dist/mcp/server.d.ts.map +1 -0
  62. package/dist/mcp/server.js +189 -0
  63. package/dist/mcp/sseClient.d.ts +7 -1
  64. package/dist/mcp/sseClient.d.ts.map +1 -1
  65. package/dist/mcp/sseClient.js +45 -1
  66. package/dist/mcp/stats.d.ts +41 -0
  67. package/dist/mcp/stats.d.ts.map +1 -0
  68. package/dist/mcp/stats.js +108 -0
  69. package/dist/permissions/destructive.d.ts +2 -0
  70. package/dist/permissions/destructive.d.ts.map +1 -1
  71. package/dist/permissions/destructive.js +6 -2
  72. package/dist/permissions/destructiveTokens.d.ts +5 -0
  73. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  74. package/dist/permissions/destructiveTokens.js +9 -3
  75. package/dist/permissions/modePolicy.d.ts.map +1 -1
  76. package/dist/permissions/modePolicy.js +5 -2
  77. package/dist/permissions/rules.d.ts +4 -1
  78. package/dist/permissions/rules.d.ts.map +1 -1
  79. package/dist/permissions/rules.js +29 -0
  80. package/dist/plugins/data.d.ts +23 -0
  81. package/dist/plugins/data.d.ts.map +1 -0
  82. package/dist/plugins/data.js +141 -0
  83. package/dist/plugins/index.d.ts +14 -2
  84. package/dist/plugins/index.d.ts.map +1 -1
  85. package/dist/plugins/index.js +45 -4
  86. package/dist/plugins/installer.d.ts +83 -0
  87. package/dist/plugins/installer.d.ts.map +1 -1
  88. package/dist/plugins/installer.js +207 -1
  89. package/dist/types.d.ts +45 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +1 -1
@@ -0,0 +1,167 @@
1
+ import type { Message, ToolResult } from '../types';
2
+ /**
3
+ * Deepest nesting level allowed to spawn: env → settings.json → default 3.
4
+ *
5
+ * Was a hard-coded 1, i.e. "sub-agents are leaves". That was the right default while the
6
+ * three capability decisions disagreed with each other (see canSpawnSubAgents), but it is
7
+ * no longer where the ecosystem is: Claude Code lifted the no-nesting rule and, after a
8
+ * brief period with it disabled entirely, settled on a configurable default of 3.
9
+ *
10
+ * 3, not 5, deliberately. Depth is a budget you SPEND, not headroom you fill: every level
11
+ * is a context window that receives only a dispatch prompt on the way down and returns
12
+ * only a summary on the way up, so the deeper frames pay full freight to carry less
13
+ * information. 3 covers orchestrator → worker → helper, which is where the observed value
14
+ * is; beyond that latency and token cost tend to exceed the benefit.
15
+ *
16
+ * Depth 1 remains available (`maxSubagentDepth: 1`) for anyone who wants leaves-only.
17
+ */
18
+ export declare function resolveMaxSubagentDepth(settingsRaw?: Record<string, unknown>): number;
19
+ /**
20
+ * The ONE explanation for "this run may not spawn a sub-agent", shared by every place
21
+ * that can refuse it, so the same impossibility never gets two different stories.
22
+ *
23
+ * Parameterised on the REASON because the reasons are no longer interchangeable. It used
24
+ * to be a flat constant reading "Sub-agents cannot spawn further sub-agents — this is a
25
+ * structural limit"; with nesting configurable that sentence is now false for most runs,
26
+ * and telling a depth-1 agent its limit is structural when the user could raise it by one
27
+ * line of settings is the same class of misdirection as the "needs a different agent" text
28
+ * this replaced. A refusal has to be accurate about whether it can be lifted, or the model
29
+ * either gives up when it shouldn't or hunts for an escape that doesn't exist.
30
+ */
31
+ export declare function noSpawnReason(kind: 'depth' | 'denied', limit?: number): string;
32
+ /**
33
+ * Whether a run at `depth` may spawn sub-agents.
34
+ *
35
+ * The single source of truth for THREE things that must agree: the `<available_subagents>`
36
+ * catalogue in the system prompt, whether the backend is asked to send the `task` tool
37
+ * schema at all, and which prompt block teaches delegation. They used to be decided
38
+ * independently, and the result was a sub-agent that got the tool plus instructions to use
39
+ * it but no catalogue — then a permission-gate refusal telling it not to ask for approval.
40
+ *
41
+ * `depth < limit` because the children this run would create land at `depth + 1`; the
42
+ * guard inside runSubTask mirrors it as `depth >= limit`.
43
+ *
44
+ * `limit` is injected rather than read from module state so this stays pure and testable.
45
+ * Callers pass the resolved per-workspace value; it defaults to the built-in for the
46
+ * handful of call sites that have no settings in hand.
47
+ */
48
+ /**
49
+ * A child sub-agent's effective tool allowlist: its own, narrowed by its parent's.
50
+ *
51
+ * Exported and pure because it is a SECURITY boundary and was previously verified only by
52
+ * grepping the source for the intersection expression — which matched happily while the
53
+ * code threw a TypeError on one of its own four cases. A boundary needs behavioural tests.
54
+ *
55
+ * `null` means "no allowlist" (unrestricted), and it is returned only when BOTH sides say
56
+ * so. The four cases:
57
+ * own + parent → intersection (a child can narrow, never widen)
58
+ * own only → own (the main agent, which has no allowlist, spawning a specialist)
59
+ * parent only → a COPY of parent (an unnamed/general-purpose child inherits the
60
+ * restriction instead of resetting to full access — this is the
61
+ * escalation path, since `general-purpose` declares no tools at all)
62
+ * neither → null
63
+ *
64
+ * The parent-only case must COPY: the caller adds AGENT_MEMORY_TOOL to the returned set,
65
+ * which would otherwise mutate the parent's live allowlist.
66
+ */
67
+ export declare function intersectAllowlists(own: Set<string> | null, parent: Set<string> | undefined): Set<string> | null;
68
+ export declare function canSpawnSubAgents(depth: number, allowedTools?: Set<string>, limit?: number): boolean;
69
+ /**
70
+ * How long a sub-agent may make NO progress before it is stopped.
71
+ *
72
+ * Precedence matches every other tunable in this file
73
+ * (env → settings.json → default) — it used to be env-ONLY, which meant a project that
74
+ * legitimately needed longer sub-tasks had no way to say so in the file where every
75
+ * other such preference lives, and the limit was invisible to anyone reading settings.
76
+ *
77
+ * Exported for tests: the behaviour that matters (a long-but-productive sub-agent is
78
+ * NOT killed) takes minutes of wall clock to exercise through the real loop.
79
+ */
80
+ export declare function resolveSubtaskTimeoutMs(settingsRaw: Record<string, unknown>): number;
81
+ /**
82
+ * Thrown by a sub-agent's permission gate when the AGENT DEFINITION forbids a
83
+ * tool — as opposed to the user declining it.
84
+ *
85
+ * The distinction matters to the model, which is why this is an exception rather
86
+ * than a `false`: both used to collapse into "Permission denied by user", so an
87
+ * agent blocked by its own allowlist (very often a mistyped tool name) was told
88
+ * the human had refused. The rational response to that is to ask again, which
89
+ * can never succeed. Carrying a reason lets the tool_result say what is actually
90
+ * true and what to do instead.
91
+ */
92
+ export declare class ToolNotAllowedError extends Error {
93
+ constructor(message: string);
94
+ }
95
+ /**
96
+ * Reduce a sub-agent's message history to the text its parent should receive.
97
+ *
98
+ * Pure + exported so the salvage rules can be tested without running a real
99
+ * sub-agent (which needs a live model stream and, for the timeout path, ten
100
+ * minutes of wall clock).
101
+ *
102
+ * `preferLast` — the normal completion path — returns the final assistant
103
+ * message, which is the sub-agent's actual answer.
104
+ *
105
+ * `preferLast: false` is the SALVAGE path, used when the sub-agent was cut off.
106
+ * A stopped sub-agent usually has no closing summary at all (it was killed
107
+ * mid-tool-round), so the last assistant message is frequently empty or a
108
+ * fragment. Concatenating what it did produce is far more useful to the parent
109
+ * model than nothing: it can build on the work instead of redoing it.
110
+ */
111
+ export declare function extractSubTaskText(messages: Message[], preferLast?: boolean): string;
112
+ /** Apply the parent-context cap to a sub-task's text, keeping head + tail. */
113
+ export declare function capSubTaskText(text: string, max?: number): string;
114
+ /**
115
+ * Summarise what a cut-short sub-agent actually accomplished, so the parent model
116
+ * can continue from it rather than starting over.
117
+ *
118
+ * This is the whole point of the salvage path. Previously a timed-out sub-agent
119
+ * returned ONLY an error string: ten minutes of work, dozens of tool calls and
120
+ * any files it wrote were invisible to the parent, which typically responded by
121
+ * re-running the same work from scratch — while the tokens for the discarded run
122
+ * had already been billed in full.
123
+ */
124
+ export declare function summariseSubTaskProgress(messages: Message[]): string;
125
+ /**
126
+ * The last N tool results from a transcript, newest last, each truncated.
127
+ *
128
+ * Pure + exported so the salvage rules are testable without a real sub-agent.
129
+ *
130
+ * Truncation keeps the HEAD of each result: tool output is overwhelmingly
131
+ * front-loaded (a file starts with its imports, a failing command starts with its
132
+ * error), and a head slice is the half that identifies what was found.
133
+ */
134
+ export declare function lastToolResults(messages: Message[], count: number, maxChars: number): string;
135
+ export declare const HARD_STOPPED: unique symbol;
136
+ export declare function hardStopGraceMs(): number;
137
+ export declare function raceHardStop<T>(run: Promise<T>, abort: {
138
+ aborted: boolean;
139
+ }): Promise<T | typeof HARD_STOPPED>;
140
+ /** Returned instead of waiting forever for a tool that ignored Stop. */
141
+ export declare const STOP_TIMEOUT_RESULT: ToolResult;
142
+ export declare function stopAwaiting<T>(run: Promise<T>, abort?: {
143
+ aborted: boolean;
144
+ }): Promise<T | typeof STOP_TIMEOUT_RESULT>;
145
+ /** Default turn limit for a sub-agent whose definition sets no `maxTurns`. */
146
+ export declare const DEFAULT_SUBAGENT_MAX_TURNS = 200;
147
+ /** Stop reasons that mean the sub-agent did NOT deliver a finished report. */
148
+ export declare const ABNORMAL_SUBAGENT_STOPS: Set<string>;
149
+ /**
150
+ * Prepended to EVERY sub-agent's instructions (named or not) — Claude Code's
151
+ * "you are a sub-agent; your final message is the report" contract.
152
+ */
153
+ export declare const SUBAGENT_PREAMBLE: string;
154
+ /**
155
+ * The preamble for a FORKED sub-agent (`/subtask`): it was started by the USER from this
156
+ * conversation and inherits the whole transcript, so SUBAGENT_PREAMBLE's "you cannot see
157
+ * its conversation with the user — only the task you were given" would be a lie that
158
+ * makes it re-derive everything it can already read.
159
+ *
160
+ * What still holds is the part that made the original contract work: nobody is watching
161
+ * it live, nothing it writes is seen until it finishes, it cannot ask a question, and its
162
+ * final message is the entire result.
163
+ */
164
+ export declare const FORKED_SUBAGENT_PREAMBLE: string;
165
+ export declare function formatTokenCount(n: number): string;
166
+ export declare function formatElapsed(ms: number): string;
167
+ //# sourceMappingURL=subTaskSupport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subTaskSupport.d.ts","sourceRoot":"","sources":["../../src/agent/subTaskSupport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAmCpD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAOzF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAe9E;AAED;;;;;;;;;;;;;;;GAeG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EACvB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,GAC9B,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAIpB;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,KAAK,GAAE,MAA+B,GACrC,OAAO,CAMT;AAaD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAOpF;AAMD;;;;;;;;;;GAUG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAQD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,UAAU,UAAO,GAAG,MAAM,CAYjF;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAc,GAAG,MAAM,CAKtE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAgCpE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA6B5F;AAqBD,eAAO,MAAM,YAAY,eAAyB,CAAC;AACnD,wBAAgB,eAAe,IAAI,MAAM,CAGxC;AACD,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,CAAC,GAAG,OAAO,YAAY,CAAC,CAW9G;AAED,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,EAAE,UAGjC,CAAC;AAEF,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,CAAC,GAAG,OAAO,mBAAmB,CAAC,CAUtH;AAED,8EAA8E;AAC9E,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,aAMlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAIlB,CAAC;AAEb;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,QAKzB,CAAC;AAEb,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGhD"}
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FORKED_SUBAGENT_PREAMBLE = exports.SUBAGENT_PREAMBLE = exports.ABNORMAL_SUBAGENT_STOPS = exports.DEFAULT_SUBAGENT_MAX_TURNS = exports.STOP_TIMEOUT_RESULT = exports.HARD_STOPPED = exports.ToolNotAllowedError = void 0;
4
+ exports.resolveMaxSubagentDepth = resolveMaxSubagentDepth;
5
+ exports.noSpawnReason = noSpawnReason;
6
+ exports.intersectAllowlists = intersectAllowlists;
7
+ exports.canSpawnSubAgents = canSpawnSubAgents;
8
+ exports.resolveSubtaskTimeoutMs = resolveSubtaskTimeoutMs;
9
+ exports.extractSubTaskText = extractSubTaskText;
10
+ exports.capSubTaskText = capSubTaskText;
11
+ exports.summariseSubTaskProgress = summariseSubTaskProgress;
12
+ exports.lastToolResults = lastToolResults;
13
+ exports.hardStopGraceMs = hardStopGraceMs;
14
+ exports.raceHardStop = raceHardStop;
15
+ exports.stopAwaiting = stopAwaiting;
16
+ exports.formatTokenCount = formatTokenCount;
17
+ exports.formatElapsed = formatElapsed;
18
+ const safeSlice_1 = require("../util/safeSlice");
19
+ // ─── Sub-task runner ──────────────────────────────────────────────────────────
20
+ // Nesting depth. The main agent runs at depth 0, the sub-agents it spawns at depth 1.
21
+ //
22
+ // A run at `depth` may spawn when `depth < limit`, so a limit of N means N generations
23
+ // below the main agent. This used to be a hard-coded 1 with the historical note "This is
24
+ // 1, not 2" — that note was about making the CONSTANT agree with a system prompt that
25
+ // promised "sub-agents cannot spawn further sub-agents". Both sides of that agreement have
26
+ // now moved: nesting is a supported, configured capability, and the prompt is generated
27
+ // from the same predicate that enforces it (canSpawnSubAgents), so the two cannot drift
28
+ // again regardless of the value.
29
+ //
30
+ // What made the old value load-bearing was that fan-out had no TOTAL bound — only a
31
+ // per-moment concurrency gate. Depth × fan-out is multiplicative, so raising depth without
32
+ // a session ceiling converts a bounded queue into an unbounded tree. That ceiling
33
+ // (resolveMaxSubagentsPerSession) is what makes a depth > 1 safe to default to.
34
+ const DEFAULT_MAX_TASK_DEPTH = 3;
35
+ /**
36
+ * Hard ceiling on `maxSubagentDepth`, independent of what anyone configures.
37
+ *
38
+ * 5 matches the deepest tier Anthropic shipped for Claude Code. It exists because depth
39
+ * is MULTIPLICATIVE with fan-out: at 4-wide, depth 5 is 4^5 = 1024 possible loops. The
40
+ * per-depth limiter and the session ceiling below are what actually bound that, but a
41
+ * typo'd `maxSubagentDepth: 50` should degrade to "deep" rather than to a fork bomb —
42
+ * same reasoning as the clamp on maxConcurrentSubtasks.
43
+ */
44
+ const HARD_MAX_TASK_DEPTH = 5;
45
+ /**
46
+ * Deepest nesting level allowed to spawn: env → settings.json → default 3.
47
+ *
48
+ * Was a hard-coded 1, i.e. "sub-agents are leaves". That was the right default while the
49
+ * three capability decisions disagreed with each other (see canSpawnSubAgents), but it is
50
+ * no longer where the ecosystem is: Claude Code lifted the no-nesting rule and, after a
51
+ * brief period with it disabled entirely, settled on a configurable default of 3.
52
+ *
53
+ * 3, not 5, deliberately. Depth is a budget you SPEND, not headroom you fill: every level
54
+ * is a context window that receives only a dispatch prompt on the way down and returns
55
+ * only a summary on the way up, so the deeper frames pay full freight to carry less
56
+ * information. 3 covers orchestrator → worker → helper, which is where the observed value
57
+ * is; beyond that latency and token cost tend to exceed the benefit.
58
+ *
59
+ * Depth 1 remains available (`maxSubagentDepth: 1`) for anyone who wants leaves-only.
60
+ */
61
+ function resolveMaxSubagentDepth(settingsRaw = {}) {
62
+ const clamp = (n) => Math.max(1, Math.min(Math.floor(n), HARD_MAX_TASK_DEPTH));
63
+ const fromEnv = Number(process.env.NEXRALL_MAX_SUBAGENT_DEPTH);
64
+ if (Number.isFinite(fromEnv) && fromEnv > 0)
65
+ return clamp(fromEnv);
66
+ const fromSettings = Number(settingsRaw.maxSubagentDepth);
67
+ if (Number.isFinite(fromSettings) && fromSettings > 0)
68
+ return clamp(fromSettings);
69
+ return DEFAULT_MAX_TASK_DEPTH;
70
+ }
71
+ /**
72
+ * The ONE explanation for "this run may not spawn a sub-agent", shared by every place
73
+ * that can refuse it, so the same impossibility never gets two different stories.
74
+ *
75
+ * Parameterised on the REASON because the reasons are no longer interchangeable. It used
76
+ * to be a flat constant reading "Sub-agents cannot spawn further sub-agents — this is a
77
+ * structural limit"; with nesting configurable that sentence is now false for most runs,
78
+ * and telling a depth-1 agent its limit is structural when the user could raise it by one
79
+ * line of settings is the same class of misdirection as the "needs a different agent" text
80
+ * this replaced. A refusal has to be accurate about whether it can be lifted, or the model
81
+ * either gives up when it shouldn't or hunts for an escape that doesn't exist.
82
+ */
83
+ function noSpawnReason(kind, limit) {
84
+ if (kind === 'denied') {
85
+ return ('This sub-agent\'s definition does not grant `task`, so it may not delegate. That is a ' +
86
+ 'deliberate restriction on this agent type — do not ask for approval and do not look for ' +
87
+ 'a way around it. Do the work with the tools you have, or report back what is missing.');
88
+ }
89
+ return (`Maximum sub-agent nesting depth (${limit ?? DEFAULT_MAX_TASK_DEPTH}) reached, so this run ` +
90
+ 'is a leaf and cannot delegate further. Depth is a budget, not a bug: finish this work ' +
91
+ 'yourself, or report back so a shallower frame can decide. (The ceiling is ' +
92
+ '"maxSubagentDepth" in .nexrall/settings.json, but raising it mid-task will not help you — ' +
93
+ 'it applies from the next session.)');
94
+ }
95
+ /**
96
+ * Whether a run at `depth` may spawn sub-agents.
97
+ *
98
+ * The single source of truth for THREE things that must agree: the `<available_subagents>`
99
+ * catalogue in the system prompt, whether the backend is asked to send the `task` tool
100
+ * schema at all, and which prompt block teaches delegation. They used to be decided
101
+ * independently, and the result was a sub-agent that got the tool plus instructions to use
102
+ * it but no catalogue — then a permission-gate refusal telling it not to ask for approval.
103
+ *
104
+ * `depth < limit` because the children this run would create land at `depth + 1`; the
105
+ * guard inside runSubTask mirrors it as `depth >= limit`.
106
+ *
107
+ * `limit` is injected rather than read from module state so this stays pure and testable.
108
+ * Callers pass the resolved per-workspace value; it defaults to the built-in for the
109
+ * handful of call sites that have no settings in hand.
110
+ */
111
+ /**
112
+ * A child sub-agent's effective tool allowlist: its own, narrowed by its parent's.
113
+ *
114
+ * Exported and pure because it is a SECURITY boundary and was previously verified only by
115
+ * grepping the source for the intersection expression — which matched happily while the
116
+ * code threw a TypeError on one of its own four cases. A boundary needs behavioural tests.
117
+ *
118
+ * `null` means "no allowlist" (unrestricted), and it is returned only when BOTH sides say
119
+ * so. The four cases:
120
+ * own + parent → intersection (a child can narrow, never widen)
121
+ * own only → own (the main agent, which has no allowlist, spawning a specialist)
122
+ * parent only → a COPY of parent (an unnamed/general-purpose child inherits the
123
+ * restriction instead of resetting to full access — this is the
124
+ * escalation path, since `general-purpose` declares no tools at all)
125
+ * neither → null
126
+ *
127
+ * The parent-only case must COPY: the caller adds AGENT_MEMORY_TOOL to the returned set,
128
+ * which would otherwise mutate the parent's live allowlist.
129
+ */
130
+ function intersectAllowlists(own, parent) {
131
+ if (own && parent)
132
+ return new Set([...own].filter((t) => parent.has(t)));
133
+ if (own)
134
+ return own;
135
+ return parent ? new Set(parent) : null;
136
+ }
137
+ function canSpawnSubAgents(depth, allowedTools, limit = DEFAULT_MAX_TASK_DEPTH) {
138
+ if (depth >= limit)
139
+ return false;
140
+ // An allowlist that omits `task` is the other reason a run cannot delegate. No
141
+ // allowlist at all (the main agent, or a general-purpose sub-task) means no
142
+ // restriction from this clause — the depth check above still applies.
143
+ return allowedTools ? allowedTools.has('task') : true;
144
+ }
145
+ // A sub-agent that stalls (hung tool, model provider stuck, infinite tool-call
146
+ // loop bypassing the iteration budget somehow) used to have NO ceiling of its
147
+ // own — it shared only the PARENT's overall iteration budget, so a genuinely
148
+ // stuck sub-agent could silently occupy the whole run with no distinct signal
149
+ // pointing at it specifically. Give every sub-task an explicit wall-clock cap:
150
+ // if it hasn't finished by then, fail it clearly instead of hanging the parent
151
+ // turn indefinitely. Overridable via env for slow CI machines / huge sub-tasks.
152
+ const DEFAULT_SUBTASK_TIMEOUT_MS = 10 * 60 * 1000; // 10 min of NO PROGRESS (see the watchdog)
153
+ /**
154
+ * How long a sub-agent may make NO progress before it is stopped.
155
+ *
156
+ * Precedence matches every other tunable in this file
157
+ * (env → settings.json → default) — it used to be env-ONLY, which meant a project that
158
+ * legitimately needed longer sub-tasks had no way to say so in the file where every
159
+ * other such preference lives, and the limit was invisible to anyone reading settings.
160
+ *
161
+ * Exported for tests: the behaviour that matters (a long-but-productive sub-agent is
162
+ * NOT killed) takes minutes of wall clock to exercise through the real loop.
163
+ */
164
+ function resolveSubtaskTimeoutMs(settingsRaw) {
165
+ const fromEnv = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS);
166
+ if (Number.isFinite(fromEnv) && fromEnv > 0)
167
+ return Math.floor(fromEnv);
168
+ const raw = settingsRaw.subtaskTimeoutMs;
169
+ const fromSettings = Number(raw);
170
+ if (Number.isFinite(fromSettings) && fromSettings > 0)
171
+ return Math.floor(fromSettings);
172
+ return DEFAULT_SUBTASK_TIMEOUT_MS;
173
+ }
174
+ /** Cap on the text a sub-task hands back, so one verbose sub-agent can't blow up
175
+ * the PARENT's context in a single tool_result. */
176
+ const SUBTASK_MAX = 48000; // chars (~12k tokens)
177
+ /**
178
+ * Thrown by a sub-agent's permission gate when the AGENT DEFINITION forbids a
179
+ * tool — as opposed to the user declining it.
180
+ *
181
+ * The distinction matters to the model, which is why this is an exception rather
182
+ * than a `false`: both used to collapse into "Permission denied by user", so an
183
+ * agent blocked by its own allowlist (very often a mistyped tool name) was told
184
+ * the human had refused. The rational response to that is to ask again, which
185
+ * can never succeed. Carrying a reason lets the tool_result say what is actually
186
+ * true and what to do instead.
187
+ */
188
+ class ToolNotAllowedError extends Error {
189
+ constructor(message) {
190
+ super(message);
191
+ this.name = 'ToolNotAllowedError';
192
+ }
193
+ }
194
+ exports.ToolNotAllowedError = ToolNotAllowedError;
195
+ // sliceSafeEnd/sliceSafeStart moved to ../util/safeSlice so every module that
196
+ // truncates model-facing/wire-facing text (loop.ts and tools/executor.ts) shares
197
+ // ONE surrogate-safe implementation instead of drifting copies. See that file's
198
+ // header for why raw `.slice()` on these strings caused a 400
199
+ // "no low surrogate in string" from the Anthropic API.
200
+ /**
201
+ * Reduce a sub-agent's message history to the text its parent should receive.
202
+ *
203
+ * Pure + exported so the salvage rules can be tested without running a real
204
+ * sub-agent (which needs a live model stream and, for the timeout path, ten
205
+ * minutes of wall clock).
206
+ *
207
+ * `preferLast` — the normal completion path — returns the final assistant
208
+ * message, which is the sub-agent's actual answer.
209
+ *
210
+ * `preferLast: false` is the SALVAGE path, used when the sub-agent was cut off.
211
+ * A stopped sub-agent usually has no closing summary at all (it was killed
212
+ * mid-tool-round), so the last assistant message is frequently empty or a
213
+ * fragment. Concatenating what it did produce is far more useful to the parent
214
+ * model than nothing: it can build on the work instead of redoing it.
215
+ */
216
+ function extractSubTaskText(messages, preferLast = true) {
217
+ const assistants = messages.filter((m) => m.role === 'assistant');
218
+ const textOf = (m) => (m?.content ?? [])
219
+ .filter((b) => b.type === 'text' && typeof b.text === 'string')
220
+ .map((b) => b.text)
221
+ .join('')
222
+ .trim();
223
+ if (preferLast)
224
+ return textOf(assistants[assistants.length - 1]);
225
+ return assistants.map(textOf).filter(Boolean).join('\n\n').trim();
226
+ }
227
+ /** Apply the parent-context cap to a sub-task's text, keeping head + tail. */
228
+ function capSubTaskText(text, max = SUBTASK_MAX) {
229
+ if (text.length <= max)
230
+ return text;
231
+ const head = (0, safeSlice_1.sliceSafeEnd)(text, Math.floor(max * 0.6));
232
+ const tail = (0, safeSlice_1.sliceSafeStart)(text, text.length - Math.floor(max * 0.4));
233
+ return `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
234
+ }
235
+ /**
236
+ * Summarise what a cut-short sub-agent actually accomplished, so the parent model
237
+ * can continue from it rather than starting over.
238
+ *
239
+ * This is the whole point of the salvage path. Previously a timed-out sub-agent
240
+ * returned ONLY an error string: ten minutes of work, dozens of tool calls and
241
+ * any files it wrote were invisible to the parent, which typically responded by
242
+ * re-running the same work from scratch — while the tokens for the discarded run
243
+ * had already been billed in full.
244
+ */
245
+ function summariseSubTaskProgress(messages) {
246
+ const toolNames = [];
247
+ for (const m of messages) {
248
+ if (m.role !== 'assistant' || !Array.isArray(m.content))
249
+ continue;
250
+ for (const b of m.content) {
251
+ if (b?.type === 'tool_use' && typeof b.name === 'string')
252
+ toolNames.push(b.name);
253
+ }
254
+ }
255
+ if (toolNames.length === 0)
256
+ return '';
257
+ // The FINDINGS, not just the activity log.
258
+ //
259
+ // An inventory of tool names ("read_file ×9, bash ×14") tells the parent that work
260
+ // happened but nothing about what was learned, so it re-derives everything. A stalled
261
+ // research agent's value is almost entirely in what its last few tool calls RETURNED
262
+ // — the file it had just read, the command output it was about to interpret — because
263
+ // its own prose summary is exactly the thing it never got to write.
264
+ const recentFindings = lastToolResults(messages, SALVAGE_RESULT_COUNT, SALVAGE_RESULT_CHARS);
265
+ // Collapse to "name ×N" so a 40-call run reads as a short inventory rather
266
+ // than forty repeated lines of the same tool name.
267
+ const counts = new Map();
268
+ for (const n of toolNames)
269
+ counts.set(n, (counts.get(n) ?? 0) + 1);
270
+ const inventory = [...counts.entries()]
271
+ .sort((a, b) => b[1] - a[1])
272
+ .map(([name, n]) => (n > 1 ? `${name} ×${n}` : name))
273
+ .join(', ');
274
+ const header = `Tool calls completed before it was stopped (${toolNames.length} total): ${inventory}.`;
275
+ return recentFindings
276
+ ? `${header}\n\nWhat its most recent tool calls actually returned (use this instead of repeating them):\n${recentFindings}`
277
+ : header;
278
+ }
279
+ /** How many trailing tool results to salvage, and how much of each. */
280
+ const SALVAGE_RESULT_COUNT = 4;
281
+ const SALVAGE_RESULT_CHARS = 2000;
282
+ /**
283
+ * The last N tool results from a transcript, newest last, each truncated.
284
+ *
285
+ * Pure + exported so the salvage rules are testable without a real sub-agent.
286
+ *
287
+ * Truncation keeps the HEAD of each result: tool output is overwhelmingly
288
+ * front-loaded (a file starts with its imports, a failing command starts with its
289
+ * error), and a head slice is the half that identifies what was found.
290
+ */
291
+ function lastToolResults(messages, count, maxChars) {
292
+ // Map tool_use id → tool name, so a salvaged result can say WHICH tool produced it.
293
+ const nameById = new Map();
294
+ for (const m of messages) {
295
+ if (m.role !== 'assistant' || !Array.isArray(m.content))
296
+ continue;
297
+ for (const b of m.content) {
298
+ if (b?.type === 'tool_use' && b.id && typeof b.name === 'string')
299
+ nameById.set(b.id, b.name);
300
+ }
301
+ }
302
+ const out = [];
303
+ // Walk backwards and stop early: only the most recent results are worth the tokens,
304
+ // and a long research run may hold hundreds.
305
+ for (let i = messages.length - 1; i >= 0 && out.length < count; i--) {
306
+ const m = messages[i];
307
+ if (m.role !== 'user' || !Array.isArray(m.content))
308
+ continue;
309
+ for (const b of [...m.content].reverse()) {
310
+ if (out.length >= count)
311
+ break;
312
+ if (b?.type !== 'tool_result')
313
+ continue;
314
+ const text = toolResultText(b);
315
+ if (!text)
316
+ continue;
317
+ const name = nameById.get(String(b.tool_use_id ?? '')) ?? 'tool';
318
+ const body = text.length > maxChars
319
+ ? `${(0, safeSlice_1.sliceSafeEnd)(text, maxChars)}\n… [truncated]`
320
+ : text;
321
+ out.push(`• ${name}:\n${body}`);
322
+ }
323
+ }
324
+ return out.reverse().join('\n\n');
325
+ }
326
+ /** Extract readable text from a tool_result block, whose content may be string or blocks. */
327
+ function toolResultText(block) {
328
+ const c = block.content;
329
+ if (typeof c === 'string')
330
+ return c.trim();
331
+ if (Array.isArray(c)) {
332
+ return c
333
+ .filter((x) => x?.type === 'text' && typeof x.text === 'string')
334
+ .map((x) => x.text)
335
+ .join('\n')
336
+ .trim();
337
+ }
338
+ return '';
339
+ }
340
+ // ── Hard stop ─────────────────────────────────────────────────────────────────
341
+ // The stall watchdog and the parent's Stop only SET a flag; runAgentLoop notices it at
342
+ // the next boundary. A tool that never returns (an MCP server that ignores the abort,
343
+ // a stuck editor-side call) meant that boundary never came and the parent hung forever.
344
+ // After the flag is set, the child gets this long to wind down before we stop waiting.
345
+ exports.HARD_STOPPED = Symbol('hard-stopped');
346
+ function hardStopGraceMs() {
347
+ const v = Number(process.env.NEXRALL_SUBAGENT_HARD_STOP_MS);
348
+ return Number.isFinite(v) && v > 0 ? v : 30000;
349
+ }
350
+ function raceHardStop(run, abort) {
351
+ return new Promise((resolve, reject) => {
352
+ let grace = null;
353
+ const poll = setInterval(() => {
354
+ if (abort.aborted && !grace) {
355
+ grace = setTimeout(() => { clearInterval(poll); resolve(exports.HARD_STOPPED); }, hardStopGraceMs());
356
+ }
357
+ }, 250);
358
+ const settle = () => { clearInterval(poll); if (grace)
359
+ clearTimeout(grace); };
360
+ run.then((v) => { settle(); resolve(v); }, (e) => { settle(); reject(e); });
361
+ });
362
+ }
363
+ /** Returned instead of waiting forever for a tool that ignored Stop. */
364
+ exports.STOP_TIMEOUT_RESULT = {
365
+ error: 'Interrupted: this tool did not respond to Stop, so the agent stopped waiting for it.',
366
+ interrupted: true,
367
+ };
368
+ const STOP_GRACE_MS = 5000;
369
+ function stopAwaiting(run, abort) {
370
+ if (!abort)
371
+ return run;
372
+ return new Promise((resolve, reject) => {
373
+ let grace = null;
374
+ const poll = setInterval(() => {
375
+ if (abort.aborted && !grace)
376
+ grace = setTimeout(() => { clearInterval(poll); resolve(exports.STOP_TIMEOUT_RESULT); }, STOP_GRACE_MS);
377
+ }, 200);
378
+ const settle = () => { clearInterval(poll); if (grace)
379
+ clearTimeout(grace); };
380
+ run.then((v) => { settle(); resolve(v); }, (e) => { settle(); reject(e); });
381
+ });
382
+ }
383
+ /** Default turn limit for a sub-agent whose definition sets no `maxTurns`. */
384
+ exports.DEFAULT_SUBAGENT_MAX_TURNS = 200;
385
+ /** Stop reasons that mean the sub-agent did NOT deliver a finished report. */
386
+ exports.ABNORMAL_SUBAGENT_STOPS = new Set([
387
+ 'budget', 'stalled', 'stalled-repeat', 'output-limit', 'empty-response',
388
+ 'no-balance', 'no-team-budget', 'team-unavailable', 'reported-elsewhere',
389
+ // A hook stopping the batch means the sub-agent stopped short of its report, so the
390
+ // parent must not read the result as a finished one.
391
+ 'hook-blocked',
392
+ ]);
393
+ /**
394
+ * Prepended to EVERY sub-agent's instructions (named or not) — Claude Code's
395
+ * "you are a sub-agent; your final message is the report" contract.
396
+ */
397
+ exports.SUBAGENT_PREAMBLE = [
398
+ '# You are a sub-agent',
399
+ 'The main agent started you for ONE delegated task. You cannot see its conversation with the user — only the task you were given — and you cannot ask the user anything: if something is ambiguous, make the most reasonable assumption and state it.',
400
+ 'Your FINAL message is the only thing returned to the main agent, and the user does not see it directly. Make it a concise, self-contained report: what you found or changed (with file paths and line numbers), what you verified and how, and anything left undone or uncertain. Do not pad it, and do not paste whole files — cite locations instead.',
401
+ ].join('\n');
402
+ /**
403
+ * The preamble for a FORKED sub-agent (`/subtask`): it was started by the USER from this
404
+ * conversation and inherits the whole transcript, so SUBAGENT_PREAMBLE's "you cannot see
405
+ * its conversation with the user — only the task you were given" would be a lie that
406
+ * makes it re-derive everything it can already read.
407
+ *
408
+ * What still holds is the part that made the original contract work: nobody is watching
409
+ * it live, nothing it writes is seen until it finishes, it cannot ask a question, and its
410
+ * final message is the entire result.
411
+ */
412
+ exports.FORKED_SUBAGENT_PREAMBLE = [
413
+ '# You are a forked sub-agent',
414
+ 'You were forked from this conversation and CAN read the whole transcript above — the request, the files already read, the conclusions already reached, the work already done. Your fork is INDEPENDENT: the main agent is not watching you work, no one can answer a question mid-run, and nothing you write is seen until you finish.',
415
+ 'Carry the task through to the end on your own: read what else you need, make the changes the task calls for, and run the checks that prove them. If something is genuinely ambiguous, choose the reading that best fits the conversation, say which reading you chose, and continue.',
416
+ 'Your FINAL message is the only thing that comes back. Make it a concise, self-contained report: what you changed or found (file paths and line numbers), what you verified and how, and anything left undone or uncertain. Do not replay the transcript and do not paste whole files — cite locations instead. If you hit a limit you cannot work around (a permission you could not be granted, the turn budget), say exactly what was left.',
417
+ ].join('\n');
418
+ function formatTokenCount(n) {
419
+ return n >= 1000000 ? `${(n / 1000000).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
420
+ }
421
+ function formatElapsed(ms) {
422
+ const s = Math.round(ms / 1000);
423
+ return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
424
+ }
425
+ //# sourceMappingURL=subTaskSupport.js.map
@@ -0,0 +1,3 @@
1
+ export declare function renderPeerMessages(msgs: import('./peerTransport').PeerMessage[]): string;
2
+ export declare function humanDescription(name: string, input: Record<string, unknown>): string;
3
+ //# sourceMappingURL=toolDescriptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolDescriptions.d.ts","sourceRoot":"","sources":["../../src/agent/toolDescriptions.ts"],"names":[],"mappings":"AASA,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,iBAAiB,EAAE,WAAW,EAAE,GAAG,MAAM,CAMxF;AAID,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CA0HrF"}