@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.
@@ -1,7 +1,9 @@
1
1
  import { LLMClient } from "../llm/client.js";
2
- import { buildSubAgentTask, formatSubAgentResult, runSubAgent } from "../llm/subagent.js";
2
+ import { buildSubAgentTask, formatSubAgentResult, runSubAgent, subAgentRounds } from "../llm/subagent.js";
3
3
  import { DELEGATE_TOOL, getSubAgentToolDefinitions } from "../llm/tools.js";
4
+ import { loadConfig } from "../config.js";
4
5
  import { ACTIVE_PRO_ROLES, PRO_ROLES, loadProPrompt } from "./config.js";
6
+ import { currentTask, TaskRuntime, TaskControlError, taskLimits } from "../llm/runtime.js";
5
7
  export const PRO_AGENT_TOOL = "call_pro_agent";
6
8
  const CALL_AGENT = {
7
9
  type: "function",
@@ -16,6 +18,8 @@ const CALL_AGENT = {
16
18
  role: { type: "string", enum: [...ACTIVE_PRO_ROLES] },
17
19
  task: { type: "string", description: "Complete task, expected deliverables and acceptance criteria." },
18
20
  context: { type: "string", description: "Relevant evidence, constraints and decisions; the agent cannot see the parent's conversation." },
21
+ read_only: { type: "boolean", description: "Restrict this task and all descendants to read-only tools." },
22
+ write_paths: { type: "array", items: { type: "string" }, description: "Optional allowed write paths. Descendants can only narrow this scope; arbitrary shell commands are unavailable under a path restriction." },
19
23
  reason: { type: "string", description: "Why this specialist is needed for the user's request." },
20
24
  },
21
25
  required: ["role", "task", "reason"],
@@ -28,10 +32,12 @@ export function getProToolDefinitions() {
28
32
  }
29
33
  /** A resolved model team belongs to one task; substitutions never touch disk. */
30
34
  export class ProAgentTeam {
35
+ runtime;
31
36
  options;
32
37
  maxDepth;
33
38
  constructor(options) {
34
39
  this.options = options;
40
+ this.runtime = options.runtime ?? currentTask()?.runtime ?? new TaskRuntime({ request: options.request ?? "", cwd: options.cwd, limits: taskLimits(loadConfig("pro")) });
35
41
  this.maxDepth = Number.isSafeInteger(options.maxDepth) && options.maxDepth > 0 ? options.maxDepth : 10;
36
42
  }
37
43
  client(role) {
@@ -40,36 +46,56 @@ export class ProAgentTeam {
40
46
  throw new Error(`Pro agent ${role} is unavailable`);
41
47
  return new LLMClient(selection.model, selection.provider, selection.intensity, { professional: true });
42
48
  }
43
- async call(role, task, context = "", depth = 1, readOnly = false) {
49
+ async call(role, task, context = "", depth = 1, readOnly = false, writePaths) {
50
+ return formatSubAgentResult(await this.callResult(role, task, context, depth, readOnly, writePaths));
51
+ }
52
+ contextWindow(role) {
53
+ const selection = this.options.models[role];
54
+ const overrides = loadConfig("pro").model_context_windows;
55
+ const value = selection && overrides?.[`${selection.provider}/${selection.model.name}`];
56
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : selection?.context_window ?? 1_000_000;
57
+ }
58
+ async callResult(role, task, context = "", depth = 1, readOnly = false, writePaths) {
59
+ const failure = (code, message) => {
60
+ const result = { runId: currentTask()?.runId ?? this.runtime.rootRunId,
61
+ status: code === "cancelled" ? "cancelled" : "failed", report: "", evidenceIds: [], artifacts: [], blockers: [],
62
+ error: { code, message }, aborted: code === "cancelled", exhausted: false, rounds: 0, calls: 0 };
63
+ this.runtime.recordResult(result);
64
+ return result;
65
+ };
44
66
  if (!PRO_ROLES.includes(role))
45
- return "[Error: unknown Pro agent role]";
67
+ return failure("unknown_role", "unknown Pro agent role");
46
68
  if (role === "genimage_model")
47
- return "[Error: image generation is reserved and is not enabled]";
48
- if (this.options.cancelled())
49
- return "[Pro agent stopped: the turn was paused or replaced.]";
69
+ return failure("reserved", "image generation is reserved and is not enabled");
70
+ if (this.options.cancelled() || this.runtime.signal.aborted)
71
+ return failure("cancelled", "the turn was paused or replaced");
50
72
  if (!Number.isSafeInteger(depth) || depth < 0 || depth > this.maxDepth) {
51
- return `[Error: Pro agent recursion limit (${this.maxDepth}) reached. Complete the work without further delegation.]`;
73
+ return failure("recursion_limit", `Pro agent recursion limit (${this.maxDepth}) reached. Complete the work without further delegation.`);
52
74
  }
53
75
  if (typeof task !== "string" || !task.trim())
54
- return "[Error: a Pro agent needs a nonempty task]";
76
+ return failure("invalid_task", "a Pro agent needs a nonempty task");
55
77
  const selection = this.options.models[role];
56
78
  if (!selection)
57
- return `[Error: Pro agent ${role} is unavailable]`;
79
+ return failure("unavailable", `Pro agent ${role} is unavailable`);
58
80
  // Reviewers are always read-only, including when consulted as specialists.
59
- const onlyRead = readOnly || role === "confirm_model";
81
+ const parent = currentTask()?.runtime === this.runtime ? currentTask() : this.runtime.root;
82
+ const onlyRead = readOnly || role === "confirm_model" || parent.policy.readOnly;
60
83
  const tools = getProToolDefinitions().filter((tool) => !onlyRead ||
61
- ["read_file", "list_files", PRO_AGENT_TOOL].includes(tool.function.name));
84
+ ["read_file", "list_files", "web_search", PRO_AGENT_TOOL].includes(tool.function.name));
62
85
  const names = new Set(tools.map((tool) => tool.function.name));
63
86
  const label = `${role} · ${selection.model.name} · ${depth}/${this.maxDepth}`;
64
87
  try {
65
- const result = await runSubAgent({
88
+ const maxRounds = subAgentRounds(loadConfig("pro").subagent_max_rounds);
89
+ return await this.runtime.child(role, task, async (scope) => runSubAgent({
90
+ runtime: this.runtime, scope,
66
91
  createClient: () => this.client(role),
67
92
  system: `${loadProPrompt(role)}\n\nDelegation depth: ${depth}/${this.maxDepth}. ` +
68
93
  (onlyRead ? "This consultation is read-only. Do not modify files or execute commands, including through another agent." :
69
94
  "Use the shared approval gate for every tool call. Return a self-contained report with changes, evidence and unresolved work."),
70
- task: buildSubAgentTask({ task, context, cwd: this.options.cwd }),
95
+ task: buildSubAgentTask({ task, context, cwd: this.options.cwd, rounds: maxRounds }),
71
96
  tools,
72
- contextWindow: selection.context_window,
97
+ contextWindow: this.contextWindow(role),
98
+ maxRounds,
73
99
  approve: (name, args) => {
74
100
  if (!names.has(name) && name !== DELEGATE_TOOL)
75
101
  return Promise.resolve({
@@ -82,8 +108,9 @@ export class ProAgentTeam {
82
108
  const nextRole = typeof args.role === "string" ? args.role : "backend_output_model";
83
109
  const nextTask = typeof args.task === "string" ? args.task : "";
84
110
  const nextContext = typeof args.context === "string" ? args.context : "";
85
- const report = await this.call(nextRole, nextTask, nextContext, depth + 1, onlyRead);
86
- this.options.onProgress?.(label);
111
+ const report = await this.call(nextRole, nextTask, nextContext, depth + 1, onlyRead || args.read_only === true, Array.isArray(args.write_paths) ? args.write_paths.filter((p) => typeof p === "string") : undefined);
112
+ if (!this.options.cancelled() && !this.runtime.signal.aborted)
113
+ this.options.onProgress?.(label);
87
114
  return report;
88
115
  }
89
116
  if (!names.has(name))
@@ -94,11 +121,14 @@ export class ProAgentTeam {
94
121
  onUsage: this.options.onUsage,
95
122
  onRetry: this.options.onRetry,
96
123
  onProgress: (activity) => this.options.onProgress?.(`${label} · ${activity}`),
97
- });
98
- return formatSubAgentResult(result);
124
+ }), { readOnly: onlyRead, writePaths });
125
+ }
126
+ catch (error) {
127
+ return failure(error instanceof TaskControlError ? error.code : "agent_error", error instanceof Error ? error.message : String(error));
99
128
  }
100
129
  finally {
101
- this.options.onProgress?.(null);
130
+ if (!this.options.cancelled() && !this.runtime.signal.aborted)
131
+ this.options.onProgress?.(null);
102
132
  }
103
133
  }
104
134
  }
@@ -1,4 +1,7 @@
1
1
  import { loadModels } from "../config.js";
2
+ import { completionLimit } from "../llm/client.js";
3
+ import { currentTask, TaskControlError } from "../llm/runtime.js";
4
+ import { inputTokenBound, DEFAULT_OUTPUT_TOKENS } from "../llm/context.js";
2
5
  import { ACTIVE_PRO_ROLES, loadProModels, } from "./config.js";
3
6
  export class ProPreflightError extends Error {
4
7
  fallbacks;
@@ -50,6 +53,13 @@ function resolveSetting(role, preset, catalog) {
50
53
  /** An authenticated, tiny inference validates the same intensity/tool contract. */
51
54
  async function probe(model, options) {
52
55
  const controller = new AbortController();
56
+ const runtime = currentTask()?.runtime;
57
+ const outputTokens = runtime ? DEFAULT_OUTPUT_TOKENS : 4096;
58
+ const release = runtime?.activity();
59
+ const rootAbort = () => controller.abort(runtime?.signal.reason);
60
+ runtime?.signal.addEventListener("abort", rootAbort, { once: true });
61
+ if (runtime?.signal.aborted)
62
+ rootAbort();
53
63
  const relay = () => controller.abort(options.signal?.reason);
54
64
  if (options.signal?.aborted)
55
65
  relay();
@@ -62,16 +72,20 @@ async function probe(model, options) {
62
72
  name: "pro_availability_check", description: "A harmless availability check; this tool is never executed.",
63
73
  };
64
74
  const payload = anthropic ? {
65
- model: model.model.name, stream: false, max_tokens: 4096,
75
+ model: model.model.name, stream: false, max_tokens: outputTokens,
66
76
  output_config: { effort: model.intensity },
67
77
  messages: [{ role: "user", content: "Availability check. Reply only OK. Do not call tools." }],
68
78
  tools: [{ ...tool, input_schema: parameters }],
69
79
  } : {
70
80
  model: model.model.name, stream: false, reasoning_effort: model.intensity,
81
+ ...(runtime ? completionLimit(model.model, model.provider, outputTokens) : {}),
71
82
  messages: [{ role: "user", content: "Availability check. Reply only OK. Do not call tools." }],
72
83
  tools: [{ type: "function", function: { ...tool, parameters } }],
73
84
  };
85
+ let reservation;
86
+ let used = inputTokenBound(payload) + outputTokens;
74
87
  try {
88
+ reservation = runtime?.request(inputTokenBound(payload), outputTokens);
75
89
  const response = await fetch(model.model.url.replace(/\/+$/, "") + (anthropic ? "/messages" : "/chat/completions"), {
76
90
  method: "POST", signal: controller.signal,
77
91
  headers: {
@@ -94,16 +108,24 @@ async function probe(model, options) {
94
108
  if (body.usage) {
95
109
  const prompt = body.usage.prompt_tokens ?? body.usage.input_tokens ?? 0;
96
110
  const completion = body.usage.completion_tokens ?? body.usage.output_tokens ?? 0;
111
+ used = prompt + completion;
97
112
  options.onUsage?.({ prompt, completion, total: prompt + completion });
98
113
  }
99
114
  return { ok: true, reason: "" };
100
115
  }
101
- catch {
116
+ catch (error) {
117
+ if (error instanceof TaskControlError)
118
+ throw error;
119
+ if (runtime?.signal.aborted)
120
+ throw new Error("Pro availability check was cancelled");
102
121
  if (options.signal?.aborted)
103
122
  throw new Error("Pro availability check was cancelled");
104
123
  return { ok: false, reason: controller.signal.aborted ? "availability request timed out" : "availability request failed" };
105
124
  }
106
125
  finally {
126
+ reservation?.settle(used);
127
+ release?.();
128
+ runtime?.signal.removeEventListener("abort", rootAbort);
107
129
  clearTimeout(timer);
108
130
  options.signal?.removeEventListener("abort", relay);
109
131
  }