@kontextmind/kxm 0.7.7 → 0.7.9

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.
@@ -15163,6 +15163,7 @@ function defaultSpawn(command, args, options) {
15163
15163
  let child;
15164
15164
  const finish = () => {
15165
15165
  if (finished) return;
15166
+ if (stopping && !killSent) return;
15166
15167
  finished = true;
15167
15168
  for (const timer of [wallTimer, killTimer, drainTimer, reapTimer]) clearTimeout(timer);
15168
15169
  options.signal?.removeEventListener("abort", onAbort);
@@ -15172,7 +15173,18 @@ function defaultSpawn(command, args, options) {
15172
15173
  child?.unref();
15173
15174
  const started = Boolean(child?.pid);
15174
15175
  if (started && !observedChildExit) error ??= new Error("process_exit_unobserved");
15175
- resolve7({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8"), code, signal, started, observedChildExit, terminationRequested: stopping, ...error ? { error } : {} });
15176
+ const unverifiedDescendants = stopping || started && !observedChildExit;
15177
+ resolve7({
15178
+ stdout: Buffer.concat(stdout).toString("utf8"),
15179
+ stderr: Buffer.concat(stderr).toString("utf8"),
15180
+ code,
15181
+ signal,
15182
+ started,
15183
+ observedChildExit,
15184
+ terminationRequested: stopping,
15185
+ ...unverifiedDescendants ? { unverifiedDescendants: true } : {},
15186
+ ...error ? { error } : {}
15187
+ });
15176
15188
  };
15177
15189
  const kill = (requested) => {
15178
15190
  killProcessTree(child, requested);
@@ -15203,7 +15215,7 @@ function defaultSpawn(command, args, options) {
15203
15215
  cwd: options.cwd,
15204
15216
  env: options.env ?? process.env,
15205
15217
  stdio: ["pipe", "pipe", "pipe"],
15206
- shell: false,
15218
+ shell: options.shell === true,
15207
15219
  windowsHide: true,
15208
15220
  detached: process.platform !== "win32"
15209
15221
  });
@@ -15247,7 +15259,7 @@ function defaultSpawn(command, args, options) {
15247
15259
  closed = true;
15248
15260
  code = exitCode;
15249
15261
  signal = exitSignal;
15250
- if (!stopping || killSent) finish();
15262
+ finish();
15251
15263
  });
15252
15264
  options.signal?.addEventListener("abort", onAbort, { once: true });
15253
15265
  if (options.signal?.aborted) onAbort();
@@ -15688,6 +15700,31 @@ function defaultRunner(env) {
15688
15700
  }
15689
15701
  };
15690
15702
  }
15703
+ function commandResultFromSpawn(result) {
15704
+ const errno = result.error && "code" in result.error ? result.error.code : void 0;
15705
+ const error = (typeof errno === "string" && errno ? errno : void 0) ?? result.error?.message ?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : void 0);
15706
+ return {
15707
+ ok: result.code === 0 && !error,
15708
+ code: result.code,
15709
+ stdout: result.stdout,
15710
+ stderr: result.stderr,
15711
+ ...error ? { error } : {}
15712
+ };
15713
+ }
15714
+ function defaultAsyncRunner(env, signal) {
15715
+ return async (command, args, timeoutMs) => {
15716
+ try {
15717
+ return commandResultFromSpawn(await defaultSpawn(command, args, {
15718
+ env,
15719
+ timeoutMs,
15720
+ signal,
15721
+ shell: harnessSpawnUsesShell(command)
15722
+ }));
15723
+ } catch (error) {
15724
+ return { ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : "spawn_failed" };
15725
+ }
15726
+ };
15727
+ }
15691
15728
  function firstLine(text) {
15692
15729
  const line = text.split(/\r?\n/).map((candidate) => candidate.trim()).find(Boolean);
15693
15730
  return line && line.length <= 200 ? line : void 0;
@@ -16103,35 +16140,31 @@ var PendingHarnessCommand = class {
16103
16140
  this.key = key;
16104
16141
  }
16105
16142
  };
