@xingjisoft/neutrino 0.6.2 → 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 +19 -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/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 +77 -4
- 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 +4 -2
- 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/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 +139 -21
- 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 +153 -19
- 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 +27 -6
- package/web/app.js +201 -32
- package/web/i18n.js +15 -0
- package/web/index.html +3 -1
- package/web/plugins.css +10 -0
- package/web/plugins.js +230 -0
- package/web/style.css +11 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { outputAllowance, prepareContext } from "../llm/context.js";
|
|
2
|
+
import { currentTask, TaskControlError } from "../llm/runtime.js";
|
|
3
|
+
import { FastTaskError } from "./scheduler.js";
|
|
4
|
+
// Fast-only guidance; batching never changes tool availability, ordering or approval.
|
|
5
|
+
export 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. ";
|
|
6
|
+
export 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. ";
|
|
7
|
+
const FINISH = "finish_task";
|
|
8
|
+
const list = (value) => Array.isArray(value) ? value.filter((s) => typeof s === "string") : [];
|
|
9
|
+
function finishTool(verification) {
|
|
10
|
+
return { type: "function", function: {
|
|
11
|
+
name: FINISH,
|
|
12
|
+
description: verification
|
|
13
|
+
? "Finish independent local verification with an explicit boolean verdict. Completed/true means all original acceptance criteria and actual necessary checks passed. Cite actual evidence and use false/partial for unresolved work."
|
|
14
|
+
: "Finish this work task with a factual self-contained report. Keep original evidence and state unresolved work. A separate verifier checks every acceptance criterion.",
|
|
15
|
+
parameters: { type: "object", properties: {
|
|
16
|
+
status: { type: "string", enum: ["completed", "partial", "blocked", "failed"], description: "Status of THIS task_goal/output_contract only. Other DAG tasks and later global integration do not make this task partial." },
|
|
17
|
+
report: { type: "string" }, conclusions: { type: "array", items: { type: "string" } },
|
|
18
|
+
evidence: { type: "array", items: { type: "string" } }, assumptions: { type: "array", items: { type: "string" } },
|
|
19
|
+
unresolved: { type: "array", items: { type: "string" }, description: "Only unmet acceptance criteria of THIS task. Use [] when none. Do not list 'none', harmless presentation differences, out-of-scope tasks or future integration." },
|
|
20
|
+
...(verification ? { verification_passed: { type: "boolean" } } : {}),
|
|
21
|
+
}, required: ["status", "report", "unresolved", ...(verification ? ["verification_passed"] : [])] },
|
|
22
|
+
} };
|
|
23
|
+
}
|
|
24
|
+
/** Strict control results with correction inside the same private conversation. Standard's child loop is untouched. */
|
|
25
|
+
export async function runFastWorker(options) {
|
|
26
|
+
const runtime = options.runtime;
|
|
27
|
+
return runtime.child("subagent", options.task, async (scope) => runtime.withSignal(options.signal, async () => {
|
|
28
|
+
const signal = currentTask().signal;
|
|
29
|
+
const records = () => runtime.evidenceFor(scope.runId);
|
|
30
|
+
const controlRejected = (reason_code) => runtime.diagnostics?.emit("control_rejected", { reason_code }, currentTask());
|
|
31
|
+
const sideEffects = () => records().some(e => !["read_file", "list_files", "web_search", "delegate", "read_task_evidence"].includes(e.name) && e.status !== "denied");
|
|
32
|
+
const messages = [{ role: "system", content: `${options.system}\n\n${runtime.instructions()}\n\nUse finish_task to conclude. ` +
|
|
33
|
+
"Your scope is this DAG task, not the entire global request. Preserve every global constraint. Status and unresolved refer only to THIS task's acceptance criteria, not to remaining DAG tasks. Use unresolved: [] when this task has no blockers; out-of-scope future work belongs in assumptions, not unresolved. " +
|
|
34
|
+
(options.verification
|
|
35
|
+
? "Perform the independent acceptance checks for this task against original sources and requested outputs. A scratch helper or verifier script is not a deliverable unless the user requested it. Do not test the tests, inject faults into scratch copies, replay an already evidenced pipeline in a new directory, or add cleanliness/byte-forensics checks unless the original request explicitly requires that coverage or an actual contradictory result requires diagnosis. Required negative/edge tests for the delivered product remain mandatory. Reuse actual execution evidence to audit recorded actions. All necessary source and correctness checks remain mandatory. "
|
|
36
|
+
: "Produce the task output and necessary self-checks. The separate verifier owns independent acceptance checks; do not repeat a full independent/global audit in this worker. Prefer read_file/list_files for inspection, and run_command for necessary computation or tests. ") +
|
|
37
|
+
FAST_INSPECTION_POLICY + (scope.policy.readOnly ? "" : FAST_COMMAND_POLICY) +
|
|
38
|
+
"Keep the report factual and concise; include requested findings and cite actual evidence IDs. Do not reproduce raw records, command bodies or long checklists already preserved in the ledger unless the output contract requires them. conclusions/evidence/assumptions are optional when they add no new facts; all actual tool evidence is attached automatically. This does not reduce reasoning or verification requirements. " +
|
|
39
|
+
(options.verification ? "Your result MUST include top-level verification_passed (boolean), unresolved (string[]), and a factual report. Merely writing PASS or verification_passed=true inside prose is insufficient. " : "") +
|
|
40
|
+
"Correct tool/input mistakes in this conversation, preserving actual failed and successful evidence. Do not repeat a denied call unchanged. Never claim that missing checks passed." },
|
|
41
|
+
{ role: "user", content: options.task }];
|
|
42
|
+
const control = finishTool(options.verification);
|
|
43
|
+
const tools = runtime.availableTools([...options.tools.filter(t => t.function.name !== FINISH), control]);
|
|
44
|
+
const names = new Set(tools.map(t => t.function.name));
|
|
45
|
+
let lastPromptTokens = 0, denied = false, lastReport = "";
|
|
46
|
+
const accept = (value) => {
|
|
47
|
+
const result = typeof value.report === "string" ? value.report : typeof value.result === "string" ? value.result : "";
|
|
48
|
+
const status = typeof value.status === "string" ? value.status : "completed";
|
|
49
|
+
if (!result.trim() || !["completed", "partial", "blocked", "failed"].includes(status))
|
|
50
|
+
return undefined;
|
|
51
|
+
if (options.verification && (typeof value.verification_passed !== "boolean" || !Array.isArray(value.unresolved) || value.unresolved.some(v => typeof v !== "string")))
|
|
52
|
+
return undefined;
|
|
53
|
+
if (options.verification && value.verification_passed === true && (status !== "completed" || list(value.unresolved).length))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (denied || status === "blocked" || status === "failed")
|
|
56
|
+
throw new FastTaskError(`${status}: ${result}`, sideEffects());
|
|
57
|
+
const pending = list(value.unresolved);
|
|
58
|
+
if (status === "partial" && !pending.length)
|
|
59
|
+
pending.push("Worker explicitly reported partial work; independent verification must resolve it.");
|
|
60
|
+
const output = { result, conclusions: list(value.conclusions),
|
|
61
|
+
evidence: [...new Set([...records().map(e => e.id), ...list(value.evidence)])],
|
|
62
|
+
assumptions: list(value.assumptions), unresolved: pending, side_effects: sideEffects(),
|
|
63
|
+
...(options.verification ? { verification_passed: status === "completed" && value.verification_passed === true } : {}),
|
|
64
|
+
};
|
|
65
|
+
runtime.recordResult({ runId: scope.runId, status: status === "partial" ? "partial" : "completed", report: JSON.stringify(output),
|
|
66
|
+
evidenceIds: records().map(e => e.id), artifacts: records().filter(e => e.name === "write_file" && e.status === "completed").map(e => String(e.arguments.path)), blockers: pending });
|
|
67
|
+
return output;
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
for (let round = 1; round <= options.maxRounds; round++) {
|
|
71
|
+
if (signal.aborted)
|
|
72
|
+
throw new FastTaskError(String(signal.reason), sideEffects());
|
|
73
|
+
// Like ordinary children, the last round is for reporting, with no filesystem/network tools.
|
|
74
|
+
const available = round === options.maxRounds ? [control] : tools;
|
|
75
|
+
if (round === options.maxRounds)
|
|
76
|
+
messages.push({ role: "user", content: "Final report round. Only finish_task is available. Return the actual verified result or report partial/blocked; do not invent completion." });
|
|
77
|
+
await prepareContext({ messages, tools: available, capacity: options.contextWindow, lastPromptTokens,
|
|
78
|
+
createClient: options.createClient, stream: { signal }, preserveBrief: true });
|
|
79
|
+
const client = options.createClient();
|
|
80
|
+
let text = "";
|
|
81
|
+
for await (const chunk of client.streamTurn(messages, available, { signal, maxOutputTokens: outputAllowance(options.contextWindow), final: round === options.maxRounds }))
|
|
82
|
+
text += chunk;
|
|
83
|
+
lastPromptTokens = client.usage?.prompt ?? 0;
|
|
84
|
+
if (signal.aborted || client.outcome?.status === "cancelled")
|
|
85
|
+
throw new FastTaskError(String(signal.reason ?? "Cancelled"), sideEffects());
|
|
86
|
+
if (client.outcome?.status === "failed")
|
|
87
|
+
throw new FastTaskError(client.outcome.error?.message ?? "Worker request failed", sideEffects());
|
|
88
|
+
lastReport = text || lastReport;
|
|
89
|
+
const calls = client.toolCalls;
|
|
90
|
+
messages.push({ role: "assistant", content: text,
|
|
91
|
+
...(client.reasoningContent ? { reasoning_content: client.reasoningContent } : {}),
|
|
92
|
+
...(client.anthropicContent.length ? { anthropic_content: client.anthropicContent } : {}),
|
|
93
|
+
...(calls.length ? { tool_calls: calls.map(call => ({ id: call.id, type: "function", function: { name: call.name, arguments: call.arguments } })) } : {}),
|
|
94
|
+
});
|
|
95
|
+
if (!calls.length) {
|
|
96
|
+
// A provider returning the requested JSON directly is equivalent to a control call.
|
|
97
|
+
let value;
|
|
98
|
+
try {
|
|
99
|
+
value = JSON.parse(text.trim().replace(/^```json\s*/i, "").replace(/\s*```$/, ""));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
value = null;
|
|
103
|
+
}
|
|
104
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
105
|
+
const output = accept(value);
|
|
106
|
+
if (output)
|
|
107
|
+
return output;
|
|
108
|
+
}
|
|
109
|
+
controlRejected("missing_control_fields");
|
|
110
|
+
messages.push({ role: "user", content: "The final result is missing required machine-readable fields. Conclude with finish_task using status, report, conclusions, evidence, assumptions, unresolved" +
|
|
111
|
+
(options.verification ? ", and top-level verification_passed: true/false. Keep all original evidence; do not rerun completed writes to fix output formatting." : ". Do not rerun completed writes to fix output formatting.") });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const call of calls) {
|
|
115
|
+
if (signal.aborted)
|
|
116
|
+
throw new FastTaskError(String(signal.reason), sideEffects());
|
|
117
|
+
let args;
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(call.arguments);
|
|
120
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
121
|
+
throw new Error("Arguments must be an object");
|
|
122
|
+
args = parsed;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
controlRejected("invalid_tool_arguments");
|
|
126
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: "Invalid JSON object; correct the arguments." });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (call.name === FINISH) {
|
|
130
|
+
if (calls.length !== 1) {
|
|
131
|
+
controlRejected("mixed_control_and_tools");
|
|
132
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: "finish_task must be sent alone after all requested operations settle. Complete the other calls, then return the final result." });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const output = accept(args);
|
|
136
|
+
if (output)
|
|
137
|
+
return output;
|
|
138
|
+
controlRejected("contradictory_control_result");
|
|
139
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: "Invalid or contradictory final result. Include a nonempty report, unresolved array and, for verification, a top-level boolean verification_passed. Status and unresolved concern only THIS task's acceptance criteria. If those passed, use completed/true and unresolved: []; put out-of-scope work in assumptions. If a criterion remains unmet, state it and use false. Clarify using existing evidence without replaying writes or omitting necessary checks." });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let result;
|
|
143
|
+
if (!names.has(call.name) || round === options.maxRounds) {
|
|
144
|
+
result = "Tool unavailable in this scope/round; use finish_task or correct the call.";
|
|
145
|
+
runtime.recordTool(call.name, args, result, "denied");
|
|
146
|
+
denied = true;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
let decision;
|
|
150
|
+
try {
|
|
151
|
+
runtime.assertTool(call.name, args);
|
|
152
|
+
decision = await options.approve(call.name, args);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (!(error instanceof TaskControlError) || error.code !== "workspace_scope")
|
|
156
|
+
throw error;
|
|
157
|
+
decision = { approved: false, source: "input", reason: error.message };
|
|
158
|
+
}
|
|
159
|
+
if (signal.aborted)
|
|
160
|
+
throw new FastTaskError(String(signal.reason), sideEffects());
|
|
161
|
+
if (decision.approved)
|
|
162
|
+
result = await runtime.tool(call.name, args, () => options.execute(call.name, args));
|
|
163
|
+
else {
|
|
164
|
+
result = `Not executed: ${decision.reason}. Do not retry a denied call unchanged.`;
|
|
165
|
+
runtime.recordTool(call.name, args, result, "denied");
|
|
166
|
+
// An input scope error ran nothing and can be corrected locally. A safety/human refusal remains a blocker.
|
|
167
|
+
if (decision.source !== "input")
|
|
168
|
+
denied = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const evidence = records().at(-1);
|
|
172
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: result + (evidence ? `\n[Evidence ${evidence.id}; status=${evidence.status}]` : "") });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw new FastTaskError(`Worker exhausted its configured ${options.maxRounds} rounds. Partial report: ${lastReport}`, sideEffects());
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
runtime.recordResult({ runId: scope.runId, status: signal.aborted ? "cancelled" : "failed", report: lastReport,
|
|
179
|
+
evidenceIds: records().map(e => e.id), artifacts: [], blockers: [error instanceof Error ? error.message : String(error)] });
|
|
180
|
+
if (error instanceof FastTaskError)
|
|
181
|
+
throw error;
|
|
182
|
+
throw new FastTaskError(error instanceof Error ? error.message : String(error), sideEffects());
|
|
183
|
+
}
|
|
184
|
+
}), { readOnly: options.readOnly, writePaths: options.writePaths });
|
|
185
|
+
}
|
package/dist/header.d.ts
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { App } from "./app.js";
|
|
2
|
+
export { defaultState, type AppState } from "./state.js";
|
|
3
|
+
export { INTENSITY_LEVELS, INTENSITY_PARAMS, DEFAULT_MODELS, MODES, normalizeMode, normalizeIntensity, modelIntensities, intensityForModel, findConfiguredModel, loadModels, saveModels, loadConfig, saveConfig, listSessions, loadSession, deleteSession, saveSession, type Mode, type ModelsData, type ModelInfo, type Provider, type SessionData, type SessionMeta, } from "./config.js";
|
|
4
|
+
export { LLMClient, type ToolCall } from "./llm/client.js";
|
|
5
|
+
export { executeTool, getToolDefinitions } from "./llm/tools.js";
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval gate for risky tool calls.
|
|
3
|
+
*
|
|
4
|
+
* Three ways a call can be cleared:
|
|
5
|
+
*
|
|
6
|
+
* 1. The user's prompt opted out. `无需审批` covers the `high` class only;
|
|
7
|
+
* `我已知悉风险,同意给予完整权限,无需任何审批` also covers delete-class
|
|
8
|
+
* (`extreme`) commands.
|
|
9
|
+
* 2. Independent safety reviews clear it; task relevance is advisory except
|
|
10
|
+
* for a unanimously identified, extreme and obvious task mismatch.
|
|
11
|
+
* This only happens when the conversation's Auto Approval switch is on;
|
|
12
|
+
* `extreme` also requires the "extreme-risk auto-approval" setting and
|
|
13
|
+
* then runs that panel three times, in three separate rounds, each of
|
|
14
|
+
* which has to approve on its own.
|
|
15
|
+
* 3. A human presses "approve".
|
|
16
|
+
*
|
|
17
|
+
* The reviewer is a separate agent with its own system prompt. It never runs
|
|
18
|
+
* tools; it only reads what is about to happen and answers with JSON.
|
|
19
|
+
*/
|
|
20
|
+
import type { ModelInfo } from "../config.js";
|
|
21
|
+
import { LLMClient } from "./client.js";
|
|
22
|
+
import type { RiskAssessment } from "./risk.js";
|
|
23
|
+
/** Opt out of approval for the `high` class. */
|
|
24
|
+
export declare const NO_APPROVAL_PHRASE = "\u65E0\u9700\u5BA1\u6279";
|
|
25
|
+
/** Opt out of approval for every class, acknowledging the risk. */
|
|
26
|
+
export declare const FULL_TRUST_PHRASE = "\u6211\u5DF2\u77E5\u6089\u98CE\u9669\uFF0C\u540C\u610F\u7ED9\u4E88\u5B8C\u6574\u6743\u9650\uFF0C\u65E0\u9700\u4EFB\u4F55\u5BA1\u6279";
|
|
27
|
+
/** Every automatically reviewed tool call receives two fresh assessments. */
|
|
28
|
+
export declare const AUTO_REVIEWERS = 2;
|
|
29
|
+
/**
|
|
30
|
+
* Delete-class commands are reviewed this many times once extreme-risk
|
|
31
|
+
* auto-approval is on: a separate round each time, with fresh reviewers, and
|
|
32
|
+
* every round has to approve. One confident round cannot carry a deletion.
|
|
33
|
+
*/
|
|
34
|
+
export declare const EXTREME_ROUNDS = 3;
|
|
35
|
+
export interface PromptDirectives {
|
|
36
|
+
/** `无需审批`: high-risk actions run without asking. */
|
|
37
|
+
noApproval: boolean;
|
|
38
|
+
/** Full-trust sentence: every class runs without asking. */
|
|
39
|
+
fullTrust: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Read the approval directives out of the user's prompt for this turn. */
|
|
42
|
+
export declare function parsePromptDirectives(prompt: string): PromptDirectives;
|
|
43
|
+
export interface ApprovalContext {
|
|
44
|
+
/** Literal user message for this turn, kept separate from historical data. */
|
|
45
|
+
prompt: string;
|
|
46
|
+
/** Earlier user messages, in order, for resolving follow-ups like "repeat". */
|
|
47
|
+
priorUserRequests: string[];
|
|
48
|
+
/** Supporting historical activity, never a source of user authorization. */
|
|
49
|
+
context: string;
|
|
50
|
+
}
|
|
51
|
+
interface ReviewMessage {
|
|
52
|
+
role: string;
|
|
53
|
+
content?: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
result?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Capture original user messages before compression or queued input can change the turn. */
|
|
58
|
+
export declare function captureApprovalContext(messages: readonly ReviewMessage[]): ApprovalContext;
|
|
59
|
+
/** Include the actual input in a denial so context mistakes can be diagnosed. */
|
|
60
|
+
export declare function describeApprovalIntent(context: ApprovalContext): string;
|
|
61
|
+
export interface ApprovalRequest {
|
|
62
|
+
/** Tool name, e.g. `run_command`. */
|
|
63
|
+
tool: string;
|
|
64
|
+
/** Parsed tool arguments. */
|
|
65
|
+
args: Record<string, unknown>;
|
|
66
|
+
/** Directory the tool will run in. */
|
|
67
|
+
cwd: string;
|
|
68
|
+
/** The user message that started this turn. */
|
|
69
|
+
prompt: string;
|
|
70
|
+
priorUserRequests?: string[];
|
|
71
|
+
context?: string;
|
|
72
|
+
risk: RiskAssessment;
|
|
73
|
+
}
|
|
74
|
+
export type ApprovalSource = "prompt" | "human" | "ai"
|
|
75
|
+
/** The request itself was malformed: nobody was asked and nothing ran. */
|
|
76
|
+
| "input";
|
|
77
|
+
export interface ApprovalVote {
|
|
78
|
+
approved: boolean;
|
|
79
|
+
reason: string;
|
|
80
|
+
/** Which reviewer produced this vote (1-based, within its round). */
|
|
81
|
+
reviewer: number;
|
|
82
|
+
/** Which review round produced this vote (1-based). */
|
|
83
|
+
round?: number;
|
|
84
|
+
safetyScore?: number;
|
|
85
|
+
relevanceScore?: number;
|
|
86
|
+
hardBlock?: boolean;
|
|
87
|
+
obviousMismatch?: boolean;
|
|
88
|
+
mismatchEvidence?: string;
|
|
89
|
+
/** A failed review is not a low-confidence vote that another can outweigh. */
|
|
90
|
+
failed?: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface ApprovalDecision {
|
|
93
|
+
approved: boolean;
|
|
94
|
+
source: ApprovalSource;
|
|
95
|
+
/** One-line explanation, written into the transcript. */
|
|
96
|
+
reason: string;
|
|
97
|
+
/** Reviewer votes, when the decision came from the reviewer agent. */
|
|
98
|
+
votes?: ApprovalVote[];
|
|
99
|
+
/** Relevance is diagnostic only; combined mirrors safety for older consumers. */
|
|
100
|
+
assessment?: {
|
|
101
|
+
safety: number;
|
|
102
|
+
relevance: number;
|
|
103
|
+
combined: number;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* How long a single reviewer may take before it is treated as a denial.
|
|
108
|
+
*
|
|
109
|
+
* The reviewer sits on the critical path of a tool call, so an unreachable or
|
|
110
|
+
* wedged provider must not be able to freeze the conversation: a slow verdict
|
|
111
|
+
* is a "no", not a hang.
|
|
112
|
+
*/
|
|
113
|
+
export declare const REVIEW_TIMEOUT_MS = 45000;
|
|
114
|
+
export interface ApprovalGateOptions {
|
|
115
|
+
/** True approves, false denies, null reroutes after a mode change. */
|
|
116
|
+
askHuman(request: ApprovalRequest): Promise<boolean | null>;
|
|
117
|
+
/** Route requests to the reviewer agent. Prompt text cannot enable this. */
|
|
118
|
+
autoApprovalEnabled?(): boolean;
|
|
119
|
+
/** Reviewer model; defaults to the chat model. */
|
|
120
|
+
reviewer(): {
|
|
121
|
+
model: ModelInfo;
|
|
122
|
+
provider: string;
|
|
123
|
+
intensity: string;
|
|
124
|
+
};
|
|
125
|
+
/** Pro selects its independent reviewer from the resolved task team. */
|
|
126
|
+
createReviewerClient?(): LLMClient;
|
|
127
|
+
/** Role instructions supplement, but never replace, the approval policy. */
|
|
128
|
+
reviewerSystemPrompt?(): string;
|
|
129
|
+
/** A stopped Pro turn also stops its in-flight independent reviews. */
|
|
130
|
+
cancelled?(): boolean;
|
|
131
|
+
/** Whether automatic review may also clear delete-class commands. */
|
|
132
|
+
extremeAutoApprovalEnabled(): boolean;
|
|
133
|
+
/** Recent conversation, for the reviewer's context. */
|
|
134
|
+
context?(): string;
|
|
135
|
+
/**
|
|
136
|
+
* Called with the reviewer count when an automated review starts (and again
|
|
137
|
+
* at each new round), then with null when it finishes, so the UI can explain
|
|
138
|
+
* the wait.
|
|
139
|
+
*/
|
|
140
|
+
onReview?(info: {
|
|
141
|
+
total: number;
|
|
142
|
+
round?: number;
|
|
143
|
+
rounds?: number;
|
|
144
|
+
} | null): void;
|
|
145
|
+
/** Overridable only so tests do not have to wait 45 s. */
|
|
146
|
+
reviewTimeoutMs?(): number;
|
|
147
|
+
}
|
|
148
|
+
/** Tool argument that must carry the requester's own justification. */
|
|
149
|
+
export declare const REASON_ARG = "reason";
|
|
150
|
+
/** Shorter justifications are placeholders ("ok", "test"), not reasons. */
|
|
151
|
+
export declare const MIN_REASON_CHARS = 4;
|
|
152
|
+
/** Longest justification passed through to reviewers and the UIs. */
|
|
153
|
+
export declare const MAX_REASON_CHARS = 600;
|
|
154
|
+
/**
|
|
155
|
+
* The agent's stated reason for a call, folded onto one line, or null when it
|
|
156
|
+
* gave nothing usable.
|
|
157
|
+
*
|
|
158
|
+
* This text is a claim made by whoever proposed the call, never evidence. The
|
|
159
|
+
* reviewers are told to check it against the actual arguments, because a
|
|
160
|
+
* fabricated reason ("the user already approved this") would otherwise be a way
|
|
161
|
+
* to talk the gate into approving something the user never asked for.
|
|
162
|
+
*/
|
|
163
|
+
export declare function statedReason(args: Record<string, unknown> | undefined): string | null;
|
|
164
|
+
export declare class ApprovalGate {
|
|
165
|
+
private readonly options;
|
|
166
|
+
constructor(options: ApprovalGateOptions);
|
|
167
|
+
/**
|
|
168
|
+
* The single entry point for callers.
|
|
169
|
+
*
|
|
170
|
+
* Anything unexpected inside the gate (unreadable config, no reviewer model,
|
|
171
|
+
* a throwing UI callback) must not escape as a rejection: callers treat that
|
|
172
|
+
* as a crashed turn, and an error must never read as consent. It becomes an
|
|
173
|
+
* explicit denial instead.
|
|
174
|
+
*/
|
|
175
|
+
decide(request: ApprovalRequest): Promise<ApprovalDecision>;
|
|
176
|
+
/** Route one request to the prompt, the human, or the reviewers. */
|
|
177
|
+
private route;
|
|
178
|
+
private human;
|
|
179
|
+
private automated;
|
|
180
|
+
/**
|
|
181
|
+
* Combine the review rounds into one decision.
|
|
182
|
+
*
|
|
183
|
+
* With more than one round the votes are deliberately not pooled: each round
|
|
184
|
+
* is judged on its own and every round must clear the call, so one confident
|
|
185
|
+
* round can never outweigh another round's objection. The averaged scores are
|
|
186
|
+
* reported for diagnosis only; they never overturn a round.
|
|
187
|
+
*/
|
|
188
|
+
private fromVotes;
|
|
189
|
+
/**
|
|
190
|
+
* Independent reviews for this call.
|
|
191
|
+
*
|
|
192
|
+
* Rounds run one after another and never see each other's verdicts, so an
|
|
193
|
+
* approval cannot ride on an earlier yes. A round that denies ends the
|
|
194
|
+
* sequence: a delete-class command does not get further chances to pass.
|
|
195
|
+
*/
|
|
196
|
+
private reviewAll;
|
|
197
|
+
/** Missing context only makes a review less informed, never a failure. */
|
|
198
|
+
private conversationContext;
|
|
199
|
+
/** Progress callbacks are best-effort: a broken UI must not fail the gate. */
|
|
200
|
+
private notifyReview;
|
|
201
|
+
/** One reviewer = one fresh agent with its own client and prompt. */
|
|
202
|
+
private review;
|
|
203
|
+
}
|
|
204
|
+
export {};
|
package/dist/llm/approval.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LLMClient } from "./client.js";
|
|
2
|
+
import { currentTask } from "./runtime.js";
|
|
2
3
|
/** Opt out of approval for the `high` class. */
|
|
3
4
|
export const NO_APPROVAL_PHRASE = "无需审批";
|
|
4
5
|
/** Opt out of approval for every class, acknowledging the risk. */
|
|
@@ -307,8 +308,12 @@ export class ApprovalGate {
|
|
|
307
308
|
* explicit denial instead.
|
|
308
309
|
*/
|
|
309
310
|
async decide(request) {
|
|
311
|
+
const scope = currentTask();
|
|
312
|
+
const span = scope?.runtime.diagnostics?.span("approval_decision", { tool_name: request.tool }, scope);
|
|
310
313
|
try {
|
|
311
|
-
|
|
314
|
+
const decision = await this.route(request, 0);
|
|
315
|
+
span?.end({ approved: decision.approved, status: decision.source });
|
|
316
|
+
return decision;
|
|
312
317
|
}
|
|
313
318
|
catch (err) {
|
|
314
319
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -318,6 +323,9 @@ export class ApprovalGate {
|
|
|
318
323
|
reason: `the approval gate could not run (${detail}); the call was not approved`,
|
|
319
324
|
};
|
|
320
325
|
}
|
|
326
|
+
finally {
|
|
327
|
+
span?.end();
|
|
328
|
+
}
|
|
321
329
|
}
|
|
322
330
|
/** Route one request to the prompt, the human, or the reviewers. */
|
|
323
331
|
async route(request, depth = 0) {
|
|
@@ -363,7 +371,9 @@ export class ApprovalGate {
|
|
|
363
371
|
const malformed = refuseUnjustified(request);
|
|
364
372
|
if (malformed)
|
|
365
373
|
return malformed;
|
|
366
|
-
const
|
|
374
|
+
const scope = currentTask();
|
|
375
|
+
const span = scope?.runtime.diagnostics?.span("human_approval", { tool_name: request.tool }, scope);
|
|
376
|
+
const approved = await this.options.askHuman(request).finally(() => span?.end());
|
|
367
377
|
// A mode change reroutes the same request without treating it as consent.
|
|
368
378
|
if (approved === null)
|
|
369
379
|
return this.route(request, depth + 1);
|
|
@@ -516,7 +526,8 @@ export class ApprovalGate {
|
|
|
516
526
|
const rolePrompt = this.options.reviewerSystemPrompt?.();
|
|
517
527
|
const system = rolePrompt ? `${rolePrompt}\n\n${REVIEW_SYSTEM_PROMPT}` : REVIEW_SYSTEM_PROMPT;
|
|
518
528
|
const controller = new AbortController();
|
|
519
|
-
const raw = await withTimeout(client.complete(system, prompt, { signal: controller.signal, cancelled: this.options.cancelled
|
|
529
|
+
const raw = await withTimeout(client.complete(system, prompt, { signal: controller.signal, cancelled: this.options.cancelled,
|
|
530
|
+
...(currentTask()?.runtime.diagnostics ? { diagnostic: { role: "approval" } } : {}) }), timeout)
|
|
520
531
|
.finally(() => controller.abort());
|
|
521
532
|
if (raw === null) {
|
|
522
533
|
// Fail closed: a reviewer that never answers must not stall the turn.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { type ModelInfo } from "../config.js";
|
|
2
|
+
import type { ToolDefinition } from "./tools.js";
|
|
3
|
+
import { DiagnosticRequest, type DiagnosticRequestContext } from "./diagnostics.js";
|
|
4
|
+
import { type AgentError } from "./runtime.js";
|
|
5
|
+
import { type RetryNotice, type RetryPolicy } from "./retry.js";
|
|
6
|
+
export { describeError } from "./retry.js";
|
|
7
|
+
export interface ToolCall {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
arguments: string;
|
|
11
|
+
}
|
|
12
|
+
export interface UsageInfo {
|
|
13
|
+
prompt: number;
|
|
14
|
+
completion: number;
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
/** OpenAI reasoning models require max_completion_tokens; other compatible APIs use max_tokens. */
|
|
18
|
+
export declare function completionLimit(model: ModelInfo, provider: string, tokens: number): Record<string, number>;
|
|
19
|
+
interface ApiMessage {
|
|
20
|
+
role: string;
|
|
21
|
+
content?: string;
|
|
22
|
+
reasoning_content?: string;
|
|
23
|
+
tool_calls?: unknown[];
|
|
24
|
+
tool_call_id?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
/** Original Anthropic blocks, including signed thinking between tool calls. */
|
|
27
|
+
anthropic_content?: unknown[];
|
|
28
|
+
}
|
|
29
|
+
export interface StreamOptions {
|
|
30
|
+
/** Internal opt-in timing metadata; never included in a provider payload. */
|
|
31
|
+
diagnostic?: DiagnosticRequestContext;
|
|
32
|
+
diagnosticObserver?: DiagnosticRequest;
|
|
33
|
+
maxOutputTokens?: number;
|
|
34
|
+
/** Reserved final request after normal request capacity has been spent. */
|
|
35
|
+
final?: boolean;
|
|
36
|
+
/** Cancels both the connection and an in-progress response body. */
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
/**
|
|
39
|
+
* Reported before each wait so the UI can explain why the stream went quiet
|
|
40
|
+
* for up to 160s instead of looking hung.
|
|
41
|
+
*/
|
|
42
|
+
onRetry?: (notice: RetryNotice) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Polled during the waits: a paused turn, a newer message or a quit has no
|
|
45
|
+
* interest in the reply any more, and must not be held up by a long backoff.
|
|
46
|
+
*/
|
|
47
|
+
cancelled?: () => boolean;
|
|
48
|
+
/** Override the policy (tests, or a caller with its own budget). */
|
|
49
|
+
retry?: Partial<RetryPolicy>;
|
|
50
|
+
}
|
|
51
|
+
export declare class LLMClient {
|
|
52
|
+
readonly professional: boolean;
|
|
53
|
+
model: ModelInfo;
|
|
54
|
+
provider: string;
|
|
55
|
+
intensity: string;
|
|
56
|
+
temperature: number;
|
|
57
|
+
top_p: number;
|
|
58
|
+
toolCalls: ToolCall[];
|
|
59
|
+
usage: UsageInfo | null;
|
|
60
|
+
outcome: {
|
|
61
|
+
status: "completed" | "failed" | "cancelled";
|
|
62
|
+
error?: AgentError;
|
|
63
|
+
} | null;
|
|
64
|
+
finishReason: string | null;
|
|
65
|
+
/**
|
|
66
|
+
* The thinking text streamed alongside the reply. DeepSeek's thinking mode
|
|
67
|
+
* requires it to be echoed back on the assistant message that carries a tool
|
|
68
|
+
* call, so the agent loops attach it to that message.
|
|
69
|
+
*/
|
|
70
|
+
reasoningContent: string;
|
|
71
|
+
anthropicContent: unknown[];
|
|
72
|
+
constructor(model: ModelInfo, provider: string, intensity: string, options?: {
|
|
73
|
+
professional?: boolean;
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Ask the model to compress a transcript into a short summary. Returns an
|
|
77
|
+
* empty string on any failure so the caller can fall back to simply dropping
|
|
78
|
+
* the old messages. Uses a throw-away request (no tools) via the normal
|
|
79
|
+
* streaming path, so provider differences are handled for free.
|
|
80
|
+
*/
|
|
81
|
+
summarize(transcript: string, options?: StreamOptions): Promise<string>;
|
|
82
|
+
/**
|
|
83
|
+
* One-shot completion with its own system prompt. The approval reviewers use
|
|
84
|
+
* this so they never share context with the conversation they are judging.
|
|
85
|
+
*/
|
|
86
|
+
complete(system: string, user: string, options?: StreamOptions): Promise<string>;
|
|
87
|
+
streamTurn(messages: ApiMessage[], tools?: ToolDefinition[], options?: StreamOptions): AsyncGenerator<string>;
|
|
88
|
+
/**
|
|
89
|
+
* Drive one request, retrying while the failure is transient.
|
|
90
|
+
*
|
|
91
|
+
* A response can be cut off on the very first read — "terminated" when a
|
|
92
|
+
* proxy or the server closes the connection mid-flight — or the request can
|
|
93
|
+
* never leave at all. Nothing has reached the user yet, so re-issuing is
|
|
94
|
+
* invisible and cannot duplicate text; the attempts and the 5s/10s/20s/…
|
|
95
|
+
* backoff live in `retry.ts`.
|
|
96
|
+
*
|
|
97
|
+
* Once a token has been emitted a retry would replay the reply from its
|
|
98
|
+
* beginning, so the error is surfaced instead and the partial text is kept.
|
|
99
|
+
*/
|
|
100
|
+
private streamOpenAI;
|
|
101
|
+
/**
|
|
102
|
+
* DeepSeek's thinking mode rejects a tools-bearing request when any assistant
|
|
103
|
+
* message in its history carries no `reasoning_content` ("The
|
|
104
|
+
* `reasoning_content` in the thinking mode must be passed back to the API").
|
|
105
|
+
* An empty string is accepted, which is the honest value for a message that
|
|
106
|
+
* predates this turn's thinking: a compressed summary, the planner's plan, an
|
|
107
|
+
* error note or a session saved before this rule existed. Endpoints outside
|
|
108
|
+
* thinking mode must not receive the field at all.
|
|
109
|
+
*/
|
|
110
|
+
private wireMessages;
|
|
111
|
+
/** A single streaming attempt; see `streamOpenAI` for the retry policy. */
|
|
112
|
+
private openAIAttempt;
|
|
113
|
+
/** Anthropic counterpart of `streamOpenAI`, with the same retry policy. */
|
|
114
|
+
private streamAnthropic;
|
|
115
|
+
/** A single streaming attempt; see `streamAnthropic` for the retry policy. */
|
|
116
|
+
private anthropicAttempt;
|
|
117
|
+
private captureAnthropicContent;
|
|
118
|
+
}
|