open-agents-ai 0.187.448 → 0.187.450

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/dist/index.js CHANGED
@@ -505627,6 +505627,18 @@ var init_agent_tool = __esm({
505627
505627
  description: {
505628
505628
  type: "string",
505629
505629
  description: "Short label for this agent (3-5 words, shown in status)"
505630
+ },
505631
+ // Phase 4 — explicit handoff inputs (clean isolation: parent decides
505632
+ // exactly what the sub-agent sees, instead of inheriting raw history).
505633
+ relevant_files: {
505634
+ type: "array",
505635
+ items: { type: "string" },
505636
+ description: "List of file paths to pre-load into the sub-agent's context. When set, the sub-agent sees these files in its initial system message and does not need to re-discover them. Saves tokens and prevents duplicate file_read calls in the parent's transcript."
505637
+ },
505638
+ constraints: {
505639
+ type: "array",
505640
+ items: { type: "string" },
505641
+ description: "Explicit rules the sub-agent must follow (e.g. 'do not modify foo.ts', 'use only TypeScript', 'return JSON only'). Rendered as a [Constraints] section in the sub-agent's initial system message."
505630
505642
  }
505631
505643
  },
505632
505644
  required: ["prompt"]
@@ -505648,6 +505660,8 @@ var init_agent_tool = __esm({
505648
505660
  const modelOverride = args["model"] ? String(args["model"]) : void 0;
505649
505661
  const isolation = args["isolation"] ? String(args["isolation"]) : void 0;
505650
505662
  const description = args["description"] ? String(args["description"]) : void 0;
505663
+ const relevantFilePaths = Array.isArray(args["relevant_files"]) ? args["relevant_files"].map((s2) => String(s2)) : [];
505664
+ const constraints = Array.isArray(args["constraints"]) ? args["constraints"].map((s2) => String(s2)) : [];
505651
505665
  if (!prompt) {
505652
505666
  return { success: false, output: "", error: "prompt is required", durationMs: performance.now() - start2 };
505653
505667
  }
@@ -505672,11 +505686,58 @@ var init_agent_tool = __esm({
505672
505686
  const model = modelOverride ?? this.config.model;
505673
505687
  const agentId = generateAgentId(subagentType);
505674
505688
  const label = description ?? `${subagentType}: ${prompt.slice(0, 40)}`;
505689
+ const preloadedFiles = [];
505690
+ if (relevantFilePaths.length > 0) {
505691
+ const fs7 = await import("node:fs");
505692
+ const path8 = await import("node:path");
505693
+ const FILE_CAP = 8 * 1024;
505694
+ const TOTAL_FILE_CAP = 5;
505695
+ for (const p2 of relevantFilePaths.slice(0, TOTAL_FILE_CAP)) {
505696
+ try {
505697
+ const abs = path8.isAbsolute(p2) ? p2 : path8.join(this.config.workingDir, p2);
505698
+ const content = fs7.readFileSync(abs, "utf-8");
505699
+ preloadedFiles.push({
505700
+ path: p2,
505701
+ content: content.length > FILE_CAP ? content.slice(0, FILE_CAP) + `
505702
+ [... truncated, original ${content.length} bytes]` : content
505703
+ });
505704
+ } catch {
505705
+ }
505706
+ }
505707
+ }
505708
+ const composedPrompt = (() => {
505709
+ if (preloadedFiles.length === 0 && constraints.length === 0)
505710
+ return prompt;
505711
+ const lines = [];
505712
+ lines.push("[Sub-Agent Handoff — parent has provided this scoped context]");
505713
+ lines.push("");
505714
+ if (preloadedFiles.length > 0) {
505715
+ lines.push("## Pre-loaded files");
505716
+ lines.push("These files have already been read for you. Reference them directly without calling file_read.");
505717
+ lines.push("");
505718
+ for (const f2 of preloadedFiles) {
505719
+ lines.push(`### ${f2.path}`);
505720
+ lines.push("```");
505721
+ lines.push(f2.content);
505722
+ lines.push("```");
505723
+ lines.push("");
505724
+ }
505725
+ }
505726
+ if (constraints.length > 0) {
505727
+ lines.push("## Constraints (must follow)");
505728
+ for (const c9 of constraints)
505729
+ lines.push(`- ${c9}`);
505730
+ lines.push("");
505731
+ }
505732
+ lines.push("## Task");
505733
+ lines.push(prompt);
505734
+ return lines.join("\n");
505735
+ })();
505675
505736
  if (isolation === "worktree") {
505676
505737
  this.callbacks.onViewRegister?.(agentId, label);
505677
505738
  const spawn27 = this.callbacks.spawnSubprocess({
505678
505739
  id: agentId,
505679
- task: prompt,
505740
+ task: composedPrompt,
505680
505741
  model,
505681
505742
  workingDir: this.config.workingDir
505682
505743
  });
@@ -505694,11 +505755,14 @@ var init_agent_tool = __esm({
505694
505755
  this.callbacks.onViewRegister?.(agentId, label);
505695
505756
  const promise = this.callbacks.spawnInProcess({
505696
505757
  id: agentId,
505697
- task: prompt,
505758
+ task: composedPrompt,
505698
505759
  toolNames: resolved.toolNames,
505699
505760
  maxTurns: resolved.maxTurns,
505700
505761
  model,
505701
- systemPromptAddition: resolved.systemPromptAddition
505762
+ systemPromptAddition: resolved.systemPromptAddition,
505763
+ relevantFiles: preloadedFiles,
505764
+ constraints,
505765
+ subAgentMode: true
505702
505766
  }).then((result) => {
505703
505767
  this.callbacks?.onViewStatus?.(agentId, result.completed ? "completed" : "failed");
505704
505768
  return result.completed ? `Agent completed: ${result.summary} (${result.turns} turns, ${result.toolCalls} tool calls)` : `Agent incomplete after ${result.turns} turns`;
@@ -505721,19 +505785,26 @@ var init_agent_tool = __esm({
505721
505785
  try {
505722
505786
  const result = await this.callbacks.spawnInProcess({
505723
505787
  id: agentId,
505724
- task: prompt,
505788
+ task: composedPrompt,
505725
505789
  toolNames: resolved.toolNames,
505726
505790
  maxTurns: resolved.maxTurns,
505727
505791
  model,
505728
- systemPromptAddition: resolved.systemPromptAddition
505792
+ systemPromptAddition: resolved.systemPromptAddition,
505793
+ relevantFiles: preloadedFiles,
505794
+ constraints,
505795
+ subAgentMode: true
505729
505796
  });
505730
505797
  if (result.completed) {
505798
+ const filesLine = result.filesModified && result.filesModified.length > 0 ? `
505799
+ Files modified: ${result.filesModified.slice(0, 8).join(", ")}${result.filesModified.length > 8 ? ` (+${result.filesModified.length - 8} more)` : ""}` : "";
505800
+ const artifactLine = result.artifactIds && result.artifactIds.length > 0 ? `
505801
+ Artifacts: ${result.artifactIds.join(", ")} (call memex_retrieve to view full sub-agent transcript)` : "";
505731
505802
  return {
505732
505803
  success: true,
505733
505804
  output: `Agent completed: ${result.summary}
505734
505805
  Type: ${subagentType}
505735
505806
  Turns: ${result.turns}, Tool calls: ${result.toolCalls}
505736
- Duration: ${(result.durationMs / 1e3).toFixed(1)}s`,
505807
+ Duration: ${(result.durationMs / 1e3).toFixed(1)}s` + filesLine + artifactLine,
505737
505808
  durationMs: performance.now() - start2
505738
505809
  };
505739
505810
  }
@@ -516249,6 +516320,442 @@ var init_messageLog = __esm({
516249
516320
  }
516250
516321
  });
516251
516322
 
516323
+ // packages/orchestrator/dist/contextTree.js
516324
+ function classifyToolCall(name10, argsJson) {
516325
+ const direct = TOOL_PHASE_MAP[name10];
516326
+ if (direct)
516327
+ return direct;
516328
+ if (name10 === "shell" || name10 === "shell_async") {
516329
+ if (!argsJson)
516330
+ return "implement";
516331
+ const lower = argsJson.toLowerCase();
516332
+ if (/(?:^|"command":")[^"]*(?:test|pytest|jest|mocha|vitest|cargo test|go test|npm test|pnpm test|tsc|lint|check|verify)\b/i.test(lower)) {
516333
+ return "verify";
516334
+ }
516335
+ return "implement";
516336
+ }
516337
+ return null;
516338
+ }
516339
+ function detectPhase(toolCallLog, window2 = 10) {
516340
+ if (toolCallLog.length === 0)
516341
+ return "explore";
516342
+ const recent = toolCallLog.slice(-window2);
516343
+ const counts = {};
516344
+ for (const tc of recent) {
516345
+ const p2 = classifyToolCall(tc.name, tc.argsKey);
516346
+ if (p2)
516347
+ counts[p2] = (counts[p2] ?? 0) + 1;
516348
+ }
516349
+ const total = Object.values(counts).reduce((a2, b) => a2 + b, 0);
516350
+ if (total === 0)
516351
+ return "mixed";
516352
+ let best = null;
516353
+ for (const [phase, n2] of Object.entries(counts)) {
516354
+ if (!best || n2 > best.n)
516355
+ best = { phase, n: n2 };
516356
+ }
516357
+ if (!best || best.n / total < 0.4)
516358
+ return "mixed";
516359
+ return best.phase;
516360
+ }
516361
+ function extractAnchorsFromMessages(messages2, turn) {
516362
+ const anchors = [];
516363
+ const seenSummaries = /* @__PURE__ */ new Set();
516364
+ let counter = 0;
516365
+ const nextId2 = (kind) => `anc-${turn}-${kind}-${counter++}`;
516366
+ for (const msg of messages2) {
516367
+ const content = typeof msg.content === "string" ? msg.content : "";
516368
+ if (!content)
516369
+ continue;
516370
+ if (msg.role === "tool" || msg.role === "assistant") {
516371
+ const matches = content.match(FILE_PATH_RE) ?? [];
516372
+ const distinctPaths = Array.from(new Set(matches.filter((p2) => p2.length > 2 && p2.length < 120))).slice(0, 5);
516373
+ for (const p2 of distinctPaths) {
516374
+ const summary = `file: ${p2}`;
516375
+ if (seenSummaries.has(summary))
516376
+ continue;
516377
+ seenSummaries.add(summary);
516378
+ anchors.push({
516379
+ id: nextId2("file"),
516380
+ type: "file",
516381
+ summary,
516382
+ keywords: [p2.toLowerCase(), ...p2.toLowerCase().split(/[\/\.]/).filter((s2) => s2.length > 2)],
516383
+ turn
516384
+ });
516385
+ }
516386
+ }
516387
+ if (msg.role === "assistant" && DECISION_HINTS.test(content)) {
516388
+ const sentence = content.split(/(?<=[.!?])\s+/).find((s2) => DECISION_HINTS.test(s2)) ?? content.slice(0, 200);
516389
+ const summary = `decision: ${sentence.slice(0, 140).replace(/\s+/g, " ").trim()}`;
516390
+ if (!seenSummaries.has(summary)) {
516391
+ seenSummaries.add(summary);
516392
+ anchors.push({
516393
+ id: nextId2("dec"),
516394
+ type: "decision",
516395
+ summary,
516396
+ keywords: extractKeywords(sentence),
516397
+ turn
516398
+ });
516399
+ }
516400
+ }
516401
+ if (msg.role === "tool" && ERROR_HINTS.test(content)) {
516402
+ const firstErrLine = content.split("\n").find((l2) => ERROR_HINTS.test(l2)) ?? content.slice(0, 200);
516403
+ const summary = `error: ${firstErrLine.slice(0, 140).replace(/\s+/g, " ").trim()}`;
516404
+ if (!seenSummaries.has(summary)) {
516405
+ seenSummaries.add(summary);
516406
+ anchors.push({
516407
+ id: nextId2("err"),
516408
+ type: "error",
516409
+ summary,
516410
+ keywords: extractKeywords(firstErrLine),
516411
+ turn
516412
+ });
516413
+ }
516414
+ }
516415
+ }
516416
+ return anchors;
516417
+ }
516418
+ function extractKeywords(text) {
516419
+ const tokens = text.toLowerCase().replace(/[^\w./-]+/g, " ").split(/\s+/);
516420
+ const out = [];
516421
+ const seen = /* @__PURE__ */ new Set();
516422
+ for (const tk of tokens) {
516423
+ if (tk.length < 3)
516424
+ continue;
516425
+ if (STOPWORDS.has(tk))
516426
+ continue;
516427
+ if (seen.has(tk))
516428
+ continue;
516429
+ seen.add(tk);
516430
+ out.push(tk);
516431
+ if (out.length >= 8)
516432
+ break;
516433
+ }
516434
+ return out;
516435
+ }
516436
+ var TOOL_PHASE_MAP, FILE_PATH_RE, DECISION_HINTS, ERROR_HINTS, STOPWORDS, DEFAULT_OPTS, ContextTree;
516437
+ var init_contextTree = __esm({
516438
+ "packages/orchestrator/dist/contextTree.js"() {
516439
+ "use strict";
516440
+ TOOL_PHASE_MAP = {
516441
+ // explore
516442
+ file_read: "explore",
516443
+ list_directory: "explore",
516444
+ find_files: "explore",
516445
+ grep_search: "explore",
516446
+ file_explore: "explore",
516447
+ glob_find: "explore",
516448
+ // plan
516449
+ todo_write: "plan",
516450
+ todo_read: "plan",
516451
+ working_notes: "plan",
516452
+ memory_write: "plan",
516453
+ memory_read: "plan",
516454
+ // implement
516455
+ file_write: "implement",
516456
+ file_edit: "implement",
516457
+ file_patch: "implement",
516458
+ batch_edit: "implement",
516459
+ // verify (heuristic — shell with test-like commands also contributes;
516460
+ // unknown shell defaults to implement)
516461
+ run_tests: "verify"
516462
+ };
516463
+ FILE_PATH_RE = /[\w./-]*\/[\w./-]+(?:\.[a-z0-9]{1,6})?/gi;
516464
+ DECISION_HINTS = /(?:I'll |Let me |I will |I'm going to |Plan: |Decision: |Approach: )/i;
516465
+ ERROR_HINTS = /(?:error|fail|exception|traceback|enoent|enotfound|fatal)/i;
516466
+ STOPWORDS = /* @__PURE__ */ new Set([
516467
+ "the",
516468
+ "and",
516469
+ "or",
516470
+ "of",
516471
+ "to",
516472
+ "in",
516473
+ "is",
516474
+ "it",
516475
+ "this",
516476
+ "that",
516477
+ "for",
516478
+ "on",
516479
+ "with",
516480
+ "as",
516481
+ "by",
516482
+ "from",
516483
+ "at",
516484
+ "an",
516485
+ "be",
516486
+ "are",
516487
+ "was",
516488
+ "were",
516489
+ "if",
516490
+ "then",
516491
+ "you",
516492
+ "we",
516493
+ "i",
516494
+ "but",
516495
+ "not",
516496
+ "no",
516497
+ "do",
516498
+ "does",
516499
+ "did",
516500
+ "have",
516501
+ "has",
516502
+ "had",
516503
+ "will",
516504
+ "would",
516505
+ "should",
516506
+ "can",
516507
+ "could"
516508
+ ]);
516509
+ DEFAULT_OPTS = {
516510
+ maxActiveMessages: 60,
516511
+ maxAnchorsPerNode: 12,
516512
+ phaseWindow: 10
516513
+ };
516514
+ ContextTree = class {
516515
+ snapshot;
516516
+ opts;
516517
+ currentPhase = "mixed";
516518
+ currentTurn = 0;
516519
+ toolCallLog = [];
516520
+ constructor(systemPromptHash, activeGoal, opts) {
516521
+ this.opts = { ...DEFAULT_OPTS, ...opts };
516522
+ this.snapshot = {
516523
+ rootSystemPromptHash: systemPromptHash,
516524
+ activeGoal,
516525
+ phases: {},
516526
+ history: [],
516527
+ archive: []
516528
+ };
516529
+ }
516530
+ /** Returns the current detected phase. */
516531
+ getCurrentPhase() {
516532
+ return this.currentPhase;
516533
+ }
516534
+ /** Returns a read-only snapshot for inspection / serialization. */
516535
+ getSnapshot() {
516536
+ return this.snapshot;
516537
+ }
516538
+ /** Update the active goal (e.g. after compaction rewrites the goal). */
516539
+ setActiveGoal(goal) {
516540
+ this.snapshot.activeGoal = goal;
516541
+ }
516542
+ /**
516543
+ * Record a tool call. Updates phase classification but does not move
516544
+ * messages between nodes (caller must invoke maybeTransition).
516545
+ */
516546
+ observeToolCall(name10, argsKey, turn) {
516547
+ this.toolCallLog.push({ name: name10, argsKey, turn });
516548
+ if (this.toolCallLog.length > 200) {
516549
+ this.toolCallLog = this.toolCallLog.slice(-100);
516550
+ }
516551
+ this.currentTurn = turn;
516552
+ }
516553
+ /**
516554
+ * Detect a phase transition. Returns the previous phase if a transition
516555
+ * happened, or null otherwise. Callers should follow up with
516556
+ * contractInactive to summarize the just-exited phase.
516557
+ */
516558
+ maybeTransition(turn) {
516559
+ const detected = detectPhase(this.toolCallLog, this.opts.phaseWindow);
516560
+ if (detected === this.currentPhase)
516561
+ return null;
516562
+ const from3 = this.currentPhase;
516563
+ this.currentPhase = detected;
516564
+ if (!this.snapshot.phases[detected]) {
516565
+ this.snapshot.phases[detected] = {
516566
+ status: "active",
516567
+ messages: [],
516568
+ anchors: [],
516569
+ startedAtTurn: turn
516570
+ };
516571
+ } else {
516572
+ this.snapshot.phases[detected].status = "active";
516573
+ }
516574
+ return { from: from3, to: detected };
516575
+ }
516576
+ /**
516577
+ * Sync a slice of messages into the current active phase node.
516578
+ * Caller is expected to provide ALL messages owned by the current
516579
+ * phase since the last sync (typically the suffix beyond a known
516580
+ * boundary index).
516581
+ */
516582
+ observePhaseMessages(messages2) {
516583
+ const node = this.snapshot.phases[this.currentPhase];
516584
+ if (!node)
516585
+ return;
516586
+ node.messages = messages2;
516587
+ if (messages2.length > this.opts.maxActiveMessages) {
516588
+ this.contract(this.currentPhase, this.currentTurn);
516589
+ }
516590
+ }
516591
+ /**
516592
+ * Contract the named phase: extract anchors, generate a stub summary,
516593
+ * mark the node `contracted`. The caller may then archive via the
516594
+ * archive callback (Phase 1 hands off to memex).
516595
+ *
516596
+ * `summarizer` may be sync (returns string) or async (returns Promise<string>).
516597
+ * When async, the node is contracted IMMEDIATELY with the default stub
516598
+ * summary; the real summary lands later via the returned promise's
516599
+ * resolution updating `node.summary` in place. The next compactMessages
516600
+ * render therefore picks up the LLM summary without blocking the turn
516601
+ * loop on it.
516602
+ */
516603
+ contract(phase, turn, summarizer) {
516604
+ const node = this.snapshot.phases[phase];
516605
+ if (!node || node.status !== "active")
516606
+ return null;
516607
+ const anchors = extractAnchorsFromMessages(node.messages, turn).slice(0, this.opts.maxAnchorsPerNode);
516608
+ node.anchors = [...node.anchors, ...anchors].slice(-this.opts.maxAnchorsPerNode);
516609
+ node.summary = this.defaultSummary(phase, node.messages);
516610
+ node.status = "contracted";
516611
+ node.contractedAtTurn = turn;
516612
+ if (summarizer) {
516613
+ try {
516614
+ const result = summarizer(node.messages);
516615
+ if (result && typeof result.then === "function") {
516616
+ result.then((s2) => {
516617
+ if (typeof s2 === "string" && s2.length > 0)
516618
+ node.summary = s2;
516619
+ }).catch(() => {
516620
+ });
516621
+ } else if (typeof result === "string") {
516622
+ node.summary = result;
516623
+ }
516624
+ } catch {
516625
+ }
516626
+ }
516627
+ this.snapshot.history.push({ ...node, phase });
516628
+ if (this.snapshot.history.length > 32) {
516629
+ this.snapshot.history = this.snapshot.history.slice(-32);
516630
+ }
516631
+ return node;
516632
+ }
516633
+ /** Contract every active phase except the current one. */
516634
+ contractInactive(turn, summarizer) {
516635
+ const contracted = [];
516636
+ for (const [phase, node] of Object.entries(this.snapshot.phases)) {
516637
+ if (phase === this.currentPhase)
516638
+ continue;
516639
+ if (node.status !== "active")
516640
+ continue;
516641
+ this.contract(phase, turn, summarizer);
516642
+ contracted.push(phase);
516643
+ }
516644
+ return contracted;
516645
+ }
516646
+ /**
516647
+ * Mark a contracted node as archived after its summary has been moved
516648
+ * to memex. Caller passes the memex id.
516649
+ */
516650
+ archive(phase, memexId) {
516651
+ const node = this.snapshot.phases[phase];
516652
+ if (!node || node.status !== "contracted")
516653
+ return;
516654
+ node.status = "archived";
516655
+ node.summary = void 0;
516656
+ if (!this.snapshot.archive.includes(memexId)) {
516657
+ this.snapshot.archive.push(memexId);
516658
+ }
516659
+ }
516660
+ /**
516661
+ * Restore a contracted phase: pulls the last contracted node for the phase
516662
+ * back into active state. Returns the restored node or null if none found.
516663
+ */
516664
+ expand(phase) {
516665
+ const node = this.snapshot.phases[phase];
516666
+ if (node && node.status === "contracted") {
516667
+ node.status = "active";
516668
+ return node;
516669
+ }
516670
+ const histNode = [...this.snapshot.history].reverse().find((h) => h.phase === phase);
516671
+ if (histNode) {
516672
+ const restored = {
516673
+ status: "active",
516674
+ messages: [],
516675
+ anchors: histNode.anchors,
516676
+ startedAtTurn: this.currentTurn
516677
+ };
516678
+ this.snapshot.phases[phase] = restored;
516679
+ return restored;
516680
+ }
516681
+ return null;
516682
+ }
516683
+ /** Render a compact phase-status block for inclusion in prompts. */
516684
+ renderStatusBlock() {
516685
+ const lines = [`### Phase Status`, `- current: **${this.currentPhase}**`];
516686
+ const phaseSummaries = [];
516687
+ for (const [phase, node] of Object.entries(this.snapshot.phases)) {
516688
+ if (phase === this.currentPhase)
516689
+ continue;
516690
+ const status = node.status;
516691
+ const anchorCount = node.anchors.length;
516692
+ phaseSummaries.push(`- ${phase}: ${status}${anchorCount > 0 ? ` (${anchorCount} anchors)` : ""}`);
516693
+ }
516694
+ if (phaseSummaries.length > 0) {
516695
+ lines.push(...phaseSummaries);
516696
+ }
516697
+ if (this.snapshot.archive.length > 0) {
516698
+ lines.push(`- archived: ${this.snapshot.archive.length} (use memex_retrieve)`);
516699
+ }
516700
+ return lines.join("\n");
516701
+ }
516702
+ /** Render all retained anchors (across phases) for compaction summary. */
516703
+ renderAnchorBlock(maxPerPhase = 5) {
516704
+ const lines = [];
516705
+ for (const [phase, node] of Object.entries(this.snapshot.phases)) {
516706
+ if (node.anchors.length === 0)
516707
+ continue;
516708
+ lines.push(`**${phase} anchors:**`);
516709
+ for (const a2 of node.anchors.slice(-maxPerPhase)) {
516710
+ lines.push(`- [${a2.type}] ${a2.summary}`);
516711
+ }
516712
+ }
516713
+ return lines.length > 0 ? `### Phase Anchors
516714
+ ${lines.join("\n")}` : "";
516715
+ }
516716
+ /** Look up anchors whose keywords overlap the given query (Phase 6 hook). */
516717
+ findAnchorsByKeywords(query, max = 5) {
516718
+ const tokens = extractKeywords(query.toLowerCase());
516719
+ if (tokens.length === 0)
516720
+ return [];
516721
+ const tokenSet = new Set(tokens);
516722
+ const scored = [];
516723
+ for (const node of Object.values(this.snapshot.phases)) {
516724
+ for (const a2 of node.anchors) {
516725
+ let score = 0;
516726
+ for (const k of a2.keywords) {
516727
+ if (tokenSet.has(k))
516728
+ score += 2;
516729
+ for (const t2 of tokenSet) {
516730
+ if (k.includes(t2) || t2.includes(k))
516731
+ score += 1;
516732
+ }
516733
+ }
516734
+ if (score > 0)
516735
+ scored.push({ anchor: a2, score });
516736
+ }
516737
+ }
516738
+ scored.sort((a2, b) => b.score - a2.score);
516739
+ return scored.slice(0, max).map((s2) => s2.anchor);
516740
+ }
516741
+ defaultSummary(phase, messages2) {
516742
+ const toolCallNames = /* @__PURE__ */ new Set();
516743
+ let assistantWords = 0;
516744
+ for (const m2 of messages2) {
516745
+ if (m2.role === "assistant" && m2.tool_calls) {
516746
+ for (const tc of m2.tool_calls)
516747
+ toolCallNames.add(tc.function.name);
516748
+ }
516749
+ if (m2.role === "assistant" && typeof m2.content === "string") {
516750
+ assistantWords += m2.content.split(/\s+/).length;
516751
+ }
516752
+ }
516753
+ return `${phase} phase: ${messages2.length} messages, ${toolCallNames.size} distinct tools used, ${assistantWords} words of assistant output.`;
516754
+ }
516755
+ };
516756
+ }
516757
+ });
516758
+
516252
516759
  // packages/orchestrator/dist/codeGraphLink.js
516253
516760
  function isCodeGraphLinkEnabled() {
516254
516761
  return process.env["OA_CODEGRAPH_LINK"] !== "0";
@@ -517095,7 +517602,7 @@ function classifyThinkOutcome(raw) {
517095
517602
  }
517096
517603
  return null;
517097
517604
  }
517098
- var SYSTEM_PROMPT, SYSTEM_PROMPT_MEDIUM, SYSTEM_PROMPT_SMALL, VISUAL_TOOLS, AUDIO_TOOLS, SOCIAL_TOOLS, SPATIAL_TOOLS, CODE_TOOLS, AgenticRunner, OllamaAgenticBackend;
517605
+ var TOOL_SUBSETS, TOOL_AUTO_DEMOTE_TURNS, SYSTEM_PROMPT, SYSTEM_PROMPT_MEDIUM, SYSTEM_PROMPT_SMALL, VISUAL_TOOLS, AUDIO_TOOLS, SOCIAL_TOOLS, SPATIAL_TOOLS, CODE_TOOLS, AgenticRunner, OllamaAgenticBackend;
517099
517606
  var init_agenticRunner = __esm({
517100
517607
  "packages/orchestrator/dist/agenticRunner.js"() {
517101
517608
  "use strict";
@@ -517109,12 +517616,23 @@ var init_agenticRunner = __esm({
517109
517616
  init_reflectionBuffer();
517110
517617
  init_taskHandoff();
517111
517618
  init_messageLog();
517619
+ init_contextTree();
517112
517620
  init_codeGraphLink();
517113
517621
  init_dist5();
517114
517622
  init_tool_batching();
517115
517623
  init_hooks();
517116
517624
  init_app_state();
517117
517625
  init_streaming_executor();
517626
+ TOOL_SUBSETS = {
517627
+ web: ["web_search", "web_fetch", "web_crawl"],
517628
+ code: ["file_patch", "file_explore", "batch_edit", "file_read", "file_write", "file_edit"],
517629
+ agent: ["agent", "sub_agent", "background_run"],
517630
+ memory: ["memory_search", "memory_read", "memory_write", "memex_retrieve", "working_notes"],
517631
+ shell: ["shell", "shell_async", "kill_proc", "run_tests"],
517632
+ graph: ["graph_query", "graph_traverse", "code_graph"],
517633
+ skill: ["skill_list", "skill_execute", "skill_search"]
517634
+ };
517635
+ TOOL_AUTO_DEMOTE_TURNS = 10;
517118
517636
  SYSTEM_PROMPT = loadPrompt("agentic/system-large.md");
517119
517637
  SYSTEM_PROMPT_MEDIUM = loadPrompt("agentic/system-medium.md");
517120
517638
  SYSTEM_PROMPT_SMALL = loadPrompt("agentic/system-small.md");
@@ -517222,6 +517740,26 @@ var init_agenticRunner = __esm({
517222
517740
  _patchHistoryStore = null;
517223
517741
  _toolSequence = [];
517224
517742
  // Track tool calls for pattern detection
517743
+ // Phase 2 — tool subset expansion and usage tracking.
517744
+ // Tools promoted from deferred → inline by tool_search (or by subset
517745
+ // expansion). They stay inline for the rest of the run unless idle past
517746
+ // TOOL_AUTO_DEMOTE_TURNS, at which point buildToolDefinitions drops them
517747
+ // back to deferred to reclaim token budget.
517748
+ _activatedTools = /* @__PURE__ */ new Set();
517749
+ _toolLastUsedTurn = /* @__PURE__ */ new Map();
517750
+ // Phase 1 — Context Tree. Tracks current phase + per-phase anchors so
517751
+ // compactMessages can summarize-by-phase and Phase 6 can surface anchors
517752
+ // by keyword. Initialized lazily in run() because the system-prompt hash
517753
+ // depends on the actual prompt resolved at run time.
517754
+ _contextTree = null;
517755
+ // Phase 1 — index in `messages` where the current phase's slice begins.
517756
+ // On phase transition we capture messages[_phaseMessageStartIdx..now]
517757
+ // as the OUTGOING phase's owned slice (via tree.observePhaseMessages),
517758
+ // then advance the cursor so the new phase starts fresh.
517759
+ _phaseMessageStartIdx = 0;
517760
+ // Phase 6 — last-surface guard so we don't re-inject the same anchor on
517761
+ // consecutive turns when its keywords still match.
517762
+ _lastSurfacedAnchorIds = /* @__PURE__ */ new Set();
517225
517763
  /** WO-AM-10: Process pending episode embeddings in background batches */
517226
517764
  async processPendingEmbeddings() {
517227
517765
  if (this._pendingEmbeddings.length === 0 || !this._episodeStore)
@@ -517280,7 +517818,11 @@ var init_agenticRunner = __esm({
517280
517818
  personality: options2?.personality ?? PERSONALITY_PRESETS.balanced,
517281
517819
  personalityName: options2?.personalityName ?? "",
517282
517820
  finalVarResolver: options2?.finalVarResolver ?? void 0,
517283
- observerMode: options2?.observerMode ?? "both"
517821
+ observerMode: options2?.observerMode ?? "both",
517822
+ // Phase 4 — sub-agent isolation flag (defaults false). When true, this
517823
+ // runner skips cross-task handoff inheritance from the parent's
517824
+ // session.
517825
+ subAgent: options2?.subAgent ?? false
517284
517826
  };
517285
517827
  this._observerMode = this.options.observerMode;
517286
517828
  }
@@ -517305,6 +517847,32 @@ var init_agenticRunner = __esm({
517305
517847
  return this._fileRegistry;
517306
517848
  }
517307
517849
  /** Get a Memex archive entry by ID (for the dereference tool) */
517850
+ /**
517851
+ * Phase 4 (Task C) — public read access to the runner's TaskState. Used
517852
+ * by the CLI's spawnInProcess wiring to extract `modifiedFiles` from a
517853
+ * sub-agent run and surface them on the parent's `InProcessResult`. The
517854
+ * caller treats the returned object as read-only.
517855
+ */
517856
+ getTaskState() {
517857
+ return this._taskState;
517858
+ }
517859
+ /**
517860
+ * Phase 4 (Task C) — direct memex archive helper for parent runners
517861
+ * that want to file a sub-agent's verbose result under a known id.
517862
+ * Returns the assigned id so the caller can surface it on the result.
517863
+ */
517864
+ archiveToMemex(opts) {
517865
+ const id = opts.id ?? this.quickHash(`${opts.toolName}|${opts.summary}|${Date.now()}`);
517866
+ this._memexArchive.set(id, {
517867
+ id,
517868
+ toolName: opts.toolName,
517869
+ summary: opts.summary,
517870
+ fullContent: opts.fullContent,
517871
+ turn: opts.turn ?? this._taskState.toolCallCount,
517872
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
517873
+ });
517874
+ return id;
517875
+ }
517308
517876
  getMemexEntry(id) {
517309
517877
  return this._memexArchive.get(id);
517310
517878
  }
@@ -517685,6 +518253,265 @@ ${body}`;
517685
518253
  * Hannover reference: services/compact/apiMicrocompact.ts
517686
518254
  * Research: arXiv:2307.03172 (Lost in the Middle — recent context matters most)
517687
518255
  */
518256
+ /**
518257
+ * Phase 5 — Proactive context pruning (precedes microcompact). Targets
518258
+ * specific waste patterns rather than blanket-clearing old tool results:
518259
+ *
518260
+ * 1. Duplicate tool calls — same (name, args) called multiple times.
518261
+ * Older instances replaced with "[deduped — see turn N]".
518262
+ * 2. Aged file_read results — file_read older than `agedFileReadTurns`
518263
+ * replaced with a summary stub (path + size + first-line preview).
518264
+ * 3. Successful shell/test runs — shell without error markers older
518265
+ * than `agedShellTurns` replaced with a "succeeded" stub.
518266
+ *
518267
+ * Mutates `messages` in place. Cheap O(n) walk; safe to run every turn.
518268
+ * Refs: proposal §3 Phase 5, AgentFold (arXiv:2510.24699) progressive
518269
+ * summarization, RECOMP (ICLR 2024) observation masking.
518270
+ */
518271
+ proactivePrune(messages2, currentTurn) {
518272
+ if (process.env["OA_DISABLE_PROACTIVE_PRUNE"] === "1")
518273
+ return;
518274
+ const AGED_FILE_READ_TURNS = 10;
518275
+ const AGED_SHELL_TURNS = 5;
518276
+ const ERROR_MARKERS = /(?:error|fail|exception|traceback|enoent|enotfound|exit code [^0]|status[: ]+1\d?\d?)/i;
518277
+ const PRUNE_PREFIX = "[Tool result cleared";
518278
+ const DEDUPE_PREFIX = "[deduped — same call as turn";
518279
+ const FILE_AGED_PREFIX = "[file_read aged out, summary:";
518280
+ const SHELL_AGED_PREFIX = "[shell succeeded, output pruned —";
518281
+ const seen = /* @__PURE__ */ new Map();
518282
+ const pending = [];
518283
+ let scanTurn = 0;
518284
+ for (let i2 = 0; i2 < messages2.length; i2++) {
518285
+ const msg = messages2[i2];
518286
+ if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
518287
+ scanTurn++;
518288
+ for (const tc of msg.tool_calls) {
518289
+ const name10 = tc.function.name;
518290
+ const argsRaw = (tc.function.arguments ?? "").slice(0, 200);
518291
+ const fp = `${name10}|${argsRaw}`;
518292
+ let resultIdx = -1;
518293
+ for (let j = i2 + 1; j < Math.min(messages2.length, i2 + 8); j++) {
518294
+ const r2 = messages2[j];
518295
+ if (r2.role === "tool" && r2.tool_call_id === tc.id) {
518296
+ resultIdx = j;
518297
+ break;
518298
+ }
518299
+ }
518300
+ if (resultIdx === -1)
518301
+ continue;
518302
+ const resultMsg = messages2[resultIdx];
518303
+ const content = typeof resultMsg.content === "string" ? resultMsg.content : "";
518304
+ if (content.startsWith(PRUNE_PREFIX) || content.startsWith(DEDUPE_PREFIX) || content.startsWith(FILE_AGED_PREFIX) || content.startsWith(SHELL_AGED_PREFIX)) {
518305
+ if (!seen.has(fp))
518306
+ seen.set(fp, { turn: scanTurn, idx: resultIdx });
518307
+ continue;
518308
+ }
518309
+ const prior = seen.get(fp);
518310
+ if (prior) {
518311
+ pending.push({
518312
+ idx: prior.idx,
518313
+ reason: "dedupe",
518314
+ replacement: `${DEDUPE_PREFIX} ${scanTurn} — duplicate ${name10}() call]`
518315
+ });
518316
+ seen.set(fp, { turn: scanTurn, idx: resultIdx });
518317
+ continue;
518318
+ }
518319
+ seen.set(fp, { turn: scanTurn, idx: resultIdx });
518320
+ const ageTurns = currentTurn - scanTurn;
518321
+ if (name10 === "file_read" && ageTurns > AGED_FILE_READ_TURNS && content.length > 200) {
518322
+ const pathArg = (() => {
518323
+ try {
518324
+ const o2 = JSON.parse(tc.function.arguments || "{}");
518325
+ return String(o2.path ?? o2.file ?? "?");
518326
+ } catch {
518327
+ return "?";
518328
+ }
518329
+ })();
518330
+ const firstLine = content.split("\n")[0]?.slice(0, 80) ?? "";
518331
+ pending.push({
518332
+ idx: resultIdx,
518333
+ reason: "aged_file",
518334
+ replacement: `${FILE_AGED_PREFIX} path=${pathArg}, size=${content.length} chars, first-line="${firstLine}"]`
518335
+ });
518336
+ continue;
518337
+ }
518338
+ if ((name10 === "shell" || name10 === "shell_async" || name10 === "run_tests") && ageTurns > AGED_SHELL_TURNS && content.length > 200 && !ERROR_MARKERS.test(content)) {
518339
+ const cmdArg = (() => {
518340
+ try {
518341
+ const o2 = JSON.parse(tc.function.arguments || "{}");
518342
+ return String(o2.command ?? o2.cmd ?? "?").slice(0, 80);
518343
+ } catch {
518344
+ return "?";
518345
+ }
518346
+ })();
518347
+ pending.push({
518348
+ idx: resultIdx,
518349
+ reason: "aged_shell",
518350
+ replacement: `${SHELL_AGED_PREFIX} command="${cmdArg}", output ${content.length} chars, no error markers detected]`
518351
+ });
518352
+ }
518353
+ }
518354
+ }
518355
+ }
518356
+ if (pending.length === 0)
518357
+ return;
518358
+ let dedupes = 0, agedFiles = 0, agedShells = 0;
518359
+ for (const p2 of pending) {
518360
+ const m2 = messages2[p2.idx];
518361
+ if (!m2)
518362
+ continue;
518363
+ messages2[p2.idx] = { ...m2, content: p2.replacement };
518364
+ if (p2.reason === "dedupe")
518365
+ dedupes++;
518366
+ else if (p2.reason === "aged_file")
518367
+ agedFiles++;
518368
+ else if (p2.reason === "aged_shell")
518369
+ agedShells++;
518370
+ }
518371
+ const parts = [];
518372
+ if (dedupes > 0)
518373
+ parts.push(`${dedupes} duplicate call(s)`);
518374
+ if (agedFiles > 0)
518375
+ parts.push(`${agedFiles} aged file_read(s)`);
518376
+ if (agedShells > 0)
518377
+ parts.push(`${agedShells} aged shell run(s)`);
518378
+ if (parts.length > 0) {
518379
+ this.emit({
518380
+ type: "status",
518381
+ content: `Proactive prune: replaced ${parts.join(", ")} with summary stubs`,
518382
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
518383
+ });
518384
+ }
518385
+ }
518386
+ /**
518387
+ * Phase 1 (Task B) — build an async LLM summarizer for ContextTree.contract().
518388
+ *
518389
+ * Returns a function that takes a phase's owned message slice and asks the
518390
+ * backend for a 2-3 sentence summary of what the phase accomplished. Hard
518391
+ * caps the messages slice at ~3000 chars (≈750 tokens) of representative
518392
+ * content to keep the cost bounded. Never blocks the turn loop — the
518393
+ * returned promise updates ContextNode.summary in place when it resolves.
518394
+ *
518395
+ * Returns null when the disable knob is set or the backend is missing the
518396
+ * chatCompletion method.
518397
+ */
518398
+ makePhaseSummarizer() {
518399
+ if (process.env["OA_DISABLE_PHASE_SUMMARIZER"] === "1")
518400
+ return null;
518401
+ if (!this.backend || typeof this.backend.chatCompletion !== "function")
518402
+ return null;
518403
+ return async (phase, msgs) => {
518404
+ const tail = msgs.slice(-8);
518405
+ const blob = tail.map((m2) => {
518406
+ const role = m2.role;
518407
+ const content = typeof m2.content === "string" ? m2.content.slice(0, 400) : "";
518408
+ if (m2.role === "assistant" && m2.tool_calls && m2.tool_calls.length > 0) {
518409
+ const calls = m2.tool_calls.map((tc) => `${tc.function.name}(${(tc.function.arguments || "").slice(0, 80)})`).join(", ");
518410
+ return `[${role}] ${content || ""} | tool_calls: ${calls}`.slice(0, 500);
518411
+ }
518412
+ return `[${role}] ${content}`.slice(0, 500);
518413
+ }).join("\n");
518414
+ const prompt = `You are summarizing the "${phase}" phase of an agent's task. Below is the message slice owned by this phase. Write a 2-3 sentence summary focused on WHAT was done and any KEY FINDINGS. No fluff. No "the agent did X" framing — just the facts.
518415
+
518416
+ === ${phase} phase messages ===
518417
+ ${blob}
518418
+ === end ===`;
518419
+ try {
518420
+ const response = await this.backend.chatCompletion({
518421
+ messages: [
518422
+ { role: "system", content: "You write extremely concise phase summaries for agent context management." },
518423
+ { role: "user", content: prompt }
518424
+ ],
518425
+ tools: [],
518426
+ temperature: 0,
518427
+ maxTokens: 256,
518428
+ timeoutMs: 3e4
518429
+ });
518430
+ const text = response.choices?.[0]?.message?.content;
518431
+ if (typeof text === "string" && text.length > 0) {
518432
+ return `${phase}: ${text.replace(/\s+/g, " ").trim().slice(0, 600)}`;
518433
+ }
518434
+ } catch {
518435
+ }
518436
+ return "";
518437
+ };
518438
+ }
518439
+ /**
518440
+ * Phase 6 — Anchor Surfacing.
518441
+ *
518442
+ * Scans the most recent user / assistant text for keyword overlap with
518443
+ * the ContextTree's anchor index plus the memex archive index. When
518444
+ * matches are found, injects a single-line system hint listing the
518445
+ * available anchors so the model knows what archived context exists
518446
+ * (and may call memex_retrieve to expand it).
518447
+ *
518448
+ * Refs: proposal §3 Phase 6, Lost-in-the-Middle (arXiv:2307.03172) —
518449
+ * surfacing relevant context near the tail combats positional decay.
518450
+ */
518451
+ surfaceAnchors(messages2) {
518452
+ if (process.env["OA_DISABLE_ANCHOR_SURFACING"] === "1")
518453
+ return;
518454
+ if (!this._contextTree)
518455
+ return;
518456
+ if (messages2.length === 0)
518457
+ return;
518458
+ const tail = messages2.slice(-6);
518459
+ const queryParts = [];
518460
+ for (const m2 of tail) {
518461
+ if (typeof m2.content === "string" && m2.content.length > 0) {
518462
+ if (m2.role === "user" || m2.role === "assistant") {
518463
+ queryParts.push(m2.content);
518464
+ }
518465
+ }
518466
+ }
518467
+ const query = queryParts.join(" ").slice(0, 800);
518468
+ if (!query)
518469
+ return;
518470
+ const anchors = this._contextTree.findAnchorsByKeywords(query, 5);
518471
+ const memexMatches = [];
518472
+ if (this._memexArchive.size > 0) {
518473
+ const tokens = query.toLowerCase().replace(/[^\w./-]+/g, " ").split(/\s+/).filter((t2) => t2.length > 3);
518474
+ const tokenSet = new Set(tokens);
518475
+ for (const e2 of this._memexArchive.values()) {
518476
+ const summaryLower = e2.summary.toLowerCase();
518477
+ let score = 0;
518478
+ for (const t2 of tokenSet) {
518479
+ if (summaryLower.includes(t2))
518480
+ score += 2;
518481
+ }
518482
+ if (e2.toolName === "todo_complete" && tokenSet.has("todo"))
518483
+ score += 1;
518484
+ if (score > 0)
518485
+ memexMatches.push({ id: e2.id, summary: e2.summary, score });
518486
+ }
518487
+ memexMatches.sort((a2, b) => b.score - a2.score);
518488
+ }
518489
+ const newAnchors = anchors.filter((a2) => !this._lastSurfacedAnchorIds.has(a2.id)).slice(0, 3);
518490
+ const newMemex = memexMatches.filter((m2) => !this._lastSurfacedAnchorIds.has(m2.id)).slice(0, 2);
518491
+ if (newAnchors.length === 0 && newMemex.length === 0)
518492
+ return;
518493
+ const lines = [`[Anchor surface] Relevant archived context for the current activity:`];
518494
+ for (const a2 of newAnchors) {
518495
+ lines.push(` - [${a2.type}] ${a2.summary}`);
518496
+ }
518497
+ for (const m2 of newMemex) {
518498
+ lines.push(` - [memex:${m2.id}] ${m2.summary} — call memex_retrieve("${m2.id}") for full body`);
518499
+ }
518500
+ lines.push(`(Anchors are reminders. Pull only what you actually need; ignore otherwise.)`);
518501
+ messages2.push({
518502
+ role: "system",
518503
+ content: lines.join("\n")
518504
+ });
518505
+ for (const a2 of newAnchors)
518506
+ this._lastSurfacedAnchorIds.add(a2.id);
518507
+ for (const m2 of newMemex)
518508
+ this._lastSurfacedAnchorIds.add(m2.id);
518509
+ this.emit({
518510
+ type: "status",
518511
+ content: `Anchor surface: surfaced ${newAnchors.length} anchor(s) + ${newMemex.length} memex entry(ies)`,
518512
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
518513
+ });
518514
+ }
517688
518515
  microcompact(messages2, recentToolResults) {
517689
518516
  const tier = this.options.modelTier ?? "large";
517690
518517
  let keepResults = tier === "small" ? 6 : tier === "medium" ? 10 : 20;
@@ -518056,6 +518883,10 @@ Respond with your assessment, then take action.`;
518056
518883
  this.aborted = false;
518057
518884
  this._paused = false;
518058
518885
  this._toolSequence = [];
518886
+ this._activatedTools.clear();
518887
+ this._toolLastUsedTurn.clear();
518888
+ this._contextTree = null;
518889
+ this._lastSurfacedAnchorIds.clear();
518059
518890
  if (!this._memoryInitialized) {
518060
518891
  try {
518061
518892
  const path8 = await import("node:path");
@@ -518135,6 +518966,8 @@ Respond with your assessment, then take action.`;
518135
518966
  this._hookManager.runSessionHook("session_start", this._sessionId);
518136
518967
  const contextComposition = await this.assembleContext(task, context2);
518137
518968
  const systemPrompt = contextComposition.assembled;
518969
+ this._contextTree = new ContextTree(`sys-${systemPrompt.length}`, cleanedTask.slice(0, 200));
518970
+ this._phaseMessageStartIdx = 2;
518138
518971
  this.emit({
518139
518972
  type: "status",
518140
518973
  content: `Context assembled: ${contextComposition.sections.map((s2) => `${s2.label}(${s2.tokenEstimate}t)`).join(" + ")} = ~${contextComposition.totalTokenEstimate}t`,
@@ -518152,6 +518985,8 @@ TASK: ${task}` : task;
518152
518985
  { role: "user", content: userContent }
518153
518986
  ];
518154
518987
  try {
518988
+ if (this.options.subAgent)
518989
+ throw "skip-handoff-subagent";
518155
518990
  const oaDir = this._workingDirectory ? _pathJoin(this._workingDirectory, ".oa") : _pathJoin(process.cwd(), ".oa");
518156
518991
  const chainPairs = loadMessagePairsFromLog(oaDir, { currentTask: cleanedTask });
518157
518992
  if (chainPairs.length > 0) {
@@ -518590,7 +519425,9 @@ ${memoryLines.join("\n")}`
518590
519425
  }
518591
519426
  }
518592
519427
  this._lastAssistantTimestamp = Date.now();
519428
+ this.proactivePrune(compacted, turn);
518593
519429
  this.microcompact(compacted, recentToolResults);
519430
+ this.surfaceAnchors(compacted);
518594
519431
  const { maxOutputTokens: effectiveMaxTokens } = this.contextLimits();
518595
519432
  const chatRequest = {
518596
519433
  messages: compacted,
@@ -518879,6 +519716,37 @@ ${memoryLines.join("\n")}`
518879
519716
  toolCallCount++;
518880
519717
  const argsKey = Object.entries(tc.arguments ?? {}).sort(([a2], [b]) => a2.localeCompare(b)).map(([k, v]) => `${k}=${typeof v === "string" ? v.slice(0, 160) : JSON.stringify(v).slice(0, 160)}`).join(",");
518881
519718
  toolCallLog.push({ name: tc.name, argsKey, turn, timestampMs: Date.now() });
519719
+ this._toolLastUsedTurn.set(tc.name, turn);
519720
+ if (this._contextTree) {
519721
+ this._contextTree.observeToolCall(tc.name, argsKey, turn);
519722
+ const transition = this._contextTree.maybeTransition(turn);
519723
+ if (transition) {
519724
+ const phaseSlice = messages2.slice(this._phaseMessageStartIdx);
519725
+ if (phaseSlice.length > 0) {
519726
+ const fromNode = this._contextTree.getSnapshot().phases[transition.from];
519727
+ if (fromNode) {
519728
+ fromNode.messages = phaseSlice;
519729
+ }
519730
+ }
519731
+ this.emit({
519732
+ type: "status",
519733
+ content: `Phase: ${transition.from} → ${transition.to}`,
519734
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
519735
+ });
519736
+ const summarizer = this.makePhaseSummarizer();
519737
+ const contracted = this._contextTree.contractInactive(turn, summarizer ? ((msgs) => summarizer(transition.from, msgs)) : void 0);
519738
+ if (contracted.length > 0) {
519739
+ this.emit({
519740
+ type: "status",
519741
+ content: `Phase contraction: ${contracted.join(", ")} → contracted (${summarizer ? "LLM-summarized" : "stub-summarized"}, anchors retained)`,
519742
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
519743
+ });
519744
+ }
519745
+ this._phaseMessageStartIdx = messages2.length;
519746
+ this._taskState.phase = transition.to;
519747
+ this._taskState.phaseSince = turn;
519748
+ }
519749
+ }
518882
519750
  const budgetRemaining = toolCallBudget.get(tc.name);
518883
519751
  if (budgetRemaining !== void 0) {
518884
519752
  if (budgetRemaining <= 0) {
@@ -519891,7 +520759,9 @@ Integrate this guidance into your current approach. Continue working on the task
519891
520759
  } else {
519892
520760
  compactedMsgs = await this.compactMessages(messages2, this._skillCompactionStrategy ?? "default");
519893
520761
  }
520762
+ this.proactivePrune(compactedMsgs, this._taskState.toolCallCount);
519894
520763
  this.microcompact(compactedMsgs);
520764
+ this.surfaceAnchors(compactedMsgs);
519895
520765
  const chatRequest = { messages: compactedMsgs, tools: toolDefs, temperature: this.options.temperature, maxTokens: this.options.maxTokens, timeoutMs: this.options.requestTimeoutMs };
519896
520766
  let response;
519897
520767
  try {
@@ -520238,6 +521108,8 @@ Full content available via: repl_exec(code="data = retrieve('${handleId}')") or
520238
521108
  }
520239
521109
  }
520240
521110
  try {
521111
+ if (this.options.subAgent)
521112
+ throw "skip-handoff-write-subagent";
520241
521113
  const outcome = consolidation.outcome === "success" ? "success" : consolidation.outcome === "aborted" ? "aborted" : consolidation.outcome === "timeout" ? "timeout" : "failed";
520242
521114
  const oaDir = this._workingDirectory ? _pathJoin(this._workingDirectory, ".oa") : _pathJoin(process.cwd(), ".oa");
520243
521115
  const transcriptPath = _pathJoin(oaDir, "consolidations", `${this._sessionId}.json`);
@@ -520678,6 +521550,9 @@ Actions: (1) list_directory on the parent directory to see what's there, (2) Che
520678
521550
  if (ts.currentStep) {
520679
521551
  parts.push(`**Currently doing:** ${ts.currentStep}`);
520680
521552
  }
521553
+ if (ts.phase) {
521554
+ parts.push(`**Phase:** ${ts.phase}`);
521555
+ }
520681
521556
  if (ts.completedSteps.length > 0) {
520682
521557
  parts.push(`**Completed:**`);
520683
521558
  for (const s2 of ts.completedSteps.slice(-10))
@@ -520702,9 +521577,75 @@ Actions: (1) list_directory on the parent directory to see what's there, (2) Che
520702
521577
  parts.push(`- \`${path8}\` (${action})`);
520703
521578
  }
520704
521579
  }
521580
+ const todoBlock = this.formatCompletedTodoAnchors();
521581
+ if (todoBlock)
521582
+ parts.push(todoBlock);
520705
521583
  parts.push(`**Tool calls:** ${ts.toolCallCount}`);
520706
521584
  return parts.join("\n");
520707
521585
  }
521586
+ /**
521587
+ * Phase 3 — render completed todos as compact anchor cards. Older
521588
+ * completed todos beyond the top-3 are archived to memex on first sight
521589
+ * so the model can still reach them via memex_retrieve. Returns the empty
521590
+ * string when the session has no todos.
521591
+ *
521592
+ * Anchor card shape: `[done] {content slice} (files: a.ts, b.ts) → outcome`
521593
+ */
521594
+ formatCompletedTodoAnchors() {
521595
+ try {
521596
+ const sessionId = process.env["OA_SESSION_ID"] || this._sessionId;
521597
+ const todos = readTodos(sessionId);
521598
+ if (todos.length === 0)
521599
+ return "";
521600
+ const completed = todos.filter((t2) => t2.status === "completed");
521601
+ const blocked = todos.filter((t2) => t2.status === "blocked");
521602
+ if (completed.length === 0 && blocked.length === 0)
521603
+ return "";
521604
+ const parts = [];
521605
+ const topRecent = completed.sort((a2, b) => (b.completedAt || b.updatedAt) - (a2.completedAt || a2.updatedAt)).slice(0, 3);
521606
+ if (topRecent.length > 0) {
521607
+ parts.push(`**Recently completed (anchors):**`);
521608
+ for (const t2 of topRecent) {
521609
+ parts.push(`- [done] ${this.todoAnchorLine(t2)}`);
521610
+ }
521611
+ }
521612
+ const olderCompleted = completed.slice(3);
521613
+ let archivedCount = 0;
521614
+ for (const t2 of olderCompleted) {
521615
+ const memexId = `todo-${t2.id.slice(0, 8)}`;
521616
+ if (!this._memexArchive.has(memexId)) {
521617
+ this._memexArchive.set(memexId, {
521618
+ id: memexId,
521619
+ toolName: "todo_complete",
521620
+ summary: this.todoAnchorLine(t2),
521621
+ fullContent: JSON.stringify(t2, null, 2),
521622
+ turn: this._taskState.toolCallCount,
521623
+ timestamp: new Date(t2.completedAt || t2.updatedAt).toISOString()
521624
+ });
521625
+ archivedCount++;
521626
+ }
521627
+ }
521628
+ if (archivedCount > 0) {
521629
+ parts.push(`*(${archivedCount} older completed todo(s) archived → memex_retrieve("todo-{id}") for details, ${olderCompleted.length} total older)*`);
521630
+ } else if (olderCompleted.length > 0) {
521631
+ parts.push(`*(${olderCompleted.length} older completed todo(s) in memex archive)*`);
521632
+ }
521633
+ if (blocked.length > 0) {
521634
+ parts.push(`**Blocked:**`);
521635
+ for (const t2 of blocked.slice(0, 5)) {
521636
+ parts.push(`- [blocked] ${t2.content}${t2.blocker ? ` — ${t2.blocker}` : ""}`);
521637
+ }
521638
+ }
521639
+ return parts.length > 0 ? parts.join("\n") : "";
521640
+ } catch {
521641
+ return "";
521642
+ }
521643
+ }
521644
+ /** Build a single-line anchor for a completed todo (Phase 3 helper). */
521645
+ todoAnchorLine(t2) {
521646
+ const content = t2.content.length > 80 ? t2.content.slice(0, 77) + "..." : t2.content;
521647
+ return content;
521648
+ }
520708
521649
  /**
520709
521650
  * Format file state registry as compact markdown for domain-aware compaction.
520710
521651
  * Only includes files with meaningful state (modified or recently accessed).
@@ -520954,6 +521895,33 @@ ${taskStateStr}
520954
521895
  if (memexIndexStr)
520955
521896
  enrichments.push(memexIndexStr);
520956
521897
  }
521898
+ if (this._contextTree) {
521899
+ const droppedSlice = messages2.slice(headEndIdx, recentStart);
521900
+ const freshAnchors = extractAnchorsFromMessages(droppedSlice, this._taskState.toolCallCount);
521901
+ if (freshAnchors.length > 0) {
521902
+ const tree2 = this._contextTree;
521903
+ const phase = tree2.getCurrentPhase();
521904
+ const snap = tree2.getSnapshot();
521905
+ if (!snap.phases[phase]) {
521906
+ snap.phases[phase] = {
521907
+ status: "active",
521908
+ messages: [],
521909
+ anchors: [],
521910
+ startedAtTurn: this._taskState.toolCallCount
521911
+ };
521912
+ }
521913
+ snap.phases[phase].anchors = [
521914
+ ...snap.phases[phase].anchors,
521915
+ ...freshAnchors
521916
+ ].slice(-12);
521917
+ }
521918
+ const phaseStatus = this._contextTree.renderStatusBlock();
521919
+ if (phaseStatus)
521920
+ enrichments.push(phaseStatus);
521921
+ const anchorBlock = this._contextTree.renderAnchorBlock();
521922
+ if (anchorBlock)
521923
+ enrichments.push(anchorBlock);
521924
+ }
520957
521925
  const postCompactRestore = [];
520958
521926
  const planSkel = this.buildPlanSkeleton();
520959
521927
  if (planSkel)
@@ -522185,7 +523153,7 @@ ${transcript}`
522185
523153
  const allTools = Array.from(this.tools.values()).filter((tool) => tool.name !== "tool_search");
522186
523154
  const tier = this.options.modelTier ?? "large";
522187
523155
  const taskGoal = this._taskState.goal || "";
522188
- const STOPWORDS = /* @__PURE__ */ new Set([
523156
+ const STOPWORDS2 = /* @__PURE__ */ new Set([
522189
523157
  "with",
522190
523158
  "that",
522191
523159
  "this",
@@ -522235,7 +523203,7 @@ ${transcript}`
522235
523203
  const getDesc = (tool) => dynamicDescs.get(tool.name) ?? tool.description;
522236
523204
  const getIndexLabel = (tool) => {
522237
523205
  const desc = getDesc(tool).toLowerCase().replace(/[`"'()[\]{}:;,.!?/\\|-]+/g, " ");
522238
- const keywords = Array.from(new Set(desc.split(/\s+/).filter((word2) => word2.length > 2 && !STOPWORDS.has(word2) && !tool.name.toLowerCase().includes(word2)))).slice(0, 4);
523206
+ const keywords = Array.from(new Set(desc.split(/\s+/).filter((word2) => word2.length > 2 && !STOPWORDS2.has(word2) && !tool.name.toLowerCase().includes(word2)))).slice(0, 4);
522239
523207
  return keywords.length > 0 ? `${tool.name}(${keywords.join(",")})` : tool.name;
522240
523208
  };
522241
523209
  const CORE_TOOLS2 = /* @__PURE__ */ new Set([
@@ -522283,9 +523251,26 @@ ${transcript}`
522283
523251
  scored.sort((a2, b) => b.score - a2.score);
522284
523252
  const maxInlineExtra = tier === "small" ? 4 : 8;
522285
523253
  const inlineExtras = scored.slice(0, maxInlineExtra).filter((s2) => s2.score > 0);
523254
+ const currentTurn = this._taskState.toolCallCount;
523255
+ let demoted = 0;
523256
+ for (const promoted of this._activatedTools) {
523257
+ const lastUsed = this._toolLastUsedTurn.get(promoted) ?? -1;
523258
+ if (lastUsed >= 0 && currentTurn - lastUsed > TOOL_AUTO_DEMOTE_TURNS) {
523259
+ this._activatedTools.delete(promoted);
523260
+ demoted++;
523261
+ }
523262
+ }
523263
+ if (demoted > 0) {
523264
+ this.emit({
523265
+ type: "status",
523266
+ content: `Tool auto-demote: ${demoted} idle promoted tool(s) dropped back to deferred`,
523267
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
523268
+ });
523269
+ }
522286
523270
  const inlineNames = /* @__PURE__ */ new Set([
522287
523271
  ...allTools.filter((t2) => CORE_TOOLS2.has(t2.name)).map((t2) => t2.name),
522288
- ...inlineExtras.map((s2) => s2.tool.name)
523272
+ ...inlineExtras.map((s2) => s2.tool.name),
523273
+ ...Array.from(this._activatedTools).filter((name10) => allTools.some((t2) => t2.name === name10))
522289
523274
  ]);
522290
523275
  const deferred = allTools.filter((t2) => !inlineNames.has(t2.name));
522291
523276
  const inlineTools = allTools.filter((t2) => inlineNames.has(t2.name));
@@ -522317,11 +523302,12 @@ ${transcript}`
522317
523302
  lines[lineIdx].push(entry);
522318
523303
  return lines;
522319
523304
  }, []).map((line) => `- ${line.join(", ")}`).join("\n");
523305
+ const subsetNames = Object.keys(TOOL_SUBSETS).join(", ");
522320
523306
  defs.push({
522321
523307
  type: "function",
522322
523308
  function: {
522323
523309
  name: "tool_search",
522324
- description: `Search for and activate additional tools not in your current tool list. Call this when your task needs a tool you don't have. Pass a search query describing what you need. The catalog below is a compact index; full schemas are returned only after search.
523310
+ description: `Search for and activate additional tools not in your current tool list. Call this when your task needs a tool you don't have. Pass a search query describing what you need. The catalog below is a compact index; full schemas are returned only after search. Subset shortcuts (call tool_search with one of these as the query to promote the whole group at once): ${subsetNames}.
522325
523311
 
522326
523312
  Available tools (${deferred.length}):
522327
523313
  ${catalog}`,
@@ -522334,16 +523320,64 @@ ${catalog}`,
522334
523320
  }
522335
523321
  }
522336
523322
  });
523323
+ const activatedToolsRef = this._activatedTools;
523324
+ const subsetCatalog = TOOL_SUBSETS;
522337
523325
  this.tools.set("tool_search", {
522338
523326
  name: "tool_search",
522339
523327
  description: "Search for deferred tools",
522340
523328
  parameters: {},
522341
523329
  execute: async (args) => {
522342
- const query = String(args["query"] ?? "").toLowerCase();
523330
+ const query = String(args["query"] ?? "").toLowerCase().trim();
523331
+ const subsetMatch = subsetCatalog[query];
523332
+ if (subsetMatch && subsetMatch.length > 0) {
523333
+ const newlyPromoted = [];
523334
+ const alreadyAvailable = [];
523335
+ const unknown = [];
523336
+ for (const name10 of subsetMatch) {
523337
+ const tool = deferred.find((t2) => t2.name === name10) ?? allTools.find((t2) => t2.name === name10);
523338
+ if (!tool) {
523339
+ unknown.push(name10);
523340
+ continue;
523341
+ }
523342
+ if (inlineNames.has(name10)) {
523343
+ alreadyAvailable.push(name10);
523344
+ continue;
523345
+ }
523346
+ activatedToolsRef.add(name10);
523347
+ newlyPromoted.push(name10);
523348
+ }
523349
+ const lines = [`Subset "${query}" expanded:`];
523350
+ if (newlyPromoted.length > 0) {
523351
+ lines.push(` Promoted to inline (${newlyPromoted.length}): ${newlyPromoted.join(", ")}`);
523352
+ for (const name10 of newlyPromoted) {
523353
+ const tool = allTools.find((t2) => t2.name === name10);
523354
+ if (!tool)
523355
+ continue;
523356
+ lines.push("");
523357
+ lines.push(`## ${tool.name}`);
523358
+ lines.push(getDesc(tool));
523359
+ lines.push(`Parameters: ${JSON.stringify(tool.parameters)}`);
523360
+ }
523361
+ }
523362
+ if (alreadyAvailable.length > 0) {
523363
+ lines.push(` Already inline (${alreadyAvailable.length}): ${alreadyAvailable.join(", ")}`);
523364
+ }
523365
+ if (unknown.length > 0) {
523366
+ lines.push(` Not registered (${unknown.length}): ${unknown.join(", ")}`);
523367
+ }
523368
+ return { success: true, output: lines.join("\n") };
523369
+ }
522343
523370
  const matches = deferred.filter((t2) => t2.name.toLowerCase().includes(query) || getDesc(t2).toLowerCase().includes(query)).slice(0, 5);
522344
523371
  if (matches.length === 0) {
522345
- return { success: false, output: "", error: `No tools matching "${query}". Try a broader search.` };
523372
+ const subsetHint = Object.keys(subsetCatalog).join(", ");
523373
+ return {
523374
+ success: false,
523375
+ output: "",
523376
+ error: `No tools matching "${query}". Try a broader search, or call a subset name directly: ${subsetHint}.`
523377
+ };
522346
523378
  }
523379
+ for (const t2 of matches)
523380
+ activatedToolsRef.add(t2.name);
522347
523381
  const result = matches.map((t2) => {
522348
523382
  const paramsStr = JSON.stringify(t2.parameters, null, 2);
522349
523383
  return `## ${t2.name}
@@ -522354,7 +523388,7 @@ ${paramsStr}`;
522354
523388
  }).join("\n\n---\n\n");
522355
523389
  return {
522356
523390
  success: true,
522357
- output: `Found ${matches.length} tool(s). You can now call them directly:
523391
+ output: `Found ${matches.length} tool(s) promoted to inline for the rest of this run:
522358
523392
 
522359
523393
  ${result}`
522360
523394
  };
@@ -583889,6 +584923,119 @@ ${incompleteList}${more}
583889
584923
  }
583890
584924
  };
583891
584925
  }
584926
+ function wireAgentToolMinimal(tool, config, repoRoot) {
584927
+ const typeRegistry = getAgentTypeRegistry();
584928
+ const allToolNames = [
584929
+ "file_read",
584930
+ "file_write",
584931
+ "file_edit",
584932
+ "shell",
584933
+ "grep",
584934
+ "glob",
584935
+ "list_directory",
584936
+ "web_fetch",
584937
+ "web_search",
584938
+ "memory_read",
584939
+ "memory_write",
584940
+ "task_complete",
584941
+ "batch_edit",
584942
+ "file_patch",
584943
+ "git_info",
584944
+ "agent",
584945
+ "send_message"
584946
+ ];
584947
+ tool.setCallbacks({
584948
+ resolveType: (typeName) => {
584949
+ const def = typeRegistry.get(typeName);
584950
+ if (!def) return void 0;
584951
+ const resolvedNames = typeRegistry.resolveTools(typeName, allToolNames);
584952
+ return {
584953
+ type: def.type,
584954
+ maxTurns: def.maxTurns,
584955
+ toolNames: resolvedNames,
584956
+ systemPromptAddition: def.systemPromptAddition,
584957
+ canSpawnAgents: def.canSpawnAgents
584958
+ };
584959
+ },
584960
+ listTypes: () => typeRegistry.listTypes(),
584961
+ spawnInProcess: async (opts) => {
584962
+ let backend;
584963
+ if (config.backendType === "nexus") {
584964
+ const nexusTool = new NexusTool(repoRoot);
584965
+ backend = new NexusAgenticBackend(nexusTool.sendCommand.bind(nexusTool), opts.model, void 0, config.apiKey);
584966
+ } else {
584967
+ backend = new OllamaAgenticBackend(config.backendUrl, opts.model, config.apiKey);
584968
+ }
584969
+ const subTier = getModelTier(opts.model);
584970
+ const subCompaction = subTier === "small" ? 12e3 : subTier === "medium" ? 24e3 : 4e4;
584971
+ const subRunner = new AgenticRunner(backend, {
584972
+ maxTurns: opts.maxTurns,
584973
+ maxTokens: 16384,
584974
+ temperature: 0,
584975
+ requestTimeoutMs: config.timeoutMs,
584976
+ taskTimeoutMs: config.timeoutMs * 2,
584977
+ compactionThreshold: subCompaction,
584978
+ contextWindowSize: 0,
584979
+ modelTier: subTier,
584980
+ subAgent: opts.subAgentMode === true
584981
+ });
584982
+ subRunner.setWorkingDirectory(repoRoot);
584983
+ const allSafe = buildSubAgentTools(repoRoot, config);
584984
+ const nameSet = new Set(opts.toolNames || []);
584985
+ const subToolInstances = nameSet.size > 0 ? allSafe.filter((t2) => nameSet.has(t2.name)) : allSafe.slice();
584986
+ const nameOf = (t2) => t2.name;
584987
+ if (!subToolInstances.some((t2) => nameOf(t2) === "todo_write")) subToolInstances.push(new TodoWriteTool());
584988
+ if (!subToolInstances.some((t2) => nameOf(t2) === "todo_read")) subToolInstances.push(new TodoReadTool());
584989
+ subRunner.registerTools(subToolInstances.map(adaptTool6));
584990
+ subRunner.registerTool(createTaskCompleteTool(subTier));
584991
+ const systemCtx = opts.systemPromptAddition ? `Working directory: ${repoRoot}
584992
+
584993
+ ${opts.systemPromptAddition}` : `Working directory: ${repoRoot}`;
584994
+ const result = await subRunner.run(opts.task, systemCtx);
584995
+ const subState = subRunner.getTaskState();
584996
+ const filesModified = Array.from(subState.modifiedFiles.keys());
584997
+ const artifactIds = [];
584998
+ if (_parentRunnerForArchive && result.summary && result.summary.length > 0) {
584999
+ const artifactId = `subagent-${opts.id}-result`;
585000
+ try {
585001
+ _parentRunnerForArchive.archiveToMemex({
585002
+ id: artifactId,
585003
+ toolName: "agent",
585004
+ summary: `Sub-agent ${opts.id}: ${result.summary.slice(0, 120)}`,
585005
+ fullContent: `Sub-agent task: ${opts.task.slice(0, 1e3)}
585006
+
585007
+ Completed: ${result.completed}
585008
+ Turns: ${result.turns}, Tool calls: ${result.toolCalls}
585009
+ Files modified: ${filesModified.join(", ") || "(none)"}
585010
+
585011
+ Summary:
585012
+ ${result.summary}`
585013
+ });
585014
+ artifactIds.push(artifactId);
585015
+ } catch {
585016
+ }
585017
+ }
585018
+ return {
585019
+ completed: result.completed,
585020
+ summary: result.summary,
585021
+ turns: result.turns,
585022
+ toolCalls: result.toolCalls,
585023
+ durationMs: result.durationMs,
585024
+ filesModified,
585025
+ artifactIds
585026
+ };
585027
+ },
585028
+ spawnSubprocess: (opts) => {
585029
+ const entry = spawnFullSubAgent(
585030
+ opts.task,
585031
+ { model: opts.model, backendUrl: config.backendUrl, workingDir: opts.workingDir }
585032
+ );
585033
+ return { id: entry.id, pid: entry.pid ?? 0 };
585034
+ },
585035
+ trackTask: () => {
585036
+ }
585037
+ });
585038
+ }
583892
585039
  function buildSubAgentTools(repoRoot, config) {
583893
585040
  return [
583894
585041
  // File + search
@@ -584770,7 +585917,8 @@ RULES:
584770
585917
  }
584771
585918
  } catch {
584772
585919
  }
584773
- const compactionThreshold = modelTier === "small" ? 12e3 : modelTier === "medium" ? 24e3 : 4e4;
585920
+ const envOverride = Number.parseInt(process.env["OA_COMPACTION_THRESHOLD"] ?? "", 10);
585921
+ const compactionThreshold = Number.isFinite(envOverride) && envOverride > 0 ? envOverride : modelTier === "small" ? 12e3 : modelTier === "medium" ? 24e3 : 4e4;
584774
585922
  let identityInjection = "";
584775
585923
  try {
584776
585924
  const ikStateFile = join109(repoRoot, ".oa", "identity", "self-state.json");
@@ -584818,6 +585966,7 @@ RULES:
584818
585966
  });
584819
585967
  runner.setWorkingDirectory(repoRoot);
584820
585968
  _activeRunnerRef = runner;
585969
+ _parentRunnerForArchive = runner;
584821
585970
  let _checkinPoller = null;
584822
585971
  try {
584823
585972
  const oaSessionId = process.env["OA_SESSION_ID"];
@@ -586285,7 +587434,12 @@ Review its full output in the [${id}] tab or via sub_agent(action='output', id='
586285
587434
  compactionThreshold: subCompaction,
586286
587435
  contextWindowSize: 0,
586287
587436
  // sub-agents discover their own context window
586288
- modelTier: subTier
587437
+ modelTier: subTier,
587438
+ // Phase 4 — sub-agent isolation: skip parent's cross-task handoff
587439
+ // read AND write. The sub-agent gets only what AgentTool composed
587440
+ // into the prompt (relevantFiles + constraints + task), nothing
587441
+ // leaked from the parent's session log or .oa/handoffs/latest.json.
587442
+ subAgent: opts.subAgentMode === true
586289
587443
  });
586290
587444
  const allSafe = buildSubAgentTools(repoRoot, config);
586291
587445
  const nameSet = new Set(opts.toolNames || []);
@@ -586299,12 +587453,38 @@ Review its full output in the [${id}] tab or via sub_agent(action='output', id='
586299
587453
 
586300
587454
  ${opts.systemPromptAddition}` : `Working directory: ${repoRoot}`;
586301
587455
  const result = await subRunner.run(opts.task, systemCtx);
587456
+ const subState = subRunner.getTaskState();
587457
+ const filesModified = Array.from(subState.modifiedFiles.keys());
587458
+ const artifactIds = [];
587459
+ if (_parentRunnerForArchive && result.summary && result.summary.length > 0) {
587460
+ const artifactId = `subagent-${opts.id}-result`;
587461
+ try {
587462
+ _parentRunnerForArchive.archiveToMemex({
587463
+ id: artifactId,
587464
+ toolName: "agent",
587465
+ summary: `Sub-agent ${opts.id}: ${result.summary.slice(0, 120)}`,
587466
+ fullContent: `Sub-agent task: ${opts.task.slice(0, 1e3)}
587467
+
587468
+ Completed: ${result.completed}
587469
+ Turns: ${result.turns}, Tool calls: ${result.toolCalls}
587470
+ Duration: ${result.durationMs}ms
587471
+ Files modified: ${filesModified.join(", ") || "(none)"}
587472
+
587473
+ Summary:
587474
+ ${result.summary}`
587475
+ });
587476
+ artifactIds.push(artifactId);
587477
+ } catch {
587478
+ }
587479
+ }
586302
587480
  return {
586303
587481
  completed: result.completed,
586304
587482
  summary: result.summary,
586305
587483
  turns: result.turns,
586306
587484
  toolCalls: result.toolCalls,
586307
- durationMs: result.durationMs
587485
+ durationMs: result.durationMs,
587486
+ filesModified,
587487
+ artifactIds
586308
587488
  };
586309
587489
  },
586310
587490
  spawnSubprocess: (opts) => {
@@ -589922,6 +591102,9 @@ async function runWithTUI(task, config, repoPath, callbacks) {
589922
591102
  renderCompactHeader(config.model);
589923
591103
  renderUserMessage(task);
589924
591104
  setTerminalTitle(task.slice(0, 60), getVersion4());
591105
+ if (!_wireAgentToolCallbacks) {
591106
+ _wireAgentToolCallbacks = (tool) => wireAgentToolMinimal(tool, config, repoRoot);
591107
+ }
589925
591108
  try {
589926
591109
  const handle2 = startTask(task, config, repoRoot);
589927
591110
  await handle2.promise;
@@ -590187,7 +591370,7 @@ Rules:
590187
591370
  process.exit(1);
590188
591371
  }
590189
591372
  }
590190
- var _interactiveSessionActive, _interactiveSessionReason, _voiceChatSession, taskManager, _apiCallbacks, _shellToolRef, _replToolRef, _fullSubAgentToolRef, _agentToolRef, _sendMessageToolRef, _agentLifecycleMgr, _activeRunnerRef, _wireSubAgentCallbacks, _wireAgentToolCallbacks, _wireSubAgentToolCallbacks, _autoUpdatedThisSession, _mcpManager, _pluginManager, _mcpTools, SELF_IMPROVE_INTERVAL, _tasksSinceImprove;
591373
+ var _interactiveSessionActive, _interactiveSessionReason, _voiceChatSession, taskManager, _apiCallbacks, _shellToolRef, _replToolRef, _fullSubAgentToolRef, _agentToolRef, _sendMessageToolRef, _agentLifecycleMgr, _activeRunnerRef, _parentRunnerForArchive, _wireSubAgentCallbacks, _wireAgentToolCallbacks, _wireSubAgentToolCallbacks, _autoUpdatedThisSession, _mcpManager, _pluginManager, _mcpTools, SELF_IMPROVE_INTERVAL, _tasksSinceImprove;
590191
591374
  var init_interactive = __esm({
590192
591375
  "packages/cli/src/tui/interactive.ts"() {
590193
591376
  "use strict";
@@ -590245,6 +591428,7 @@ var init_interactive = __esm({
590245
591428
  _sendMessageToolRef = null;
590246
591429
  _agentLifecycleMgr = getSharedTaskManager();
590247
591430
  _activeRunnerRef = null;
591431
+ _parentRunnerForArchive = null;
590248
591432
  _wireSubAgentCallbacks = null;
590249
591433
  _wireAgentToolCallbacks = null;
590250
591434
  _wireSubAgentToolCallbacks = null;