open-agents-ai 0.187.448 → 0.187.449

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,11 +505785,14 @@ 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) {
505731
505798
  return {
@@ -516249,6 +516316,420 @@ var init_messageLog = __esm({
516249
516316
  }
516250
516317
  });
516251
516318
 
516319
+ // packages/orchestrator/dist/contextTree.js
516320
+ function classifyToolCall(name10, argsJson) {
516321
+ const direct = TOOL_PHASE_MAP[name10];
516322
+ if (direct)
516323
+ return direct;
516324
+ if (name10 === "shell" || name10 === "shell_async") {
516325
+ if (!argsJson)
516326
+ return "implement";
516327
+ const lower = argsJson.toLowerCase();
516328
+ if (/(?:^|"command":")[^"]*(?:test|pytest|jest|mocha|vitest|cargo test|go test|npm test|pnpm test|tsc|lint|check|verify)\b/i.test(lower)) {
516329
+ return "verify";
516330
+ }
516331
+ return "implement";
516332
+ }
516333
+ return null;
516334
+ }
516335
+ function detectPhase(toolCallLog, window2 = 10) {
516336
+ if (toolCallLog.length === 0)
516337
+ return "explore";
516338
+ const recent = toolCallLog.slice(-window2);
516339
+ const counts = {};
516340
+ for (const tc of recent) {
516341
+ const p2 = classifyToolCall(tc.name, tc.argsKey);
516342
+ if (p2)
516343
+ counts[p2] = (counts[p2] ?? 0) + 1;
516344
+ }
516345
+ const total = Object.values(counts).reduce((a2, b) => a2 + b, 0);
516346
+ if (total === 0)
516347
+ return "mixed";
516348
+ let best = null;
516349
+ for (const [phase, n2] of Object.entries(counts)) {
516350
+ if (!best || n2 > best.n)
516351
+ best = { phase, n: n2 };
516352
+ }
516353
+ if (!best || best.n / total < 0.4)
516354
+ return "mixed";
516355
+ return best.phase;
516356
+ }
516357
+ function extractAnchorsFromMessages(messages2, turn) {
516358
+ const anchors = [];
516359
+ const seenSummaries = /* @__PURE__ */ new Set();
516360
+ let counter = 0;
516361
+ const nextId2 = (kind) => `anc-${turn}-${kind}-${counter++}`;
516362
+ for (const msg of messages2) {
516363
+ const content = typeof msg.content === "string" ? msg.content : "";
516364
+ if (!content)
516365
+ continue;
516366
+ if (msg.role === "tool" || msg.role === "assistant") {
516367
+ const matches = content.match(FILE_PATH_RE) ?? [];
516368
+ const distinctPaths = Array.from(new Set(matches.filter((p2) => p2.length > 2 && p2.length < 120))).slice(0, 5);
516369
+ for (const p2 of distinctPaths) {
516370
+ const summary = `file: ${p2}`;
516371
+ if (seenSummaries.has(summary))
516372
+ continue;
516373
+ seenSummaries.add(summary);
516374
+ anchors.push({
516375
+ id: nextId2("file"),
516376
+ type: "file",
516377
+ summary,
516378
+ keywords: [p2.toLowerCase(), ...p2.toLowerCase().split(/[\/\.]/).filter((s2) => s2.length > 2)],
516379
+ turn
516380
+ });
516381
+ }
516382
+ }
516383
+ if (msg.role === "assistant" && DECISION_HINTS.test(content)) {
516384
+ const sentence = content.split(/(?<=[.!?])\s+/).find((s2) => DECISION_HINTS.test(s2)) ?? content.slice(0, 200);
516385
+ const summary = `decision: ${sentence.slice(0, 140).replace(/\s+/g, " ").trim()}`;
516386
+ if (!seenSummaries.has(summary)) {
516387
+ seenSummaries.add(summary);
516388
+ anchors.push({
516389
+ id: nextId2("dec"),
516390
+ type: "decision",
516391
+ summary,
516392
+ keywords: extractKeywords(sentence),
516393
+ turn
516394
+ });
516395
+ }
516396
+ }
516397
+ if (msg.role === "tool" && ERROR_HINTS.test(content)) {
516398
+ const firstErrLine = content.split("\n").find((l2) => ERROR_HINTS.test(l2)) ?? content.slice(0, 200);
516399
+ const summary = `error: ${firstErrLine.slice(0, 140).replace(/\s+/g, " ").trim()}`;
516400
+ if (!seenSummaries.has(summary)) {
516401
+ seenSummaries.add(summary);
516402
+ anchors.push({
516403
+ id: nextId2("err"),
516404
+ type: "error",
516405
+ summary,
516406
+ keywords: extractKeywords(firstErrLine),
516407
+ turn
516408
+ });
516409
+ }
516410
+ }
516411
+ }
516412
+ return anchors;
516413
+ }
516414
+ function extractKeywords(text) {
516415
+ const tokens = text.toLowerCase().replace(/[^\w./-]+/g, " ").split(/\s+/);
516416
+ const out = [];
516417
+ const seen = /* @__PURE__ */ new Set();
516418
+ for (const tk of tokens) {
516419
+ if (tk.length < 3)
516420
+ continue;
516421
+ if (STOPWORDS.has(tk))
516422
+ continue;
516423
+ if (seen.has(tk))
516424
+ continue;
516425
+ seen.add(tk);
516426
+ out.push(tk);
516427
+ if (out.length >= 8)
516428
+ break;
516429
+ }
516430
+ return out;
516431
+ }
516432
+ var TOOL_PHASE_MAP, FILE_PATH_RE, DECISION_HINTS, ERROR_HINTS, STOPWORDS, DEFAULT_OPTS, ContextTree;
516433
+ var init_contextTree = __esm({
516434
+ "packages/orchestrator/dist/contextTree.js"() {
516435
+ "use strict";
516436
+ TOOL_PHASE_MAP = {
516437
+ // explore
516438
+ file_read: "explore",
516439
+ list_directory: "explore",
516440
+ find_files: "explore",
516441
+ grep_search: "explore",
516442
+ file_explore: "explore",
516443
+ glob_find: "explore",
516444
+ // plan
516445
+ todo_write: "plan",
516446
+ todo_read: "plan",
516447
+ working_notes: "plan",
516448
+ memory_write: "plan",
516449
+ memory_read: "plan",
516450
+ // implement
516451
+ file_write: "implement",
516452
+ file_edit: "implement",
516453
+ file_patch: "implement",
516454
+ batch_edit: "implement",
516455
+ // verify (heuristic — shell with test-like commands also contributes;
516456
+ // unknown shell defaults to implement)
516457
+ run_tests: "verify"
516458
+ };
516459
+ FILE_PATH_RE = /[\w./-]*\/[\w./-]+(?:\.[a-z0-9]{1,6})?/gi;
516460
+ DECISION_HINTS = /(?:I'll |Let me |I will |I'm going to |Plan: |Decision: |Approach: )/i;
516461
+ ERROR_HINTS = /(?:error|fail|exception|traceback|enoent|enotfound|fatal)/i;
516462
+ STOPWORDS = /* @__PURE__ */ new Set([
516463
+ "the",
516464
+ "and",
516465
+ "or",
516466
+ "of",
516467
+ "to",
516468
+ "in",
516469
+ "is",
516470
+ "it",
516471
+ "this",
516472
+ "that",
516473
+ "for",
516474
+ "on",
516475
+ "with",
516476
+ "as",
516477
+ "by",
516478
+ "from",
516479
+ "at",
516480
+ "an",
516481
+ "be",
516482
+ "are",
516483
+ "was",
516484
+ "were",
516485
+ "if",
516486
+ "then",
516487
+ "you",
516488
+ "we",
516489
+ "i",
516490
+ "but",
516491
+ "not",
516492
+ "no",
516493
+ "do",
516494
+ "does",
516495
+ "did",
516496
+ "have",
516497
+ "has",
516498
+ "had",
516499
+ "will",
516500
+ "would",
516501
+ "should",
516502
+ "can",
516503
+ "could"
516504
+ ]);
516505
+ DEFAULT_OPTS = {
516506
+ maxActiveMessages: 60,
516507
+ maxAnchorsPerNode: 12,
516508
+ phaseWindow: 10
516509
+ };
516510
+ ContextTree = class {
516511
+ snapshot;
516512
+ opts;
516513
+ currentPhase = "mixed";
516514
+ currentTurn = 0;
516515
+ toolCallLog = [];
516516
+ constructor(systemPromptHash, activeGoal, opts) {
516517
+ this.opts = { ...DEFAULT_OPTS, ...opts };
516518
+ this.snapshot = {
516519
+ rootSystemPromptHash: systemPromptHash,
516520
+ activeGoal,
516521
+ phases: {},
516522
+ history: [],
516523
+ archive: []
516524
+ };
516525
+ }
516526
+ /** Returns the current detected phase. */
516527
+ getCurrentPhase() {
516528
+ return this.currentPhase;
516529
+ }
516530
+ /** Returns a read-only snapshot for inspection / serialization. */
516531
+ getSnapshot() {
516532
+ return this.snapshot;
516533
+ }
516534
+ /** Update the active goal (e.g. after compaction rewrites the goal). */
516535
+ setActiveGoal(goal) {
516536
+ this.snapshot.activeGoal = goal;
516537
+ }
516538
+ /**
516539
+ * Record a tool call. Updates phase classification but does not move
516540
+ * messages between nodes (caller must invoke maybeTransition).
516541
+ */
516542
+ observeToolCall(name10, argsKey, turn) {
516543
+ this.toolCallLog.push({ name: name10, argsKey, turn });
516544
+ if (this.toolCallLog.length > 200) {
516545
+ this.toolCallLog = this.toolCallLog.slice(-100);
516546
+ }
516547
+ this.currentTurn = turn;
516548
+ }
516549
+ /**
516550
+ * Detect a phase transition. Returns the previous phase if a transition
516551
+ * happened, or null otherwise. Callers should follow up with
516552
+ * contractInactive to summarize the just-exited phase.
516553
+ */
516554
+ maybeTransition(turn) {
516555
+ const detected = detectPhase(this.toolCallLog, this.opts.phaseWindow);
516556
+ if (detected === this.currentPhase)
516557
+ return null;
516558
+ const from3 = this.currentPhase;
516559
+ this.currentPhase = detected;
516560
+ if (!this.snapshot.phases[detected]) {
516561
+ this.snapshot.phases[detected] = {
516562
+ status: "active",
516563
+ messages: [],
516564
+ anchors: [],
516565
+ startedAtTurn: turn
516566
+ };
516567
+ } else {
516568
+ this.snapshot.phases[detected].status = "active";
516569
+ }
516570
+ return { from: from3, to: detected };
516571
+ }
516572
+ /**
516573
+ * Sync a slice of messages into the current active phase node.
516574
+ * Caller is expected to provide ALL messages owned by the current
516575
+ * phase since the last sync (typically the suffix beyond a known
516576
+ * boundary index).
516577
+ */
516578
+ observePhaseMessages(messages2) {
516579
+ const node = this.snapshot.phases[this.currentPhase];
516580
+ if (!node)
516581
+ return;
516582
+ node.messages = messages2;
516583
+ if (messages2.length > this.opts.maxActiveMessages) {
516584
+ this.contract(this.currentPhase, this.currentTurn);
516585
+ }
516586
+ }
516587
+ /**
516588
+ * Contract the named phase: extract anchors, generate a stub summary,
516589
+ * mark the node `contracted`. The caller may then archive via the
516590
+ * archive callback (Phase 1 hands off to memex).
516591
+ */
516592
+ contract(phase, turn, summarizer) {
516593
+ const node = this.snapshot.phases[phase];
516594
+ if (!node || node.status !== "active")
516595
+ return null;
516596
+ const anchors = extractAnchorsFromMessages(node.messages, turn).slice(0, this.opts.maxAnchorsPerNode);
516597
+ node.anchors = [...node.anchors, ...anchors].slice(-this.opts.maxAnchorsPerNode);
516598
+ node.summary = summarizer ? summarizer(node.messages) : this.defaultSummary(phase, node.messages);
516599
+ node.status = "contracted";
516600
+ node.contractedAtTurn = turn;
516601
+ this.snapshot.history.push({ ...node, phase });
516602
+ if (this.snapshot.history.length > 32) {
516603
+ this.snapshot.history = this.snapshot.history.slice(-32);
516604
+ }
516605
+ return node;
516606
+ }
516607
+ /** Contract every active phase except the current one. */
516608
+ contractInactive(turn, summarizer) {
516609
+ const contracted = [];
516610
+ for (const [phase, node] of Object.entries(this.snapshot.phases)) {
516611
+ if (phase === this.currentPhase)
516612
+ continue;
516613
+ if (node.status !== "active")
516614
+ continue;
516615
+ this.contract(phase, turn, summarizer);
516616
+ contracted.push(phase);
516617
+ }
516618
+ return contracted;
516619
+ }
516620
+ /**
516621
+ * Mark a contracted node as archived after its summary has been moved
516622
+ * to memex. Caller passes the memex id.
516623
+ */
516624
+ archive(phase, memexId) {
516625
+ const node = this.snapshot.phases[phase];
516626
+ if (!node || node.status !== "contracted")
516627
+ return;
516628
+ node.status = "archived";
516629
+ node.summary = void 0;
516630
+ if (!this.snapshot.archive.includes(memexId)) {
516631
+ this.snapshot.archive.push(memexId);
516632
+ }
516633
+ }
516634
+ /**
516635
+ * Restore a contracted phase: pulls the last contracted node for the phase
516636
+ * back into active state. Returns the restored node or null if none found.
516637
+ */
516638
+ expand(phase) {
516639
+ const node = this.snapshot.phases[phase];
516640
+ if (node && node.status === "contracted") {
516641
+ node.status = "active";
516642
+ return node;
516643
+ }
516644
+ const histNode = [...this.snapshot.history].reverse().find((h) => h.phase === phase);
516645
+ if (histNode) {
516646
+ const restored = {
516647
+ status: "active",
516648
+ messages: [],
516649
+ anchors: histNode.anchors,
516650
+ startedAtTurn: this.currentTurn
516651
+ };
516652
+ this.snapshot.phases[phase] = restored;
516653
+ return restored;
516654
+ }
516655
+ return null;
516656
+ }
516657
+ /** Render a compact phase-status block for inclusion in prompts. */
516658
+ renderStatusBlock() {
516659
+ const lines = [`### Phase Status`, `- current: **${this.currentPhase}**`];
516660
+ const phaseSummaries = [];
516661
+ for (const [phase, node] of Object.entries(this.snapshot.phases)) {
516662
+ if (phase === this.currentPhase)
516663
+ continue;
516664
+ const status = node.status;
516665
+ const anchorCount = node.anchors.length;
516666
+ phaseSummaries.push(`- ${phase}: ${status}${anchorCount > 0 ? ` (${anchorCount} anchors)` : ""}`);
516667
+ }
516668
+ if (phaseSummaries.length > 0) {
516669
+ lines.push(...phaseSummaries);
516670
+ }
516671
+ if (this.snapshot.archive.length > 0) {
516672
+ lines.push(`- archived: ${this.snapshot.archive.length} (use memex_retrieve)`);
516673
+ }
516674
+ return lines.join("\n");
516675
+ }
516676
+ /** Render all retained anchors (across phases) for compaction summary. */
516677
+ renderAnchorBlock(maxPerPhase = 5) {
516678
+ const lines = [];
516679
+ for (const [phase, node] of Object.entries(this.snapshot.phases)) {
516680
+ if (node.anchors.length === 0)
516681
+ continue;
516682
+ lines.push(`**${phase} anchors:**`);
516683
+ for (const a2 of node.anchors.slice(-maxPerPhase)) {
516684
+ lines.push(`- [${a2.type}] ${a2.summary}`);
516685
+ }
516686
+ }
516687
+ return lines.length > 0 ? `### Phase Anchors
516688
+ ${lines.join("\n")}` : "";
516689
+ }
516690
+ /** Look up anchors whose keywords overlap the given query (Phase 6 hook). */
516691
+ findAnchorsByKeywords(query, max = 5) {
516692
+ const tokens = extractKeywords(query.toLowerCase());
516693
+ if (tokens.length === 0)
516694
+ return [];
516695
+ const tokenSet = new Set(tokens);
516696
+ const scored = [];
516697
+ for (const node of Object.values(this.snapshot.phases)) {
516698
+ for (const a2 of node.anchors) {
516699
+ let score = 0;
516700
+ for (const k of a2.keywords) {
516701
+ if (tokenSet.has(k))
516702
+ score += 2;
516703
+ for (const t2 of tokenSet) {
516704
+ if (k.includes(t2) || t2.includes(k))
516705
+ score += 1;
516706
+ }
516707
+ }
516708
+ if (score > 0)
516709
+ scored.push({ anchor: a2, score });
516710
+ }
516711
+ }
516712
+ scored.sort((a2, b) => b.score - a2.score);
516713
+ return scored.slice(0, max).map((s2) => s2.anchor);
516714
+ }
516715
+ defaultSummary(phase, messages2) {
516716
+ const toolCallNames = /* @__PURE__ */ new Set();
516717
+ let assistantWords = 0;
516718
+ for (const m2 of messages2) {
516719
+ if (m2.role === "assistant" && m2.tool_calls) {
516720
+ for (const tc of m2.tool_calls)
516721
+ toolCallNames.add(tc.function.name);
516722
+ }
516723
+ if (m2.role === "assistant" && typeof m2.content === "string") {
516724
+ assistantWords += m2.content.split(/\s+/).length;
516725
+ }
516726
+ }
516727
+ return `${phase} phase: ${messages2.length} messages, ${toolCallNames.size} distinct tools used, ${assistantWords} words of assistant output.`;
516728
+ }
516729
+ };
516730
+ }
516731
+ });
516732
+
516252
516733
  // packages/orchestrator/dist/codeGraphLink.js
516253
516734
  function isCodeGraphLinkEnabled() {
516254
516735
  return process.env["OA_CODEGRAPH_LINK"] !== "0";
@@ -517095,7 +517576,7 @@ function classifyThinkOutcome(raw) {
517095
517576
  }
517096
517577
  return null;
517097
517578
  }
517098
- var SYSTEM_PROMPT, SYSTEM_PROMPT_MEDIUM, SYSTEM_PROMPT_SMALL, VISUAL_TOOLS, AUDIO_TOOLS, SOCIAL_TOOLS, SPATIAL_TOOLS, CODE_TOOLS, AgenticRunner, OllamaAgenticBackend;
517579
+ 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
517580
  var init_agenticRunner = __esm({
517100
517581
  "packages/orchestrator/dist/agenticRunner.js"() {
517101
517582
  "use strict";
@@ -517109,12 +517590,23 @@ var init_agenticRunner = __esm({
517109
517590
  init_reflectionBuffer();
517110
517591
  init_taskHandoff();
517111
517592
  init_messageLog();
517593
+ init_contextTree();
517112
517594
  init_codeGraphLink();
517113
517595
  init_dist5();
517114
517596
  init_tool_batching();
517115
517597
  init_hooks();
517116
517598
  init_app_state();
517117
517599
  init_streaming_executor();
517600
+ TOOL_SUBSETS = {
517601
+ web: ["web_search", "web_fetch", "web_crawl"],
517602
+ code: ["file_patch", "file_explore", "batch_edit", "file_read", "file_write", "file_edit"],
517603
+ agent: ["agent", "sub_agent", "background_run"],
517604
+ memory: ["memory_search", "memory_read", "memory_write", "memex_retrieve", "working_notes"],
517605
+ shell: ["shell", "shell_async", "kill_proc", "run_tests"],
517606
+ graph: ["graph_query", "graph_traverse", "code_graph"],
517607
+ skill: ["skill_list", "skill_execute", "skill_search"]
517608
+ };
517609
+ TOOL_AUTO_DEMOTE_TURNS = 10;
517118
517610
  SYSTEM_PROMPT = loadPrompt("agentic/system-large.md");
517119
517611
  SYSTEM_PROMPT_MEDIUM = loadPrompt("agentic/system-medium.md");
517120
517612
  SYSTEM_PROMPT_SMALL = loadPrompt("agentic/system-small.md");
@@ -517222,6 +517714,21 @@ var init_agenticRunner = __esm({
517222
517714
  _patchHistoryStore = null;
517223
517715
  _toolSequence = [];
517224
517716
  // Track tool calls for pattern detection
517717
+ // Phase 2 — tool subset expansion and usage tracking.
517718
+ // Tools promoted from deferred → inline by tool_search (or by subset
517719
+ // expansion). They stay inline for the rest of the run unless idle past
517720
+ // TOOL_AUTO_DEMOTE_TURNS, at which point buildToolDefinitions drops them
517721
+ // back to deferred to reclaim token budget.
517722
+ _activatedTools = /* @__PURE__ */ new Set();
517723
+ _toolLastUsedTurn = /* @__PURE__ */ new Map();
517724
+ // Phase 1 — Context Tree. Tracks current phase + per-phase anchors so
517725
+ // compactMessages can summarize-by-phase and Phase 6 can surface anchors
517726
+ // by keyword. Initialized lazily in run() because the system-prompt hash
517727
+ // depends on the actual prompt resolved at run time.
517728
+ _contextTree = null;
517729
+ // Phase 6 — last-surface guard so we don't re-inject the same anchor on
517730
+ // consecutive turns when its keywords still match.
517731
+ _lastSurfacedAnchorIds = /* @__PURE__ */ new Set();
517225
517732
  /** WO-AM-10: Process pending episode embeddings in background batches */
517226
517733
  async processPendingEmbeddings() {
517227
517734
  if (this._pendingEmbeddings.length === 0 || !this._episodeStore)
@@ -517280,7 +517787,11 @@ var init_agenticRunner = __esm({
517280
517787
  personality: options2?.personality ?? PERSONALITY_PRESETS.balanced,
517281
517788
  personalityName: options2?.personalityName ?? "",
517282
517789
  finalVarResolver: options2?.finalVarResolver ?? void 0,
517283
- observerMode: options2?.observerMode ?? "both"
517790
+ observerMode: options2?.observerMode ?? "both",
517791
+ // Phase 4 — sub-agent isolation flag (defaults false). When true, this
517792
+ // runner skips cross-task handoff inheritance from the parent's
517793
+ // session.
517794
+ subAgent: options2?.subAgent ?? false
517284
517795
  };
517285
517796
  this._observerMode = this.options.observerMode;
517286
517797
  }
@@ -517685,6 +518196,212 @@ ${body}`;
517685
518196
  * Hannover reference: services/compact/apiMicrocompact.ts
517686
518197
  * Research: arXiv:2307.03172 (Lost in the Middle — recent context matters most)
517687
518198
  */
518199
+ /**
518200
+ * Phase 5 — Proactive context pruning (precedes microcompact). Targets
518201
+ * specific waste patterns rather than blanket-clearing old tool results:
518202
+ *
518203
+ * 1. Duplicate tool calls — same (name, args) called multiple times.
518204
+ * Older instances replaced with "[deduped — see turn N]".
518205
+ * 2. Aged file_read results — file_read older than `agedFileReadTurns`
518206
+ * replaced with a summary stub (path + size + first-line preview).
518207
+ * 3. Successful shell/test runs — shell without error markers older
518208
+ * than `agedShellTurns` replaced with a "succeeded" stub.
518209
+ *
518210
+ * Mutates `messages` in place. Cheap O(n) walk; safe to run every turn.
518211
+ * Refs: proposal §3 Phase 5, AgentFold (arXiv:2510.24699) progressive
518212
+ * summarization, RECOMP (ICLR 2024) observation masking.
518213
+ */
518214
+ proactivePrune(messages2, currentTurn) {
518215
+ if (process.env["OA_DISABLE_PROACTIVE_PRUNE"] === "1")
518216
+ return;
518217
+ const AGED_FILE_READ_TURNS = 10;
518218
+ const AGED_SHELL_TURNS = 5;
518219
+ const ERROR_MARKERS = /(?:error|fail|exception|traceback|enoent|enotfound|exit code [^0]|status[: ]+1\d?\d?)/i;
518220
+ const PRUNE_PREFIX = "[Tool result cleared";
518221
+ const DEDUPE_PREFIX = "[deduped — same call as turn";
518222
+ const FILE_AGED_PREFIX = "[file_read aged out, summary:";
518223
+ const SHELL_AGED_PREFIX = "[shell succeeded, output pruned —";
518224
+ const seen = /* @__PURE__ */ new Map();
518225
+ const pending = [];
518226
+ let scanTurn = 0;
518227
+ for (let i2 = 0; i2 < messages2.length; i2++) {
518228
+ const msg = messages2[i2];
518229
+ if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
518230
+ scanTurn++;
518231
+ for (const tc of msg.tool_calls) {
518232
+ const name10 = tc.function.name;
518233
+ const argsRaw = (tc.function.arguments ?? "").slice(0, 200);
518234
+ const fp = `${name10}|${argsRaw}`;
518235
+ let resultIdx = -1;
518236
+ for (let j = i2 + 1; j < Math.min(messages2.length, i2 + 8); j++) {
518237
+ const r2 = messages2[j];
518238
+ if (r2.role === "tool" && r2.tool_call_id === tc.id) {
518239
+ resultIdx = j;
518240
+ break;
518241
+ }
518242
+ }
518243
+ if (resultIdx === -1)
518244
+ continue;
518245
+ const resultMsg = messages2[resultIdx];
518246
+ const content = typeof resultMsg.content === "string" ? resultMsg.content : "";
518247
+ if (content.startsWith(PRUNE_PREFIX) || content.startsWith(DEDUPE_PREFIX) || content.startsWith(FILE_AGED_PREFIX) || content.startsWith(SHELL_AGED_PREFIX)) {
518248
+ if (!seen.has(fp))
518249
+ seen.set(fp, { turn: scanTurn, idx: resultIdx });
518250
+ continue;
518251
+ }
518252
+ const prior = seen.get(fp);
518253
+ if (prior) {
518254
+ pending.push({
518255
+ idx: prior.idx,
518256
+ reason: "dedupe",
518257
+ replacement: `${DEDUPE_PREFIX} ${scanTurn} — duplicate ${name10}() call]`
518258
+ });
518259
+ seen.set(fp, { turn: scanTurn, idx: resultIdx });
518260
+ continue;
518261
+ }
518262
+ seen.set(fp, { turn: scanTurn, idx: resultIdx });
518263
+ const ageTurns = currentTurn - scanTurn;
518264
+ if (name10 === "file_read" && ageTurns > AGED_FILE_READ_TURNS && content.length > 200) {
518265
+ const pathArg = (() => {
518266
+ try {
518267
+ const o2 = JSON.parse(tc.function.arguments || "{}");
518268
+ return String(o2.path ?? o2.file ?? "?");
518269
+ } catch {
518270
+ return "?";
518271
+ }
518272
+ })();
518273
+ const firstLine = content.split("\n")[0]?.slice(0, 80) ?? "";
518274
+ pending.push({
518275
+ idx: resultIdx,
518276
+ reason: "aged_file",
518277
+ replacement: `${FILE_AGED_PREFIX} path=${pathArg}, size=${content.length} chars, first-line="${firstLine}"]`
518278
+ });
518279
+ continue;
518280
+ }
518281
+ if ((name10 === "shell" || name10 === "shell_async" || name10 === "run_tests") && ageTurns > AGED_SHELL_TURNS && content.length > 200 && !ERROR_MARKERS.test(content)) {
518282
+ const cmdArg = (() => {
518283
+ try {
518284
+ const o2 = JSON.parse(tc.function.arguments || "{}");
518285
+ return String(o2.command ?? o2.cmd ?? "?").slice(0, 80);
518286
+ } catch {
518287
+ return "?";
518288
+ }
518289
+ })();
518290
+ pending.push({
518291
+ idx: resultIdx,
518292
+ reason: "aged_shell",
518293
+ replacement: `${SHELL_AGED_PREFIX} command="${cmdArg}", output ${content.length} chars, no error markers detected]`
518294
+ });
518295
+ }
518296
+ }
518297
+ }
518298
+ }
518299
+ if (pending.length === 0)
518300
+ return;
518301
+ let dedupes = 0, agedFiles = 0, agedShells = 0;
518302
+ for (const p2 of pending) {
518303
+ const m2 = messages2[p2.idx];
518304
+ if (!m2)
518305
+ continue;
518306
+ messages2[p2.idx] = { ...m2, content: p2.replacement };
518307
+ if (p2.reason === "dedupe")
518308
+ dedupes++;
518309
+ else if (p2.reason === "aged_file")
518310
+ agedFiles++;
518311
+ else if (p2.reason === "aged_shell")
518312
+ agedShells++;
518313
+ }
518314
+ const parts = [];
518315
+ if (dedupes > 0)
518316
+ parts.push(`${dedupes} duplicate call(s)`);
518317
+ if (agedFiles > 0)
518318
+ parts.push(`${agedFiles} aged file_read(s)`);
518319
+ if (agedShells > 0)
518320
+ parts.push(`${agedShells} aged shell run(s)`);
518321
+ if (parts.length > 0) {
518322
+ this.emit({
518323
+ type: "status",
518324
+ content: `Proactive prune: replaced ${parts.join(", ")} with summary stubs`,
518325
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
518326
+ });
518327
+ }
518328
+ }
518329
+ /**
518330
+ * Phase 6 — Anchor Surfacing.
518331
+ *
518332
+ * Scans the most recent user / assistant text for keyword overlap with
518333
+ * the ContextTree's anchor index plus the memex archive index. When
518334
+ * matches are found, injects a single-line system hint listing the
518335
+ * available anchors so the model knows what archived context exists
518336
+ * (and may call memex_retrieve to expand it).
518337
+ *
518338
+ * Refs: proposal §3 Phase 6, Lost-in-the-Middle (arXiv:2307.03172) —
518339
+ * surfacing relevant context near the tail combats positional decay.
518340
+ */
518341
+ surfaceAnchors(messages2) {
518342
+ if (process.env["OA_DISABLE_ANCHOR_SURFACING"] === "1")
518343
+ return;
518344
+ if (!this._contextTree)
518345
+ return;
518346
+ if (messages2.length === 0)
518347
+ return;
518348
+ const tail = messages2.slice(-6);
518349
+ const queryParts = [];
518350
+ for (const m2 of tail) {
518351
+ if (typeof m2.content === "string" && m2.content.length > 0) {
518352
+ if (m2.role === "user" || m2.role === "assistant") {
518353
+ queryParts.push(m2.content);
518354
+ }
518355
+ }
518356
+ }
518357
+ const query = queryParts.join(" ").slice(0, 800);
518358
+ if (!query)
518359
+ return;
518360
+ const anchors = this._contextTree.findAnchorsByKeywords(query, 5);
518361
+ const memexMatches = [];
518362
+ if (this._memexArchive.size > 0) {
518363
+ const tokens = query.toLowerCase().replace(/[^\w./-]+/g, " ").split(/\s+/).filter((t2) => t2.length > 3);
518364
+ const tokenSet = new Set(tokens);
518365
+ for (const e2 of this._memexArchive.values()) {
518366
+ const summaryLower = e2.summary.toLowerCase();
518367
+ let score = 0;
518368
+ for (const t2 of tokenSet) {
518369
+ if (summaryLower.includes(t2))
518370
+ score += 2;
518371
+ }
518372
+ if (e2.toolName === "todo_complete" && tokenSet.has("todo"))
518373
+ score += 1;
518374
+ if (score > 0)
518375
+ memexMatches.push({ id: e2.id, summary: e2.summary, score });
518376
+ }
518377
+ memexMatches.sort((a2, b) => b.score - a2.score);
518378
+ }
518379
+ const newAnchors = anchors.filter((a2) => !this._lastSurfacedAnchorIds.has(a2.id)).slice(0, 3);
518380
+ const newMemex = memexMatches.filter((m2) => !this._lastSurfacedAnchorIds.has(m2.id)).slice(0, 2);
518381
+ if (newAnchors.length === 0 && newMemex.length === 0)
518382
+ return;
518383
+ const lines = [`[Anchor surface] Relevant archived context for the current activity:`];
518384
+ for (const a2 of newAnchors) {
518385
+ lines.push(` - [${a2.type}] ${a2.summary}`);
518386
+ }
518387
+ for (const m2 of newMemex) {
518388
+ lines.push(` - [memex:${m2.id}] ${m2.summary} — call memex_retrieve("${m2.id}") for full body`);
518389
+ }
518390
+ lines.push(`(Anchors are reminders. Pull only what you actually need; ignore otherwise.)`);
518391
+ messages2.push({
518392
+ role: "system",
518393
+ content: lines.join("\n")
518394
+ });
518395
+ for (const a2 of newAnchors)
518396
+ this._lastSurfacedAnchorIds.add(a2.id);
518397
+ for (const m2 of newMemex)
518398
+ this._lastSurfacedAnchorIds.add(m2.id);
518399
+ this.emit({
518400
+ type: "status",
518401
+ content: `Anchor surface: surfaced ${newAnchors.length} anchor(s) + ${newMemex.length} memex entry(ies)`,
518402
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
518403
+ });
518404
+ }
517688
518405
  microcompact(messages2, recentToolResults) {
517689
518406
  const tier = this.options.modelTier ?? "large";
517690
518407
  let keepResults = tier === "small" ? 6 : tier === "medium" ? 10 : 20;
@@ -518056,6 +518773,10 @@ Respond with your assessment, then take action.`;
518056
518773
  this.aborted = false;
518057
518774
  this._paused = false;
518058
518775
  this._toolSequence = [];
518776
+ this._activatedTools.clear();
518777
+ this._toolLastUsedTurn.clear();
518778
+ this._contextTree = null;
518779
+ this._lastSurfacedAnchorIds.clear();
518059
518780
  if (!this._memoryInitialized) {
518060
518781
  try {
518061
518782
  const path8 = await import("node:path");
@@ -518135,6 +518856,7 @@ Respond with your assessment, then take action.`;
518135
518856
  this._hookManager.runSessionHook("session_start", this._sessionId);
518136
518857
  const contextComposition = await this.assembleContext(task, context2);
518137
518858
  const systemPrompt = contextComposition.assembled;
518859
+ this._contextTree = new ContextTree(`sys-${systemPrompt.length}`, cleanedTask.slice(0, 200));
518138
518860
  this.emit({
518139
518861
  type: "status",
518140
518862
  content: `Context assembled: ${contextComposition.sections.map((s2) => `${s2.label}(${s2.tokenEstimate}t)`).join(" + ")} = ~${contextComposition.totalTokenEstimate}t`,
@@ -518152,6 +518874,8 @@ TASK: ${task}` : task;
518152
518874
  { role: "user", content: userContent }
518153
518875
  ];
518154
518876
  try {
518877
+ if (this.options.subAgent)
518878
+ throw "skip-handoff-subagent";
518155
518879
  const oaDir = this._workingDirectory ? _pathJoin(this._workingDirectory, ".oa") : _pathJoin(process.cwd(), ".oa");
518156
518880
  const chainPairs = loadMessagePairsFromLog(oaDir, { currentTask: cleanedTask });
518157
518881
  if (chainPairs.length > 0) {
@@ -518590,7 +519314,9 @@ ${memoryLines.join("\n")}`
518590
519314
  }
518591
519315
  }
518592
519316
  this._lastAssistantTimestamp = Date.now();
519317
+ this.proactivePrune(compacted, turn);
518593
519318
  this.microcompact(compacted, recentToolResults);
519319
+ this.surfaceAnchors(compacted);
518594
519320
  const { maxOutputTokens: effectiveMaxTokens } = this.contextLimits();
518595
519321
  const chatRequest = {
518596
519322
  messages: compacted,
@@ -518879,6 +519605,28 @@ ${memoryLines.join("\n")}`
518879
519605
  toolCallCount++;
518880
519606
  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
519607
  toolCallLog.push({ name: tc.name, argsKey, turn, timestampMs: Date.now() });
519608
+ this._toolLastUsedTurn.set(tc.name, turn);
519609
+ if (this._contextTree) {
519610
+ this._contextTree.observeToolCall(tc.name, argsKey, turn);
519611
+ const transition = this._contextTree.maybeTransition(turn);
519612
+ if (transition) {
519613
+ this.emit({
519614
+ type: "status",
519615
+ content: `Phase: ${transition.from} → ${transition.to}`,
519616
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
519617
+ });
519618
+ const contracted = this._contextTree.contractInactive(turn);
519619
+ if (contracted.length > 0) {
519620
+ this.emit({
519621
+ type: "status",
519622
+ content: `Phase contraction: ${contracted.join(", ")} → contracted (anchors retained)`,
519623
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
519624
+ });
519625
+ }
519626
+ this._taskState.phase = transition.to;
519627
+ this._taskState.phaseSince = turn;
519628
+ }
519629
+ }
518882
519630
  const budgetRemaining = toolCallBudget.get(tc.name);
518883
519631
  if (budgetRemaining !== void 0) {
518884
519632
  if (budgetRemaining <= 0) {
@@ -519891,7 +520639,9 @@ Integrate this guidance into your current approach. Continue working on the task
519891
520639
  } else {
519892
520640
  compactedMsgs = await this.compactMessages(messages2, this._skillCompactionStrategy ?? "default");
519893
520641
  }
520642
+ this.proactivePrune(compactedMsgs, this._taskState.toolCallCount);
519894
520643
  this.microcompact(compactedMsgs);
520644
+ this.surfaceAnchors(compactedMsgs);
519895
520645
  const chatRequest = { messages: compactedMsgs, tools: toolDefs, temperature: this.options.temperature, maxTokens: this.options.maxTokens, timeoutMs: this.options.requestTimeoutMs };
519896
520646
  let response;
519897
520647
  try {
@@ -520238,6 +520988,8 @@ Full content available via: repl_exec(code="data = retrieve('${handleId}')") or
520238
520988
  }
520239
520989
  }
520240
520990
  try {
520991
+ if (this.options.subAgent)
520992
+ throw "skip-handoff-write-subagent";
520241
520993
  const outcome = consolidation.outcome === "success" ? "success" : consolidation.outcome === "aborted" ? "aborted" : consolidation.outcome === "timeout" ? "timeout" : "failed";
520242
520994
  const oaDir = this._workingDirectory ? _pathJoin(this._workingDirectory, ".oa") : _pathJoin(process.cwd(), ".oa");
520243
520995
  const transcriptPath = _pathJoin(oaDir, "consolidations", `${this._sessionId}.json`);
@@ -520678,6 +521430,9 @@ Actions: (1) list_directory on the parent directory to see what's there, (2) Che
520678
521430
  if (ts.currentStep) {
520679
521431
  parts.push(`**Currently doing:** ${ts.currentStep}`);
520680
521432
  }
521433
+ if (ts.phase) {
521434
+ parts.push(`**Phase:** ${ts.phase}`);
521435
+ }
520681
521436
  if (ts.completedSteps.length > 0) {
520682
521437
  parts.push(`**Completed:**`);
520683
521438
  for (const s2 of ts.completedSteps.slice(-10))
@@ -520702,9 +521457,75 @@ Actions: (1) list_directory on the parent directory to see what's there, (2) Che
520702
521457
  parts.push(`- \`${path8}\` (${action})`);
520703
521458
  }
520704
521459
  }
521460
+ const todoBlock = this.formatCompletedTodoAnchors();
521461
+ if (todoBlock)
521462
+ parts.push(todoBlock);
520705
521463
  parts.push(`**Tool calls:** ${ts.toolCallCount}`);
520706
521464
  return parts.join("\n");
520707
521465
  }
521466
+ /**
521467
+ * Phase 3 — render completed todos as compact anchor cards. Older
521468
+ * completed todos beyond the top-3 are archived to memex on first sight
521469
+ * so the model can still reach them via memex_retrieve. Returns the empty
521470
+ * string when the session has no todos.
521471
+ *
521472
+ * Anchor card shape: `[done] {content slice} (files: a.ts, b.ts) → outcome`
521473
+ */
521474
+ formatCompletedTodoAnchors() {
521475
+ try {
521476
+ const sessionId = process.env["OA_SESSION_ID"] || this._sessionId;
521477
+ const todos = readTodos(sessionId);
521478
+ if (todos.length === 0)
521479
+ return "";
521480
+ const completed = todos.filter((t2) => t2.status === "completed");
521481
+ const blocked = todos.filter((t2) => t2.status === "blocked");
521482
+ if (completed.length === 0 && blocked.length === 0)
521483
+ return "";
521484
+ const parts = [];
521485
+ const topRecent = completed.sort((a2, b) => (b.completedAt || b.updatedAt) - (a2.completedAt || a2.updatedAt)).slice(0, 3);
521486
+ if (topRecent.length > 0) {
521487
+ parts.push(`**Recently completed (anchors):**`);
521488
+ for (const t2 of topRecent) {
521489
+ parts.push(`- [done] ${this.todoAnchorLine(t2)}`);
521490
+ }
521491
+ }
521492
+ const olderCompleted = completed.slice(3);
521493
+ let archivedCount = 0;
521494
+ for (const t2 of olderCompleted) {
521495
+ const memexId = `todo-${t2.id.slice(0, 8)}`;
521496
+ if (!this._memexArchive.has(memexId)) {
521497
+ this._memexArchive.set(memexId, {
521498
+ id: memexId,
521499
+ toolName: "todo_complete",
521500
+ summary: this.todoAnchorLine(t2),
521501
+ fullContent: JSON.stringify(t2, null, 2),
521502
+ turn: this._taskState.toolCallCount,
521503
+ timestamp: new Date(t2.completedAt || t2.updatedAt).toISOString()
521504
+ });
521505
+ archivedCount++;
521506
+ }
521507
+ }
521508
+ if (archivedCount > 0) {
521509
+ parts.push(`*(${archivedCount} older completed todo(s) archived → memex_retrieve("todo-{id}") for details, ${olderCompleted.length} total older)*`);
521510
+ } else if (olderCompleted.length > 0) {
521511
+ parts.push(`*(${olderCompleted.length} older completed todo(s) in memex archive)*`);
521512
+ }
521513
+ if (blocked.length > 0) {
521514
+ parts.push(`**Blocked:**`);
521515
+ for (const t2 of blocked.slice(0, 5)) {
521516
+ parts.push(`- [blocked] ${t2.content}${t2.blocker ? ` — ${t2.blocker}` : ""}`);
521517
+ }
521518
+ }
521519
+ return parts.length > 0 ? parts.join("\n") : "";
521520
+ } catch {
521521
+ return "";
521522
+ }
521523
+ }
521524
+ /** Build a single-line anchor for a completed todo (Phase 3 helper). */
521525
+ todoAnchorLine(t2) {
521526
+ const content = t2.content.length > 80 ? t2.content.slice(0, 77) + "..." : t2.content;
521527
+ return content;
521528
+ }
520708
521529
  /**
520709
521530
  * Format file state registry as compact markdown for domain-aware compaction.
520710
521531
  * Only includes files with meaningful state (modified or recently accessed).
@@ -520954,6 +521775,33 @@ ${taskStateStr}
520954
521775
  if (memexIndexStr)
520955
521776
  enrichments.push(memexIndexStr);
520956
521777
  }
521778
+ if (this._contextTree) {
521779
+ const droppedSlice = messages2.slice(headEndIdx, recentStart);
521780
+ const freshAnchors = extractAnchorsFromMessages(droppedSlice, this._taskState.toolCallCount);
521781
+ if (freshAnchors.length > 0) {
521782
+ const tree2 = this._contextTree;
521783
+ const phase = tree2.getCurrentPhase();
521784
+ const snap = tree2.getSnapshot();
521785
+ if (!snap.phases[phase]) {
521786
+ snap.phases[phase] = {
521787
+ status: "active",
521788
+ messages: [],
521789
+ anchors: [],
521790
+ startedAtTurn: this._taskState.toolCallCount
521791
+ };
521792
+ }
521793
+ snap.phases[phase].anchors = [
521794
+ ...snap.phases[phase].anchors,
521795
+ ...freshAnchors
521796
+ ].slice(-12);
521797
+ }
521798
+ const phaseStatus = this._contextTree.renderStatusBlock();
521799
+ if (phaseStatus)
521800
+ enrichments.push(phaseStatus);
521801
+ const anchorBlock = this._contextTree.renderAnchorBlock();
521802
+ if (anchorBlock)
521803
+ enrichments.push(anchorBlock);
521804
+ }
520957
521805
  const postCompactRestore = [];
520958
521806
  const planSkel = this.buildPlanSkeleton();
520959
521807
  if (planSkel)
@@ -522185,7 +523033,7 @@ ${transcript}`
522185
523033
  const allTools = Array.from(this.tools.values()).filter((tool) => tool.name !== "tool_search");
522186
523034
  const tier = this.options.modelTier ?? "large";
522187
523035
  const taskGoal = this._taskState.goal || "";
522188
- const STOPWORDS = /* @__PURE__ */ new Set([
523036
+ const STOPWORDS2 = /* @__PURE__ */ new Set([
522189
523037
  "with",
522190
523038
  "that",
522191
523039
  "this",
@@ -522235,7 +523083,7 @@ ${transcript}`
522235
523083
  const getDesc = (tool) => dynamicDescs.get(tool.name) ?? tool.description;
522236
523084
  const getIndexLabel = (tool) => {
522237
523085
  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);
523086
+ 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
523087
  return keywords.length > 0 ? `${tool.name}(${keywords.join(",")})` : tool.name;
522240
523088
  };
522241
523089
  const CORE_TOOLS2 = /* @__PURE__ */ new Set([
@@ -522283,9 +523131,26 @@ ${transcript}`
522283
523131
  scored.sort((a2, b) => b.score - a2.score);
522284
523132
  const maxInlineExtra = tier === "small" ? 4 : 8;
522285
523133
  const inlineExtras = scored.slice(0, maxInlineExtra).filter((s2) => s2.score > 0);
523134
+ const currentTurn = this._taskState.toolCallCount;
523135
+ let demoted = 0;
523136
+ for (const promoted of this._activatedTools) {
523137
+ const lastUsed = this._toolLastUsedTurn.get(promoted) ?? -1;
523138
+ if (lastUsed >= 0 && currentTurn - lastUsed > TOOL_AUTO_DEMOTE_TURNS) {
523139
+ this._activatedTools.delete(promoted);
523140
+ demoted++;
523141
+ }
523142
+ }
523143
+ if (demoted > 0) {
523144
+ this.emit({
523145
+ type: "status",
523146
+ content: `Tool auto-demote: ${demoted} idle promoted tool(s) dropped back to deferred`,
523147
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
523148
+ });
523149
+ }
522286
523150
  const inlineNames = /* @__PURE__ */ new Set([
522287
523151
  ...allTools.filter((t2) => CORE_TOOLS2.has(t2.name)).map((t2) => t2.name),
522288
- ...inlineExtras.map((s2) => s2.tool.name)
523152
+ ...inlineExtras.map((s2) => s2.tool.name),
523153
+ ...Array.from(this._activatedTools).filter((name10) => allTools.some((t2) => t2.name === name10))
522289
523154
  ]);
522290
523155
  const deferred = allTools.filter((t2) => !inlineNames.has(t2.name));
522291
523156
  const inlineTools = allTools.filter((t2) => inlineNames.has(t2.name));
@@ -522317,11 +523182,12 @@ ${transcript}`
522317
523182
  lines[lineIdx].push(entry);
522318
523183
  return lines;
522319
523184
  }, []).map((line) => `- ${line.join(", ")}`).join("\n");
523185
+ const subsetNames = Object.keys(TOOL_SUBSETS).join(", ");
522320
523186
  defs.push({
522321
523187
  type: "function",
522322
523188
  function: {
522323
523189
  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.
523190
+ 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
523191
 
522326
523192
  Available tools (${deferred.length}):
522327
523193
  ${catalog}`,
@@ -522334,16 +523200,64 @@ ${catalog}`,
522334
523200
  }
522335
523201
  }
522336
523202
  });
523203
+ const activatedToolsRef = this._activatedTools;
523204
+ const subsetCatalog = TOOL_SUBSETS;
522337
523205
  this.tools.set("tool_search", {
522338
523206
  name: "tool_search",
522339
523207
  description: "Search for deferred tools",
522340
523208
  parameters: {},
522341
523209
  execute: async (args) => {
522342
- const query = String(args["query"] ?? "").toLowerCase();
523210
+ const query = String(args["query"] ?? "").toLowerCase().trim();
523211
+ const subsetMatch = subsetCatalog[query];
523212
+ if (subsetMatch && subsetMatch.length > 0) {
523213
+ const newlyPromoted = [];
523214
+ const alreadyAvailable = [];
523215
+ const unknown = [];
523216
+ for (const name10 of subsetMatch) {
523217
+ const tool = deferred.find((t2) => t2.name === name10) ?? allTools.find((t2) => t2.name === name10);
523218
+ if (!tool) {
523219
+ unknown.push(name10);
523220
+ continue;
523221
+ }
523222
+ if (inlineNames.has(name10)) {
523223
+ alreadyAvailable.push(name10);
523224
+ continue;
523225
+ }
523226
+ activatedToolsRef.add(name10);
523227
+ newlyPromoted.push(name10);
523228
+ }
523229
+ const lines = [`Subset "${query}" expanded:`];
523230
+ if (newlyPromoted.length > 0) {
523231
+ lines.push(` Promoted to inline (${newlyPromoted.length}): ${newlyPromoted.join(", ")}`);
523232
+ for (const name10 of newlyPromoted) {
523233
+ const tool = allTools.find((t2) => t2.name === name10);
523234
+ if (!tool)
523235
+ continue;
523236
+ lines.push("");
523237
+ lines.push(`## ${tool.name}`);
523238
+ lines.push(getDesc(tool));
523239
+ lines.push(`Parameters: ${JSON.stringify(tool.parameters)}`);
523240
+ }
523241
+ }
523242
+ if (alreadyAvailable.length > 0) {
523243
+ lines.push(` Already inline (${alreadyAvailable.length}): ${alreadyAvailable.join(", ")}`);
523244
+ }
523245
+ if (unknown.length > 0) {
523246
+ lines.push(` Not registered (${unknown.length}): ${unknown.join(", ")}`);
523247
+ }
523248
+ return { success: true, output: lines.join("\n") };
523249
+ }
522343
523250
  const matches = deferred.filter((t2) => t2.name.toLowerCase().includes(query) || getDesc(t2).toLowerCase().includes(query)).slice(0, 5);
522344
523251
  if (matches.length === 0) {
522345
- return { success: false, output: "", error: `No tools matching "${query}". Try a broader search.` };
523252
+ const subsetHint = Object.keys(subsetCatalog).join(", ");
523253
+ return {
523254
+ success: false,
523255
+ output: "",
523256
+ error: `No tools matching "${query}". Try a broader search, or call a subset name directly: ${subsetHint}.`
523257
+ };
522346
523258
  }
523259
+ for (const t2 of matches)
523260
+ activatedToolsRef.add(t2.name);
522347
523261
  const result = matches.map((t2) => {
522348
523262
  const paramsStr = JSON.stringify(t2.parameters, null, 2);
522349
523263
  return `## ${t2.name}
@@ -522354,7 +523268,7 @@ ${paramsStr}`;
522354
523268
  }).join("\n\n---\n\n");
522355
523269
  return {
522356
523270
  success: true,
522357
- output: `Found ${matches.length} tool(s). You can now call them directly:
523271
+ output: `Found ${matches.length} tool(s) promoted to inline for the rest of this run:
522358
523272
 
522359
523273
  ${result}`
522360
523274
  };
@@ -586285,7 +587199,12 @@ Review its full output in the [${id}] tab or via sub_agent(action='output', id='
586285
587199
  compactionThreshold: subCompaction,
586286
587200
  contextWindowSize: 0,
586287
587201
  // sub-agents discover their own context window
586288
- modelTier: subTier
587202
+ modelTier: subTier,
587203
+ // Phase 4 — sub-agent isolation: skip parent's cross-task handoff
587204
+ // read AND write. The sub-agent gets only what AgentTool composed
587205
+ // into the prompt (relevantFiles + constraints + task), nothing
587206
+ // leaked from the parent's session log or .oa/handoffs/latest.json.
587207
+ subAgent: opts.subAgentMode === true
586289
587208
  });
586290
587209
  const allSafe = buildSubAgentTools(repoRoot, config);
586291
587210
  const nameSet = new Set(opts.toolNames || []);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.448",
3
+ "version": "0.187.449",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "open-agents-ai",
9
- "version": "0.187.448",
9
+ "version": "0.187.449",
10
10
  "hasInstallScript": true,
11
11
  "license": "CC-BY-NC-4.0",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.448",
3
+ "version": "0.187.449",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",