@xingjisoft/neutrino 0.6.0 → 0.7.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 +6 -1
- package/assets/fast-planner.txt +57 -0
- package/assets/fast-replanner.txt +28 -0
- package/dist/agents.d.ts +25 -0
- package/dist/app.d.ts +206 -0
- package/dist/app.js +15 -0
- package/dist/background.d.ts +96 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +40 -3
- package/dist/browser.d.ts +18 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/colors.d.ts +29 -0
- package/dist/compact.d.ts +52 -0
- package/dist/config.d.ts +221 -0
- package/dist/config.js +22 -7
- package/dist/cwd.d.ts +19 -0
- package/dist/fast/artifacts.d.ts +26 -0
- package/dist/fast/artifacts.js +109 -0
- package/dist/fast/engine.d.ts +78 -0
- package/dist/fast/engine.js +657 -0
- package/dist/fast/evidence.d.ts +21 -0
- package/dist/fast/evidence.js +68 -0
- package/dist/fast/planner.d.ts +10 -0
- package/dist/fast/planner.js +51 -0
- package/dist/fast/pool.d.ts +17 -0
- package/dist/fast/pool.js +78 -0
- package/dist/fast/scheduler.d.ts +54 -0
- package/dist/fast/scheduler.js +383 -0
- package/dist/fast/types.d.ts +100 -0
- package/dist/fast/types.js +21 -0
- package/dist/fast/worker.d.ts +23 -0
- package/dist/fast/worker.js +185 -0
- package/dist/header.d.ts +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/llm/approval.d.ts +204 -0
- package/dist/llm/approval.js +14 -3
- package/dist/llm/client.d.ts +118 -0
- package/dist/llm/client.js +57 -8
- package/dist/llm/codeExecutor.d.ts +14 -0
- package/dist/llm/codemode.d.ts +15 -0
- package/dist/llm/context.d.ts +29 -0
- package/dist/llm/context.js +17 -4
- package/dist/llm/diagnostics.d.ts +91 -0
- package/dist/llm/diagnostics.js +242 -0
- package/dist/llm/plan.d.ts +148 -0
- package/dist/llm/retry.d.ts +80 -0
- package/dist/llm/risk.d.ts +21 -0
- package/dist/llm/risk.js +7 -0
- package/dist/llm/runtime.d.ts +189 -0
- package/dist/llm/runtime.js +178 -24
- package/dist/llm/search.d.ts +2 -0
- package/dist/llm/searchNetwork.d.ts +8 -0
- package/dist/llm/subagent.d.ts +112 -0
- package/dist/llm/subagent.js +25 -12
- package/dist/llm/tools.d.ts +20 -0
- package/dist/llm/tools.js +4 -1
- package/dist/llm/workspace.d.ts +6 -0
- package/dist/llm/workspace.js +271 -0
- package/dist/markdown.d.ts +58 -0
- package/dist/migration.d.ts +8 -0
- package/dist/persistence.d.ts +14 -0
- package/dist/plugins/catalog.d.ts +20 -0
- package/dist/plugins/catalog.js +45 -0
- package/dist/plugins/cli.d.ts +1 -0
- package/dist/plugins/cli.js +106 -0
- package/dist/plugins/host.d.ts +121 -0
- package/dist/plugins/host.js +450 -0
- package/dist/plugins/manager.d.ts +24 -0
- package/dist/plugins/manager.js +288 -0
- package/dist/plugins/package.d.ts +5 -0
- package/dist/plugins/package.js +53 -0
- package/dist/plugins/runner.d.ts +1 -0
- package/dist/plugins/runner.js +227 -0
- package/dist/plugins/session.d.ts +12 -0
- package/dist/plugins/session.js +97 -0
- package/dist/plugins/terminal.d.ts +22 -0
- package/dist/plugins/terminal.js +33 -0
- package/dist/plugins/types.d.ts +178 -0
- package/dist/plugins/types.js +6 -0
- package/dist/plugins/validation.d.ts +14 -0
- package/dist/plugins/validation.js +174 -0
- package/dist/plugins/web-sdk.d.ts +58 -0
- package/dist/plugins/web-sdk.js +1 -0
- package/dist/pro/agents.d.ts +34 -0
- package/dist/pro/agents.js +1 -1
- package/dist/pro/config.d.ts +75 -0
- package/dist/pro/config.js +3 -2
- package/dist/pro/preflight.d.ts +21 -0
- package/dist/sanitize.d.ts +34 -0
- package/dist/screens/agentConversation.d.ts +28 -0
- package/dist/screens/agentHistory.d.ts +25 -0
- package/dist/screens/agentSelect.d.ts +16 -0
- package/dist/screens/chat.d.ts +292 -0
- package/dist/screens/chat.js +179 -65
- package/dist/screens/history.d.ts +23 -0
- package/dist/screens/modelSelect.d.ts +29 -0
- package/dist/screens/plugins.d.ts +29 -0
- package/dist/screens/plugins.js +196 -0
- package/dist/screens/settings.d.ts +42 -0
- package/dist/screens/start.d.ts +43 -0
- package/dist/screens/start.js +25 -17
- package/dist/screens/updatePrompt.d.ts +41 -0
- package/dist/selection.d.ts +75 -0
- package/dist/state.d.ts +26 -0
- package/dist/terminal/buffer.d.ts +47 -0
- package/dist/terminal/input.d.ts +70 -0
- package/dist/terminal/renderer.d.ts +43 -0
- package/dist/terminal/restore.d.ts +41 -0
- package/dist/terminal/screen.d.ts +57 -0
- package/dist/tray.d.ts +14 -0
- package/dist/ui.d.ts +123 -0
- package/dist/update.d.ts +71 -0
- package/dist/updateWatch.d.ts +43 -0
- package/dist/web/approvalQueue.d.ts +8 -0
- package/dist/web/approvalQueue.js +59 -0
- package/dist/web/auth.d.ts +14 -0
- package/dist/web/clientState.d.ts +16 -0
- package/dist/web/folderPicker.d.ts +34 -0
- package/dist/web/folderPicker.js +544 -0
- package/dist/web/hub.d.ts +213 -0
- package/dist/web/hub.js +14 -2
- package/dist/web/localApi.d.ts +11 -0
- package/dist/web/localApi.js +260 -0
- package/dist/web/plugins.d.ts +8 -0
- package/dist/web/plugins.js +64 -0
- package/dist/web/server.d.ts +81 -0
- package/dist/web/server.js +127 -19
- package/dist/web/service.d.ts +7 -0
- package/dist/web/service.js +1 -0
- package/dist/web/session.d.ts +176 -0
- package/dist/web/session.js +170 -21
- package/dist/web/settings.d.ts +65 -0
- package/dist/web/terminalBridge.d.ts +15 -0
- package/dist/web/terminalBridge.js +8 -2
- package/dist/width.d.ts +6 -0
- package/docs/plugins/README.md +126 -0
- package/docs/plugins/agent-sdk.md +92 -0
- package/docs/plugins/architecture.md +64 -0
- package/docs/plugins/compatibility.md +34 -0
- package/docs/plugins/development.md +55 -0
- package/docs/plugins/manifest.md +59 -0
- package/docs/plugins/manifest.schema.json +286 -0
- package/docs/plugins/security.md +53 -0
- package/docs/plugins/skills-prompts.md +69 -0
- package/docs/plugins/ui-sdk.md +109 -0
- package/examples/local-api.py +105 -0
- package/examples/plugins/cards/card.html +6 -0
- package/examples/plugins/cards/neutrino.plugin.json +42 -0
- package/examples/plugins/composer/editor.html +10 -0
- package/examples/plugins/composer/neutrino.plugin.json +10 -0
- package/examples/plugins/function-tools/agent.js +32 -0
- package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/panel.html +6 -0
- package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
- package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
- package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
- package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
- package/examples/plugins/theme/neutrino.plugin.json +12 -0
- package/package.json +66 -42
- package/web/LOCAL-API.md +65 -0
- package/web/README.md +30 -8
- package/web/app.js +256 -67
- package/web/i18n.js +18 -3
- package/web/index.html +3 -1
- package/web/plugins.css +10 -0
- package/web/plugins.js +230 -0
- package/web/style.css +12 -3
- package/web/turns.js +7 -8
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { performance } from "node:perf_hooks";
|
|
4
|
+
import { linkSignals } from "../llm/runtime.js";
|
|
5
|
+
export class FastTaskError extends Error {
|
|
6
|
+
sideEffects;
|
|
7
|
+
constructor(message, sideEffects = false) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.sideEffects = sideEffects;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const verifyId = (id) => `${id}.verify`;
|
|
13
|
+
const overlaps = (a, b) => {
|
|
14
|
+
const rel = path.relative(a, b);
|
|
15
|
+
return rel === "" || (!path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`));
|
|
16
|
+
};
|
|
17
|
+
function canonical(input) {
|
|
18
|
+
const tail = [];
|
|
19
|
+
let cursor = path.resolve(input);
|
|
20
|
+
while (!fs.existsSync(cursor)) {
|
|
21
|
+
const parent = path.dirname(cursor);
|
|
22
|
+
if (parent === cursor)
|
|
23
|
+
break;
|
|
24
|
+
tail.unshift(path.basename(cursor));
|
|
25
|
+
cursor = parent;
|
|
26
|
+
}
|
|
27
|
+
return path.join(fs.realpathSync(cursor), ...tail).toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
export function withinLease(file, roots, cwd) {
|
|
30
|
+
const target = canonical(path.resolve(cwd, file));
|
|
31
|
+
return roots.some(root => overlaps(canonical(path.resolve(cwd, root)), target));
|
|
32
|
+
}
|
|
33
|
+
/** Event driven DAG: a verified dependency unlocks its successors immediately. */
|
|
34
|
+
export class DynamicScheduler {
|
|
35
|
+
store;
|
|
36
|
+
settings;
|
|
37
|
+
options;
|
|
38
|
+
tasks = new Map();
|
|
39
|
+
running = new Map();
|
|
40
|
+
wake;
|
|
41
|
+
change = 0;
|
|
42
|
+
closed = false;
|
|
43
|
+
producerError;
|
|
44
|
+
cancelled = false;
|
|
45
|
+
pinned = new Map();
|
|
46
|
+
diagnosticWaits = new Map();
|
|
47
|
+
schedulingMs = 0;
|
|
48
|
+
retries = 0;
|
|
49
|
+
constructor(store, settings, options) {
|
|
50
|
+
this.store = store;
|
|
51
|
+
this.settings = settings;
|
|
52
|
+
this.options = options;
|
|
53
|
+
}
|
|
54
|
+
add(definition) {
|
|
55
|
+
if (this.plannedTaskCount >= this.settings.maxTasks)
|
|
56
|
+
throw new Error("Fast task count limit exceeded");
|
|
57
|
+
if (!/^[a-zA-Z0-9_-]{1,80}$/.test(definition.task_id) || this.tasks.has(definition.task_id))
|
|
58
|
+
throw new Error("Invalid or duplicate task_id");
|
|
59
|
+
this.validate(definition);
|
|
60
|
+
if (["verifies", "needs_repair", "repair_from"].some(key => key in definition))
|
|
61
|
+
throw new Error("Planner cannot set runtime verification or repair fields");
|
|
62
|
+
const task = { ...structuredClone(definition), kind: definition.kind ?? "work", status: "PENDING", attempts: 0, generation: 0 };
|
|
63
|
+
this.tasks.set(task.task_id, task);
|
|
64
|
+
try {
|
|
65
|
+
this.checkCycles();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.tasks.delete(task.task_id);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
this.pinned.set(task.task_id, { ...task.input_versions });
|
|
72
|
+
if (task.kind === "work")
|
|
73
|
+
this.tasks.set(verifyId(task.task_id), { ...structuredClone(task), task_id: verifyId(task.task_id),
|
|
74
|
+
task_goal: `Verify: ${task.task_goal}`, dependencies: [task.task_id], kind: "verification", verifies: task.task_id,
|
|
75
|
+
// A verifier may run necessary tests, so it holds an exclusive lease if its source can write.
|
|
76
|
+
priority: task.priority + 1, read_only: task.read_only, exclusive: !task.read_only,
|
|
77
|
+
});
|
|
78
|
+
this.options.diagnostics?.emit("task_defined", { task_id: task.task_id, role: task.kind === "verification" ? "verification" : "worker", dependencies: task.dependencies,
|
|
79
|
+
read_only: task.read_only, exclusive: task.exclusive, write_scope_count: task.write_paths.length });
|
|
80
|
+
if (task.kind === "work")
|
|
81
|
+
this.options.diagnostics?.emit("task_defined", { task_id: verifyId(task.task_id), role: "verification", verifies: task.task_id,
|
|
82
|
+
dependencies: [task.task_id], read_only: task.read_only, exclusive: !task.read_only, write_scope_count: task.write_paths.length });
|
|
83
|
+
this.notify();
|
|
84
|
+
}
|
|
85
|
+
update(id, dependencies, reason) {
|
|
86
|
+
const task = this.planned(id);
|
|
87
|
+
this.validate({ ...task, dependencies });
|
|
88
|
+
const previous = task.dependencies;
|
|
89
|
+
task.dependencies = [...dependencies];
|
|
90
|
+
try {
|
|
91
|
+
this.checkCycles();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
task.dependencies = previous;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
this.pinned.set(id, {});
|
|
98
|
+
this.invalidate(id, reason);
|
|
99
|
+
}
|
|
100
|
+
invalidate(id, reason) {
|
|
101
|
+
this.planned(id);
|
|
102
|
+
const affected = new Set([id]);
|
|
103
|
+
let changed = true;
|
|
104
|
+
while (changed) {
|
|
105
|
+
changed = false;
|
|
106
|
+
for (const task of this.tasks.values()) {
|
|
107
|
+
if (!task.verifies && !affected.has(task.task_id) && task.dependencies.some(dep => affected.has(dep))) {
|
|
108
|
+
affected.add(task.task_id);
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const key of affected) {
|
|
114
|
+
for (const target of [key, verifyId(key)]) {
|
|
115
|
+
const task = this.tasks.get(target);
|
|
116
|
+
if (!task)
|
|
117
|
+
continue;
|
|
118
|
+
if (!task.read_only && !task.verifies && (task.attempts > 0 || this.store.latest(target)?.side_effects)) {
|
|
119
|
+
task.needs_repair = true;
|
|
120
|
+
}
|
|
121
|
+
task.generation++;
|
|
122
|
+
task.status = "INVALIDATED";
|
|
123
|
+
task.error = reason;
|
|
124
|
+
task.attempts = 0;
|
|
125
|
+
this.running.get(target)?.abort.abort(`Invalidated: ${reason}`);
|
|
126
|
+
this.store.invalidate(target);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.notify();
|
|
130
|
+
}
|
|
131
|
+
/** A repair is a new explicit task accounting for prior side effects, followed by original acceptance checks. */
|
|
132
|
+
repair(id, replacement, reason) {
|
|
133
|
+
const task = this.planned(id);
|
|
134
|
+
if (id === replacement || !/^[a-zA-Z0-9_-]{1,80}$/.test(replacement))
|
|
135
|
+
throw new Error("Invalid repair task");
|
|
136
|
+
// An additional check must retain the original inputs as well as the repair evidence.
|
|
137
|
+
const dependencies = task.kind === "verification" ? [...new Set([...task.dependencies, replacement])] : [replacement];
|
|
138
|
+
this.update(id, dependencies, reason);
|
|
139
|
+
task.repair_from = replacement;
|
|
140
|
+
task.needs_repair = false;
|
|
141
|
+
this.notify();
|
|
142
|
+
}
|
|
143
|
+
get plannedTaskCount() { return [...this.tasks.values()].filter(task => !task.verifies).length; }
|
|
144
|
+
snapshot() { return structuredClone([...this.tasks.values()]); }
|
|
145
|
+
get successful() { return this.tasks.size > 0 && [...this.tasks.values()].every(t => t.status === "COMPLETED"); }
|
|
146
|
+
get failed() { return this.snapshot().filter(t => ["FAILED", "BLOCKED"].includes(t.status)); }
|
|
147
|
+
async run(produce) {
|
|
148
|
+
this.closed = false;
|
|
149
|
+
this.producerError = undefined;
|
|
150
|
+
this.cancelled = false;
|
|
151
|
+
const abort = () => {
|
|
152
|
+
this.cancelled = true;
|
|
153
|
+
for (const entry of this.running.values())
|
|
154
|
+
entry.abort.abort(this.options.signal?.reason);
|
|
155
|
+
this.notify();
|
|
156
|
+
};
|
|
157
|
+
this.options.signal?.addEventListener("abort", abort, { once: true });
|
|
158
|
+
if (this.options.signal?.aborted)
|
|
159
|
+
abort();
|
|
160
|
+
const producer = Promise.resolve().then(() => produce(this)).catch(error => {
|
|
161
|
+
this.producerError = error instanceof Error ? error : new Error(String(error));
|
|
162
|
+
this.cancelled = true;
|
|
163
|
+
for (const entry of this.running.values())
|
|
164
|
+
entry.abort.abort(this.producerError);
|
|
165
|
+
}).finally(() => { this.closed = true; this.notify(); });
|
|
166
|
+
try {
|
|
167
|
+
while (true) {
|
|
168
|
+
const revision = this.change;
|
|
169
|
+
if (!this.cancelled)
|
|
170
|
+
this.schedule();
|
|
171
|
+
if (this.cancelled && !this.running.size)
|
|
172
|
+
break;
|
|
173
|
+
if (this.closed && !this.running.size) {
|
|
174
|
+
for (const task of this.tasks.values()) {
|
|
175
|
+
if (!["COMPLETED", "FAILED"].includes(task.status)) {
|
|
176
|
+
task.status = "BLOCKED";
|
|
177
|
+
task.error ||= "Dependency missing, failed, or invalidated; replan required";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
this.notify();
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (revision === this.change)
|
|
184
|
+
await new Promise(resolve => { this.wake = resolve; });
|
|
185
|
+
}
|
|
186
|
+
await producer;
|
|
187
|
+
if (this.producerError)
|
|
188
|
+
throw this.producerError;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
for (const entry of this.running.values())
|
|
192
|
+
entry.abort.abort("Scheduler stopped");
|
|
193
|
+
await Promise.allSettled([...this.running.values()].map(entry => entry.promise));
|
|
194
|
+
if (this.cancelled)
|
|
195
|
+
for (const task of this.tasks.values()) {
|
|
196
|
+
if (!["COMPLETED", "FAILED"].includes(task.status)) {
|
|
197
|
+
if (!task.read_only && !task.verifies && task.attempts > 0)
|
|
198
|
+
task.needs_repair = true;
|
|
199
|
+
task.status = "BLOCKED";
|
|
200
|
+
task.error ||= "Execution cancelled";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
this.options.signal?.removeEventListener("abort", abort);
|
|
204
|
+
this.notify();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
schedule() {
|
|
208
|
+
const start = performance.now();
|
|
209
|
+
for (const task of this.tasks.values()) {
|
|
210
|
+
if (task.needs_repair || this.running.has(task.task_id) || !["PENDING", "INVALIDATED", "BLOCKED", "READY"].includes(task.status))
|
|
211
|
+
continue;
|
|
212
|
+
const ready = task.verifies
|
|
213
|
+
? this.tasks.get(task.verifies)?.status === "VERIFYING" && this.store.latest(task.verifies)?.verification === "pending"
|
|
214
|
+
: task.dependencies.every(dep => this.tasks.get(dep)?.status === "COMPLETED" && this.store.latest(dep)?.verification === "verified");
|
|
215
|
+
if (ready && !task.verifies) {
|
|
216
|
+
const actual = this.versions(task);
|
|
217
|
+
if (Object.entries(this.pinned.get(task.task_id) ?? {}).some(([key, version]) => actual[key] !== version)) {
|
|
218
|
+
task.status = "BLOCKED";
|
|
219
|
+
task.error = "Pinned input version mismatch; update dependencies or replan before execution";
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (ready && task.status !== "READY")
|
|
224
|
+
this.options.diagnostics?.emit("task_ready", { task_id: task.task_id, generation: task.generation });
|
|
225
|
+
task.status = ready ? "READY" : task.status === "READY" ? "PENDING" : task.status;
|
|
226
|
+
}
|
|
227
|
+
const ready = [...this.tasks.values()].filter(t => t.status === "READY" && !this.running.has(t.task_id))
|
|
228
|
+
.sort((a, b) => b.priority - a.priority || b.estimated_duration - a.estimated_duration);
|
|
229
|
+
for (const task of ready) {
|
|
230
|
+
if (this.running.size >= this.settings.workerSlots) {
|
|
231
|
+
this.options.diagnostics?.emit("worker_capacity_wait", { task_id: task.task_id, active: this.running.size, capacity: this.settings.workerSlots });
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
const blocker = [...this.running.keys()].find(id => this.conflicts(task, this.tasks.get(id)));
|
|
235
|
+
if (blocker) {
|
|
236
|
+
if (this.options.diagnostics && this.diagnosticWaits.get(task.task_id) !== blocker) {
|
|
237
|
+
this.diagnosticWaits.set(task.task_id, blocker);
|
|
238
|
+
this.options.diagnostics.emit("workspace_wait", { task_id: task.task_id, blocker_ids: [blocker],
|
|
239
|
+
exclusive: task.exclusive, blocker_exclusive: this.tasks.get(blocker).exclusive });
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
this.launch(task);
|
|
244
|
+
}
|
|
245
|
+
this.schedulingMs += performance.now() - start;
|
|
246
|
+
}
|
|
247
|
+
launch(task) {
|
|
248
|
+
task.status = task.kind === "verification" ? "VERIFYING" : "RUNNING";
|
|
249
|
+
task.attempts++;
|
|
250
|
+
this.diagnosticWaits.delete(task.task_id);
|
|
251
|
+
this.options.diagnostics?.emit("workspace_acquired", { task_id: task.task_id, role: task.kind === "verification" ? "verification" : "worker",
|
|
252
|
+
generation: task.generation, attempt: task.attempts, read_only: task.read_only, exclusive: task.exclusive });
|
|
253
|
+
const generation = task.generation;
|
|
254
|
+
task.input_versions = this.versions(task);
|
|
255
|
+
const abort = new AbortController();
|
|
256
|
+
const linked = linkSignals(abort.signal, this.options.signal);
|
|
257
|
+
// Timeout aborts the underlying request/command; the lease is held until it drains.
|
|
258
|
+
const timer = setTimeout(() => abort.abort("Fast task timed out"), this.settings.taskTimeoutMs);
|
|
259
|
+
const promise = Promise.resolve().then(async () => {
|
|
260
|
+
try {
|
|
261
|
+
const repaired = task.kind === "work" && task.repair_from ? this.store.latest(task.repair_from) : undefined;
|
|
262
|
+
const output = repaired?.verification === "verified" ? repaired : await this.options.execute(structuredClone(task), linked.signal);
|
|
263
|
+
if (generation !== task.generation || this.cancelled)
|
|
264
|
+
return;
|
|
265
|
+
if (linked.signal.aborted)
|
|
266
|
+
throw new FastTaskError(String(linked.signal.reason), output.side_effects);
|
|
267
|
+
if (JSON.stringify(task.input_versions) !== JSON.stringify(this.versions(task))) {
|
|
268
|
+
this.invalidate(task.verifies ?? task.task_id, "Input version changed during execution");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.store.put(task.task_id, output, task.input_versions);
|
|
272
|
+
if (task.kind === "work")
|
|
273
|
+
task.status = "VERIFYING";
|
|
274
|
+
else {
|
|
275
|
+
if (output.verification_passed !== true || output.unresolved.length)
|
|
276
|
+
throw new FastTaskError("Local verification failed", output.side_effects);
|
|
277
|
+
this.store.verify(task.task_id);
|
|
278
|
+
if (task.verifies)
|
|
279
|
+
this.store.verify(task.verifies);
|
|
280
|
+
const source = task.verifies ? this.planned(task.verifies) : task;
|
|
281
|
+
source.status = "VERIFIED";
|
|
282
|
+
this.notify();
|
|
283
|
+
source.status = "COMPLETED";
|
|
284
|
+
task.status = "COMPLETED";
|
|
285
|
+
this.options.diagnostics?.emit("artifact_verified", { task_id: source.task_id, generation: source.generation });
|
|
286
|
+
this.options.onVerified?.(structuredClone(source));
|
|
287
|
+
}
|
|
288
|
+
task.error = undefined;
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (generation !== task.generation || this.cancelled)
|
|
292
|
+
return;
|
|
293
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
294
|
+
const safe = task.read_only && !(error instanceof FastTaskError && error.sideEffects);
|
|
295
|
+
if (safe && task.attempts <= this.settings.retries && !this.options.signal?.aborted) {
|
|
296
|
+
task.status = "PENDING";
|
|
297
|
+
this.retries++;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
task.status = "FAILED";
|
|
301
|
+
if (task.verifies) {
|
|
302
|
+
const source = this.planned(task.verifies);
|
|
303
|
+
source.status = "FAILED";
|
|
304
|
+
source.error = task.error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
linked.dispose();
|
|
311
|
+
this.running.delete(task.task_id);
|
|
312
|
+
this.options.diagnostics?.emit("workspace_released", { task_id: task.task_id, generation, status: task.status });
|
|
313
|
+
this.notify();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
this.running.set(task.task_id, { promise, abort, generation });
|
|
317
|
+
this.notify();
|
|
318
|
+
}
|
|
319
|
+
versions(task) {
|
|
320
|
+
const versions = Object.create(null);
|
|
321
|
+
for (const key of ["request", "history", ...task.required_context]) {
|
|
322
|
+
const context = this.store.readContext(key);
|
|
323
|
+
if (context)
|
|
324
|
+
versions[key] = context.version;
|
|
325
|
+
}
|
|
326
|
+
for (const id of task.dependencies) {
|
|
327
|
+
const artifact = this.store.latest(id);
|
|
328
|
+
if (artifact)
|
|
329
|
+
versions[id] = artifact.version;
|
|
330
|
+
}
|
|
331
|
+
return versions;
|
|
332
|
+
}
|
|
333
|
+
conflicts(a, b) {
|
|
334
|
+
// Readers must also be isolated from writers: file content is not a versioned artifact.
|
|
335
|
+
if (a.read_only && b.read_only)
|
|
336
|
+
return false;
|
|
337
|
+
if (a.exclusive || b.exclusive || a.read_only || b.read_only)
|
|
338
|
+
return true;
|
|
339
|
+
const paths = (task) => task.write_paths.map(p => canonical(path.resolve(this.options.cwd, p)));
|
|
340
|
+
return paths(a).some(left => paths(b).some(right => overlaps(left, right) || overlaps(right, left)));
|
|
341
|
+
}
|
|
342
|
+
validate(task) {
|
|
343
|
+
if (!task || typeof task.task_goal !== "string" || !task.task_goal.trim() ||
|
|
344
|
+
typeof task.output_contract !== "string" || !task.output_contract.trim() || !Array.isArray(task.acceptance_criteria) || !task.acceptance_criteria.length ||
|
|
345
|
+
(task.kind !== undefined && task.kind !== "work" && task.kind !== "verification") ||
|
|
346
|
+
!Array.isArray(task.dependencies) || !Array.isArray(task.required_context) || !Array.isArray(task.write_paths) ||
|
|
347
|
+
task.dependencies.some(dep => typeof dep !== "string" || dep === task.task_id || !/^[a-zA-Z0-9_-]{1,80}$/.test(dep)) ||
|
|
348
|
+
task.acceptance_criteria.some(c => typeof c !== "string" || !c.trim()) ||
|
|
349
|
+
task.required_context.some(c => typeof c !== "string") || task.write_paths.some(p => typeof p !== "string" || !p.trim()) ||
|
|
350
|
+
!task.input_versions || typeof task.input_versions !== "object" || Array.isArray(task.input_versions) ||
|
|
351
|
+
Object.values(task.input_versions).some(v => typeof v !== "string" && (typeof v !== "number" || !Number.isFinite(v))) ||
|
|
352
|
+
task.required_context.some(c => !["request", "history", ...task.dependencies].includes(c) && !this.store.readContext(c)) ||
|
|
353
|
+
!Number.isFinite(task.priority) || !Number.isFinite(task.estimated_duration) || task.estimated_duration < 0 ||
|
|
354
|
+
typeof task.read_only !== "boolean" || typeof task.exclusive !== "boolean" ||
|
|
355
|
+
(!task.read_only && !task.exclusive && !task.write_paths.length) ||
|
|
356
|
+
(task.kind === "verification" && (!task.dependencies.length || (!task.read_only && !task.exclusive))))
|
|
357
|
+
throw new Error("Incomplete Fast task definition");
|
|
358
|
+
}
|
|
359
|
+
planned(id) {
|
|
360
|
+
const task = this.tasks.get(id);
|
|
361
|
+
if (!task || task.verifies)
|
|
362
|
+
throw new Error(`Unknown planned task: ${id}`);
|
|
363
|
+
return task;
|
|
364
|
+
}
|
|
365
|
+
checkCycles() {
|
|
366
|
+
const visiting = new Set(), done = new Set();
|
|
367
|
+
const visit = (id) => {
|
|
368
|
+
if (visiting.has(id))
|
|
369
|
+
throw new Error("Cyclic Fast task dependencies");
|
|
370
|
+
if (done.has(id) || !this.tasks.has(id))
|
|
371
|
+
return;
|
|
372
|
+
visiting.add(id);
|
|
373
|
+
for (const dep of this.tasks.get(id).dependencies)
|
|
374
|
+
visit(dep);
|
|
375
|
+
visiting.delete(id);
|
|
376
|
+
done.add(id);
|
|
377
|
+
};
|
|
378
|
+
for (const task of this.tasks.values())
|
|
379
|
+
if (!task.verifies)
|
|
380
|
+
visit(task.task_id);
|
|
381
|
+
}
|
|
382
|
+
notify() { this.change++; this.wake?.(); this.wake = undefined; this.options.onChange?.(); }
|
|
383
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export declare const FAST_MODE: "fast_dag";
|
|
2
|
+
export type FastStatus = "PENDING" | "READY" | "RUNNING" | "VERIFYING" | "VERIFIED" | "FAILED" | "BLOCKED" | "INVALIDATED" | "COMPLETED";
|
|
3
|
+
export interface FastTask {
|
|
4
|
+
task_id: string;
|
|
5
|
+
task_goal: string;
|
|
6
|
+
dependencies: string[];
|
|
7
|
+
required_context: string[];
|
|
8
|
+
input_versions: Record<string, number | string>;
|
|
9
|
+
acceptance_criteria: string[];
|
|
10
|
+
output_contract: string;
|
|
11
|
+
priority: number;
|
|
12
|
+
estimated_duration: number;
|
|
13
|
+
read_only: boolean;
|
|
14
|
+
/** Disjoint file trees may run concurrently; commands take an exclusive workspace lease. */
|
|
15
|
+
write_paths: string[];
|
|
16
|
+
exclusive: boolean;
|
|
17
|
+
status: FastStatus;
|
|
18
|
+
/** Planned work gets an automatic verifier; planned verification executes its checks once. */
|
|
19
|
+
kind: "work" | "verification";
|
|
20
|
+
/** Present only on the automatic verifier of a work task. */
|
|
21
|
+
verifies?: string;
|
|
22
|
+
attempts: number;
|
|
23
|
+
generation: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
needs_repair?: boolean;
|
|
26
|
+
repair_from?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface FastOutput {
|
|
29
|
+
result: string;
|
|
30
|
+
conclusions: string[];
|
|
31
|
+
evidence: string[];
|
|
32
|
+
assumptions: string[];
|
|
33
|
+
unresolved: string[];
|
|
34
|
+
/** A failed attempt which touched external state must never be retried automatically. */
|
|
35
|
+
side_effects: boolean;
|
|
36
|
+
verification_passed?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface FastArtifact extends FastOutput {
|
|
39
|
+
task_id: string;
|
|
40
|
+
version: number;
|
|
41
|
+
input_versions: Record<string, number | string>;
|
|
42
|
+
verification: "pending" | "verified" | "invalidated";
|
|
43
|
+
created_at: string;
|
|
44
|
+
}
|
|
45
|
+
export interface FastUsage {
|
|
46
|
+
prompt: number;
|
|
47
|
+
completion: number;
|
|
48
|
+
requests: number;
|
|
49
|
+
}
|
|
50
|
+
export interface FastMetrics {
|
|
51
|
+
started_at: string;
|
|
52
|
+
elapsed_ms: number;
|
|
53
|
+
planning: FastUsage;
|
|
54
|
+
workers: Record<string, FastUsage>;
|
|
55
|
+
verification: FastUsage;
|
|
56
|
+
integration: FastUsage;
|
|
57
|
+
total: FastUsage;
|
|
58
|
+
retries: number;
|
|
59
|
+
replans: number;
|
|
60
|
+
tool_wait_ms: number;
|
|
61
|
+
scheduling_ms: number;
|
|
62
|
+
context_cache_hits: number;
|
|
63
|
+
/** The existing provider interface does not expose reliable cache billing. */
|
|
64
|
+
provider_cached_tokens: null;
|
|
65
|
+
tokens_per_second: number;
|
|
66
|
+
}
|
|
67
|
+
export interface FastSnapshot {
|
|
68
|
+
run_id: string;
|
|
69
|
+
actual_mode: typeof FAST_MODE;
|
|
70
|
+
phase: "planning" | "executing" | "verifying" | "integrating" | "reviewing" | "completed" | "failed" | "cancelling" | "cancelled";
|
|
71
|
+
tasks: FastTask[];
|
|
72
|
+
artifacts: FastArtifact[];
|
|
73
|
+
metrics: FastMetrics;
|
|
74
|
+
resources: {
|
|
75
|
+
model: number;
|
|
76
|
+
tools: number;
|
|
77
|
+
background: number;
|
|
78
|
+
};
|
|
79
|
+
integrated?: Record<string, number>;
|
|
80
|
+
review?: Array<{
|
|
81
|
+
accepted: boolean;
|
|
82
|
+
reason: string;
|
|
83
|
+
evidence_ids: string[];
|
|
84
|
+
}>;
|
|
85
|
+
planning_errors?: string[];
|
|
86
|
+
error?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface FastSettings {
|
|
89
|
+
enabled: boolean;
|
|
90
|
+
modelSlots: number;
|
|
91
|
+
toolSlots: number;
|
|
92
|
+
backgroundSlots: number;
|
|
93
|
+
workerSlots: number;
|
|
94
|
+
taskTimeoutMs: number;
|
|
95
|
+
maxTasks: number;
|
|
96
|
+
retries: number;
|
|
97
|
+
replans: number;
|
|
98
|
+
}
|
|
99
|
+
export declare function fastSettings(config: Record<string, unknown>): FastSettings;
|
|
100
|
+
export declare function assertFastAvailable(config: Record<string, unknown>): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const FAST_MODE = "fast_dag";
|
|
2
|
+
export function fastSettings(config) {
|
|
3
|
+
const limit = (key, fallback, max) => {
|
|
4
|
+
const n = config[key];
|
|
5
|
+
return typeof n === "number" && Number.isSafeInteger(n) && n > 0 ? Math.min(n, max) : fallback;
|
|
6
|
+
};
|
|
7
|
+
const modelSlots = limit("fast_model_slots", 4, 16);
|
|
8
|
+
return {
|
|
9
|
+
enabled: process.env.NEUTRINO_FAST_MODE !== "0" && config.fast_mode_enabled !== false,
|
|
10
|
+
modelSlots, toolSlots: limit("fast_tool_slots", 8, 32), backgroundSlots: limit("fast_background_slots", 2, 8),
|
|
11
|
+
workerSlots: limit("fast_worker_slots", modelSlots * 2, 32),
|
|
12
|
+
taskTimeoutMs: limit("fast_task_timeout_ms", 30 * 60_000, 24 * 60 * 60_000),
|
|
13
|
+
maxTasks: limit("fast_max_tasks", 24, 128),
|
|
14
|
+
retries: config.fast_retries === 0 ? 0 : limit("fast_retries", 1, 3),
|
|
15
|
+
replans: config.fast_replans === 0 ? 0 : limit("fast_replans", 2, 4),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function assertFastAvailable(config) {
|
|
19
|
+
if (!fastSettings(config).enabled)
|
|
20
|
+
throw new Error("Fast Mode is disabled. Select Standard mode explicitly to continue.");
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LLMClient } from "../llm/client.js";
|
|
2
|
+
import { type TaskRuntime } from "../llm/runtime.js";
|
|
3
|
+
import type { ToolDefinition } from "../llm/tools.js";
|
|
4
|
+
import type { ApprovalDecision } from "../llm/approval.js";
|
|
5
|
+
import type { FastOutput } from "./types.js";
|
|
6
|
+
export declare const FAST_INSPECTION_POLICY = "Read known required paths directly without a separate directory-listing round. If a listing is needed, request it together with independent reads whose paths are already known. Discover unknown paths before reading them. ";
|
|
7
|
+
export declare const FAST_COMMAND_POLICY = "When commands are available and necessary, combine related reads, computation, writes and read-back assertions for THIS task in one command when the full sequence is known. Preserve prerequisite order and stop on failed assertions. Split calls when later actions require diagnosis or a separate safety decision. The full command body still requires normal approval. Keep independent verification in its separate agent scope. ";
|
|
8
|
+
/** Strict control results with correction inside the same private conversation. Standard's child loop is untouched. */
|
|
9
|
+
export declare function runFastWorker(options: {
|
|
10
|
+
runtime: TaskRuntime;
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
task: string;
|
|
13
|
+
system: string;
|
|
14
|
+
verification: boolean;
|
|
15
|
+
readOnly: boolean;
|
|
16
|
+
writePaths?: readonly string[];
|
|
17
|
+
tools: ToolDefinition[];
|
|
18
|
+
maxRounds: number;
|
|
19
|
+
contextWindow: number;
|
|
20
|
+
createClient(): LLMClient;
|
|
21
|
+
approve(name: string, args: Record<string, unknown>): Promise<ApprovalDecision>;
|
|
22
|
+
execute(name: string, args: Record<string, unknown>): Promise<string>;
|
|
23
|
+
}): Promise<FastOutput>;
|