@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/screens/chat.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { Screen } from "../terminal/screen.js";
|
|
2
3
|
import { C } from "../colors.js";
|
|
3
|
-
import { findModel, findConfiguredModel, intensityForModel, contextCompressionThreshold, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled,
|
|
4
|
+
import { findModel, findConfiguredModel, intensityForModel, contextCompressionThreshold, modelContextWindow, sessionDir, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadAgentPrompt, normalizeAgentMode, saveSession, } from "../config.js";
|
|
4
5
|
import { drawHeader } from "../header.js";
|
|
5
6
|
import { computeInputWindow, cursorFromColumn, drawInputBox, drawScrollbar, wrapText, approvalToggle, drawApprovalToggle, inRect } from "../ui.js";
|
|
6
7
|
import { drawSpans, renderMarkdownText } from "../markdown.js";
|
|
7
8
|
import { ellipsize, displayWidth } from "../width.js";
|
|
8
9
|
import { sanitizeText } from "../sanitize.js";
|
|
10
|
+
import { TaskRuntime, currentTask, linkSignals, taskLimits, resumeTaskSnapshot } from "../llm/runtime.js";
|
|
11
|
+
import { prepareContext, outputAllowance, finalTaskReport } from "../llm/context.js";
|
|
12
|
+
import { settlePlan } from "../llm/plan.js";
|
|
9
13
|
import { LLMClient, describeError } from "../llm/client.js";
|
|
10
14
|
import { formatDelay } from "../llm/retry.js";
|
|
11
15
|
import { advancePlan, buildPlannerInput, lastRequestText, needsPlan, parsePlan, planCardRows, planMessage, planRowsWidth, planTranscript, renderStepDirective, resumePlan, stopPlan, } from "../llm/plan.js";
|
|
12
16
|
import { DELEGATE_TOOL, executeTool, getSubAgentToolDefinitions, getToolDefinitions } from "../llm/tools.js";
|
|
13
|
-
import { buildSubAgentTask, formatSubAgentResult, runSubAgent as runSubAgentTask, } from "../llm/subagent.js";
|
|
17
|
+
import { buildSubAgentTask, formatSubAgentResult, runSubAgent as runSubAgentTask, subAgentRounds, } from "../llm/subagent.js";
|
|
14
18
|
import { assessToolCall } from "../llm/risk.js";
|
|
15
19
|
import { ApprovalGate, captureApprovalContext, describeApprovalIntent, statedReason } from "../llm/approval.js";
|
|
16
20
|
import { COMPACT_KEEP, COMPACT_MAX_CHARS_PER_MESSAGE, planCompaction, transcriptFor, modelHistory, normalizeContext, } from "../compact.js";
|
|
@@ -79,6 +83,8 @@ export class ChatScreen extends Screen {
|
|
|
79
83
|
loadedContext = null;
|
|
80
84
|
loadedApprovalMode = null;
|
|
81
85
|
loadedPlan = null;
|
|
86
|
+
loadedTask;
|
|
87
|
+
loadedAgentMode = null;
|
|
82
88
|
approvalMode = "request";
|
|
83
89
|
modelProvider = "DeepSeek";
|
|
84
90
|
model = { name: "deepseek-chat", url: "https://api.deepseek.com/v1", api_key: "", supports_streaming: true };
|
|
@@ -105,7 +111,13 @@ export class ChatScreen extends Screen {
|
|
|
105
111
|
busy = false;
|
|
106
112
|
paused = false;
|
|
107
113
|
queued = null;
|
|
114
|
+
taskRuntime = null;
|
|
115
|
+
lastTask;
|
|
116
|
+
taskOutcome = "completed";
|
|
108
117
|
turnToken = 0;
|
|
118
|
+
agentMode = "standard";
|
|
119
|
+
activeAgentMode = "standard";
|
|
120
|
+
turnAbort = null;
|
|
109
121
|
inputValue = "";
|
|
110
122
|
cursor = 0;
|
|
111
123
|
scrollTop = 0;
|
|
@@ -164,6 +176,7 @@ export class ChatScreen extends Screen {
|
|
|
164
176
|
if (this.initialized)
|
|
165
177
|
return;
|
|
166
178
|
this.initialized = true;
|
|
179
|
+
this.agentMode = normalizeAgentMode(this.loadedAgentMode);
|
|
167
180
|
this.approvalMode = normalizeApprovalMode(this.loadedApprovalMode ?? loadConfig().approval_mode);
|
|
168
181
|
const st = this.app.state;
|
|
169
182
|
this.modelProvider = this.loadedProvider ?? st.selectedProvider;
|
|
@@ -201,6 +214,7 @@ export class ChatScreen extends Screen {
|
|
|
201
214
|
for (const start of this.userMessageIndexes())
|
|
202
215
|
this.finishedTurns.add(start);
|
|
203
216
|
this.plan = resumePlan(this.loadedPlan);
|
|
217
|
+
this.lastTask = resumeTaskSnapshot(this.loadedTask);
|
|
204
218
|
this.context = normalizeContext(this.loadedContext, this.messages.filter((m) => m.role !== "system").length);
|
|
205
219
|
this.liveDir = this.loadedCwd ?? process.cwd();
|
|
206
220
|
this.checkpoint.register();
|
|
@@ -258,6 +272,9 @@ export class ChatScreen extends Screen {
|
|
|
258
272
|
model: this.model.name,
|
|
259
273
|
intensity: this.intensity,
|
|
260
274
|
approvalMode: this.approvalMode,
|
|
275
|
+
agentMode: this.agentMode,
|
|
276
|
+
activeAgentMode: this.busy ? this.activeAgentMode : this.agentMode,
|
|
277
|
+
task: this.taskRuntime?.snapshot() ?? this.lastTask,
|
|
261
278
|
busy: this.busy,
|
|
262
279
|
paused: this.paused,
|
|
263
280
|
queued: this.queued,
|
|
@@ -300,6 +317,11 @@ export class ChatScreen extends Screen {
|
|
|
300
317
|
if (this.paused || (!this.busy && !this.queued))
|
|
301
318
|
return;
|
|
302
319
|
this.paused = true;
|
|
320
|
+
this.subagent = null;
|
|
321
|
+
this.retryNotice = null;
|
|
322
|
+
this.reviewing = null;
|
|
323
|
+
this.planning = false;
|
|
324
|
+
this.turnAbort?.abort();
|
|
303
325
|
this.turnToken++;
|
|
304
326
|
// A queued decision would otherwise keep the agent awaiting forever.
|
|
305
327
|
if (this.pendingApproval)
|
|
@@ -323,6 +345,12 @@ export class ChatScreen extends Screen {
|
|
|
323
345
|
this.settleApproval(approved);
|
|
324
346
|
return true;
|
|
325
347
|
}
|
|
348
|
+
liveSetAgentMode(mode) {
|
|
349
|
+
this.agentMode = mode;
|
|
350
|
+
this.save();
|
|
351
|
+
this.publishLive();
|
|
352
|
+
return { ok: true };
|
|
353
|
+
}
|
|
326
354
|
liveSetApprovalMode(mode) {
|
|
327
355
|
if (this.approvalMode === mode)
|
|
328
356
|
return;
|
|
@@ -344,7 +372,10 @@ export class ChatScreen extends Screen {
|
|
|
344
372
|
this.app.render();
|
|
345
373
|
return;
|
|
346
374
|
}
|
|
375
|
+
this.turnAbort?.abort();
|
|
347
376
|
this.turnToken++;
|
|
377
|
+
this.taskRuntime = null;
|
|
378
|
+
this.lastTask = undefined;
|
|
348
379
|
if (this.pendingApproval)
|
|
349
380
|
this.settleApproval(false);
|
|
350
381
|
this.busy = false;
|
|
@@ -658,6 +689,11 @@ export class ChatScreen extends Screen {
|
|
|
658
689
|
return;
|
|
659
690
|
this.paused = !this.paused;
|
|
660
691
|
if (this.paused) {
|
|
692
|
+
this.reviewing = null;
|
|
693
|
+
this.planning = false;
|
|
694
|
+
this.subagent = null;
|
|
695
|
+
this.retryNotice = null;
|
|
696
|
+
this.turnAbort?.abort();
|
|
661
697
|
this.turnToken++;
|
|
662
698
|
this.busy = false;
|
|
663
699
|
this.app.state.isThinking = false;
|
|
@@ -723,9 +759,9 @@ export class ChatScreen extends Screen {
|
|
|
723
759
|
return null;
|
|
724
760
|
}
|
|
725
761
|
finally {
|
|
726
|
-
this.planning = false;
|
|
727
|
-
this.retryNotice = null;
|
|
728
762
|
if (token === this.turnToken) {
|
|
763
|
+
this.planning = false;
|
|
764
|
+
this.retryNotice = null;
|
|
729
765
|
this.app.render();
|
|
730
766
|
this.publishLive();
|
|
731
767
|
}
|
|
@@ -754,16 +790,58 @@ export class ChatScreen extends Screen {
|
|
|
754
790
|
* own. Without a plan the turn runs in one pass, as before.
|
|
755
791
|
*/
|
|
756
792
|
async runTurn(token, turnMessages, approvalContext) {
|
|
793
|
+
const runtime = new TaskRuntime({
|
|
794
|
+
request: approvalContext.prompt, priorUserRequests: approvalContext.priorUserRequests,
|
|
795
|
+
cwd: this.liveDir, signal: this.turnAbort?.signal, limits: taskLimits(loadConfig("standard")),
|
|
796
|
+
journalDir: path.join(sessionDir("standard"), "tasks", path.basename(this.sessionId || "unsaved")),
|
|
797
|
+
});
|
|
798
|
+
this.taskRuntime = runtime;
|
|
799
|
+
this.taskOutcome = "completed";
|
|
800
|
+
let failed = false;
|
|
801
|
+
try {
|
|
802
|
+
await runtime.run(() => this.runTaskTurn(token, turnMessages, approvalContext));
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
failed = true;
|
|
806
|
+
throw error;
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
await runtime.drain();
|
|
810
|
+
if (runtime.signal.aborted)
|
|
811
|
+
settlePlan(runtime.plan ?? null, true);
|
|
812
|
+
runtime.finish(runtime.signal.aborted ? "cancelled" : failed ? "failed" :
|
|
813
|
+
runtime.plan?.steps.some(step => step.status === "failed") ? "failed" :
|
|
814
|
+
runtime.plan?.steps.some(step => !["done", "skipped"].includes(step.status)) ? "partial" : this.taskOutcome);
|
|
815
|
+
if (this.taskRuntime === runtime) {
|
|
816
|
+
if (runtime.signal.aborted && token === this.turnToken && this.busy && !this.paused) {
|
|
817
|
+
if (this.taskOutcome !== "cancelled")
|
|
818
|
+
this.messages.push({ role: "assistant", content: `[Task stopped: ${String(runtime.signal.reason)}]\n${runtime.partialReport()}` });
|
|
819
|
+
this.streamingContent = null;
|
|
820
|
+
this.onTurnFinished(false);
|
|
821
|
+
}
|
|
822
|
+
if (this.taskRuntime === runtime) {
|
|
823
|
+
this.lastTask = runtime.snapshot();
|
|
824
|
+
this.save();
|
|
825
|
+
this.publishLive();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async runTaskTurn(token, turnMessages, approvalContext) {
|
|
757
831
|
// Shrink the history before building the request when it has outgrown the
|
|
758
832
|
// configured budget.
|
|
759
833
|
await this.autoCompress(token);
|
|
760
834
|
if (token !== this.turnToken)
|
|
761
835
|
return;
|
|
762
|
-
|
|
836
|
+
if (this.activeAgentMode !== "standard")
|
|
837
|
+
this.plan = null;
|
|
838
|
+
const plan = this.activeAgentMode === "standard" ? await this.makePlan(token) : null;
|
|
763
839
|
if (token !== this.turnToken)
|
|
764
840
|
return;
|
|
765
841
|
if (plan) {
|
|
766
842
|
this.plan = plan;
|
|
843
|
+
if (currentTask())
|
|
844
|
+
currentTask().runtime.plan = plan;
|
|
767
845
|
// The plan is an ordinary assistant message: it is saved, folded, replayed
|
|
768
846
|
// and sent back to the model exactly like any other answer, so later turns
|
|
769
847
|
// (and the user) still know what was agreed.
|
|
@@ -783,7 +861,7 @@ export class ChatScreen extends Screen {
|
|
|
783
861
|
const finished = await this.agentLoop(this.buildApiMessages(this.turnView(turnMessages), directive), token, approvalContext);
|
|
784
862
|
if (token !== this.turnToken)
|
|
785
863
|
return;
|
|
786
|
-
this.setStep(i, finished ? "done" : "failed");
|
|
864
|
+
this.setStep(i, currentTask()?.signal.aborted ? "cancelled" : finished ? "done" : "failed");
|
|
787
865
|
// A step that failed stops the plan: the remaining steps assumed this one
|
|
788
866
|
// succeeded, and the user has to see the failure before anything runs on
|
|
789
867
|
// top of it.
|
|
@@ -833,6 +911,19 @@ export class ChatScreen extends Screen {
|
|
|
833
911
|
this.startTurn();
|
|
834
912
|
}
|
|
835
913
|
startTurn() {
|
|
914
|
+
if (this.taskRuntime?.settling) {
|
|
915
|
+
const previous = this.taskRuntime;
|
|
916
|
+
this.queued = lastRequestText(this.messages);
|
|
917
|
+
this.paused = false;
|
|
918
|
+
void previous.drain().then(() => {
|
|
919
|
+
if (this.taskRuntime === previous && this.queued && !this.paused) {
|
|
920
|
+
this.queued = null;
|
|
921
|
+
this.startTurn();
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
this.publishLive();
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
836
927
|
// Sending closes every earlier turn, including one left unfinished by
|
|
837
928
|
// ESC ESC: it is folded from here on, but never while it is still paused.
|
|
838
929
|
const starts = this.userMessageIndexes();
|
|
@@ -846,6 +937,9 @@ export class ChatScreen extends Screen {
|
|
|
846
937
|
this.busy = true;
|
|
847
938
|
this.paused = false;
|
|
848
939
|
this.app.state.isThinking = true;
|
|
940
|
+
this.activeAgentMode = this.agentMode;
|
|
941
|
+
this.turnAbort?.abort();
|
|
942
|
+
this.turnAbort = new AbortController();
|
|
849
943
|
const token = ++this.turnToken;
|
|
850
944
|
this.streamingContent = "";
|
|
851
945
|
this.app.render();
|
|
@@ -890,7 +984,9 @@ export class ChatScreen extends Screen {
|
|
|
890
984
|
buildApiMessages(messages = this.messages, directive = "") {
|
|
891
985
|
// A step directive extends the system prompt rather than adding a user
|
|
892
986
|
// message: the plan is an instruction, not something the user just said.
|
|
893
|
-
const
|
|
987
|
+
const prompt = loadAgentPrompt(this.activeAgentMode);
|
|
988
|
+
const system = (directive ? `${prompt}\n\n${directive}` : prompt) +
|
|
989
|
+
(currentTask() ? `\n\n${currentTask().runtime.instructions()}` : "");
|
|
894
990
|
const api = [{ role: "system", content: system }];
|
|
895
991
|
for (const m of modelHistory(messages, this.context)) {
|
|
896
992
|
if (m.role === "user" || m.role === "assistant") {
|
|
@@ -912,6 +1008,9 @@ export class ChatScreen extends Screen {
|
|
|
912
1008
|
* because a planned turn runs this once per step.
|
|
913
1009
|
*/
|
|
914
1010
|
async agentLoop(apiMessages, token, approvalContext) {
|
|
1011
|
+
const tools = this.activeAgentMode === "ptc"
|
|
1012
|
+
? [...TOOLS, (await import("../llm/codemode.js")).getCodeToolDefinition()] : TOOLS;
|
|
1013
|
+
let emptyReplies = 0;
|
|
915
1014
|
try {
|
|
916
1015
|
while (true) {
|
|
917
1016
|
if (token !== this.turnToken)
|
|
@@ -919,11 +1018,19 @@ export class ChatScreen extends Screen {
|
|
|
919
1018
|
// Keep the payload inside budget during a long single turn: once the
|
|
920
1019
|
// context is too large, drop the body of older tool results.
|
|
921
1020
|
apiMessages = this.trimApiMessages(apiMessages);
|
|
1021
|
+
if (currentTask())
|
|
1022
|
+
await prepareContext({
|
|
1023
|
+
messages: apiMessages, tools, capacity: modelContextWindow(this.modelProvider, this.model, "standard"),
|
|
1024
|
+
createClient: () => new LLMClient(this.model, this.modelProvider, this.intensity),
|
|
1025
|
+
stream: { signal: currentTask().runtime.signal, cancelled: () => token !== this.turnToken || this.paused },
|
|
1026
|
+
onUsage: usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; },
|
|
1027
|
+
});
|
|
922
1028
|
const client = new LLMClient(this.model, this.modelProvider, this.intensity);
|
|
923
1029
|
let content = "";
|
|
924
1030
|
let hasContent = false;
|
|
925
1031
|
let aborted = false;
|
|
926
|
-
for await (const chunk of client.streamTurn(apiMessages,
|
|
1032
|
+
for await (const chunk of client.streamTurn(apiMessages, tools, {
|
|
1033
|
+
maxOutputTokens: outputAllowance(modelContextWindow(this.modelProvider, this.model)),
|
|
927
1034
|
// A retried request can go quiet for up to 160s, so the status line
|
|
928
1035
|
// says why instead of looking hung. The moment the user pauses or a
|
|
929
1036
|
// newer turn takes over, the backoff is dropped instead of waited out.
|
|
@@ -968,8 +1075,42 @@ export class ChatScreen extends Screen {
|
|
|
968
1075
|
this.app.render();
|
|
969
1076
|
return false;
|
|
970
1077
|
}
|
|
1078
|
+
if (currentTask()?.signal.aborted || client.outcome?.status === "cancelled") {
|
|
1079
|
+
this.taskOutcome = "cancelled";
|
|
1080
|
+
this.streamingContent = null;
|
|
1081
|
+
this.messages.push({ role: "assistant", content: `[Task stopped: ${String(currentTask()?.signal.reason ?? "Task cancelled")}]\n${currentTask()?.runtime.partialReport() ?? ""}` });
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
if (client.outcome?.error?.code.endsWith("_budget") && currentTask()) {
|
|
1085
|
+
const report = await finalTaskReport(currentTask().runtime, () => new LLMClient(this.model, this.modelProvider, this.intensity), usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; });
|
|
1086
|
+
if (token !== this.turnToken || this.paused)
|
|
1087
|
+
return false;
|
|
1088
|
+
this.taskOutcome = "partial";
|
|
1089
|
+
this.streamingContent = null;
|
|
1090
|
+
this.messages.push({ role: "assistant", content: report });
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
if (client.outcome?.status === "failed") {
|
|
1094
|
+
const recorded = currentTask()?.runtime.evidence.length ? currentTask().runtime.partialReport() : "";
|
|
1095
|
+
const message = client.outcome.error?.message ?? "Model request failed";
|
|
1096
|
+
this.taskOutcome = recorded ? "partial" : "failed";
|
|
1097
|
+
this.streamingContent = null;
|
|
1098
|
+
this.messages.push({ role: "assistant", content: content +
|
|
1099
|
+
(content.includes(message) ? "" : `\n[Error: ${message}]`) + (recorded ? `\n\n${recorded}` : "") });
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
971
1102
|
const toolCalls = client.toolCalls;
|
|
972
1103
|
if (toolCalls.length === 0) {
|
|
1104
|
+
if (!content.trim() && emptyReplies++ === 0)
|
|
1105
|
+
continue;
|
|
1106
|
+
if (!content.trim()) {
|
|
1107
|
+
this.taskOutcome = "failed";
|
|
1108
|
+
if (!content.trim())
|
|
1109
|
+
this.messages.push({ role: "assistant", content: "[Error: model returned an empty completion.]" });
|
|
1110
|
+
else
|
|
1111
|
+
this.messages.push({ role: "assistant", content });
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
973
1114
|
if (hasContent)
|
|
974
1115
|
this.messages.push({ role: "assistant", content });
|
|
975
1116
|
this.streamingContent = null;
|
|
@@ -1044,6 +1185,7 @@ export class ChatScreen extends Screen {
|
|
|
1044
1185
|
catch (e) {
|
|
1045
1186
|
if (token !== this.turnToken)
|
|
1046
1187
|
return false;
|
|
1188
|
+
this.taskOutcome = "failed";
|
|
1047
1189
|
const err = describeError(e);
|
|
1048
1190
|
this.messages.push({ role: "assistant", content: (this.streamingContent ?? "") + `\n[Error: ${err}]` });
|
|
1049
1191
|
this.streamingContent = null;
|
|
@@ -1075,18 +1217,34 @@ export class ChatScreen extends Screen {
|
|
|
1075
1217
|
}
|
|
1076
1218
|
// ---- approval gate ---------------------------------------------------
|
|
1077
1219
|
/** Classify a tool call and, when it is risky, run it past the gate. */
|
|
1078
|
-
async requestApproval(tool, args, context = captureApprovalContext(this.messages)) {
|
|
1220
|
+
async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
|
|
1221
|
+
const linked = linkSignals(signal, currentTask()?.signal);
|
|
1222
|
+
signal = linked.signal;
|
|
1223
|
+
try {
|
|
1224
|
+
currentTask()?.runtime.assertTool(tool, args);
|
|
1225
|
+
}
|
|
1226
|
+
catch (error) {
|
|
1227
|
+
linked.dispose();
|
|
1228
|
+
return { approved: false, source: "input", reason: error instanceof Error ? error.message : String(error) };
|
|
1229
|
+
}
|
|
1079
1230
|
const risk = assessToolCall(tool, args, this.liveDir);
|
|
1080
|
-
if (!risk)
|
|
1231
|
+
if (!risk) {
|
|
1232
|
+
linked.dispose();
|
|
1081
1233
|
return { approved: true, source: "prompt", reason: "routine call" };
|
|
1234
|
+
}
|
|
1235
|
+
const approvalToken = this.turnToken;
|
|
1236
|
+
const cancelled = () => approvalToken !== this.turnToken || this.paused || signal?.aborted === true;
|
|
1082
1237
|
const gate = new ApprovalGate({
|
|
1083
|
-
|
|
1238
|
+
cancelled,
|
|
1239
|
+
askHuman: () => cancelled() ? Promise.resolve(false) : this.askHuman(tool, args, risk),
|
|
1084
1240
|
reviewer: () => this.reviewerModel(),
|
|
1085
1241
|
autoApprovalEnabled: () => this.approvalMode === "auto",
|
|
1086
1242
|
extremeAutoApprovalEnabled: () => loadConfig().auto_approve_extreme_risk === true,
|
|
1087
1243
|
// Reviews happen before any card is shown, so without this the app looks
|
|
1088
1244
|
// frozen while the independent assessments run.
|
|
1089
1245
|
onReview: (info) => {
|
|
1246
|
+
if (cancelled())
|
|
1247
|
+
return;
|
|
1090
1248
|
this.reviewing = info ? {
|
|
1091
1249
|
total: info.total, round: info.round ?? 1, rounds: info.rounds ?? 1,
|
|
1092
1250
|
request: liveApprovalDetails(tool, args, risk, this.liveDir),
|
|
@@ -1103,7 +1261,18 @@ export class ChatScreen extends Screen {
|
|
|
1103
1261
|
this.app.showToast(`independent review running (${which}${info.total} reviewer${info.total > 1 ? "s" : ""})...`);
|
|
1104
1262
|
},
|
|
1105
1263
|
});
|
|
1106
|
-
|
|
1264
|
+
const abortApproval = () => {
|
|
1265
|
+
if (this.pendingApproval?.args === args)
|
|
1266
|
+
this.settleApproval(false);
|
|
1267
|
+
};
|
|
1268
|
+
signal.addEventListener("abort", abortApproval, { once: true });
|
|
1269
|
+
try {
|
|
1270
|
+
return await gate.decide({ tool, args, cwd: this.liveDir, ...context, risk });
|
|
1271
|
+
}
|
|
1272
|
+
finally {
|
|
1273
|
+
signal.removeEventListener("abort", abortApproval);
|
|
1274
|
+
linked.dispose();
|
|
1275
|
+
}
|
|
1107
1276
|
}
|
|
1108
1277
|
/** Park the turn until the user answers; the header turns orange meanwhile. */
|
|
1109
1278
|
askHuman(tool, args, risk) {
|
|
@@ -1193,15 +1362,97 @@ export class ChatScreen extends Screen {
|
|
|
1193
1362
|
};
|
|
1194
1363
|
}
|
|
1195
1364
|
// ---- sub-agents ------------------------------------------------------
|
|
1365
|
+
async runProgram(args, token, apiMessages, approvalContext) {
|
|
1366
|
+
if (this.activeAgentMode !== "ptc")
|
|
1367
|
+
return "[Error: execute_ts requires PTC mode]";
|
|
1368
|
+
const { executeCodeProgram, ProgramToolDenied } = await import("../llm/codemode.js");
|
|
1369
|
+
const signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal;
|
|
1370
|
+
const cancelled = () => token !== this.turnToken || this.paused || signal?.aborted;
|
|
1371
|
+
const result = await executeCodeProgram(args, {
|
|
1372
|
+
signal,
|
|
1373
|
+
onPlan: plan => {
|
|
1374
|
+
if (cancelled())
|
|
1375
|
+
return;
|
|
1376
|
+
this.plan = plan;
|
|
1377
|
+
if (currentTask())
|
|
1378
|
+
currentTask().runtime.plan = plan;
|
|
1379
|
+
this.save();
|
|
1380
|
+
this.publishLive();
|
|
1381
|
+
this.app.render();
|
|
1382
|
+
},
|
|
1383
|
+
onStep: (index, status) => { if (!cancelled())
|
|
1384
|
+
this.setStep(index, status); },
|
|
1385
|
+
call: async (name, callArgs, programSignal) => {
|
|
1386
|
+
if (cancelled() || programSignal?.aborted)
|
|
1387
|
+
throw new Error("Execution cancelled");
|
|
1388
|
+
const message = { role: "tool", name, arguments: callArgs, result: "…" };
|
|
1389
|
+
this.messages.push(message);
|
|
1390
|
+
this.save();
|
|
1391
|
+
this.publishLive();
|
|
1392
|
+
this.app.render();
|
|
1393
|
+
const stopApproval = () => {
|
|
1394
|
+
if (this.pendingApproval?.args !== callArgs)
|
|
1395
|
+
return;
|
|
1396
|
+
this.settleApproval(false);
|
|
1397
|
+
};
|
|
1398
|
+
programSignal?.addEventListener("abort", stopApproval, { once: true });
|
|
1399
|
+
try {
|
|
1400
|
+
const decision = await this.requestApproval(name, callArgs, {
|
|
1401
|
+
...approvalContext, context: captureApprovalContext(apiMessages).context,
|
|
1402
|
+
}, programSignal);
|
|
1403
|
+
if (cancelled() || programSignal?.aborted)
|
|
1404
|
+
throw new Error("Execution cancelled");
|
|
1405
|
+
if (!decision.approved) {
|
|
1406
|
+
message.result = `[Not executed: ${decision.reason}]`;
|
|
1407
|
+
throw new ProgramToolDenied(`Approval denied: ${decision.reason}. Do not retry or work around this denial.`);
|
|
1408
|
+
}
|
|
1409
|
+
const value = await this.runTool(name, callArgs, message, token, apiMessages, approvalContext, programSignal);
|
|
1410
|
+
message.result = value;
|
|
1411
|
+
if (cancelled() || programSignal?.aborted)
|
|
1412
|
+
throw new Error("Execution cancelled");
|
|
1413
|
+
return value;
|
|
1414
|
+
}
|
|
1415
|
+
catch (error) {
|
|
1416
|
+
if (message.result === "…")
|
|
1417
|
+
message.result = `[Error: ${error instanceof Error ? error.message : String(error)}]`;
|
|
1418
|
+
throw error;
|
|
1419
|
+
}
|
|
1420
|
+
finally {
|
|
1421
|
+
programSignal?.removeEventListener("abort", stopApproval);
|
|
1422
|
+
if (this.messages.includes(message)) {
|
|
1423
|
+
this.save();
|
|
1424
|
+
this.publishLive();
|
|
1425
|
+
this.app.render();
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
if (!cancelled()) {
|
|
1431
|
+
this.save();
|
|
1432
|
+
this.publishLive();
|
|
1433
|
+
this.app.render();
|
|
1434
|
+
}
|
|
1435
|
+
return result;
|
|
1436
|
+
}
|
|
1196
1437
|
/**
|
|
1197
1438
|
* Run one approved tool call. `delegate` is handled here rather than inside
|
|
1198
1439
|
* tools.ts: a sub-agent has to share this turn's model, approval gate, working
|
|
1199
1440
|
* directory and abort signal, and only the session knows about those.
|
|
1200
1441
|
*/
|
|
1201
|
-
async runTool(name, args, toolMsg, token, apiMessages, approvalContext) {
|
|
1442
|
+
async runTool(name, args, toolMsg, token, apiMessages, approvalContext, executionSignal) {
|
|
1443
|
+
const runtime = currentTask()?.runtime;
|
|
1444
|
+
const execute = () => this.dispatchTool(name, args, toolMsg, token, apiMessages, approvalContext, executionSignal);
|
|
1445
|
+
if (!runtime)
|
|
1446
|
+
return execute();
|
|
1447
|
+
const result = await runtime.tool(name, args, execute);
|
|
1448
|
+
return result;
|
|
1449
|
+
}
|
|
1450
|
+
async dispatchTool(name, args, toolMsg, token, apiMessages, approvalContext, signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
|
|
1451
|
+
if (name === "execute_ts")
|
|
1452
|
+
return this.runProgram(args, token, apiMessages, approvalContext);
|
|
1202
1453
|
if (name !== DELEGATE_TOOL)
|
|
1203
|
-
return executeTool(name, args, this.liveDir);
|
|
1204
|
-
return this.delegate(args, toolMsg, token, apiMessages, approvalContext);
|
|
1454
|
+
return executeTool(name, args, this.liveDir, signal);
|
|
1455
|
+
return this.delegate(args, toolMsg, token, apiMessages, approvalContext, signal);
|
|
1205
1456
|
}
|
|
1206
1457
|
/**
|
|
1207
1458
|
* Run a sub-agent for one `delegate` call and return its report.
|
|
@@ -1211,22 +1462,33 @@ export class ChatScreen extends Screen {
|
|
|
1211
1462
|
* part of this transcript. While it runs, its current phase is shown in the
|
|
1212
1463
|
* status line and in the tool block, so a quiet log still has an explanation.
|
|
1213
1464
|
*/
|
|
1214
|
-
async delegate(args, toolMsg, token, apiMessages, approvalContext) {
|
|
1465
|
+
async delegate(args, toolMsg, token, apiMessages, approvalContext, executionSignal) {
|
|
1215
1466
|
const task = String(args.task ?? "").trim();
|
|
1216
1467
|
if (!task)
|
|
1217
1468
|
return "[Error: delegate needs a task: what should the sub-agent do?]";
|
|
1218
1469
|
const context = typeof args.context === "string" ? args.context.trim() : "";
|
|
1219
1470
|
const approval = { ...approvalContext, context: captureApprovalContext(apiMessages).context };
|
|
1471
|
+
const maxRounds = subAgentRounds(loadConfig().subagent_max_rounds);
|
|
1472
|
+
const runtime = currentTask()?.runtime;
|
|
1473
|
+
const signal = executionSignal ?? runtime?.signal ?? this.turnAbort?.signal;
|
|
1474
|
+
const stopped = () => token !== this.turnToken || this.paused || signal?.aborted === true;
|
|
1220
1475
|
const run = await runSubAgentTask({
|
|
1476
|
+
runtime, signal,
|
|
1477
|
+
readOnly: args.read_only === true,
|
|
1478
|
+
writePaths: Array.isArray(args.write_paths) ? args.write_paths.filter((p) => typeof p === "string") : undefined,
|
|
1479
|
+
contextWindow: modelContextWindow(this.modelProvider, this.model, "standard"),
|
|
1221
1480
|
createClient: () => new LLMClient(this.model, this.modelProvider, this.intensity),
|
|
1222
1481
|
system: loadSubAgentPrompt(),
|
|
1223
|
-
task: buildSubAgentTask({ task, context, cwd: this.liveDir }),
|
|
1482
|
+
task: buildSubAgentTask({ task, context, cwd: this.liveDir, rounds: maxRounds }),
|
|
1224
1483
|
tools: SUB_AGENT_TOOLS,
|
|
1225
|
-
|
|
1484
|
+
maxRounds,
|
|
1485
|
+
approve: (name, callArgs) => this.requestApproval(name, callArgs, approval, signal),
|
|
1226
1486
|
execute: (name, callArgs) => name === DELEGATE_TOOL
|
|
1227
1487
|
? Promise.resolve("[Error: sub-agents cannot delegate further]")
|
|
1228
|
-
: executeTool(name, callArgs, this.liveDir),
|
|
1488
|
+
: executeTool(name, callArgs, this.liveDir, signal),
|
|
1229
1489
|
onProgress: (label) => {
|
|
1490
|
+
if (stopped())
|
|
1491
|
+
return;
|
|
1230
1492
|
this.subagent = label;
|
|
1231
1493
|
// The tool block is what a reader of the transcript sees while the child
|
|
1232
1494
|
// works; the status line says the same thing at the top of the screen.
|
|
@@ -1235,24 +1497,30 @@ export class ChatScreen extends Screen {
|
|
|
1235
1497
|
this.publishLive();
|
|
1236
1498
|
},
|
|
1237
1499
|
onUsage: (usage) => {
|
|
1500
|
+
if (stopped())
|
|
1501
|
+
return;
|
|
1238
1502
|
this.tokens.prompt += usage.prompt;
|
|
1239
1503
|
this.tokens.completion += usage.completion;
|
|
1240
1504
|
},
|
|
1241
1505
|
onRetry: (notice) => {
|
|
1506
|
+
if (stopped())
|
|
1507
|
+
return;
|
|
1242
1508
|
this.retryNotice =
|
|
1243
1509
|
`Sub-agent — network hiccup, retrying in ${formatDelay(notice.delayMs)} ` +
|
|
1244
1510
|
`(attempt ${notice.attempt}/${notice.attempts})`;
|
|
1245
1511
|
this.app.render();
|
|
1246
1512
|
},
|
|
1247
|
-
cancelled: () => token !== this.turnToken || this.paused,
|
|
1513
|
+
cancelled: () => token !== this.turnToken || this.paused || signal?.aborted === true,
|
|
1248
1514
|
});
|
|
1249
1515
|
// The final report replaces the live marker, and the status line goes back
|
|
1250
1516
|
// to the parent's own state.
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1517
|
+
if (!stopped()) {
|
|
1518
|
+
this.subagent = null;
|
|
1519
|
+
this.retryNotice = null;
|
|
1520
|
+
toolMsg.result = "";
|
|
1521
|
+
this.app.render();
|
|
1522
|
+
this.publishLive();
|
|
1523
|
+
}
|
|
1256
1524
|
return formatSubAgentResult(run);
|
|
1257
1525
|
}
|
|
1258
1526
|
// ---- turn folding ----------------------------------------------------
|
|
@@ -1525,7 +1793,7 @@ export class ChatScreen extends Screen {
|
|
|
1525
1793
|
if (chatMessages.length === 0)
|
|
1526
1794
|
return true;
|
|
1527
1795
|
try {
|
|
1528
|
-
this.sessionId = saveSession(chatMessages, this.model.name, this.modelProvider, this.intensity, this.liveDir, this.sessionId ?? undefined, this.tokens, this.context, this.approvalMode, this.plan);
|
|
1796
|
+
this.sessionId = saveSession(chatMessages, this.model.name, this.modelProvider, this.intensity, this.liveDir, this.sessionId ?? undefined, this.tokens, this.context, this.approvalMode, this.plan, "standard", undefined, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask);
|
|
1529
1797
|
this.saveError = null;
|
|
1530
1798
|
return true;
|
|
1531
1799
|
}
|
|
@@ -1621,7 +1889,7 @@ export class ChatScreen extends Screen {
|
|
|
1621
1889
|
add({ spans: [{ text: `⚙ ${m.name}`, style: { fg: C.toolHeader, bold: true } }] }, mi);
|
|
1622
1890
|
// The line under the tool name is the call's own material: a command, a
|
|
1623
1891
|
// path, or — for `delegate` — the task handed to the sub-agent.
|
|
1624
|
-
const argText = sanitizeText(m.arguments.command ?? m.arguments.path ?? m.arguments.task ?? "", { singleLine: true });
|
|
1892
|
+
const argText = sanitizeText(m.arguments.command ?? m.arguments.path ?? m.arguments.query ?? m.arguments.task ?? "", { singleLine: true });
|
|
1625
1893
|
if (argText)
|
|
1626
1894
|
add({ spans: [{ text: " " + argText, style: { fg: C.dim } }] }, mi);
|
|
1627
1895
|
const decision = this.approvals.get(m);
|
|
@@ -1674,6 +1942,10 @@ export class ChatScreen extends Screen {
|
|
|
1674
1942
|
status = `History save failed: ${sanitizeText(this.saveError, { singleLine: true })}`;
|
|
1675
1943
|
statusStyle = { fg: C.orange, bold: true };
|
|
1676
1944
|
}
|
|
1945
|
+
else if (this.taskRuntime?.snapshot().state === "cancelling") {
|
|
1946
|
+
status = "Stopping tasks…";
|
|
1947
|
+
statusStyle = { fg: C.amber, bold: true };
|
|
1948
|
+
}
|
|
1677
1949
|
else if (this.compressing) {
|
|
1678
1950
|
status = "Compressing context…";
|
|
1679
1951
|
statusStyle = { fg: C.amber, bold: true };
|
package/dist/update.js
CHANGED
|
@@ -29,9 +29,15 @@ export function updateStatus() {
|
|
|
29
29
|
* announce", which is what the next start is for. Concurrent callers share the
|
|
30
30
|
* one lookup, and a successful answer is kept for the rest of the process so
|
|
31
31
|
* the terminal and the Web console cannot disagree.
|
|
32
|
+
*
|
|
33
|
+
* Start-up asks once and is served from that answer from then on. A caller that
|
|
34
|
+
* passes `refresh` — the settings button, the background timer — queries the
|
|
35
|
+
* registry again, so an answer from an hour ago cannot make a check look idle.
|
|
36
|
+
* A failed refresh changes nothing: the last good answer stays, and the caller
|
|
37
|
+
* learns what went wrong from `error`.
|
|
32
38
|
*/
|
|
33
|
-
export function checkForUpdate() {
|
|
34
|
-
if (published)
|
|
39
|
+
export function checkForUpdate(options = {}) {
|
|
40
|
+
if (published && options.refresh !== true)
|
|
35
41
|
return Promise.resolve(updateStatus());
|
|
36
42
|
pending ??= lookupLatestVersion()
|
|
37
43
|
.then((version) => {
|