antechamber 0.2.24 → 0.2.25

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/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.25] - 2026-10-17
11
+
12
+ - feat(tools): show subagents and monitors started inside eval (#30)
13
+
10
14
  ## [0.2.24] - 2026-10-17
11
15
 
12
16
  ### Fixed
@@ -14,6 +18,11 @@ and this project uses [Semantic Versioning](https://semver.org/).
14
18
  - Show completed background bash output and detached eval results in OpenChamber, including after reloading history.
15
19
  A card that started background work keeps running until that work finishes.
16
20
 
21
+ ### Added
22
+
23
+ - Show subagents and monitors started inside an eval cell as subagent cards that open their session, live and after
24
+ reloading history. A failed `agent()` call keeps its error.
25
+
17
26
  ## [0.2.23] - 2026-10-17
18
27
 
19
28
  - feat(monitors): show matched lines in monitor sessions (#32)
@@ -37,6 +37,12 @@ export declare class MonitorSessions {
37
37
  observe(parent: ManagedSession, ev: Record<string, unknown>): void;
38
38
  /** A hidden `senpi-monitor:notification` custom message: append its lines to each watch it names. */
39
39
  notify(parent: ManagedSession, message: Record<string, unknown>): void;
40
+ /**
41
+ * The child session of the newest watch a parent started with these monitor arguments.
42
+ * A monitor call recorded inside eval keeps only its arguments, not the `bash_N` id the
43
+ * child is keyed by, so command and description identify it.
44
+ */
45
+ findChild(parentID: string, args: Record<string, unknown>): string | undefined;
40
46
  /** The agent process is gone: OmO's in-process watches died with it. */
41
47
  stopParent(parentID: string): void;
42
48
  /** Forget a deleted child, or every child of a deleted parent. */
@@ -120,6 +120,24 @@ export class MonitorSessions {
120
120
  this.publish(watch);
121
121
  }
122
122
  }
123
+ /**
124
+ * The child session of the newest watch a parent started with these monitor arguments.
125
+ * A monitor call recorded inside eval keeps only its arguments, not the `bash_N` id the
126
+ * child is keyed by, so command and description identify it.
127
+ */
128
+ findChild(parentID, args) {
129
+ let found;
130
+ for (const watch of this.watches.values()) {
131
+ const { monitor } = watch;
132
+ if (watch.session.info.parentID !== parentID || monitor.command !== args.command)
133
+ continue;
134
+ if (typeof args.description === "string" && monitor.description !== args.description)
135
+ continue;
136
+ if (found === undefined || monitor.startedAtMs > found.monitor.startedAtMs)
137
+ found = watch;
138
+ }
139
+ return found?.session.info.id;
140
+ }
123
141
  /** The agent process is gone: OmO's in-process watches died with it. */
124
142
  stopParent(parentID) {
125
143
  for (const watch of this.watches.values()) {
@@ -2,7 +2,7 @@
2
2
  import type { OCAssistantMessage, OCMessageWithParts, OCModelRef, OCEvent, OCSessionStatus, OCPart } from "./types.js";
3
3
  import type { ManagedSession } from "./sessions.js";
4
4
  import type { SessionStore } from "./session-store.js";
5
- import type { SubagentBridge } from "./subagents.js";
5
+ import { type SubagentBridge } from "./subagents.js";
6
6
  import { type MonitorSessions } from "./monitor-sessions.js";
7
7
  import { type TodoUpdated } from "./todo-bridge.js";
8
8
  export interface PiUsage {
@@ -84,6 +84,7 @@ export declare class PiEventTranslator {
84
84
  private applyToolResults;
85
85
  /** The agent process is gone, and its background shells with it: end cards still waiting on them. */
86
86
  settleBackground(s: ManagedSession): void;
87
+ private onNestedTools;
87
88
  /** Complete an ended assistant message whose completion waited on its tools; true when it changed. */
88
89
  private completeSettled;
89
90
  /** Tool parts that never received a result (abort, crash) must not spin forever. */
@@ -1,7 +1,8 @@
1
1
  /** Pi message/part translation and the live event bridge, with explicit manager services. */
2
2
  import { randomUUID } from "node:crypto";
3
+ import { evalAgentSession } from "./subagents.js";
3
4
  import { MONITOR_NOTIFICATION } from "./monitor-sessions.js";
4
- import { applyProviderToolResults, finishProviderToolParts, mapTool, providerToolParts, resultText, toolFailed, } from "./tool-map.js";
5
+ import { applyProviderToolResults, finishProviderToolParts, mapTool, nestedToolParts, providerToolParts, resultText, toolFailed, } from "./tool-map.js";
5
6
  import { TodoBridge } from "./todo-bridge.js";
6
7
  import { readSessionTree, recordTurn } from "./session-tree.js";
7
8
  import { touchedFiles } from "./session-diff.js";
@@ -476,6 +477,7 @@ export class PiEventTranslator {
476
477
  if (output.trim() !== "")
477
478
  part.state.metadata = { ...part.state.metadata, output };
478
479
  this.emitPart(s, part);
480
+ this.onNestedTools(s, part, partial !== null && typeof partial === "object" && "details" in partial ? partial.details : undefined);
479
481
  return;
480
482
  }
481
483
  case "tool_execution_end": {
@@ -752,8 +754,11 @@ export class PiEventTranslator {
752
754
  // Task expansion replaces the streaming placeholder with one or more
753
755
  // cards sharing its callID. Order those cards without reconstructing it.
754
756
  const indices = new Map([...streaming.parts].map(([index, part]) => [part.type === "tool" ? part.callID : part.id, index]));
755
- owner.parts.sort((a, b) => (indices.get(a.type === "tool" ? a.callID : a.id) ?? Infinity) -
756
- (indices.get(b.type === "tool" ? b.callID : b.id) ?? Infinity));
757
+ const order = (part) => {
758
+ const parent = part.type === "tool" && part.state.status !== "pending" ? part.state.metadata?.parentCallID : undefined;
759
+ return (indices.get(typeof parent === "string" ? parent : part.type === "tool" ? part.callID : part.id) ?? Infinity);
760
+ };
761
+ owner.parts.sort((a, b) => order(a) - order(b));
757
762
  }
758
763
  }
759
764
  /**
@@ -850,6 +855,12 @@ export class PiEventTranslator {
850
855
  continue;
851
856
  }
852
857
  const part = callID !== undefined ? s.toolByCallId.get(callID) : undefined;
858
+ const parent = part ??
859
+ s.messages
860
+ ?.flatMap((message) => message.parts)
861
+ .find((item) => item.type === "tool" && item.callID === callID);
862
+ if (parent !== undefined)
863
+ this.onNestedTools(s, parent, tr.details);
853
864
  if (!part || callID === undefined)
854
865
  continue;
855
866
  s.toolByCallId.delete(callID);
@@ -870,6 +881,24 @@ export class PiEventTranslator {
870
881
  for (const part of settleBackground(s.messages ?? [], Date.now()))
871
882
  this.emitPart(s, part);
872
883
  }
884
+ onNestedTools(s, parent, details) {
885
+ const nested = nestedToolParts(parent, details, {
886
+ monitor: (args) => this.services.monitors.findChild(s.info.id, args),
887
+ agent: (args, claimed, after) => evalAgentSession(s.piSessionId ?? s.restored?.piSessionId, args, claimed, after),
888
+ });
889
+ if (nested.length === 0)
890
+ return;
891
+ const owner = s.messages?.find((message) => message.info.id === parent.messageID);
892
+ if (owner === undefined)
893
+ return;
894
+ for (const [index, part] of nested.entries()) {
895
+ const existing = owner.parts.findIndex((item) => item.id === part.id);
896
+ if (existing >= 0)
897
+ owner.parts.splice(existing, 1);
898
+ owner.parts.splice(owner.parts.indexOf(parent) + 1 + index, 0, part);
899
+ this.emitPart(s, part);
900
+ }
901
+ }
873
902
  /** Complete an ended assistant message whose completion waited on its tools; true when it changed. */
874
903
  completeSettled(s, info) {
875
904
  if (!this.awaitingTools.has(info) || info.role !== "assistant" || hasActiveTools(s, info.id))
@@ -1,11 +1,11 @@
1
1
  /** Reconstruct OpenCode history from pi JSONL session files. */
2
2
  import { readFileSync } from "node:fs";
3
- import { applyProviderToolResults, finishProviderToolParts, mapTool, providerToolParts } from "./tool-map.js";
3
+ import { applyProviderToolResults, finishProviderToolParts, mapTool, nestedToolParts, providerToolParts, } from "./tool-map.js";
4
4
  import { TodoBridge } from "./todo-bridge.js";
5
5
  import { userMessageModel } from "./thinking-levels.js";
6
6
  import { contextMessageID } from "./prompt-parts.js";
7
7
  import { bindMessageEntries } from "./session-tree.js";
8
- import { taskParts, updateTaskParts, taskCompletions } from "./subagents.js";
8
+ import { evalAgentSession, taskParts, updateTaskParts, taskCompletions } from "./subagents.js";
9
9
  import { shellTitle, shellUserMessage } from "./session-commands.js";
10
10
  import { compactionMessages, piModelId, piStopReason, textOf } from "./pi-event-translation.js";
11
11
  import { backgroundID, backgroundMessage, backgroundResult, completeBackgroundEval, finishToolPart, releaseBackground, settleBackground, } from "./background-results.js";
@@ -407,6 +407,18 @@ export function parsePiSessionText(s, agentName, raw) {
407
407
  }
408
408
  if (callID !== undefined)
409
409
  finishToolPart(part, m, callID, created);
410
+ const nested = nestedToolParts(part, m.details, {
411
+ agent: (args, claimed, after) => evalAgentSession(s.piSessionId ?? s.restored?.piSessionId, args, claimed, after),
412
+ });
413
+ const owner = out.find((message) => message.info.id === part.messageID);
414
+ if (owner !== undefined) {
415
+ for (const card of nested) {
416
+ const existing = owner.parts.findIndex((item) => item.id === card.id);
417
+ if (existing >= 0)
418
+ owner.parts.splice(existing, 1);
419
+ }
420
+ owner.parts.splice(owner.parts.indexOf(part) + 1, 0, ...nested);
421
+ }
410
422
  }
411
423
  // system / custom entries are not rendered
412
424
  }
@@ -475,8 +487,11 @@ export function parsePiSessionText(s, agentName, raw) {
475
487
  sessionID,
476
488
  messageID: message.info.id,
477
489
  }));
478
- const identities = s.tree.messageParts?.[message.info.id];
479
- for (const [index, part] of message.parts.entries()) {
490
+ const nestedIDs = new Set(message.parts.flatMap((part) => part.type === "tool" && part.state.status !== "pending" && part.state.metadata?.nested === true
491
+ ? [part.id]
492
+ : []));
493
+ const identities = s.tree.messageParts?.[message.info.id]?.filter((identity) => !nestedIDs.has(identity));
494
+ for (const [index, part] of message.parts.filter((part) => !nestedIDs.has(part.id)).entries()) {
480
495
  const identity = identities?.[index];
481
496
  if (identity !== undefined)
482
497
  part.id = identity;
@@ -14,6 +14,12 @@ export declare function findTaskBySession(sessionId: string): {
14
14
  path: string;
15
15
  data: Record<string, unknown>;
16
16
  } | undefined;
17
+ /**
18
+ * The session of a subagent an eval cell started with `agent()`. The recorded call keeps only its
19
+ * arguments, so the task record is found by parent, prompt and label: the oldest one created since
20
+ * the cell started and not yet claimed by an earlier call of the same cell.
21
+ */
22
+ export declare function evalAgentSession(parentPiId: string | undefined, args: Record<string, unknown>, claimed: Set<string>, after: number): string | undefined;
17
23
  export declare function findTask(id: string, parentPiId: string): {
18
24
  path: string;
19
25
  data: Record<string, unknown>;
package/dist/subagents.js CHANGED
@@ -193,6 +193,38 @@ export function findTaskBySession(sessionId) {
193
193
  }
194
194
  return undefined;
195
195
  }
196
+ /**
197
+ * The session of a subagent an eval cell started with `agent()`. The recorded call keeps only its
198
+ * arguments, so the task record is found by parent, prompt and label: the oldest one created since
199
+ * the cell started and not yet claimed by an earlier call of the same cell.
200
+ */
201
+ export function evalAgentSession(parentPiId, args, claimed, after) {
202
+ if (parentPiId === undefined || typeof args.prompt !== "string")
203
+ return undefined;
204
+ let best;
205
+ for (const { id, path } of taskFiles()) {
206
+ if (claimed.has(id))
207
+ continue;
208
+ const data = childRecord(id, path);
209
+ if (data?.parent_session_id !== parentPiId)
210
+ continue;
211
+ const spec = record(data.spawn_spec) ? data.spawn_spec : {};
212
+ if (spec.prompt !== args.prompt)
213
+ continue;
214
+ if (typeof args.label === "string" && data.name !== args.label)
215
+ continue;
216
+ const created = typeof data.created_at === "string" ? Date.parse(data.created_at) : Number.NaN;
217
+ // The cell's start is taken when its card opens, a moment after OmO starts it.
218
+ if (!Number.isFinite(created) || created < after - 5_000)
219
+ continue;
220
+ if (best === undefined || created < best.created)
221
+ best = { id, created };
222
+ }
223
+ if (best === undefined)
224
+ return undefined;
225
+ claimed.add(best.id);
226
+ return ocIdFor(best.id);
227
+ }
196
228
  export function findTask(id, parentPiId) {
197
229
  if (!taskId(id))
198
230
  return undefined;
@@ -25,6 +25,21 @@ export declare function resultText(content: unknown): string;
25
25
  /** Only edit cards need patch metadata; write cards already preview input.content. */
26
26
  export declare function resultMetadata(tool: string, result: ToolResult): Record<string, unknown>;
27
27
  export declare function mapTool(omoName: string, args: unknown, result?: ToolResult): MappedTool;
28
+ /**
29
+ * What an eval cell started that has a session of its own. Its other calls are not shown:
30
+ * the cell's output already holds what it printed, and OmO records only a one-line preview.
31
+ */
32
+ export interface NestedSessions {
33
+ /** The monitor child session for a recorded `monitor` call. */
34
+ monitor?: (args: Record<string, unknown>) => string | undefined;
35
+ /** The subagent session for a recorded `agent()` call; `claimed` keeps repeated calls apart. */
36
+ agent?: (args: Record<string, unknown>, claimed: Set<string>, after: number) => string | undefined;
37
+ }
38
+ /**
39
+ * Finished eval calls, from the raw RPC capture of installed omo-ai 5.1.33 / senpi runtime, 2026-10-17.
40
+ * Monitors and `agent()` calls (recorded as `__agent__`) become subagent cards; everything else is skipped.
41
+ */
42
+ export declare function nestedToolParts(parent: OCToolPart, details: unknown, sessions?: NestedSessions): OCToolPart[];
28
43
  /** One card at each call's original block index; result blocks are not cards.
29
44
  * Shared by RPC snapshots and history so ids and renderer state survive reloads.
30
45
  */
package/dist/tool-map.js CHANGED
@@ -134,6 +134,77 @@ export function mapTool(omoName, args, result) {
134
134
  ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
135
135
  };
136
136
  }
137
+ /** A subagent-style card, as OpenChamber draws `task`: it opens `sessionID` when there is one. */
138
+ function nestedSubagent(description, prompt, type, sessionID) {
139
+ return {
140
+ tool: "task",
141
+ input: { description, prompt, subagent_type: type },
142
+ title: description.slice(0, 60),
143
+ metadata: sessionID !== undefined ? { sessionID } : {},
144
+ };
145
+ }
146
+ /**
147
+ * Finished eval calls, from the raw RPC capture of installed omo-ai 5.1.33 / senpi runtime, 2026-10-17.
148
+ * Monitors and `agent()` calls (recorded as `__agent__`) become subagent cards; everything else is skipped.
149
+ */
150
+ export function nestedToolParts(parent, details, sessions = {}) {
151
+ if (!record(details) || !Array.isArray(details.toolCalls))
152
+ return [];
153
+ const parts = [];
154
+ const start = parent.state.status === "pending" ? 0 : parent.state.time.start;
155
+ const claimed = new Set();
156
+ details.toolCalls.forEach((entry, index) => {
157
+ if (!record(entry) ||
158
+ (entry.name !== "monitor" && entry.name !== "__agent__") ||
159
+ typeof entry.ok !== "boolean" ||
160
+ typeof entry.durationMs !== "number" ||
161
+ !Number.isFinite(entry.durationMs) ||
162
+ entry.durationMs < 0)
163
+ return;
164
+ const args = record(entry.args) ? entry.args : {};
165
+ let mapped;
166
+ if (entry.name === "monitor") {
167
+ const sessionID = entry.ok ? sessions.monitor?.(args) : undefined;
168
+ if (sessionID === undefined)
169
+ return; // nothing to open; the cell's code already shows the call
170
+ const command = typeof args.command === "string" ? args.command : "";
171
+ const label = typeof args.description === "string" && args.description !== "" ? args.description : command;
172
+ mapped = nestedSubagent(`Monitor: ${label}`, command, "monitor", sessionID);
173
+ }
174
+ else {
175
+ const prompt = typeof args.prompt === "string" ? args.prompt : "";
176
+ const label = [args.label, args.description, prompt.split("\n")[0]].find((value) => typeof value === "string" && value !== "");
177
+ const sessionID = entry.ok ? sessions.agent?.(args, claimed, start) : undefined;
178
+ const type = typeof args.agent === "string" && args.agent !== "" ? args.agent : "subagent";
179
+ mapped = nestedSubagent(label ?? "subagent", prompt, type, sessionID);
180
+ }
181
+ const key = typeof entry.callId === "string" ? `id:${encodeURIComponent(entry.callId)}` : `index:${index}`;
182
+ const callID = `${encodeURIComponent(parent.callID)}:${key}`;
183
+ const state = {
184
+ input: mapped.input,
185
+ title: mapped.title,
186
+ output: "",
187
+ metadata: { ...mapped.metadata, nested: true, parentCallID: parent.callID, durationMs: entry.durationMs },
188
+ time: { start, end: start + entry.durationMs },
189
+ };
190
+ parts.push({
191
+ id: `prt_${parent.sessionID}_nested_${callID}`,
192
+ sessionID: parent.sessionID,
193
+ messageID: parent.messageID,
194
+ type: "tool",
195
+ callID: `nested:${callID}`,
196
+ tool: mapped.tool,
197
+ state: entry.ok
198
+ ? { ...state, status: "completed" }
199
+ : {
200
+ ...state,
201
+ status: "error",
202
+ error: typeof entry.error === "string" && entry.error !== "" ? entry.error : "failed",
203
+ },
204
+ });
205
+ });
206
+ return parts;
207
+ }
137
208
  /** JSON first, then the literal-only Python repr used by older session files.
138
209
  * Never execute provider content; invalid or incomplete literals are skipped.
139
210
  */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "OpenChamber extension: displays OmO tool calls the way OmO means them when OpenChamber runs on antechamber",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-graph",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "OpenChamber extension: live, interactive graph of the OmO sub-agents behind the current session",
5
5
  "private": true,
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "OpenCode-compatible HTTP/SSE server that lets OpenChamber drive the pi coding agent (and OmO) over RPC",
5
5
  "type": "module",
6
6
  "license": "MIT",