16106
- async function probeHarnessAssignmentAsync(options) {
16143
+ async function replayHarnessProbe(run, execute, limit) {
16107
16144
  const observed = /* @__PURE__ */ new Map();
16108
- const run = options.runCommand ?? (async (command, args, timeoutMs) => {
16109
- const result = await defaultSpawn(command, args, { env: options.env, timeoutMs, signal: options.signal });
16110
- const error = result.error?.message ?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : void 0);
16111
- return {
16112
- ok: result.code === 0 && !error,
16113
- code: result.code,
16114
- stdout: result.stdout,
16115
- stderr: result.stderr,
16116
- ...error ? { error } : {}
16117
- };
16118
- });
16119
- for (let commands = 0; commands <= 32; commands++) {
16145
+ for (let commands = 0; commands <= limit; commands++) {
16120
16146
  try {
16121
- return probeHarnessAssignment({ ...options, runCommand(command, args, timeoutMs) {
16147
+ return execute((command, args, timeoutMs) => {
16122
16148
  const key = JSON.stringify([command, args, timeoutMs]);
16123
16149
  const result = observed.get(key);
16124
16150
  if (result) return result;
16125
16151
  throw new PendingHarnessCommand(command, args, timeoutMs, key);
16126
- } });
16152
+ });
16127
16153
  } catch (pending) {
16128
16154
  if (!(pending instanceof PendingHarnessCommand)) throw pending;
16129
- if (commands === 32) throw new Error("auth_probe_command_limit");
16155
+ if (commands === limit) throw new Error("auth_probe_command_limit");
16130
16156
  observed.set(pending.key, await run(pending.command, pending.args, pending.timeoutMs));
16131
16157
  }
16132
16158
  }
16133
16159
  throw new Error("auth_probe_command_limit");
16134
16160
  }
16161
+ async function probeHarnessAssignmentAsync(options) {
16162
+ return replayHarnessProbe(
16163
+ options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
16164
+ (runCommand) => probeHarnessAssignment({ ...options, runCommand }),
16165
+ 32
16166
+ );
16167
+ }
16135
16168
 
16136
16169
  // plugins/kxm/src/vnext-config.ts
16137
16170
  var VnextConfigError = class extends Error {
@@ -21904,9 +21937,28 @@ import { join as join11 } from "node:path";
21904
21937
  import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
21905
21938
  import { lstat, mkdir, open, rename } from "node:fs/promises";
21906
21939
  import { join as join6 } from "node:path";
21940
+ var ONESHOT_EVIDENCE_SCHEMA = "kxm.oneshot-evidence.v2";
21941
+ var RETIRED_ONESHOT_EVIDENCE_SCHEMAS = /* @__PURE__ */ new Set(["kxm.oneshot-evidence.v1"]);
21907
21942
  var TEXT_LIMIT = 4 * 1024 * 1024;
21943
+ var ARGV_LIMIT = 64 * 1024;
21908
21944
  var RECORD_LIMIT = 16 * 1024 * 1024;
21909
21945
  var digest = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
21946
+ function parseOneShotEvidenceRecord(bytes) {
21947
+ let parsed;
21948
+ try {
21949
+ parsed = JSON.parse(bytes);
21950
+ } catch {
21951
+ throw new Error("oneshot_evidence_record_invalid");
21952
+ }
21953
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
21954
+ throw new Error("oneshot_evidence_record_invalid");
21955
+ }
21956
+ const schema = parsed.schema;
21957
+ if (typeof schema !== "string") throw new Error("oneshot_evidence_record_invalid");
21958
+ if (RETIRED_ONESHOT_EVIDENCE_SCHEMAS.has(schema)) throw new Error("oneshot_evidence_schema_retired");
21959
+ if (schema !== ONESHOT_EVIDENCE_SCHEMA) throw new Error("oneshot_evidence_schema_unknown");
21960
+ return parsed;
21961
+ }
21910
21962
  async function beginOneShotEvidence(root, intent, sensitive) {
21911
21963
  await mkdir(root, { recursive: true, mode: 448 });
21912
21964
  const stat = await lstat(root);
@@ -21920,11 +21972,11 @@ async function beginOneShotEvidence(root, intent, sensitive) {
21920
21972
  const redact = (value) => {
21921
21973
  let text = value;
21922
21974
  for (const secret of secrets) text = text.replaceAll(secret, "[REDACTED]");
21923
- return text.replace(/(\b(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\b["']?\s*[:=]\s*["']?)(?:Bearer\s+)?[^\s"',;}]+/gi, "$1[REDACTED]");
21975
+ return text.replace(/(\b(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\b["']?\s*[:=]\s*["']?)(?:Bearer\s+)?[^\s"',;}]+/gi, "$1[REDACTED]").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
21924
21976
  };
21925
- const bounded = (value) => {
21977
+ const bounded = (value, max = TEXT_LIMIT) => {
21926
21978
  const bytes = Buffer.from(redact(value));
21927
- return { text: bytes.subarray(0, TEXT_LIMIT).toString("utf8"), truncated: bytes.length > TEXT_LIMIT, sha256: digest(value) };
21979
+ return { text: bytes.subarray(0, max).toString("utf8"), truncated: bytes.length > max, sha256: digest(redact(value)) };
21928
21980
  };
21929
21981
  const write = async (name, value) => {
21930
21982
  const current = await lstat(dir);
@@ -21933,6 +21985,7 @@ async function beginOneShotEvidence(root, intent, sensitive) {
21933
21985
  }
21934
21986
  const bytes = JSON.stringify(value);
21935
21987
  if (Buffer.byteLength(bytes) > RECORD_LIMIT) throw new Error("oneshot_evidence_record_limit");
21988
+ parseOneShotEvidenceRecord(bytes);
21936
21989
  const target = join6(dir, name);
21937
21990
  const partial = `${target}.partial`;
21938
21991
  const file = await open(partial, "wx", 384);
@@ -21954,7 +22007,7 @@ async function beginOneShotEvidence(root, intent, sensitive) {
21954
22007
  return digest(bytes);
21955
22008
  };
21956
22009
  await write("intent.json", {
21957
- schema: "kxm.oneshot-evidence.v1",
22010
+ schema: ONESHOT_EVIDENCE_SCHEMA,
21958
22011
  id,
21959
22012
  recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
21960
22013
  runId: intent.runId,
@@ -21965,9 +22018,10 @@ async function beginOneShotEvidence(root, intent, sensitive) {
21965
22018
  provider: intent.provider,
21966
22019
  requestedModel: intent.model,
21967
22020
  cwd: intent.cwd,
21968
- command: intent.command,
21969
- argv: bounded(JSON.stringify(intent.args)),
22021
+ command: redact(intent.command),
22022
+ argv: bounded(JSON.stringify(intent.args), ARGV_LIMIT),
21970
22023
  stdin: intent.input === void 0 ? null : bounded(intent.input),
22024
+ env: null,
21971
22025
  acceptance: false
21972
22026
  });
21973
22027
  let finished = false;
@@ -21977,7 +22031,7 @@ async function beginOneShotEvidence(root, intent, sensitive) {
21977
22031
  if (finished) throw new Error("oneshot_evidence_already_finished");
21978
22032
  finished = true;
21979
22033
  return write("result.json", {
21980
- schema: "kxm.oneshot-evidence.v1",
22034
+ schema: ONESHOT_EVIDENCE_SCHEMA,
21981
22035
  id,
21982
22036
  recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
21983
22037
  stdout: bounded(result.stdout),
@@ -21987,6 +22041,7 @@ async function beginOneShotEvidence(root, intent, sensitive) {
21987
22041
  started: result.started ?? null,
21988
22042
  observedChildExit: result.observedChildExit ?? null,
21989
22043
  terminationRequested: result.terminationRequested ?? null,
22044
+ unverifiedDescendants: result.unverifiedDescendants ?? null,
21990
22045
  error: result.error ? bounded(result.error.message) : null,
21991
22046
  observation: bounded(JSON.stringify(observation)),
21992
22047
  acceptance: false
@@ -25648,7 +25703,8 @@ Return a final JSON object with an "outcome" field chosen from ${JSON.stringify(
25648
25703
  parsed = { text: "", isError: true };
25649
25704
  }
25650
25705
  const aborted = request.signal.aborted || procResult.error?.message === "process_aborted";
25651
- const effectUncertain = procResult.terminationRequested === true || Boolean(procResult.signal) || procResult.error?.message === "process_exit_unobserved" || procResult.observedChildExit === false && procResult.started !== false;
25706
+ const unverifiedDescendants = procResult.unverifiedDescendants === true || procResult.terminationRequested === true || procResult.error?.message === "process_exit_unobserved" || procResult.observedChildExit === false && procResult.started !== false;
25707
+ const effectUncertain = unverifiedDescendants || Boolean(procResult.signal);
25652
25708
  const transportFailed = procResult.code !== 0 || Boolean(procResult.error) || effectUncertain;
25653
25709
  const outcome = aborted ? "cancelled" : transportFailed || parsed.isError ? "failed" : determineOutcome(parsed.text, request.allowedOutcomes);
25654
25710
  const providerMetadata = {
@@ -25660,6 +25716,7 @@ Return a final JSON object with an "outcome" field chosen from ${JSON.stringify(
25660
25716
  if (procResult.observedChildExit !== void 0) providerMetadata.observedChildExit = procResult.observedChildExit;
25661
25717
  if (procResult.started !== void 0) providerMetadata.processStarted = procResult.started;
25662
25718
  if (procResult.terminationRequested !== void 0) providerMetadata.terminationRequested = procResult.terminationRequested;
25719
+ if (unverifiedDescendants) providerMetadata.descendantEffects = "unverified";
25663
25720
  if (procResult.error) {
25664
25721
  const reason = procResult.error.message;
25665
25722
  providerMetadata.processError = /^process_(aborted|timeout|output_limit|stdin_error|stdio_error|stdio_unclosed|exit_unobserved)$/.test(reason) ? reason : "process_error";
@@ -26140,7 +26197,6 @@ async function startVnextRuntimeSupervisorInner(paths, requestedPortOption, now)
26140
26197
  });
26141
26198
  } catch (err) {
26142
26199
  earlyError = err;
26143
- drivePromise = Promise.reject(err);
26144
26200
  }
26145
26201
  await Promise.resolve();
26146
26202
  if (earlyError) {
@@ -26166,6 +26222,7 @@ async function startVnextRuntimeSupervisorInner(paths, requestedPortOption, now)
26166
26222
  } catch {
26167
26223
  }
26168
26224
  }
26225
+ }).catch(() => {
26169
26226
  });
26170
26227
  sendJson(response, 202, {
26171
26228
  ok: true,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxm-claude-plugin",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -81,7 +81,7 @@ import { loadKxmUpdateConfig } from "../kxm-update-config.ts";
81
81
  import {
82
82
  formatHarnessUpdate,
83
83
  planHarnessUpdate,
84
- probeHarnesses,
84
+ probeHarnessesAsync,
85
85
  runHarnessUpdate,
86
86
  type HarnessUpdateScope,
87
87
  } from "../vnext-harness.ts";
@@ -399,7 +399,7 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
399
399
  return kxmApply?.ok === false ? 1 : 0;
400
400
  }
401
401
  const scope: HarnessUpdateScope = options.self ? "self" : options.extensions ? "extensions" : options.models ? "models" : "all";
402
- const inventory = probeHarnesses({ env: runtime.env });
402
+ const inventory = await probeHarnessesAsync({ env: runtime.env });
403
403
  const planned = planHarnessUpdate(inventory, { ...(harness ? { harness } : {}), scope });
404
404
  const steps = runHarnessUpdate(planned, { env: runtime.env, dryRun: runtime.dryRun });
405
405
  const failed = steps.some((step) => step.outcome === "failed") || kxmApply?.ok === false;
@@ -755,7 +755,7 @@ export async function maybeOfferGuideSetup(runtime: Runtime): Promise<void> {
755
755
  if (runtime.env[GUIDE_SETUP_OPT_OUT_ENV]?.trim()) return;
756
756
  let inventory;
757
757
  try {
758
- inventory = probeHarnesses({ env: runtime.env });
758
+ inventory = await probeHarnessesAsync({ env: runtime.env });
759
759
  } catch {
760
760
  return;
761
761
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { parse as parseYaml } from "yaml";
4
- import { probeHarnesses } from "../vnext-harness.ts";
4
+ import { probeHarnessesAsync } from "../vnext-harness.ts";
5
5
  import { suggestWorkflowAndRoles } from "../suggest.ts";
6
6
  import {
7
7
  createGoal,
@@ -32,7 +32,7 @@ export async function cmdSuggest(runtime: Runtime, promptParts: string[]): Promi
32
32
  runtime.io.stderr("prompt must be non-empty\n");
33
33
  return 2;
34
34
  }
35
- const inventory = probeHarnesses({ env: runtime.env });
35
+ const inventory = await probeHarnessesAsync({ env: runtime.env });
36
36
  const availableHarnesses = inventory.harnesses.map((h) => ({
37
37
  harness: h.id,
38
38
  auth: h.authenticated === true ? "authenticated" : "unauthenticated",
@@ -25,7 +25,7 @@ import {
25
25
  import { vnextRuntimePaths } from "../vnext-runtime-store.ts";
26
26
  import {
27
27
  formatHarnessInventory,
28
- probeHarnesses,
28
+ probeHarnessesAsync,
29
29
  } from "../vnext-harness.ts";
30
30
  import {
31
31
  fetchLatestKxmVersion,
@@ -580,7 +580,7 @@ export async function cmdVnextRunList(runtime: Runtime): Promise<number> {
580
580
  }
581
581
 
582
582
  export async function cmdHarnessList(runtime: Runtime): Promise<number> {
583
- const inventory = probeHarnesses({ env: runtime.env });
583
+ const inventory = await probeHarnessesAsync({ env: runtime.env });
584
584
  print(runtime.io, runtime.json, { ok: true, command: "harness list", ...inventory }, formatHarnessInventory(inventory));
585
585
  return 0;
586
586
  }
@@ -7,7 +7,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
7
7
  import { deliverInboxNotification } from "./inbox.ts";
8
8
  import type { HubEvent, MessageRecord } from "./protocol.ts";
9
9
 
10
- const VERSION = "0.7.7";
10
+ const VERSION = "0.7.9";
11
11
  const inbox = new Map<string, MessageRecord>();
12
12
  const notifiedInbox = new Set<string>();
13
13
  let meshClient: HubClient | undefined;
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
- import { spawnSync } from "node:child_process";
3
2
  import { join } from "node:path";
4
3
  import { stringify } from "yaml";
4
+ import { defaultSpawn } from "./vnext-oneshot-process.ts";
5
5
 
6
6
  export interface InventoryPrice {
7
7
  inputPerMillion: number | null;
@@ -95,10 +95,10 @@ export async function refreshModelInventory(options: { outputRoot: string; env?:
95
95
  if (item.fast === true) current.capabilities.speed = { fast: true, tiers: ["fast"], source };
96
96
  byId.set(key, current);
97
97
  };
98
- const native = (command: string, args: string[], source: string) => {
99
- const result = spawnSync(command, args, { encoding: "utf8", timeout: 20_000, shell: false });
100
- const ok = result.status === 0;
101
- sources[source] = { url: `${command} ${args.join(" ")}`, ok, ...(ok ? {} : { error: (result.stderr || "command failed").trim().slice(0, 240) }) };
98
+ const native = async (command: string, args: string[], source: string) => {
99
+ const result = await defaultSpawn(command, args, { env, timeoutMs: 20_000 });
100
+ const ok = result.code === 0 && !result.error;
101
+ sources[source] = { url: `${command} ${args.join(" ")}`, ok, ...(ok ? {} : { error: (result.stderr || result.error?.message || "command failed").trim().slice(0, 240) }) };
102
102
  if (!ok) return;
103
103
  for (const line of (result.stdout || "").split(/\r?\n/)) {
104
104
  const parts = line.trim().split(/\s+/);
@@ -109,9 +109,9 @@ export async function refreshModelInventory(options: { outputRoot: string; env?:
109
109
  }
110
110
  }
111
111
  };
112
- native("pi", ["--list-models"], "pi");
113
- native("grok", ["models"], "grok");
114
- native("agy", ["models"], "agy");
112
+ await native("pi", ["--list-models"], "pi");
113
+ await native("grok", ["models"], "grok");
114
+ await native("agy", ["models"], "agy");
115
115
  const openrouter = await fetchModels(env.KXM_OPENROUTER_MODELS_URL?.trim() || OR_URL, env.OPENROUTER_API_KEY);
116
116
  const openrouterUrl = env.KXM_OPENROUTER_MODELS_URL?.trim() || OR_URL;
117
117
  sources.openrouter = { url: openrouterUrl, ok: !openrouter.error, ...(openrouter.error ? { error: openrouter.error } : {}) };
@@ -10,3 +10,4 @@ export * from "./improve.ts";
10
10
  export * from "./browser.ts";
11
11
  export * from "./modes.ts";
12
12
  export * from "./ssh-remote.ts";
13
+ export * from "./subagent-control.ts";
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Supervisory agent control, in-process real-time steering, and work-splitting.
3
+ *
4
+ * Implements:
5
+ * - Dual-tool control plane: `agent` (spawn) and `agent_control` (manage, poll, steer, stop).
6
+ * - Strict capability narrowing via `allowed_tools[]` allowlists.
7
+ * - Mid-flight steering instruction injection.
8
+ * - Subagent state lifecycle management (working, blocked, idle, done, error).
9
+ */
10
+
11
+ import { randomUUID } from "node:crypto";
12
+ import { assertCommandSeatbelt } from "./safety-integrity.ts";
13
+
14
+ export type SubagentType = "general" | "Explore" | "Plan" | "Reviewer" | "Auditor";
15
+ export type SubagentStatus = "pending" | "running" | "blocked" | "completed" | "stopped" | "failed";
16
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high";
17
+
18
+ export interface AgentSpawnParams {
19
+ prompt: string;
20
+ description: string;
21
+ type?: SubagentType | undefined;
22
+ model?: string | undefined;
23
+ thinking?: ThinkingLevel | undefined;
24
+ allowed_tools?: string[] | undefined;
25
+ turns?: number | undefined;
26
+ background?: boolean | undefined;
27
+ parent_tools?: readonly string[] | undefined;
28
+ }
29
+
30
+ export interface AgentControlParams {
31
+ action: "info" | "result" | "steer" | "stop";
32
+ kind?: "types" | "models" | "active" | undefined;
33
+ agent_id?: string | undefined;
34
+ message?: string | undefined;
35
+ turns?: number | undefined;
36
+ verbose?: boolean | undefined;
37
+ }
38
+
39
+ export interface SubagentRecord {
40
+ id: string;
41
+ description: string;
42
+ type: SubagentType;
43
+ model: string;
44
+ thinking: ThinkingLevel;
45
+ allowedTools: string[];
46
+ status: SubagentStatus;
47
+ turnsMax: number;
48
+ turnsCompleted: number;
49
+ prompt: string;
50
+ createdAt: string;
51
+ updatedAt: string;
52
+ steeringMessages: Array<{ text: string; injectedAt: string; processed: boolean }>;
53
+ output?: string | undefined;
54
+ error?: string | undefined;
55
+ }
56
+
57
+ export interface SubagentExecutionReceipt {
58
+ ok: boolean;
59
+ action: string;
60
+ agent_id?: string | undefined;
61
+ status?: SubagentStatus | undefined;
62
+ activeAgents?: Array<{ id: string; description: string; type: SubagentType; status: SubagentStatus }> | undefined;
63
+ types?: readonly SubagentType[] | undefined;
64
+ result?: string | undefined;
65
+ steered?: boolean | undefined;
66
+ stopped?: boolean | undefined;
67
+ error?: string | undefined;
68
+ }
69
+
70
+ export const SUBAGENT_TYPES: readonly SubagentType[] = Object.freeze([
71
+ "general",
72
+ "Explore",
73
+ "Plan",
74
+ "Reviewer",
75
+ "Auditor",
76
+ ]);
77
+
78
+ export const DEFAULT_SUBAGENT_MODELS: Readonly<Record<SubagentType, string>> = Object.freeze({
79
+ general: "grok/grok-4.6",
80
+ Explore: "openrouter/qwen/qwen3-coder-plus",
81
+ Plan: "claude/fable",
82
+ Reviewer: "openai/gpt-5.6-sol",
83
+ Auditor: "claude/fable",
84
+ });
85
+
86
+ /**
87
+ * Validates and intersects child allowed tools against the parent tool surface.
88
+ * Enforces the safety invariant that a child can only narrow, never expand authority.
89
+ */
90
+ export function narrowSubagentTools(
91
+ requestedTools: readonly string[] | undefined,
92
+ parentTools?: readonly string[] | undefined,
93
+ ): string[] {
94
+ if (!requestedTools || requestedTools.length === 0) {
95
+ return parentTools ? [...parentTools] : ["read", "grep", "find"];
96
+ }
97
+
98
+ if (!parentTools || parentTools.length === 0) {
99
+ return [...requestedTools];
100
+ }
101
+
102
+ const parentSet = new Set(parentTools);
103
+ const narrowed: string[] = [];
104
+
105
+ for (const tool of requestedTools) {
106
+ if (parentSet.has(tool)) {
107
+ narrowed.push(tool);
108
+ }
109
+ }
110
+
111
+ if (narrowed.length === 0 && requestedTools.length > 0) {
112
+ throw new Error(
113
+ `Cannot spawn subagent: requested tools [${requestedTools.join(", ")}] expand beyond parent capabilities.`,
114
+ );
115
+ }
116
+
117
+ return narrowed;
118
+ }
119
+
120
+ /**
121
+ * In-Process Subagent Manager.
122
+ */
123
+ export class SubagentManager {
124
+ private readonly subagents = new Map<string, SubagentRecord>();
125
+
126
+ /**
127
+ * Spawns a new managed subagent with strict capability boundaries.
128
+ */
129
+ spawn(params: AgentSpawnParams): SubagentRecord {
130
+ if (!params.prompt || !params.prompt.trim()) {
131
+ throw new Error('Subagent "prompt" is required.');
132
+ }
133
+ if (!params.description || !params.description.trim()) {
134
+ throw new Error('Subagent "description" is required.');
135
+ }
136
+
137
+ const type = params.type ?? "general";
138
+ const model = params.model ?? DEFAULT_SUBAGENT_MODELS[type] ?? "grok/grok-4.6";
139
+ const thinking = params.thinking ?? "low";
140
+ const allowedTools = narrowSubagentTools(params.allowed_tools, params.parent_tools);
141
+ const id = `ag_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
142
+ const now = new Date().toISOString();
143
+
144
+ const record: SubagentRecord = {
145
+ id,
146
+ description: params.description.trim(),
147
+ type,
148
+ model,
149
+ thinking,
150
+ allowedTools,
151
+ status: params.background === false ? "completed" : "running",
152
+ turnsMax: params.turns ?? 10,
153
+ turnsCompleted: 0,
154
+ prompt: params.prompt.trim(),
155
+ createdAt: now,
156
+ updatedAt: now,
157
+ steeringMessages: [],
158
+ };
159
+
160
+ this.subagents.set(id, record);
161
+ return record;
162
+ }
163
+
164
+ /**
165
+ * Dispatches an action from the `agent_control` tool.
166
+ */
167
+ control(params: AgentControlParams): SubagentExecutionReceipt {
168
+ const action = params.action;
169
+
170
+ // 1. Action: "info"
171
+ if (action === "info") {
172
+ const kind = params.kind ?? "active";
173
+ if (kind === "types") {
174
+ return {
175
+ ok: true,
176
+ action: "info",
177
+ types: SUBAGENT_TYPES,
178
+ };
179
+ }
180
+ if (kind === "models") {
181
+ return {
182
+ ok: true,
183
+ action: "info",
184
+ result: JSON.stringify(DEFAULT_SUBAGENT_MODELS),
185
+ };
186
+ }
187
+ const active = Array.from(this.subagents.values()).map((ag) => ({
188
+ id: ag.id,
189
+ description: ag.description,
190
+ type: ag.type,
191
+ status: ag.status,
192
+ }));
193
+ return {
194
+ ok: true,
195
+ action: "info",
196
+ activeAgents: active,
197
+ };
198
+ }
199
+
200
+ if (!params.agent_id) {
201
+ return {
202
+ ok: false,
203
+ action,
204
+ error: 'Parameter "agent_id" is required for result, steer, and stop actions.',
205
+ };
206
+ }
207
+
208
+ const agent = this.subagents.get(params.agent_id);
209
+ if (!agent) {
210
+ return {
211
+ ok: false,
212
+ action,
213
+ agent_id: params.agent_id,
214
+ error: `Subagent "${params.agent_id}" not found.`,
215
+ };
216
+ }
217
+
218
+ // 2. Action: "result"
219
+ if (action === "result") {
220
+ return {
221
+ ok: true,
222
+ action: "result",
223
+ agent_id: agent.id,
224
+ status: agent.status,
225
+ result: agent.output ?? `[Subagent is currently ${agent.status}]`,
226
+ };
227
+ }
228
+
229
+ // 3. Action: "steer"
230
+ if (action === "steer") {
231
+ if (!params.message || !params.message.trim()) {
232
+ return {
233
+ ok: false,
234
+ action: "steer",
235
+ agent_id: agent.id,
236
+ error: 'Parameter "message" is required for steer action.',
237
+ };
238
+ }
239
+
240
+ if (agent.status === "completed" || agent.status === "stopped" || agent.status === "failed") {
241
+ return {
242
+ ok: false,
243
+ action: "steer",
244
+ agent_id: agent.id,
245
+ error: `Cannot steer subagent "${agent.id}" because it is already ${agent.status}.`,
246
+ };
247
+ }
248
+
249
+ agent.steeringMessages.push({
250
+ text: params.message.trim(),
251
+ injectedAt: new Date().toISOString(),
252
+ processed: false,
253
+ });
254
+ agent.updatedAt = new Date().toISOString();
255
+
256
+ return {
257
+ ok: true,
258
+ action: "steer",
259
+ agent_id: agent.id,
260
+ status: agent.status,
261
+ steered: true,
262
+ };
263
+ }
264
+
265
+ // 4. Action: "stop"
266
+ if (action === "stop") {
267
+ agent.status = "stopped";
268
+ agent.updatedAt = new Date().toISOString();
269
+ return {
270
+ ok: true,
271
+ action: "stop",
272
+ agent_id: agent.id,
273
+ status: "stopped",
274
+ stopped: true,
275
+ };
276
+ }
277
+
278
+ return {
279
+ ok: false,
280
+ action,
281
+ error: `Unknown control action "${action}".`,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Retrieves an agent record by ID.
287
+ */
288
+ get(agentId: string): SubagentRecord | undefined {
289
+ return this.subagents.get(agentId);
290
+ }
291
+
292
+ /**
293
+ * Marks a subagent as completed with output.
294
+ */
295
+ complete(agentId: string, output: string): void {
296
+ const agent = this.subagents.get(agentId);
297
+ if (agent) {
298
+ agent.status = "completed";
299
+ agent.output = output;
300
+ agent.updatedAt = new Date().toISOString();
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Returns all active running subagents.
306
+ */
307
+ listActive(): SubagentRecord[] {
308
+ return Array.from(this.subagents.values()).filter(
309
+ (ag) => ag.status === "running" || ag.status === "pending" || ag.status === "blocked",
310
+ );
311
+ }
312
+ }