@trim21/personal-pi-extensions 0.0.165 → 0.0.166

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/spawn-agent.ts +58 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.165",
3
+ "version": "0.0.166",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -24,9 +24,9 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
24
24
  import { tmpdir } from "node:os";
25
25
  import { basename, dirname, join } from "node:path";
26
26
 
27
- import type { AgentToolResult } from "@earendil-works/pi-agent-core";
28
- import type { Message } from "@earendil-works/pi-ai";
27
+ import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core";
29
28
  import {
29
+ type AgentSessionEvent,
30
30
  type ExtensionAPI,
31
31
  getMarkdownTheme,
32
32
  truncateTail,
@@ -72,7 +72,7 @@ interface SubagentDetails {
72
72
  agent: string;
73
73
  task: string;
74
74
  exitCode: number;
75
- messages: Message[];
75
+ messages: AgentMessage[];
76
76
  stderr: string;
77
77
  usage: UsageStats;
78
78
  model?: string;
@@ -82,7 +82,7 @@ interface SubagentDetails {
82
82
 
83
83
  // ── helpers ──────────────────────────────────────────────────────────────────
84
84
 
85
- function getFinalOutput(messages: Message[]): string {
85
+ function getFinalOutput(messages: AgentMessage[]): string {
86
86
  for (let i = messages.length - 1; i >= 0; i--) {
87
87
  const msg = messages[i];
88
88
  if (msg.role === "assistant") {
@@ -234,42 +234,47 @@ export async function runAgent(
234
234
 
235
235
  const processLine = (line: string) => {
236
236
  if (!line.trim()) return;
237
- let event: unknown;
238
- try {
239
- event = JSON.parse(line);
240
- } catch {
241
- return; // not a JSON event line
242
- }
243
- if (!isRecord(event)) return;
244
-
245
- if (event.type === "message_update" && isRecord(event.assistantMessageEvent)) {
246
- // A completed text block (text_end carries the full content) becomes a
247
- // `text:` log line. Deltas/thinking are intentionally not logged.
248
- const delta = event.assistantMessageEvent;
249
- if (delta.type === "text_end" && typeof delta.content === "string") {
250
- pushLogLine(`text: ${delta.content}`);
237
+ const event = parseJsonEvent(line);
238
+ if (!event) return;
239
+
240
+ switch (event.type) {
241
+ case "message_update": {
242
+ // A completed text block (text_end carries the full content) becomes a
243
+ // `text:` log line. Deltas/thinking are intentionally not logged.
244
+ const delta = event.assistantMessageEvent;
245
+ if (delta.type === "text_end") {
246
+ pushLogLine(`text: ${delta.content}`);
247
+ emitUpdate();
248
+ }
249
+
250
+ break;
251
+ }
252
+ case "tool_execution_start": {
253
+ pushLogLine(`tool: ${event.toolName}`);
251
254
  emitUpdate();
255
+
256
+ break;
252
257
  }
253
- } else if (event.type === "tool_execution_start" && typeof event.toolName === "string") {
254
- pushLogLine(`tool: ${event.toolName}`);
255
- emitUpdate();
256
- } else if (event.type === "message_end" && isRecord(event.message)) {
257
- const msg = event.message as unknown as Message;
258
- result.messages.push(msg);
259
- if (msg.role === "assistant") {
260
- result.usage.turns++;
261
- const usage: Record<string, unknown> = isRecord(msg.usage) ? msg.usage : {};
262
- result.usage.input += num(usage.input);
263
- result.usage.output += num(usage.output);
264
- result.usage.cacheRead += num(usage.cacheRead);
265
- result.usage.cacheWrite += num(usage.cacheWrite);
266
- result.usage.cost += num(isRecord(usage.cost) ? usage.cost.total : undefined);
267
- result.usage.contextTokens = num(usage.totalTokens);
268
- if (!result.model && typeof msg.model === "string") result.model = msg.model;
269
- if (typeof msg.stopReason === "string") result.stopReason = msg.stopReason;
270
- if (typeof msg.errorMessage === "string") result.errorMessage = msg.errorMessage;
258
+ case "message_end": {
259
+ const msg = event.message;
260
+ result.messages.push(msg);
261
+ if (msg.role === "assistant") {
262
+ result.usage.turns++;
263
+ result.usage.input += msg.usage.input;
264
+ result.usage.output += msg.usage.output;
265
+ result.usage.cacheRead += msg.usage.cacheRead;
266
+ result.usage.cacheWrite += msg.usage.cacheWrite;
267
+ result.usage.cost += msg.usage.cost.total;
268
+ result.usage.contextTokens = msg.usage.totalTokens;
269
+ if (!result.model) result.model = msg.model;
270
+ result.stopReason = msg.stopReason;
271
+ if (msg.errorMessage) result.errorMessage = msg.errorMessage;
272
+ }
273
+ emitUpdate();
274
+
275
+ break;
271
276
  }
272
- emitUpdate();
277
+ // No default
273
278
  }
274
279
  };
275
280
 
@@ -317,12 +322,22 @@ export async function runAgent(
317
322
  }
318
323
  }
319
324
 
320
- function isRecord(v: unknown): v is Record<string, unknown> {
321
- return typeof v === "object" && v !== null;
322
- }
323
-
324
- function num(v: unknown): number {
325
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
325
+ /**
326
+ * Parse one line of the subagent's `--mode json` event stream into a typed
327
+ * event. Non-JSON lines and non-event records (e.g. the session header) are
328
+ * rejected. The cast here is the single trust boundary: downstream branches
329
+ * are fully type-narrowed via the `AgentSessionEvent` discriminated union.
330
+ */
331
+ function parseJsonEvent(line: string): AgentSessionEvent | null {
332
+ let raw: unknown;
333
+ try {
334
+ raw = JSON.parse(line);
335
+ } catch {
336
+ return null;
337
+ }
338
+ if (typeof raw !== "object" || raw === null) return null;
339
+ if (typeof (raw as Record<string, unknown>).type !== "string") return null;
340
+ return raw as AgentSessionEvent;
326
341
  }
327
342
 
328
343
  /** Session entry customType used to mark the injected subagent list. */