@xingjisoft/neutrino 0.5.1 → 0.6.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.
- package/README.md +1 -0
- package/assets/fast.txt +18 -0
- package/assets/pro/common.txt +4 -0
- package/assets/pro/main_model.txt +2 -0
- package/assets/pro/plan_model.txt +3 -1
- package/assets/prompt.txt +9 -0
- package/assets/ptc.txt +27 -0
- package/assets/subagent.txt +22 -1
- package/dist/app.js +2 -0
- package/dist/config.js +30 -1
- package/dist/llm/client.js +61 -8
- package/dist/llm/codeExecutor.js +239 -0
- package/dist/llm/codemode.js +164 -0
- package/dist/llm/context.js +97 -0
- package/dist/llm/plan.js +97 -9
- package/dist/llm/risk.js +10 -0
- package/dist/llm/runtime.js +301 -0
- package/dist/llm/search.js +426 -0
- package/dist/llm/searchNetwork.js +129 -0
- package/dist/llm/subagent.js +236 -112
- package/dist/llm/tools.js +101 -4
- package/dist/pro/agents.js +49 -19
- package/dist/pro/preflight.js +24 -2
- package/dist/screens/chat.js +299 -27
- package/dist/update.js +8 -2
- package/dist/updateWatch.js +76 -0
- package/dist/web/hub.js +9 -0
- package/dist/web/server.js +183 -13
- package/dist/web/service.js +22 -0
- package/dist/web/session.js +327 -62
- package/dist/web/terminalBridge.js +10 -1
- package/package.json +42 -37
- package/web/app.js +347 -29
- package/web/i18n.js +27 -0
- package/web/index.html +465 -442
- package/web/settings.js +31 -1
- package/web/style.css +3581 -3473
package/dist/web/session.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
/**
|
|
2
3
|
* Headless conversation runner for the web UI.
|
|
3
4
|
*
|
|
@@ -11,15 +12,18 @@
|
|
|
11
12
|
* (snapshot, stream, pause, resume, message) work unchanged.
|
|
12
13
|
*/
|
|
13
14
|
import { randomUUID } from "node:crypto";
|
|
14
|
-
import { findModel, contextCompressionThreshold, intensityForModel, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels,
|
|
15
|
+
import { findModel, contextCompressionThreshold, modelContextWindow, sessionDir, intensityForModel, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels, loadAgentPrompt, normalizeAgentMode, saveSession, } from "../config.js";
|
|
15
16
|
import { COMPACT_KEEP, planCompaction, transcriptFor, modelHistory, normalizeContext, } from "../compact.js";
|
|
17
|
+
import { TaskRuntime, currentTask, linkSignals, taskLimits, resumeTaskSnapshot } from "../llm/runtime.js";
|
|
18
|
+
import { prepareContext, outputAllowance, finalTaskReport } from "../llm/context.js";
|
|
19
|
+
import { FINISH_STEP_TOOL, FINISH_STEP_DEFINITION, finishPlanStep, preparePlan, settlePlan } from "../llm/plan.js";
|
|
16
20
|
import { LLMClient, describeError } from "../llm/client.js";
|
|
17
21
|
import { defaultProPreset, loadProModels, loadProPrompt, proMaxRecursionDepth, requireProPreset } from "../pro/config.js";
|
|
18
22
|
import { resolveProTeam, ProPreflightError } from "../pro/preflight.js";
|
|
19
23
|
import { ProAgentTeam, getProToolDefinitions } from "../pro/agents.js";
|
|
20
24
|
import { advancePlan, buildPlannerInput, lastRequestText, needsPlan, parsePlan, planMessage, planTranscript, renderStepDirective, resumePlan, stopPlan, } from "../llm/plan.js";
|
|
21
25
|
import { DELEGATE_TOOL, executeTool, getSubAgentToolDefinitions, getToolDefinitions } from "../llm/tools.js";
|
|
22
|
-
import { buildSubAgentTask, formatSubAgentResult, runSubAgent as runSubAgentTask, } from "../llm/subagent.js";
|
|
26
|
+
import { buildSubAgentTask, formatSubAgentResult, runSubAgent as runSubAgentTask, subAgentRounds, } from "../llm/subagent.js";
|
|
23
27
|
import { assessToolCall } from "../llm/risk.js";
|
|
24
28
|
import { ApprovalGate, captureApprovalContext, describeApprovalIntent } from "../llm/approval.js";
|
|
25
29
|
import { SessionCheckpoint } from "../persistence.js";
|
|
@@ -45,7 +49,7 @@ export function newSessionId(now = new Date()) {
|
|
|
45
49
|
* written to disk until the first message, so an abandoned new-chat dialog
|
|
46
50
|
* cannot litter the history with empty transcripts.
|
|
47
51
|
*/
|
|
48
|
-
export function createWebSession(cwd, selection, approvalMode, mode = "standard", proPreset) {
|
|
52
|
+
export function createWebSession(cwd, selection, approvalMode, mode = "standard", proPreset, agentMode = "standard") {
|
|
49
53
|
const now = new Date();
|
|
50
54
|
const source = {
|
|
51
55
|
id: newSessionId(now),
|
|
@@ -59,6 +63,7 @@ export function createWebSession(cwd, selection, approvalMode, mode = "standard"
|
|
|
59
63
|
messages: [],
|
|
60
64
|
tokens: { prompt: 0, completion: 0 },
|
|
61
65
|
approval_mode: normalizeApprovalMode(approvalMode ?? loadConfig(mode).approval_mode),
|
|
66
|
+
agent_mode: mode === "pro" ? "standard" : agentMode,
|
|
62
67
|
...(mode === "pro" ? { pro_preset: requireProPreset(proPreset ?? defaultProPreset()) } : {}),
|
|
63
68
|
};
|
|
64
69
|
return new WebSession(source, selection.model, mode);
|
|
@@ -161,7 +166,13 @@ export class WebSession {
|
|
|
161
166
|
this.publishLive();
|
|
162
167
|
return saved;
|
|
163
168
|
});
|
|
169
|
+
taskRuntime = null;
|
|
170
|
+
lastTask;
|
|
171
|
+
taskOutcome = "completed";
|
|
164
172
|
turnToken = 0;
|
|
173
|
+
agentMode = "standard";
|
|
174
|
+
activeAgentMode = "standard";
|
|
175
|
+
turnAbort = null;
|
|
165
176
|
lastPromptTokens = 0;
|
|
166
177
|
disposed = false;
|
|
167
178
|
idleTimer = null;
|
|
@@ -171,6 +182,7 @@ export class WebSession {
|
|
|
171
182
|
reviewing = null;
|
|
172
183
|
constructor(source, model, mode = "standard") {
|
|
173
184
|
this.mode = mode;
|
|
185
|
+
this.agentMode = mode === "pro" ? "standard" : normalizeAgentMode(source.agent_mode);
|
|
174
186
|
if (mode === "pro")
|
|
175
187
|
this.proPreset = requireProPreset(source.pro_preset ?? defaultProPreset());
|
|
176
188
|
this.model = model;
|
|
@@ -184,6 +196,7 @@ export class WebSession {
|
|
|
184
196
|
this.messages = source.messages.slice();
|
|
185
197
|
this.context = normalizeContext(source.context, this.messages.filter((m) => m.role !== "system").length);
|
|
186
198
|
this.plan = resumePlan(source.plan ?? null);
|
|
199
|
+
this.lastTask = resumeTaskSnapshot(source.task_run);
|
|
187
200
|
this.tokens = {
|
|
188
201
|
prompt: source.tokens?.prompt ?? 0,
|
|
189
202
|
completion: source.tokens?.completion ?? 0,
|
|
@@ -204,6 +217,7 @@ export class WebSession {
|
|
|
204
217
|
return;
|
|
205
218
|
this.disposed = true;
|
|
206
219
|
this.proAbort?.abort();
|
|
220
|
+
this.turnAbort?.abort();
|
|
207
221
|
this.turnToken++;
|
|
208
222
|
this.busy = false;
|
|
209
223
|
if (this.pendingApproval) {
|
|
@@ -267,6 +281,9 @@ export class WebSession {
|
|
|
267
281
|
model: this.model.name,
|
|
268
282
|
intensity: this.intensity,
|
|
269
283
|
approvalMode: this.approvalMode,
|
|
284
|
+
agentMode: this.agentMode,
|
|
285
|
+
activeAgentMode: this.busy ? this.activeAgentMode : this.agentMode,
|
|
286
|
+
task: this.taskRuntime?.snapshot() ?? this.lastTask,
|
|
270
287
|
busy: this.busy,
|
|
271
288
|
paused: this.paused,
|
|
272
289
|
queued: this.queued,
|
|
@@ -316,6 +333,7 @@ export class WebSession {
|
|
|
316
333
|
return;
|
|
317
334
|
this.paused = true;
|
|
318
335
|
this.proAbort?.abort();
|
|
336
|
+
this.turnAbort?.abort();
|
|
319
337
|
this.turnToken++;
|
|
320
338
|
// A queued decision would otherwise keep the promise (and the gate) alive.
|
|
321
339
|
if (this.pendingApproval) {
|
|
@@ -328,11 +346,9 @@ export class WebSession {
|
|
|
328
346
|
this.subagent = null;
|
|
329
347
|
if (this.proState)
|
|
330
348
|
this.proState.checking = false;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.planning = false;
|
|
335
|
-
}
|
|
349
|
+
this.reviewing = null;
|
|
350
|
+
this.retrying = null;
|
|
351
|
+
this.planning = false;
|
|
336
352
|
stopPlan(this.plan);
|
|
337
353
|
this.save();
|
|
338
354
|
this.publishLive();
|
|
@@ -356,6 +372,14 @@ export class WebSession {
|
|
|
356
372
|
this.publishLive();
|
|
357
373
|
return true;
|
|
358
374
|
}
|
|
375
|
+
liveSetAgentMode(mode) {
|
|
376
|
+
if (this.mode === "pro" || this.disposed)
|
|
377
|
+
return { ok: false, error: "this session cannot change agent mode" };
|
|
378
|
+
this.agentMode = mode;
|
|
379
|
+
this.save();
|
|
380
|
+
this.publishLive();
|
|
381
|
+
return { ok: true };
|
|
382
|
+
}
|
|
359
383
|
liveSetApprovalMode(mode) {
|
|
360
384
|
if (this.approvalMode === mode)
|
|
361
385
|
return;
|
|
@@ -382,7 +406,10 @@ export class WebSession {
|
|
|
382
406
|
this.proAbort?.abort();
|
|
383
407
|
this.proTeam = null;
|
|
384
408
|
this.proState = undefined;
|
|
409
|
+
this.turnAbort?.abort();
|
|
385
410
|
this.turnToken++;
|
|
411
|
+
this.taskRuntime = null;
|
|
412
|
+
this.lastTask = undefined;
|
|
386
413
|
if (this.pendingApproval) {
|
|
387
414
|
const pending = this.pendingApproval;
|
|
388
415
|
this.pendingApproval = null;
|
|
@@ -430,9 +457,24 @@ export class WebSession {
|
|
|
430
457
|
this.startTurn();
|
|
431
458
|
}
|
|
432
459
|
startTurn() {
|
|
460
|
+
if (this.taskRuntime?.settling) {
|
|
461
|
+
const previous = this.taskRuntime;
|
|
462
|
+
this.queued = lastRequestText(this.messages);
|
|
463
|
+
void previous.drain().then(() => {
|
|
464
|
+
if (this.taskRuntime === previous && this.queued && !this.paused) {
|
|
465
|
+
this.queued = null;
|
|
466
|
+
this.startTurn();
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
this.publishLive();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
433
472
|
const turnMessages = this.messages.slice();
|
|
434
473
|
const approvalContext = captureApprovalContext(turnMessages);
|
|
435
474
|
this.busy = true;
|
|
475
|
+
this.activeAgentMode = this.agentMode;
|
|
476
|
+
this.turnAbort?.abort();
|
|
477
|
+
this.turnAbort = new AbortController();
|
|
436
478
|
const token = ++this.turnToken;
|
|
437
479
|
this.streamingContent = "";
|
|
438
480
|
this.publishLive();
|
|
@@ -449,16 +491,58 @@ export class WebSession {
|
|
|
449
491
|
* pass, exactly as it did before planning existed.
|
|
450
492
|
*/
|
|
451
493
|
async runTurn(token, turnMessages, approvalContext) {
|
|
494
|
+
const runtime = new TaskRuntime({
|
|
495
|
+
request: approvalContext.prompt, priorUserRequests: approvalContext.priorUserRequests,
|
|
496
|
+
cwd: this.dir, signal: this.turnAbort?.signal, limits: taskLimits(loadConfig(this.mode)),
|
|
497
|
+
journalDir: path.join(sessionDir(this.mode), "tasks", path.basename(this.sessionId || "unsaved")),
|
|
498
|
+
});
|
|
499
|
+
this.taskRuntime = runtime;
|
|
500
|
+
this.taskOutcome = "completed";
|
|
501
|
+
let failed = false;
|
|
502
|
+
try {
|
|
503
|
+
await runtime.run(() => this.runTaskTurn(token, turnMessages, approvalContext));
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
failed = true;
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
await runtime.drain();
|
|
511
|
+
if (runtime.signal.aborted)
|
|
512
|
+
settlePlan(runtime.plan ?? null, true);
|
|
513
|
+
runtime.finish(runtime.signal.aborted ? "cancelled" : failed ? "failed" :
|
|
514
|
+
runtime.plan?.steps.some(step => step.status === "failed") ? "failed" :
|
|
515
|
+
runtime.plan?.steps.some(step => !["done", "skipped"].includes(step.status)) ? "partial" : this.taskOutcome);
|
|
516
|
+
if (this.taskRuntime === runtime && !this.disposed) {
|
|
517
|
+
if (runtime.signal.aborted && token === this.turnToken && this.busy && !this.paused) {
|
|
518
|
+
if (this.taskOutcome !== "cancelled")
|
|
519
|
+
this.messages.push({ role: "assistant", content: `[Task stopped: ${String(runtime.signal.reason)}]\n${runtime.partialReport()}` });
|
|
520
|
+
this.streamingContent = null;
|
|
521
|
+
this.onTurnFinished();
|
|
522
|
+
}
|
|
523
|
+
if (this.taskRuntime === runtime) {
|
|
524
|
+
this.lastTask = runtime.snapshot();
|
|
525
|
+
this.save();
|
|
526
|
+
this.publishLive();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async runTaskTurn(token, turnMessages, approvalContext) {
|
|
452
532
|
if (this.mode === "pro")
|
|
453
533
|
return this.runProTurn(token, turnMessages, approvalContext);
|
|
454
534
|
await this.autoCompress(token);
|
|
455
535
|
if (token !== this.turnToken || this.paused)
|
|
456
536
|
return;
|
|
457
|
-
|
|
537
|
+
if (this.activeAgentMode !== "standard")
|
|
538
|
+
this.plan = null;
|
|
539
|
+
const plan = this.activeAgentMode === "standard" ? await this.makePlan(token) : null;
|
|
458
540
|
if (token !== this.turnToken || this.paused)
|
|
459
541
|
return;
|
|
460
542
|
if (plan) {
|
|
461
543
|
this.plan = plan;
|
|
544
|
+
if (currentTask())
|
|
545
|
+
currentTask().runtime.plan = plan;
|
|
462
546
|
this.messages.push({ role: "assistant", content: planMessage(plan) });
|
|
463
547
|
this.save();
|
|
464
548
|
this.publishLive();
|
|
@@ -470,7 +554,7 @@ export class WebSession {
|
|
|
470
554
|
const finished = await this.agentLoop(this.buildApiMessages(this.turnView(turnMessages), renderStepDirective(template, plan, i)), token, approvalContext);
|
|
471
555
|
if (token !== this.turnToken || this.paused)
|
|
472
556
|
return;
|
|
473
|
-
this.setStep(i, finished ? "done" : "failed");
|
|
557
|
+
this.setStep(i, currentTask()?.signal.aborted ? "cancelled" : finished ? "done" : "failed");
|
|
474
558
|
if (!finished)
|
|
475
559
|
break;
|
|
476
560
|
}
|
|
@@ -484,7 +568,8 @@ export class WebSession {
|
|
|
484
568
|
}
|
|
485
569
|
/** Freeze one resolved team per task, plan first, then give the whole brief to main. */
|
|
486
570
|
async runProTurn(token, turnMessages, approvalContext) {
|
|
487
|
-
const
|
|
571
|
+
const taskSignal = currentTask()?.signal;
|
|
572
|
+
const cancelled = () => token !== this.turnToken || this.paused || this.disposed || taskSignal?.aborted === true;
|
|
488
573
|
const controller = new AbortController();
|
|
489
574
|
this.proAbort = controller;
|
|
490
575
|
this.proTeam = null;
|
|
@@ -509,6 +594,7 @@ export class WebSession {
|
|
|
509
594
|
this.proState.fallbacks = resolved.fallbacks;
|
|
510
595
|
this.proState.checking = false;
|
|
511
596
|
const team = new ProAgentTeam({
|
|
597
|
+
runtime: currentTask()?.runtime, request: approvalContext.prompt,
|
|
512
598
|
models: resolved.models, maxDepth: proMaxRecursionDepth(), cwd: this.dir, cancelled,
|
|
513
599
|
approve: (name, args) => this.requestApproval(name, args, {
|
|
514
600
|
...approvalContext, context: captureApprovalContext(this.turnView(turnMessages)).context,
|
|
@@ -537,48 +623,44 @@ export class WebSession {
|
|
|
537
623
|
this.publishLive();
|
|
538
624
|
this.planning = true;
|
|
539
625
|
this.publishLive();
|
|
540
|
-
const
|
|
626
|
+
const planningResult = await team.callResult("plan_model", buildPlannerInput({
|
|
541
627
|
request: approvalContext.prompt, cwd: this.dir, transcript: planTranscript(turnMessages),
|
|
542
628
|
}), "Plan the task before execution. Do not modify files or execute commands.", 0, true);
|
|
629
|
+
const planning = planningResult.report;
|
|
543
630
|
if (cancelled())
|
|
544
631
|
return;
|
|
545
632
|
this.planning = false;
|
|
546
633
|
this.subagent = null;
|
|
547
634
|
this.retrying = null;
|
|
548
|
-
if (!planning.trim()
|
|
549
|
-
throw new Error(`Pro planning failed: ${planning}`);
|
|
550
|
-
}
|
|
551
|
-
this.plan = parsePlan(planning);
|
|
552
|
-
if (!this.plan) {
|
|
553
|
-
// Standard planning skips single actions; Pro plans every task.
|
|
554
|
-
try {
|
|
555
|
-
const single = JSON.parse(planning.trim().replace(/^```(?:json)?\s*|\s*```$/g, ""));
|
|
556
|
-
if (single.steps?.length === 1 && typeof single.steps[0].title === "string" && single.steps[0].title.trim()) {
|
|
557
|
-
this.plan = { goal: String(single.goal ?? "").slice(0, 240), steps: [{
|
|
558
|
-
title: single.steps[0].title.slice(0, 160), detail: String(single.steps[0].detail ?? "").slice(0, 400), status: "pending",
|
|
559
|
-
}] };
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
catch { /* A useful prose plan can still guide the main agent. */ }
|
|
635
|
+
if (planningResult.status !== "completed" || !planning.trim()) {
|
|
636
|
+
throw new Error(`Pro planning failed: ${planningResult.error?.message ?? planning}`);
|
|
563
637
|
}
|
|
638
|
+
this.plan = parsePlan(planning, 1);
|
|
639
|
+
if (!this.plan)
|
|
640
|
+
throw new Error("Pro planning failed: no usable plan steps were returned");
|
|
564
641
|
await this.autoCompress(token);
|
|
565
642
|
if (cancelled())
|
|
566
643
|
return;
|
|
644
|
+
if (this.plan) {
|
|
645
|
+
preparePlan(this.plan);
|
|
646
|
+
if (currentTask())
|
|
647
|
+
currentTask().runtime.plan = this.plan;
|
|
648
|
+
}
|
|
567
649
|
this.messages.push({ role: "assistant", content: this.plan ? planMessage(this.plan) : planning });
|
|
568
650
|
this.save();
|
|
569
651
|
this.publishLive();
|
|
570
|
-
const directive = `The planning agent produced this plan for the current user request:\n${planning}\n\n` +
|
|
571
|
-
"Execute the complete task
|
|
652
|
+
const directive = `The planning agent produced this plan for the current user request:\n${JSON.stringify(this.plan ?? planning)}\n\n` +
|
|
653
|
+
"Execute the complete task using the appropriate professional agents. Own integration and verification. " +
|
|
654
|
+
"Call finish_step for each accepted step using its step_id and actual evidence IDs, existing artifact paths, or the final text deliverable. " +
|
|
655
|
+
"Record blocked or failed steps explicitly. Ending a reply never completes plan steps.";
|
|
572
656
|
if (this.plan?.steps.length)
|
|
573
657
|
this.setStep(0, "active");
|
|
574
658
|
const finished = await this.agentLoop(this.buildApiMessages(this.turnView(turnMessages), directive), token, approvalContext);
|
|
575
659
|
if (cancelled())
|
|
576
660
|
return;
|
|
577
661
|
if (this.plan) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
this.setStep(i, "done");
|
|
581
|
-
else
|
|
662
|
+
settlePlan(this.plan, currentTask()?.signal.aborted === true);
|
|
663
|
+
if (!finished && this.taskOutcome !== "partial" && this.plan.steps[0]?.status === "blocked")
|
|
582
664
|
this.setStep(0, "failed");
|
|
583
665
|
}
|
|
584
666
|
}
|
|
@@ -660,10 +742,11 @@ export class WebSession {
|
|
|
660
742
|
return null;
|
|
661
743
|
}
|
|
662
744
|
finally {
|
|
663
|
-
this.
|
|
664
|
-
|
|
665
|
-
|
|
745
|
+
if (token === this.turnToken) {
|
|
746
|
+
this.planning = false;
|
|
747
|
+
this.retrying = null;
|
|
666
748
|
this.publishLive();
|
|
749
|
+
}
|
|
667
750
|
}
|
|
668
751
|
}
|
|
669
752
|
/** Ends a turn whose floating promise rejected, instead of crashing the app. */
|
|
@@ -686,9 +769,10 @@ export class WebSession {
|
|
|
686
769
|
buildApiMessages(messages = this.messages, directive = "") {
|
|
687
770
|
// A step directive extends the system prompt: the plan is an instruction, not
|
|
688
771
|
// something the user just said.
|
|
689
|
-
const prompt = this.mode === "pro" ? loadProPrompt("main_model") :
|
|
772
|
+
const prompt = this.mode === "pro" ? loadProPrompt("main_model") : loadAgentPrompt(this.activeAgentMode);
|
|
690
773
|
const api = [
|
|
691
|
-
{ role: "system", content: directive ? `${prompt}\n\n${directive}` : prompt
|
|
774
|
+
{ role: "system", content: (directive ? `${prompt}\n\n${directive}` : prompt) +
|
|
775
|
+
(currentTask() ? `\n\n${currentTask().runtime.instructions()}` : "") },
|
|
692
776
|
];
|
|
693
777
|
for (const m of modelHistory(messages, this.context)) {
|
|
694
778
|
if (m.role === "user" || m.role === "assistant") {
|
|
@@ -706,8 +790,12 @@ export class WebSession {
|
|
|
706
790
|
* the caller owns the turn's end because a planned turn runs this per step.
|
|
707
791
|
*/
|
|
708
792
|
async agentLoop(apiMessages, token, approvalContext) {
|
|
709
|
-
const tools = this.mode === "pro" ? getProToolDefinitions() :
|
|
793
|
+
const tools = this.mode === "pro" ? [...getProToolDefinitions(), FINISH_STEP_DEFINITION] : [
|
|
794
|
+
...getToolDefinitions(),
|
|
795
|
+
...(this.activeAgentMode === "ptc" ? [(await import("../llm/codemode.js")).getCodeToolDefinition()] : []),
|
|
796
|
+
];
|
|
710
797
|
const team = this.proTeam;
|
|
798
|
+
let emptyReplies = 0;
|
|
711
799
|
try {
|
|
712
800
|
while (true) {
|
|
713
801
|
if (token !== this.turnToken || this.paused)
|
|
@@ -715,11 +803,19 @@ export class WebSession {
|
|
|
715
803
|
// Match the terminal: long tasks can keep using tools until the model
|
|
716
804
|
// finishes, while older tool results are trimmed near the context limit.
|
|
717
805
|
apiMessages = this.trimApiMessages(apiMessages);
|
|
806
|
+
if (currentTask())
|
|
807
|
+
await prepareContext({
|
|
808
|
+
messages: apiMessages, tools, capacity: team?.contextWindow("main_model") ?? modelContextWindow(this.provider, this.model, this.mode),
|
|
809
|
+
createClient: () => team?.client("main_model") ?? new LLMClient(this.model, this.provider, this.intensity),
|
|
810
|
+
stream: { signal: currentTask().runtime.signal, cancelled: () => token !== this.turnToken || this.paused },
|
|
811
|
+
onUsage: usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; },
|
|
812
|
+
});
|
|
718
813
|
const client = team ? team.client("main_model") : new LLMClient(this.model, this.provider, this.intensity);
|
|
719
814
|
let text = "";
|
|
720
815
|
this.streamingContent = "";
|
|
721
816
|
this.publishLive();
|
|
722
817
|
for await (const chunk of client.streamTurn(apiMessages, tools, {
|
|
818
|
+
maxOutputTokens: outputAllowance(team?.contextWindow("main_model") ?? modelContextWindow(this.provider, this.model, this.mode)),
|
|
723
819
|
onRetry: (notice) => {
|
|
724
820
|
this.retrying = notice;
|
|
725
821
|
this.publishLive();
|
|
@@ -742,6 +838,30 @@ export class WebSession {
|
|
|
742
838
|
this.tokens.completion += client.usage.completion;
|
|
743
839
|
this.lastPromptTokens = client.usage.prompt;
|
|
744
840
|
}
|
|
841
|
+
if (currentTask()?.signal.aborted || client.outcome?.status === "cancelled") {
|
|
842
|
+
this.taskOutcome = "cancelled";
|
|
843
|
+
this.streamingContent = null;
|
|
844
|
+
this.messages.push({ role: "assistant", content: `[Task stopped: ${String(currentTask()?.signal.reason ?? "Task cancelled")}]\n${currentTask()?.runtime.partialReport() ?? ""}` });
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
if (client.outcome?.error?.code.endsWith("_budget") && currentTask()) {
|
|
848
|
+
const report = await finalTaskReport(currentTask().runtime, () => team?.client("main_model") ?? new LLMClient(this.model, this.provider, this.intensity), usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; });
|
|
849
|
+
if (token !== this.turnToken || this.paused)
|
|
850
|
+
return false;
|
|
851
|
+
this.taskOutcome = "partial";
|
|
852
|
+
this.streamingContent = null;
|
|
853
|
+
this.messages.push({ role: "assistant", content: report });
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
if (client.outcome?.status === "failed") {
|
|
857
|
+
const recorded = currentTask()?.runtime.evidence.length ? currentTask().runtime.partialReport() : "";
|
|
858
|
+
const message = client.outcome.error?.message ?? "Model request failed";
|
|
859
|
+
this.taskOutcome = recorded ? "partial" : "failed";
|
|
860
|
+
this.streamingContent = null;
|
|
861
|
+
this.messages.push({ role: "assistant", content: text +
|
|
862
|
+
(text.includes(message) ? "" : `\n[Error: ${message}]`) + (recorded ? `\n\n${recorded}` : "") });
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
745
865
|
const toolCalls = client.toolCalls;
|
|
746
866
|
if (text.trim()) {
|
|
747
867
|
this.messages.push({ role: "assistant", content: text });
|
|
@@ -770,11 +890,17 @@ export class WebSession {
|
|
|
770
890
|
this.save();
|
|
771
891
|
this.publishLive();
|
|
772
892
|
if (toolCalls.length === 0) {
|
|
773
|
-
if (
|
|
893
|
+
if (!text.trim() && emptyReplies++ === 0)
|
|
894
|
+
continue;
|
|
895
|
+
if (!text.trim()) {
|
|
896
|
+
this.taskOutcome = "failed";
|
|
774
897
|
this.messages.push({ role: "assistant", content: "[Error: Pro main model returned an empty completion.]" });
|
|
775
898
|
return false;
|
|
776
899
|
}
|
|
777
|
-
|
|
900
|
+
const success = client.outcome ? client.outcome.status === "completed" : !/\[Error:/.test(text);
|
|
901
|
+
if (!success)
|
|
902
|
+
this.taskOutcome = "failed";
|
|
903
|
+
return success;
|
|
778
904
|
}
|
|
779
905
|
for (const call of toolCalls) {
|
|
780
906
|
if (token !== this.turnToken || this.paused)
|
|
@@ -804,6 +930,7 @@ export class WebSession {
|
|
|
804
930
|
describeApprovalIntent(approvalContext) +
|
|
805
931
|
"Do not retry this call unchanged; ask the user how to proceed.";
|
|
806
932
|
toolMsg.result = denied;
|
|
933
|
+
currentTask()?.runtime.recordTool(call.name, args, denied, "denied");
|
|
807
934
|
apiMessages.push({ role: "tool", content: denied, tool_call_id: call.id });
|
|
808
935
|
this.save();
|
|
809
936
|
this.publishLive();
|
|
@@ -814,6 +941,8 @@ export class WebSession {
|
|
|
814
941
|
result = await this.runTool(call.name, args, toolMsg, token, apiMessages, approvalContext);
|
|
815
942
|
}
|
|
816
943
|
catch (err) {
|
|
944
|
+
if (token === this.turnToken)
|
|
945
|
+
this.taskOutcome = "failed";
|
|
817
946
|
result = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
818
947
|
}
|
|
819
948
|
if (this.disposed)
|
|
@@ -822,7 +951,8 @@ export class WebSession {
|
|
|
822
951
|
this.save();
|
|
823
952
|
if (token !== this.turnToken || this.paused)
|
|
824
953
|
return false;
|
|
825
|
-
apiMessages.push({ role: "tool", content:
|
|
954
|
+
apiMessages.push({ role: "tool", content: toolMsg.evidenceId
|
|
955
|
+
? `${result}\n[Task evidence: ${toolMsg.evidenceId}; status=${toolMsg.executionStatus}]` : result, tool_call_id: call.id });
|
|
826
956
|
this.publishLive();
|
|
827
957
|
}
|
|
828
958
|
this.save();
|
|
@@ -856,15 +986,26 @@ export class WebSession {
|
|
|
856
986
|
}
|
|
857
987
|
// ---- approval gate ---------------------------------------------------
|
|
858
988
|
/** Classify a tool call and, when it is risky, wait for a decision. */
|
|
859
|
-
async requestApproval(tool, args, context = captureApprovalContext(this.messages)) {
|
|
989
|
+
async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
|
|
990
|
+
const linked = linkSignals(signal, currentTask()?.signal);
|
|
991
|
+
signal = linked.signal;
|
|
992
|
+
try {
|
|
993
|
+
currentTask()?.runtime.assertTool(tool, args);
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
linked.dispose();
|
|
997
|
+
return { approved: false, source: "input", reason: error instanceof Error ? error.message : String(error) };
|
|
998
|
+
}
|
|
860
999
|
const risk = assessToolCall(tool, args, this.dir);
|
|
861
|
-
if (!risk)
|
|
1000
|
+
if (!risk) {
|
|
1001
|
+
linked.dispose();
|
|
862
1002
|
return { approved: true, source: "prompt", reason: "routine call" };
|
|
1003
|
+
}
|
|
863
1004
|
const approvalToken = this.turnToken;
|
|
864
1005
|
const team = this.proTeam;
|
|
865
1006
|
const gate = new ApprovalGate({
|
|
866
1007
|
askHuman: () => new Promise((resolve) => {
|
|
867
|
-
if (
|
|
1008
|
+
if (approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true) {
|
|
868
1009
|
resolve(false);
|
|
869
1010
|
return;
|
|
870
1011
|
}
|
|
@@ -875,12 +1016,12 @@ export class WebSession {
|
|
|
875
1016
|
...(this.mode === "pro" && team ? {
|
|
876
1017
|
createReviewerClient: () => team.client("confirm_model"),
|
|
877
1018
|
reviewerSystemPrompt: () => loadProPrompt("confirm_model"),
|
|
878
|
-
cancelled: () => approvalToken !== this.turnToken || this.paused || this.disposed,
|
|
879
1019
|
} : {}),
|
|
1020
|
+
cancelled: () => approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true,
|
|
880
1021
|
autoApprovalEnabled: () => this.approvalMode === "auto",
|
|
881
1022
|
extremeAutoApprovalEnabled: () => loadConfig(this.mode).auto_approve_extreme_risk === true,
|
|
882
1023
|
onReview: (info) => {
|
|
883
|
-
if (
|
|
1024
|
+
if (approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true)
|
|
884
1025
|
return;
|
|
885
1026
|
this.reviewing = info
|
|
886
1027
|
? {
|
|
@@ -891,7 +1032,22 @@ export class WebSession {
|
|
|
891
1032
|
this.publishLive();
|
|
892
1033
|
},
|
|
893
1034
|
});
|
|
894
|
-
|
|
1035
|
+
const abortApproval = () => {
|
|
1036
|
+
const pending = this.pendingApproval;
|
|
1037
|
+
if (pending?.args !== args)
|
|
1038
|
+
return;
|
|
1039
|
+
this.pendingApproval = null;
|
|
1040
|
+
pending.resolve(false);
|
|
1041
|
+
this.publishLive();
|
|
1042
|
+
};
|
|
1043
|
+
signal.addEventListener("abort", abortApproval, { once: true });
|
|
1044
|
+
try {
|
|
1045
|
+
return await gate.decide({ tool, args, cwd: this.dir, ...context, risk });
|
|
1046
|
+
}
|
|
1047
|
+
finally {
|
|
1048
|
+
signal.removeEventListener("abort", abortApproval);
|
|
1049
|
+
linked.dispose();
|
|
1050
|
+
}
|
|
895
1051
|
}
|
|
896
1052
|
/** Reviewer model: `approval_model` when set, otherwise the chat model. */
|
|
897
1053
|
reviewerModel() {
|
|
@@ -905,63 +1061,172 @@ export class WebSession {
|
|
|
905
1061
|
}
|
|
906
1062
|
return { model: this.model, provider: this.provider, intensity: this.intensity };
|
|
907
1063
|
}
|
|
1064
|
+
async runProgram(args, token, apiMessages, approvalContext) {
|
|
1065
|
+
if (this.activeAgentMode !== "ptc")
|
|
1066
|
+
return "[Error: execute_ts requires PTC mode]";
|
|
1067
|
+
const { executeCodeProgram, ProgramToolDenied } = await import("../llm/codemode.js");
|
|
1068
|
+
const signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal;
|
|
1069
|
+
const cancelled = () => token !== this.turnToken || this.paused || signal?.aborted;
|
|
1070
|
+
const result = await executeCodeProgram(args, {
|
|
1071
|
+
signal,
|
|
1072
|
+
onPlan: plan => {
|
|
1073
|
+
if (cancelled())
|
|
1074
|
+
return;
|
|
1075
|
+
this.plan = plan;
|
|
1076
|
+
if (currentTask())
|
|
1077
|
+
currentTask().runtime.plan = plan;
|
|
1078
|
+
this.save();
|
|
1079
|
+
this.publishLive();
|
|
1080
|
+
},
|
|
1081
|
+
onStep: (index, status) => { if (!cancelled())
|
|
1082
|
+
this.setStep(index, status); },
|
|
1083
|
+
call: async (name, callArgs, programSignal) => {
|
|
1084
|
+
if (cancelled() || programSignal?.aborted)
|
|
1085
|
+
throw new Error("Execution cancelled");
|
|
1086
|
+
const message = { role: "tool", name, arguments: callArgs, result: "…" };
|
|
1087
|
+
this.messages.push(message);
|
|
1088
|
+
this.save();
|
|
1089
|
+
this.publishLive();
|
|
1090
|
+
const stopApproval = () => {
|
|
1091
|
+
if (this.pendingApproval?.args !== callArgs)
|
|
1092
|
+
return;
|
|
1093
|
+
const pending = this.pendingApproval;
|
|
1094
|
+
this.pendingApproval = null;
|
|
1095
|
+
pending.resolve(false);
|
|
1096
|
+
this.publishLive();
|
|
1097
|
+
};
|
|
1098
|
+
programSignal?.addEventListener("abort", stopApproval, { once: true });
|
|
1099
|
+
try {
|
|
1100
|
+
const decision = await this.requestApproval(name, callArgs, {
|
|
1101
|
+
...approvalContext, context: captureApprovalContext(apiMessages).context,
|
|
1102
|
+
}, programSignal);
|
|
1103
|
+
if (cancelled() || programSignal?.aborted)
|
|
1104
|
+
throw new Error("Execution cancelled");
|
|
1105
|
+
if (!decision.approved) {
|
|
1106
|
+
message.result = `[Not executed: ${decision.reason}]`;
|
|
1107
|
+
throw new ProgramToolDenied(`Approval denied: ${decision.reason}. Do not retry or work around this denial.`);
|
|
1108
|
+
}
|
|
1109
|
+
const value = await this.runTool(name, callArgs, message, token, apiMessages, approvalContext, programSignal);
|
|
1110
|
+
message.result = value;
|
|
1111
|
+
if (cancelled() || programSignal?.aborted)
|
|
1112
|
+
throw new Error("Execution cancelled");
|
|
1113
|
+
return value;
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
if (message.result === "…")
|
|
1117
|
+
message.result = `[Error: ${error instanceof Error ? error.message : String(error)}]`;
|
|
1118
|
+
throw error;
|
|
1119
|
+
}
|
|
1120
|
+
finally {
|
|
1121
|
+
programSignal?.removeEventListener("abort", stopApproval);
|
|
1122
|
+
if (this.messages.includes(message)) {
|
|
1123
|
+
this.save();
|
|
1124
|
+
this.publishLive();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
if (!cancelled()) {
|
|
1130
|
+
this.save();
|
|
1131
|
+
this.publishLive();
|
|
1132
|
+
}
|
|
1133
|
+
return result;
|
|
1134
|
+
}
|
|
908
1135
|
// ---- sub-agents ------------------------------------------------------
|
|
909
1136
|
/**
|
|
910
1137
|
* Run one approved tool call. `delegate` runs a child agent instead of
|
|
911
1138
|
* touching the filesystem, so it is handled here, where the session's model,
|
|
912
1139
|
* approval gate and abort signal live.
|
|
913
1140
|
*/
|
|
914
|
-
async runTool(name, args, toolMsg, token, apiMessages, approvalContext) {
|
|
1141
|
+
async runTool(name, args, toolMsg, token, apiMessages, approvalContext, executionSignal) {
|
|
1142
|
+
const runtime = currentTask()?.runtime;
|
|
1143
|
+
const execute = () => this.dispatchTool(name, args, toolMsg, token, apiMessages, approvalContext, executionSignal);
|
|
1144
|
+
if (!runtime)
|
|
1145
|
+
return execute();
|
|
1146
|
+
const result = await runtime.tool(name, args, execute);
|
|
1147
|
+
const evidence = runtime.evidence.at(-1);
|
|
1148
|
+
if (evidence && this.mode === "pro") {
|
|
1149
|
+
toolMsg.evidenceId = evidence.id;
|
|
1150
|
+
toolMsg.executionStatus = evidence.status;
|
|
1151
|
+
}
|
|
1152
|
+
return result;
|
|
1153
|
+
}
|
|
1154
|
+
async dispatchTool(name, args, toolMsg, token, apiMessages, approvalContext, signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
|
|
1155
|
+
if (name === FINISH_STEP_TOOL && currentTask()) {
|
|
1156
|
+
const result = finishPlanStep(this.plan, args, currentTask().runtime);
|
|
1157
|
+
this.save();
|
|
1158
|
+
this.publishLive();
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
915
1161
|
if (this.mode === "pro" && (name === "call_pro_agent" || name === DELEGATE_TOOL)) {
|
|
916
1162
|
const team = this.proTeam;
|
|
917
1163
|
if (!team)
|
|
918
1164
|
return "[Error: Pro models have not passed preflight]";
|
|
919
1165
|
return team.call(String(args.role ?? "backend_output_model"), String(args.task ?? ""), `${typeof args.context === "string" ? args.context : ""}\n\nCurrent user request: ${approvalContext.prompt}\n` +
|
|
920
|
-
captureApprovalContext(apiMessages).context, 1);
|
|
1166
|
+
captureApprovalContext(apiMessages).context, 1, args.read_only === true, Array.isArray(args.write_paths) ? args.write_paths.filter((p) => typeof p === "string") : undefined);
|
|
921
1167
|
}
|
|
1168
|
+
if (name === "execute_ts")
|
|
1169
|
+
return this.runProgram(args, token, apiMessages, approvalContext);
|
|
922
1170
|
if (name !== DELEGATE_TOOL)
|
|
923
|
-
return executeTool(name, args, this.dir);
|
|
924
|
-
return this.delegate(args, toolMsg, token, apiMessages, approvalContext);
|
|
1171
|
+
return executeTool(name, args, this.dir, signal);
|
|
1172
|
+
return this.delegate(args, toolMsg, token, apiMessages, approvalContext, signal);
|
|
925
1173
|
}
|
|
926
1174
|
/**
|
|
927
1175
|
* Run a sub-agent for one `delegate` call and return its report. The child
|
|
928
1176
|
* works in its own transcript; only the report reaches this conversation, and
|
|
929
1177
|
* its risky calls go through the same approval gate as the parent turn.
|
|
930
1178
|
*/
|
|
931
|
-
async delegate(args, toolMsg, token, apiMessages, approvalContext) {
|
|
1179
|
+
async delegate(args, toolMsg, token, apiMessages, approvalContext, executionSignal) {
|
|
932
1180
|
const task = String(args.task ?? "").trim();
|
|
933
1181
|
if (!task)
|
|
934
1182
|
return "[Error: delegate needs a task: what should the sub-agent do?]";
|
|
935
1183
|
const context = typeof args.context === "string" ? args.context.trim() : "";
|
|
936
1184
|
const approval = { ...approvalContext, context: captureApprovalContext(apiMessages).context };
|
|
1185
|
+
const maxRounds = subAgentRounds(loadConfig(this.mode).subagent_max_rounds);
|
|
1186
|
+
const runtime = currentTask()?.runtime;
|
|
1187
|
+
const signal = executionSignal ?? runtime?.signal ?? this.turnAbort?.signal;
|
|
1188
|
+
const stopped = () => token !== this.turnToken || this.paused || signal?.aborted === true;
|
|
937
1189
|
const run = await runSubAgentTask({
|
|
1190
|
+
runtime, signal,
|
|
1191
|
+
readOnly: args.read_only === true,
|
|
1192
|
+
writePaths: Array.isArray(args.write_paths) ? args.write_paths.filter((p) => typeof p === "string") : undefined,
|
|
1193
|
+
contextWindow: modelContextWindow(this.provider, this.model, this.mode),
|
|
938
1194
|
createClient: () => new LLMClient(this.model, this.provider, this.intensity),
|
|
939
1195
|
system: loadSubAgentPrompt(),
|
|
940
|
-
task: buildSubAgentTask({ task, context, cwd: this.dir }),
|
|
1196
|
+
task: buildSubAgentTask({ task, context, cwd: this.dir, rounds: maxRounds }),
|
|
941
1197
|
tools: SUB_AGENT_TOOLS,
|
|
942
|
-
|
|
1198
|
+
maxRounds,
|
|
1199
|
+
approve: (name, callArgs) => this.requestApproval(name, callArgs, approval, signal),
|
|
943
1200
|
execute: (name, callArgs) => name === DELEGATE_TOOL
|
|
944
1201
|
? Promise.resolve("[Error: sub-agents cannot delegate further]")
|
|
945
|
-
: executeTool(name, callArgs, this.dir),
|
|
1202
|
+
: executeTool(name, callArgs, this.dir, signal),
|
|
946
1203
|
onProgress: (label) => {
|
|
1204
|
+
if (stopped())
|
|
1205
|
+
return;
|
|
947
1206
|
this.subagent = label;
|
|
948
1207
|
toolMsg.result = `… ${label}`;
|
|
949
1208
|
this.publishLive();
|
|
950
1209
|
},
|
|
951
1210
|
onUsage: (usage) => {
|
|
1211
|
+
if (stopped())
|
|
1212
|
+
return;
|
|
952
1213
|
this.tokens.prompt += usage.prompt;
|
|
953
1214
|
this.tokens.completion += usage.completion;
|
|
954
1215
|
},
|
|
955
1216
|
onRetry: (notice) => {
|
|
1217
|
+
if (stopped())
|
|
1218
|
+
return;
|
|
956
1219
|
this.retrying = notice;
|
|
957
1220
|
this.publishLive();
|
|
958
1221
|
},
|
|
959
|
-
cancelled:
|
|
1222
|
+
cancelled: stopped,
|
|
960
1223
|
});
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1224
|
+
if (!stopped()) {
|
|
1225
|
+
this.subagent = null;
|
|
1226
|
+
this.retrying = null;
|
|
1227
|
+
toolMsg.result = TOOL_PENDING;
|
|
1228
|
+
this.publishLive();
|
|
1229
|
+
}
|
|
965
1230
|
return formatSubAgentResult(run);
|
|
966
1231
|
}
|
|
967
1232
|
// ---- compression -----------------------------------------------------
|
|
@@ -1067,7 +1332,7 @@ export class WebSession {
|
|
|
1067
1332
|
if (chat.length === 0)
|
|
1068
1333
|
return true;
|
|
1069
1334
|
try {
|
|
1070
|
-
this.sessionId = saveSession(chat, this.model.name, this.provider, this.intensity, this.dir, this.sessionId, this.tokens, this.context, this.approvalMode, this.plan, this.mode, this.proPreset ?? undefined);
|
|
1335
|
+
this.sessionId = saveSession(chat, this.model.name, this.provider, this.intensity, this.dir, this.sessionId, this.tokens, this.context, this.approvalMode, this.plan, this.mode, this.proPreset ?? undefined, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask);
|
|
1071
1336
|
this.saveError = null;
|
|
1072
1337
|
return true;
|
|
1073
1338
|
}
|