@hncode/hncode 0.5.1 → 0.5.4
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/bin/install.cjs +24 -44
- package/bin/preinstall.cjs +0 -14
- package/package.json +2 -2
- package/src/agent-task.js +12 -1
- package/src/agent.js +279 -48
- package/src/cache.js +175 -130
- package/src/cell-buffer.js +5 -0
- package/src/clipboard.js +111 -8
- package/src/context-bar.js +46 -5
- package/src/doctor.js +7 -2
- package/src/git.js +35 -4
- package/src/index.js +29 -5
- package/src/keybindings.js +25 -3
- package/src/llm.js +60 -6
- package/src/mcp.js +419 -384
- package/src/permissions.js +29 -7
- package/src/plugin.js +95 -10
- package/src/registry-browser.js +21 -6
- package/src/remote.js +307 -289
- package/src/session-core.js +203 -0
- package/src/session-host.js +213 -0
- package/src/session-view.js +110 -0
- package/src/settings-schema.js +12 -9
- package/src/shell-safety.js +610 -473
- package/src/swarm-progress.js +348 -293
- package/src/tasks-browser.js +4 -1
- package/src/team/constants.js +20 -0
- package/src/team/context.js +89 -0
- package/src/team/mailbox.js +216 -0
- package/src/team/permission.js +121 -0
- package/src/team/protocol.js +167 -0
- package/src/team/runner.js +462 -0
- package/src/team/spawn.js +305 -0
- package/src/team/store.js +172 -0
- package/src/team/tasks.js +177 -0
- package/src/team/team.js +346 -0
- package/src/team-panel.js +624 -0
- package/src/term-caps.js +457 -410
- package/src/term.js +162 -19
- package/src/tool-result.js +70 -7
- package/src/tools/agent-swarm.js +114 -11
- package/src/tools/agent.js +464 -386
- package/src/tools/bash.js +53 -14
- package/src/tools/diagnostics.js +13 -0
- package/src/tools/edit.js +428 -50
- package/src/tools/git.js +15 -3
- package/src/tools/glob.js +5 -3
- package/src/tools/grep.js +19 -5
- package/src/tools/ignore.js +17 -3
- package/src/tools/index.js +19 -3
- package/src/tools/read.js +2 -1
- package/src/tools/shell.js +145 -134
- package/src/tools/tasks.js +214 -141
- package/src/tools/team.js +411 -0
- package/src/tools/utils.js +12 -3
- package/src/tools/web-search.js +53 -40
- package/src/tui.js +7496 -5366
- package/src/update.js +320 -115
- package/src/web-daemon.js +27 -4
- package/src/web-live.js +250 -0
package/src/tools/agent.js
CHANGED
|
@@ -1,387 +1,465 @@
|
|
|
1
|
-
// Agent tool — launch a subagent to handle a task, mirroring kimi-code's Agent.
|
|
2
|
-
//
|
|
3
|
-
// A subagent is a full Agent instance (same toolbelt, same protocol) with its OWN
|
|
4
|
-
// message list. The point is context isolation: the parent gets a conclusion back
|
|
5
|
-
// instead of a pile of file dumps.
|
|
6
|
-
//
|
|
7
|
-
// Supports, like kimi:
|
|
8
|
-
// prompt / description the task
|
|
9
|
-
// subagent_type profile name (we keep a small catalog)
|
|
10
|
-
// resume continue an earlier subagent by agent id
|
|
11
|
-
// run_in_background start a background task and return its id
|
|
12
|
-
// fork start from a snapshot of THIS conversation
|
|
13
|
-
// model run on another configured model
|
|
14
|
-
//
|
|
15
|
-
// The result is returned as a plain string. A subagent's output is only visible
|
|
16
|
-
// to the CALLER, never rendered to the user directly — the parent is expected to
|
|
17
|
-
// summarise it.
|
|
18
|
-
|
|
19
|
-
import { createTask, appendTaskOutput, settleTask } from '../agent-task.js';
|
|
20
|
-
import { registerRun, endRun } from '../subagent-control.js';
|
|
21
|
-
import { SUBAGENT_TYPES, DEFAULT_SUBAGENT_TYPE, availableTypes, toolsForSubagent } from '../subagent-types.js';
|
|
22
|
-
import { runShellHooks } from '../hooks.js';
|
|
23
|
-
|
|
24
|
-
// Live subagent registry, keyed by agent id. A resumed agent keeps its own
|
|
25
|
-
// message list here, which is what makes `resume` work.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ----
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
1
|
+
// Agent tool — launch a subagent to handle a task, mirroring kimi-code's Agent.
|
|
2
|
+
//
|
|
3
|
+
// A subagent is a full Agent instance (same toolbelt, same protocol) with its OWN
|
|
4
|
+
// message list. The point is context isolation: the parent gets a conclusion back
|
|
5
|
+
// instead of a pile of file dumps.
|
|
6
|
+
//
|
|
7
|
+
// Supports, like kimi:
|
|
8
|
+
// prompt / description the task
|
|
9
|
+
// subagent_type profile name (we keep a small catalog)
|
|
10
|
+
// resume continue an earlier subagent by agent id
|
|
11
|
+
// run_in_background start a background task and return its id
|
|
12
|
+
// fork start from a snapshot of THIS conversation
|
|
13
|
+
// model run on another configured model
|
|
14
|
+
//
|
|
15
|
+
// The result is returned as a plain string. A subagent's output is only visible
|
|
16
|
+
// to the CALLER, never rendered to the user directly — the parent is expected to
|
|
17
|
+
// summarise it.
|
|
18
|
+
|
|
19
|
+
import { createTask, appendTaskOutput, settleTask } from '../agent-task.js';
|
|
20
|
+
import { registerRun, endRun } from '../subagent-control.js';
|
|
21
|
+
import { SUBAGENT_TYPES, DEFAULT_SUBAGENT_TYPE, availableTypes, toolsForSubagent } from '../subagent-types.js';
|
|
22
|
+
import { runShellHooks } from '../hooks.js';
|
|
23
|
+
|
|
24
|
+
// Live subagent registry, keyed by agent id. A resumed agent keeps its own
|
|
25
|
+
// message list here, which is what makes `resume` work.
|
|
26
|
+
//
|
|
27
|
+
// Bounded: a 128-item swarm made 128 entries holding FULL transcripts, kept for
|
|
28
|
+
// the process lifetime. The oldest entry is evicted past the cap — resuming a
|
|
29
|
+
// very old subagent then reports it as unknown instead of silently holding every
|
|
30
|
+
// transcript forever.
|
|
31
|
+
const SUBAGENTS_MAX = 64;
|
|
32
|
+
const subagents = new Map();
|
|
33
|
+
function rememberSubagent(id, entry) {
|
|
34
|
+
subagents.delete(id); // re-insertion moves the entry to the newest slot
|
|
35
|
+
subagents.set(id, entry);
|
|
36
|
+
while (subagents.size > SUBAGENTS_MAX) {
|
|
37
|
+
subagents.delete(subagents.keys().next().value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let agentSeq = 0;
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export function newAgentId() {
|
|
44
|
+
agentSeq = (agentSeq + 1) % 1e6;
|
|
45
|
+
return `agent-${Date.now().toString(36)}${String(agentSeq).padStart(3, '0')}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getSubagent(id) {
|
|
49
|
+
return subagents.get(id) || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function listSubagents() {
|
|
53
|
+
return [...subagents.values()];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build the system prompt for a subagent: its profile prompt plus the shared
|
|
57
|
+
// rules, so a subagent behaves like the main agent within its remit.
|
|
58
|
+
function systemPromptFor(type, cfg, systemPrompt) {
|
|
59
|
+
const profile = SUBAGENT_TYPES[type] || SUBAGENT_TYPES[DEFAULT_SUBAGENT_TYPE];
|
|
60
|
+
const base = (cfg.systemPrompt && String(cfg.systemPrompt).trim()) || systemPrompt;
|
|
61
|
+
return `${profile.prompt}\n\n${base}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Run a subagent to completion. `history` seeds its messages (fork). Returns
|
|
65
|
+
// { text, usage } or { error }.
|
|
66
|
+
// `onProgress(text)` receives the subagent's live output (its reasoning and answer
|
|
67
|
+
// stream, and each tool it runs) so the TUI can show it under the `Using Agent`
|
|
68
|
+
// row the way Bash's output is shown. `agentId` labels the lines when several
|
|
69
|
+
// subagents run at once (AgentSwarm), so the reader can tell them apart.
|
|
70
|
+
async function runSubagent({ cfg, type, prompt, history, signal, onEvent, onProgress, onStep, agentId, onAgent }) {
|
|
71
|
+
// The parent-signal listener (registered below, removed when the run settles).
|
|
72
|
+
let onSubagentAbort = null;
|
|
73
|
+
const { Agent, SYSTEM_PROMPT } = await import('../agent.js');
|
|
74
|
+
const subCfg = {
|
|
75
|
+
...cfg,
|
|
76
|
+
systemPrompt: systemPromptFor(type, cfg, SYSTEM_PROMPT),
|
|
77
|
+
// The profile narrows the toolbelt (explore / plan / researcher are read-only)
|
|
78
|
+
// and the delegation tools are removed unconditionally — a subagent must not
|
|
79
|
+
// start another agent. `noDelegation` is the hard backstop in agent.js for the
|
|
80
|
+
// case where the model emits a call for a tool it no longer has.
|
|
81
|
+
toolFilter: toolsForSubagent(type, cfg.toolFilter),
|
|
82
|
+
noDelegation: true,
|
|
83
|
+
};
|
|
84
|
+
// fork: the snapshot comes first, then the new instruction.
|
|
85
|
+
const messages = Array.isArray(history) && history.length
|
|
86
|
+
? [...history, { role: 'user', content: prompt }]
|
|
87
|
+
: [{ role: 'user', content: prompt }];
|
|
88
|
+
|
|
89
|
+
let text = '';
|
|
90
|
+
// A failure used to leave `text` empty, so a caller could not tell "produced nothing" from
|
|
91
|
+
// "died". The Agent's only signal on error is an event, so capture it here.
|
|
92
|
+
let failure = null;
|
|
93
|
+
const agent = new Agent({
|
|
94
|
+
cfg: subCfg,
|
|
95
|
+
messages,
|
|
96
|
+
onEvent: (e) => {
|
|
97
|
+
if (e.type === 'data') text += e.text;
|
|
98
|
+
// Mirror what the subagent is actually doing into the parent's transcript:
|
|
99
|
+
// its reasoning and answer stream as `data`/`think`, and every tool it runs
|
|
100
|
+
// arrives as tool_start/tool_result. Without this the `Using Agent …` row sat
|
|
101
|
+
// silent for the whole run — the subagent looked hung.
|
|
102
|
+
if (onProgress) {
|
|
103
|
+
const tag = agentId ? `[${agentId}] ` : '';
|
|
104
|
+
if (e.type === 'data' || e.type === 'think') {
|
|
105
|
+
if (e.text) onProgress(tag + e.text);
|
|
106
|
+
} else if (e.type === 'tool_start') {
|
|
107
|
+
onProgress(`\n${tag}\u25b8 ${e.name}\n`);
|
|
108
|
+
// One more round of work done. A SUBAGENT has no fixed step budget (agent.js sets
|
|
109
|
+
// `stepLimit = Infinity` unless the caller bounds it), so "steps remaining" is not
|
|
110
|
+
// a real quantity and a percentage of it would be invented. Tool calls completed is
|
|
111
|
+
// the honest measure: it only ever grows, it is what the run is actually doing, and
|
|
112
|
+
// the swarm turns it into a bar against the pace its other agents have set.
|
|
113
|
+
if (typeof onStep === 'function') onStep();
|
|
114
|
+
} else if (e.type === 'tool_result') {
|
|
115
|
+
const body = String(e.content == null ? '' : e.content);
|
|
116
|
+
const first = body.split('\n').slice(0, 3).join('\n');
|
|
117
|
+
onProgress(`${tag}${first}${body.split('\n').length > 3 ? '\n\u2026' : ''}\n`);
|
|
118
|
+
} else if (e.type === 'error') {
|
|
119
|
+
onProgress(`\n${tag}[error] ${(e.error && e.error.message) || String(e.error)}\n`);
|
|
120
|
+
failure = (e.error && e.error.message) || String(e.error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (onEvent) onEvent(e);
|
|
124
|
+
},
|
|
125
|
+
// A subagent never prompts: it inherits the parent's permission mode by
|
|
126
|
+
// running under the same ctx (kimi does the same — approvals belong to the
|
|
127
|
+
// parent turn, not the child).
|
|
128
|
+
onApproval: async () => true,
|
|
129
|
+
});
|
|
130
|
+
if (signal) {
|
|
131
|
+
if (signal.aborted) throw new Error('Interrupted by user');
|
|
132
|
+
onSubagentAbort = () => agent.interrupt();
|
|
133
|
+
signal.addEventListener('abort', onSubagentAbort, { once: true });
|
|
134
|
+
}
|
|
135
|
+
// Hand the LIVE agent back to the caller so a control tool can steer or interrupt it
|
|
136
|
+
// while it runs. `subagents` only learns about this run when it finishes, so without
|
|
137
|
+
// this the only way to reach a running subagent would be to interrupt the whole turn.
|
|
138
|
+
if (typeof onAgent === 'function') onAgent(agent);
|
|
139
|
+
// The abort listener is removed when the run settles. `{once:true}` only makes it
|
|
140
|
+
// fire once — it stays REGISTERED until then, so every completed subagent left
|
|
141
|
+
// one closure (holding the whole subagent) pinned on the parent's signal.
|
|
142
|
+
try {
|
|
143
|
+
await agent.run();
|
|
144
|
+
} finally {
|
|
145
|
+
if (onSubagentAbort) signal.removeEventListener('abort', onSubagentAbort);
|
|
146
|
+
}
|
|
147
|
+
// SHELL HOOK: SubagentStop. The subagent finished (or was interrupted); the
|
|
148
|
+
// parent-side hook can log/archive it. Never blocks.
|
|
149
|
+
// `hookConfig` may be the raw `{event:[…]}` map or a `{hooks:{…}}` wrapper
|
|
150
|
+
// (agent.js normalises it to the wrapper) — accept either.
|
|
151
|
+
const hc = subCfg.hookConfig;
|
|
152
|
+
const hookCfg = hc ? (hc.hooks ? hc : { hooks: hc }) : null;
|
|
153
|
+
if (hookCfg) {
|
|
154
|
+
await runShellHooks(hookCfg, 'SubagentStop',
|
|
155
|
+
{ agentId: agentId || '', subagentType: type, prompt }, cfg.workspace);
|
|
156
|
+
}
|
|
157
|
+
// The parent must receive ONLY what the subagent concluded — never its tool
|
|
158
|
+
// chatter, and never its reasoning. Two things could leak in:
|
|
159
|
+
// * the aggregate `data` stream, which also contains intermediate narration;
|
|
160
|
+
// * an inline `<think>…</think>` block, when a model puts reasoning in the
|
|
161
|
+
// content stream instead of the separate reasoning channel.
|
|
162
|
+
// So: take the LAST assistant message (the handoff), and strip any think block
|
|
163
|
+
// from it. Fall back to the filtered stream text when there is no message.
|
|
164
|
+
// Strip whichever spelling the model used. `\b` alone is wrong here: `k` and `i`
|
|
165
|
+
// are both word characters, so `<think\b` does NOT match `<thinking>` — a model
|
|
166
|
+
// using the long form had its whole reasoning block leak into the handoff. Enumerate
|
|
167
|
+
// the variants instead, matching llm.js's tag table.
|
|
168
|
+
const stripThink = (s) => String(s)
|
|
169
|
+
.replace(/<(?:think|thinking)\b[^>]*>[\s\S]*?<\/(?:think|thinking)>/gi, '')
|
|
170
|
+
.replace(/<think(?:ing)?\b[^>]*>[\s\S]*$/i, '') // an unclosed block: drop to the end
|
|
171
|
+
.trim();
|
|
172
|
+
const lastAssistant = [...agent.messages].reverse()
|
|
173
|
+
.find((m) => m.role === 'assistant' && typeof m.content === 'string' && stripThink(m.content));
|
|
174
|
+
const result = (lastAssistant && stripThink(lastAssistant.content)) || stripThink(text) || '(no output)';
|
|
175
|
+
// The failure travels WITH the text. A run that died and a run that simply produced nothing
|
|
176
|
+
// both used to arrive as an empty/`(no output)` string, so the caller could not tell them apart
|
|
177
|
+
// and reported a dead agent as a successful empty answer.
|
|
178
|
+
return { text: result, messages: agent.messages, failed: failure };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const spec = {
|
|
182
|
+
name: 'Agent',
|
|
183
|
+
description: `Launch a subagent to handle a task. The subagent runs with its own context, so delegating keeps the bulk of intermediate file contents out of your own context — you get a conclusion back instead of a pile of dumps.
|
|
184
|
+
|
|
185
|
+
Writing the prompt:
|
|
186
|
+
- The subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.
|
|
187
|
+
- Lookups (read this file, run that test): put the exact path or command in the prompt.
|
|
188
|
+
- Investigations (figure out X, find why Y): give the question, not prescribed steps.
|
|
189
|
+
- Do not delegate understanding. If the task hinges on a file path or line number, find it yourself first and write it into the prompt.
|
|
190
|
+
|
|
191
|
+
Usage notes:
|
|
192
|
+
- When the task continues earlier work a subagent already did, prefer resuming that agent (pass its \`resume\` id) over spawning a fresh instance.
|
|
193
|
+
- A subagent's result is only visible to you, not to the user. Summarize the relevant parts yourself in your own reply.
|
|
194
|
+
|
|
195
|
+
When NOT to use Agent:
|
|
196
|
+
- Skip delegation for trivial work you can already do directly — reading a file whose path you know, or any task that takes a step or two.
|
|
197
|
+
- Do NOT spawn a subagent unless the user, or the applicable AGENTS.md / project instructions, explicitly asks for sub-agents, delegation, or parallel agent work. Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do NOT count as permission to spawn. Doing the work yourself is the default.
|
|
198
|
+
- Do not delegate the step you are about to be blocked on: if your very next action depends on the result, do it yourself and keep the critical path moving.
|
|
199
|
+
- Do not delegate work you are already doing — redelegating an unresumed task duplicates effort instead of saving context.
|
|
200
|
+
|
|
201
|
+
Available agent types:
|
|
202
|
+
${availableTypes()}`,
|
|
203
|
+
parameters: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
prompt: { type: 'string', description: 'Full task prompt for the subagent.' },
|
|
207
|
+
description: { type: 'string', description: 'Short task description (3-5 words) for UI display.' },
|
|
208
|
+
subagent_type: { type: 'string', description: `One of the available agent types. Defaults to "${DEFAULT_SUBAGENT_TYPE}" when omitted.` },
|
|
209
|
+
resume: { type: 'string', description: 'Optional agent id to resume instead of creating a new instance. Do not also pass subagent_type.' },
|
|
210
|
+
run_in_background: { type: 'boolean', default: false, description: 'If true, return immediately with a task id instead of waiting. The result arrives in the conversation later.' },
|
|
211
|
+
fork: { type: 'boolean', default: false, description: "Start the subagent from a snapshot of this agent's completed conversation history instead of zero context." },
|
|
212
|
+
model: { type: 'string', description: 'A configured model key to run the subagent on, or "primary" for the current model. Ignored when resuming.' },
|
|
213
|
+
},
|
|
214
|
+
required: ['prompt', 'description'],
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
async execute(args, ctx) {
|
|
218
|
+
const prompt = String(args.prompt || '').trim();
|
|
219
|
+
const description = String(args.description || '').trim();
|
|
220
|
+
if (!prompt) return 'Error: `prompt` is required.';
|
|
221
|
+
if (!description) return 'Error: `description` is required (short label for the task list).';
|
|
222
|
+
|
|
223
|
+
const resumeId = args.resume ? String(args.resume).trim() : '';
|
|
224
|
+
const wantFork = args.fork === true;
|
|
225
|
+
if (resumeId && args.subagent_type) {
|
|
226
|
+
return 'Error: cannot set subagent_type when resuming an existing agent. Resume by agent id only.';
|
|
227
|
+
}
|
|
228
|
+
if (resumeId && wantFork) {
|
|
229
|
+
return 'Error: `fork` cannot be combined with `resume`.';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- resume an existing subagent -------------------------------------
|
|
233
|
+
let existing = null;
|
|
234
|
+
if (resumeId) {
|
|
235
|
+
existing = subagents.get(resumeId);
|
|
236
|
+
if (!existing) return `Error: no such subagent: ${resumeId}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const type = existing
|
|
240
|
+
? existing.type
|
|
241
|
+
: (SUBAGENT_TYPES[args.subagent_type] ? args.subagent_type : DEFAULT_SUBAGENT_TYPE);
|
|
242
|
+
|
|
243
|
+
// ---- model override ---------------------------------------------------
|
|
244
|
+
// Precedence: the call's own `model` > the user's configured subagent model
|
|
245
|
+
// (/subagent-model) > the session's model. 'primary' always means "this
|
|
246
|
+
// session's model", so an explicit primary beats a configured default.
|
|
247
|
+
let subCfg = ctx;
|
|
248
|
+
const wantPrimary = args.model === 'primary';
|
|
249
|
+
const explicit = args.model && !wantPrimary && ctx.raw && ctx.raw.models && ctx.raw.models[args.model]
|
|
250
|
+
? args.model : '';
|
|
251
|
+
const configured = ctx.subagentModel && ctx.raw && ctx.raw.models && ctx.raw.models[ctx.subagentModel]
|
|
252
|
+
? ctx.subagentModel : '';
|
|
253
|
+
// A NAME THAT DOES NOT EXIST IS AN ERROR, not a fallback. Resolving it to "" ran the
|
|
254
|
+
// subagent on the session's model while the caller believed it had chosen — a typo, or a
|
|
255
|
+
// model configured in another session, produced a silently different agent.
|
|
256
|
+
if (args.model && !wantPrimary && !explicit) {
|
|
257
|
+
const known = Object.keys((ctx.raw && ctx.raw.models) || {});
|
|
258
|
+
const list = known.length ? known.join(', ') : 'none';
|
|
259
|
+
return `Error: unknown model "${args.model}". Configured: ${list}. Use "primary" for the session model.`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The user's `/subagent-model` setting can go stale when a model is renamed or removed, and
|
|
263
|
+
// it used to fail the same silent way. Say so, through onOutput so it reaches the transcript.
|
|
264
|
+
if (ctx.subagentModel && !configured) {
|
|
265
|
+
const list = Object.keys((ctx.raw && ctx.raw.models) || {}).join(', ') || 'none';
|
|
266
|
+
try {
|
|
267
|
+
if (typeof ctx.onOutput === 'function') {
|
|
268
|
+
ctx.onOutput(`[subagent model] "${ctx.subagentModel}" is no longer configured, so this subagent runs on the session model. Re-pick it with /swarm-sub-agent. Available: ${list}\n`);
|
|
269
|
+
}
|
|
270
|
+
} catch { /* a view that cannot be written to is not a reason to fail the call */ }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const chosen = wantPrimary ? '' : (explicit || configured);
|
|
274
|
+
if (chosen) {
|
|
275
|
+
const { resolveModelArg } = await import('../config.js');
|
|
276
|
+
subCfg = resolveModelArg(ctx, chosen);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---- fork: snapshot this conversation --------------------------------
|
|
280
|
+
let history = null;
|
|
281
|
+
if (wantFork) {
|
|
282
|
+
history = (ctx._parentMessages || []).filter(
|
|
283
|
+
(m) => m.role !== 'system' && m.role !== 'tool' && typeof m.content === 'string' && m.content.trim(),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- background -------------------------------------------------------
|
|
288
|
+
if (args.run_in_background === true) {
|
|
289
|
+
const controller = new AbortController();
|
|
290
|
+
const task = createTask(ctx, 'agent', {
|
|
291
|
+
description,
|
|
292
|
+
agentId: null,
|
|
293
|
+
subagentType: type,
|
|
294
|
+
model: args.model || '',
|
|
295
|
+
detached: true,
|
|
296
|
+
});
|
|
297
|
+
task._abort = () => controller.abort();
|
|
298
|
+
const agentId = existing ? existing.id : newAgentId();
|
|
299
|
+
task.agentId = agentId;
|
|
300
|
+
|
|
301
|
+
// Register the run so a control tool can reach it while it is in flight. The handle
|
|
302
|
+
// is created BEFORE the run so both `run_in_background` and the detached-foreground
|
|
303
|
+
// path behave the same, and cleared in `finally` so a settled run can never be
|
|
304
|
+
// listed as controllable.
|
|
305
|
+
let runId = '';
|
|
306
|
+
const early = { agent: null };
|
|
307
|
+
|
|
308
|
+
// Fire and forget: the task record carries the result when it lands.
|
|
309
|
+
void (async () => {
|
|
310
|
+
try {
|
|
311
|
+
const run = await runSubagent({
|
|
312
|
+
cfg: subCfg, type, prompt, history,
|
|
313
|
+
signal: controller.signal,
|
|
314
|
+
onAgent: (agent) => { early.agent = agent; },
|
|
315
|
+
// No live streaming into the task output: that should read as the
|
|
316
|
+
// subagent's CONCLUSION. `run.text` below is the cleaned final message.
|
|
317
|
+
});
|
|
318
|
+
const entry = existing || { id: agentId, type, messages: [] };
|
|
319
|
+
entry.messages = run.messages;
|
|
320
|
+
entry.type = type;
|
|
321
|
+
rememberSubagent(agentId, entry);
|
|
322
|
+
appendTaskOutput(task, `\n\n${run.text}`);
|
|
323
|
+
settleTask(task, 'completed');
|
|
324
|
+
if (typeof ctx._onBackgroundTaskDone === 'function') ctx._onBackgroundTaskDone(task);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
const stopped = controller.signal.aborted;
|
|
327
|
+
settleTask(task, stopped ? 'killed' : 'failed', { stopReason: stopped ? 'Stopped by user' : e.message });
|
|
328
|
+
appendTaskOutput(task, `\n[error: ${e.message}]`);
|
|
329
|
+
if (typeof ctx._onBackgroundTaskDone === 'function') ctx._onBackgroundTaskDone(task);
|
|
330
|
+
} finally {
|
|
331
|
+
if (runId) endRun(runId);
|
|
332
|
+
}
|
|
333
|
+
})();
|
|
334
|
+
runId = registerRun({
|
|
335
|
+
agentId, taskId: task.taskId, type, description,
|
|
336
|
+
steer: (text) => { if (early.agent) early.agent.steer(text); },
|
|
337
|
+
interrupt: () => controller.abort(),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return `agent_id: ${agentId}\ntask_id: ${task.taskId}\nstatus: running\nnext_step: Continue your work; the result arrives in a later message. Use TaskOutput {task_id: "${task.taskId}"} for a non-blocking check or TaskStop to cancel.`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---- foreground (the default; Ctrl+B detaches it) ---------------------
|
|
344
|
+
// Registered like Bash's foreground hook so the TUI's ctrl+b can move a long
|
|
345
|
+
// subagent into the background WITHOUT losing its work: the run keeps going,
|
|
346
|
+
// the task record gets the result when it lands, and this call resolves at
|
|
347
|
+
// once with the task id so the parent turn can continue.
|
|
348
|
+
const bgController = new AbortController();
|
|
349
|
+
const onParentAbort = () => bgController.abort();
|
|
350
|
+
if (ctx.signal) {
|
|
351
|
+
if (ctx.signal.aborted) return 'Error: the turn was interrupted before the subagent started.';
|
|
352
|
+
ctx.signal.addEventListener('abort', onParentAbort, { once: true });
|
|
353
|
+
}
|
|
354
|
+
const agentId = existing ? existing.id : newAgentId();
|
|
355
|
+
const entry = existing || { id: agentId, type, messages: [] };
|
|
356
|
+
rememberSubagent(agentId, entry);
|
|
357
|
+
|
|
358
|
+
let detachedId = null;
|
|
359
|
+
const resetForeground = () => { if (ctx && ctx._foreground && ctx._foreground.detach) ctx._foreground = null; };
|
|
360
|
+
// Start the run BEFORE the detach hook is installed, so the hook's closure can
|
|
361
|
+
// reference the in-flight promise (it waits on it to record the result).
|
|
362
|
+
// `onProgress` feeds the parent's live-output sink (ctx.onOutput), which the
|
|
363
|
+
// agent tags with THIS tool call's id — the same path Bash uses, so the TUI
|
|
364
|
+
// renders the subagent's stream under the `Using Agent` row and Ctrl+O folds it.
|
|
365
|
+
// The live Agent is captured as soon as `runSubagent` builds it, which is what makes
|
|
366
|
+
// this run controllable from another tool call (or the TUI) without waiting for it to
|
|
367
|
+
// finish. `live` is a box because the handle arrives asynchronously.
|
|
368
|
+
const live = { agent: null };
|
|
369
|
+
const runPromise = runSubagent({
|
|
370
|
+
cfg: subCfg, type, prompt, history, signal: bgController.signal,
|
|
371
|
+
onAgent: (agent) => { live.agent = agent; },
|
|
372
|
+
// `_progressTag` is set by AgentSwarm to the item index, so a swarm's
|
|
373
|
+
// concurrently-running subagents are distinguishable in the live stream.
|
|
374
|
+
// A plain Agent call has no tag and prints its stream unlabelled.
|
|
375
|
+
//
|
|
376
|
+
// `_subTaskId` is also set by AgentSwarm: it names the task THIS subagent owns, so its
|
|
377
|
+
// stream lands on its own row in /tasks instead of being folded into the swarm's single
|
|
378
|
+
// buffer. Without it a 7-item swarm had one task and no way to see what any of them was
|
|
379
|
+
// doing.
|
|
380
|
+
onProgress: (text) => {
|
|
381
|
+
if (ctx._subTaskId) {
|
|
382
|
+
const own = ctx.tasks && ctx.tasks[ctx._subTaskId];
|
|
383
|
+
if (own) appendTaskOutput(own, text);
|
|
384
|
+
}
|
|
385
|
+
if (typeof ctx.onOutput !== 'function') return;
|
|
386
|
+
const tag = ctx._progressTag ? `[${ctx._progressTag}] ` : '';
|
|
387
|
+
ctx.onOutput(tag ? text.split('\n').map((l) => (l ? tag + l : l)).join('\n') : text);
|
|
388
|
+
},
|
|
389
|
+
// Each completed tool call is one unit of progress. Only AgentSwarm supplies the sink;
|
|
390
|
+
// a lone Agent call has no grid to fill and nothing to report to.
|
|
391
|
+
onStep: () => { if (typeof ctx._onSubProgress === 'function') ctx._onSubProgress(); },
|
|
392
|
+
});
|
|
393
|
+
// Registered here so a foreground subagent is reachable by AgentMessage / AgentInterrupt
|
|
394
|
+
// too — that is precisely the case where a user wants to redirect one: it is the run
|
|
395
|
+
// they are watching. Cleared when the run settles, however it settles.
|
|
396
|
+
let runId = registerRun({
|
|
397
|
+
agentId, type, description,
|
|
398
|
+
steer: (text) => { if (live.agent) live.agent.steer(text); },
|
|
399
|
+
interrupt: () => bgController.abort(),
|
|
400
|
+
});
|
|
401
|
+
void runPromise.catch(() => {}).finally(() => { if (runId) { endRun(runId); runId = ''; } });
|
|
402
|
+
if (ctx) {
|
|
403
|
+
ctx._foreground = {
|
|
404
|
+
kind: 'Agent',
|
|
405
|
+
description,
|
|
406
|
+
detach: () => {
|
|
407
|
+
if (detachedId) return detachedId;
|
|
408
|
+
// Hand the in-flight work to a background task. The controller is NOT
|
|
409
|
+
// aborted, so the subagent keeps running.
|
|
410
|
+
const task = createTask(ctx, 'agent', {
|
|
411
|
+
description, agentId, subagentType: type, model: args.model || '', detached: true,
|
|
412
|
+
});
|
|
413
|
+
task._abort = () => bgController.abort();
|
|
414
|
+
detachedId = task.taskId;
|
|
415
|
+
resetForeground();
|
|
416
|
+
void (async () => {
|
|
417
|
+
try {
|
|
418
|
+
const r = await runPromise;
|
|
419
|
+
entry.messages = r.messages; entry.type = type;
|
|
420
|
+
rememberSubagent(agentId, entry);
|
|
421
|
+
appendTaskOutput(task, r.text);
|
|
422
|
+
settleTask(task, 'completed');
|
|
423
|
+
if (typeof ctx._onBackgroundTaskDone === 'function') ctx._onBackgroundTaskDone(task);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
const stopped = bgController.signal.aborted;
|
|
426
|
+
settleTask(task, stopped ? 'killed' : 'failed', { stopReason: stopped ? 'Stopped by user' : e.message });
|
|
427
|
+
appendTaskOutput(task, `\n[error: ${e.message}]`);
|
|
428
|
+
if (typeof ctx._onBackgroundTaskDone === 'function') ctx._onBackgroundTaskDone(task);
|
|
429
|
+
} finally {
|
|
430
|
+
// The detached run kept the parent-abort link alive on purpose (an
|
|
431
|
+
// aborted turn kills it); once it settles the link goes too.
|
|
432
|
+
if (ctx.signal) ctx.signal.removeEventListener('abort', onParentAbort);
|
|
433
|
+
}
|
|
434
|
+
})();
|
|
435
|
+
return task.taskId;
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
let run;
|
|
440
|
+
try {
|
|
441
|
+
run = await runPromise;
|
|
442
|
+
} catch (e) {
|
|
443
|
+
resetForeground();
|
|
444
|
+
if (ctx.signal) ctx.signal.removeEventListener('abort', onParentAbort);
|
|
445
|
+
// Detached: the background task owns the error now, not this call.
|
|
446
|
+
if (detachedId) return `Moved to background as task #${detachedId}. It keeps running; inspect with TaskList / TaskOutput {task_id: "${detachedId}"}.`;
|
|
447
|
+
return `Error: subagent failed: ${e.message}`;
|
|
448
|
+
}
|
|
449
|
+
if (ctx.signal) ctx.signal.removeEventListener('abort', onParentAbort);
|
|
450
|
+
resetForeground();
|
|
451
|
+
if (detachedId) {
|
|
452
|
+
return `Moved to background as task #${detachedId}. It keeps running; inspect with TaskList / TaskOutput {task_id: "${detachedId}"}.`;
|
|
453
|
+
}
|
|
454
|
+
entry.messages = run.messages;
|
|
455
|
+
entry.type = type;
|
|
456
|
+
rememberSubagent(agentId, entry);
|
|
457
|
+
|
|
458
|
+
// A FAILED subagent returns an error, not an empty string. `run.text` is '' when the Agent
|
|
459
|
+
// died, which the swarm printed as "(no output)" — so four agents that had all failed on one
|
|
460
|
+
// error looked like four agents that had found nothing to say.
|
|
461
|
+
if (run.failed) return `Error: subagent "${agentId}" failed: ${run.failed}`;
|
|
462
|
+
|
|
463
|
+
return `agent_id: ${agentId}\n\n${run.text}`;
|
|
464
|
+
},
|
|
387
465
|
};
|