@pi-ohm/subagents 0.6.4-dev.22244904817.1.e83c930 → 0.6.4-dev.22264411679.1.2aa3a99

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/README.md CHANGED
@@ -360,7 +360,8 @@ Additional hardening behaviors:
360
360
 
361
361
  Persistence details:
362
362
 
363
- - default snapshot path: `${PI_CONFIG_DIR|PI_CODING_AGENT_DIR|PI_AGENT_DIR|~/.pi/agent}/ohm.subagents.tasks.json`
363
+ - default snapshot path: `${XDG_DATA_HOME:-~/.local/share}/pi/agent/ohm.subagents.tasks.json`
364
+ - override snapshot path: `OHM_SUBAGENTS_TASK_PERSIST_PATH=/abs/path/ohm.subagents.tasks.json`
364
365
  - retention window is configurable via `OHM_SUBAGENTS_TASK_RETENTION_MS` (positive integer ms)
365
366
  - per-task structured event timeline cap is configurable via `OHM_SUBAGENTS_TASK_MAX_EVENTS`
366
367
  (default `120`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-ohm/subagents",
3
- "version": "0.6.4-dev.22244904817.1.e83c930",
3
+ "version": "0.6.4-dev.22264411679.1.2aa3a99",
4
4
  "homepage": "https://github.com/pi-ohm/pi-ohm/tree/dev/packages/subagents#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,11 +19,11 @@
19
19
  "provenance": true
20
20
  },
21
21
  "dependencies": {
22
- "@mariozechner/pi-coding-agent": "catalog:pi",
23
- "@pi-ohm/config": "0.6.4-dev.22244904817.1.e83c930",
24
- "@pi-ohm/tui": "0.6.4-dev.22244904817.1.e83c930",
25
- "better-result": "catalog:",
26
- "zod": "catalog:"
22
+ "@mariozechner/pi-coding-agent": "^0.52.0",
23
+ "@pi-ohm/config": "0.6.4-dev.22264411679.1.2aa3a99",
24
+ "@pi-ohm/tui": "0.6.4-dev.22264411679.1.2aa3a99",
25
+ "better-result": "^2.6.0",
26
+ "zod": "^4"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@mariozechner/pi-coding-agent": "*",
@@ -296,6 +296,10 @@ function toPrimaryToolCallText(subagent: OhmSubagentDefinition, params: unknown)
296
296
  }
297
297
 
298
298
  function toResultText(result: AgentToolResult<unknown>): string {
299
+ if (isTaskToolResultDetails(result.details)) {
300
+ return formatTaskToolResult(result.details, false);
301
+ }
302
+
299
303
  const textBlocks = result.content.filter(
300
304
  (part): part is { readonly type: "text"; readonly text: string } => part.type === "text",
301
305
  );
@@ -303,10 +307,6 @@ function toResultText(result: AgentToolResult<unknown>): string {
303
307
  const joined = textBlocks.map((part) => part.text).join("\n\n");
304
308
  if (joined.length > 0) return joined;
305
309
 
306
- if (isTaskToolResultDetails(result.details)) {
307
- return formatTaskToolResult(result.details, false);
308
- }
309
-
310
310
  return "primary subagent tool result unavailable";
311
311
  }
312
312
 
@@ -62,6 +62,8 @@ export interface TaskToolResultDetails {
62
62
  readonly output_truncated?: boolean;
63
63
  readonly output_total_chars?: number;
64
64
  readonly output_returned_chars?: number;
65
+ readonly updated_at_epoch_ms?: number;
66
+ readonly ended_at_epoch_ms?: number;
65
67
  readonly backend: string;
66
68
  readonly provider?: string;
67
69
  readonly model?: string;
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { resolveDefaultTaskPersistencePath } from "./defaults";
5
+ import { defineTest } from "./test-fixtures";
6
+
7
+ const ORIGINAL_XDG_DATA_HOME = process.env.XDG_DATA_HOME;
8
+ const ORIGINAL_TASK_PERSIST_PATH = process.env.OHM_SUBAGENTS_TASK_PERSIST_PATH;
9
+
10
+ function setEnv(name: "XDG_DATA_HOME" | "OHM_SUBAGENTS_TASK_PERSIST_PATH", value?: string): void {
11
+ if (value === undefined) {
12
+ delete process.env[name];
13
+ return;
14
+ }
15
+
16
+ process.env[name] = value;
17
+ }
18
+
19
+ function restoreEnv(): void {
20
+ setEnv("XDG_DATA_HOME", ORIGINAL_XDG_DATA_HOME);
21
+ setEnv("OHM_SUBAGENTS_TASK_PERSIST_PATH", ORIGINAL_TASK_PERSIST_PATH);
22
+ }
23
+
24
+ defineTest("resolveDefaultTaskPersistencePath uses explicit override when configured", () => {
25
+ try {
26
+ setEnv("XDG_DATA_HOME", "/tmp/xdg-data");
27
+ setEnv("OHM_SUBAGENTS_TASK_PERSIST_PATH", "/tmp/custom/subagent-tasks.json");
28
+
29
+ const resolved = resolveDefaultTaskPersistencePath();
30
+ assert.equal(resolved, "/tmp/custom/subagent-tasks.json");
31
+ } finally {
32
+ restoreEnv();
33
+ }
34
+ });
35
+
36
+ defineTest("resolveDefaultTaskPersistencePath uses XDG_DATA_HOME by default", () => {
37
+ try {
38
+ setEnv("OHM_SUBAGENTS_TASK_PERSIST_PATH", undefined);
39
+ setEnv("XDG_DATA_HOME", "/tmp/xdg-data");
40
+
41
+ const resolved = resolveDefaultTaskPersistencePath();
42
+ assert.equal(resolved, "/tmp/xdg-data/pi/agent/ohm.subagents.tasks.json");
43
+ } finally {
44
+ restoreEnv();
45
+ }
46
+ });
47
+
48
+ defineTest(
49
+ "resolveDefaultTaskPersistencePath falls back to ~/.local/share when XDG_DATA_HOME is unset",
50
+ () => {
51
+ try {
52
+ setEnv("OHM_SUBAGENTS_TASK_PERSIST_PATH", undefined);
53
+ setEnv("XDG_DATA_HOME", undefined);
54
+
55
+ const resolved = resolveDefaultTaskPersistencePath();
56
+ assert.equal(
57
+ resolved,
58
+ join(homedir(), ".local", "share", "pi", "agent", "ohm.subagents.tasks.json"),
59
+ );
60
+ } finally {
61
+ restoreEnv();
62
+ }
63
+ },
64
+ );
@@ -47,20 +47,33 @@ function resolveTaskPersistenceDebounceMs(): number {
47
47
  return 90;
48
48
  }
49
49
 
50
+ function parseNonEmptyEnvString(name: string): string | undefined {
51
+ const raw = process.env[name];
52
+ if (!raw) return undefined;
53
+ const trimmed = raw.trim();
54
+ if (trimmed.length === 0) return undefined;
55
+ return trimmed;
56
+ }
57
+
50
58
  export function resolveOnUpdateThrottleMs(): number {
51
59
  const fromEnv = parsePositiveIntegerEnv("OHM_SUBAGENTS_ONUPDATE_THROTTLE_MS");
52
60
  if (fromEnv !== undefined) return fromEnv;
53
61
  return 120;
54
62
  }
55
63
 
56
- function resolveDefaultTaskPersistencePath(): string {
57
- const baseDir =
58
- process.env.PI_CONFIG_DIR ??
59
- process.env.PI_CODING_AGENT_DIR ??
60
- process.env.PI_AGENT_DIR ??
61
- join(homedir(), ".pi", "agent");
64
+ function resolveTaskPersistenceDataDir(): string {
65
+ const xdgDataHome = parseNonEmptyEnvString("XDG_DATA_HOME");
66
+ if (xdgDataHome) {
67
+ return join(xdgDataHome, "pi", "agent");
68
+ }
69
+
70
+ return join(homedir(), ".local", "share", "pi", "agent");
71
+ }
62
72
 
63
- return join(baseDir, "ohm.subagents.tasks.json");
73
+ export function resolveDefaultTaskPersistencePath(): string {
74
+ const explicitPath = parseNonEmptyEnvString("OHM_SUBAGENTS_TASK_PERSIST_PATH");
75
+ if (explicitPath) return explicitPath;
76
+ return join(resolveTaskPersistenceDataDir(), "ohm.subagents.tasks.json");
64
77
  }
65
78
 
66
79
  const DEFAULT_TASK_STORE = createInMemoryTaskRuntimeStore({
@@ -127,6 +127,8 @@ export function snapshotToTaskResultDetails(
127
127
  output_truncated: resolvedOutput.output_truncated,
128
128
  output_total_chars: resolvedOutput.output_total_chars,
129
129
  output_returned_chars: resolvedOutput.output_returned_chars,
130
+ updated_at_epoch_ms: snapshot.updatedAtEpochMs,
131
+ ended_at_epoch_ms: snapshot.endedAtEpochMs,
130
132
  backend: snapshot.backend,
131
133
  provider: snapshot.provider,
132
134
  model: snapshot.model,
@@ -1337,11 +1337,13 @@ defineTest("runTaskToolMvp surfaces multiline output text in tool content", asyn
1337
1337
  }
1338
1338
 
1339
1339
  const plainText = stripAnsi(textBlock.text);
1340
- assert.match(plainText, /✓ Finder · Multiline/);
1341
- assert.match(plainText, /├── Return multiple lines/);
1340
+ assert.match(plainText, /^task_id:\s+task_/m);
1341
+ assert.match(plainText, /^timestamp:\s+\d{4}-\d{2}-\d{2}T/m);
1342
+ assert.match(plainText, /^result:$/m);
1342
1343
  assert.match(plainText, /alpha/);
1343
1344
  assert.match(plainText, /beta/);
1344
1345
  assert.match(plainText, /gamma/);
1346
+ assert.doesNotMatch(plainText, /├──|╰──|✓|✕/);
1345
1347
  });
1346
1348
 
1347
1349
  defineTest("runTaskToolMvp always returns full output for long payloads", async () => {
@@ -1391,8 +1393,12 @@ defineTest("runTaskToolMvp always returns full output for long payloads", async
1391
1393
  assert.fail("Expected text content block");
1392
1394
  }
1393
1395
 
1394
- assert.doesNotMatch(textBlock.text, /truncated/);
1395
- assert.match(textBlock.text, /abcdefghijklmnopqrstuvwxyz0123456789/);
1396
+ const plainText = stripAnsi(textBlock.text);
1397
+ assert.match(plainText, /^task_id:\s+task_/m);
1398
+ assert.match(plainText, /^timestamp:\s+\d{4}-\d{2}-\d{2}T/m);
1399
+ assert.match(plainText, /^result:$/m);
1400
+ assert.doesNotMatch(plainText, /truncated/);
1401
+ assert.match(plainText, /abcdefghijklmnopqrstuvwxyz0123456789/);
1396
1402
  } finally {
1397
1403
  if (previous === undefined) {
1398
1404
  delete process.env.OHM_SUBAGENTS_OUTPUT_MAX_CHARS;
@@ -2535,7 +2541,12 @@ defineTest("runTaskToolMvp uses assistant_text from events for terminal result r
2535
2541
  assert.fail("Expected text block");
2536
2542
  }
2537
2543
 
2538
- assert.match(textBlock.text, /structured final answer/);
2544
+ const plainText = stripAnsi(textBlock.text);
2545
+ assert.match(plainText, /^task_id:\s+task_/m);
2546
+ assert.match(plainText, /^timestamp:\s+\d{4}-\d{2}-\d{2}T/m);
2547
+ assert.match(plainText, /^result:$/m);
2548
+ assert.match(plainText, /structured final answer/);
2549
+ assert.doesNotMatch(plainText, /tool_call:|├──|╰──|✓/);
2539
2550
  });
2540
2551
 
2541
2552
  defineTest("runTaskToolMvp status aggregates runtime observability from task items", async () => {
@@ -85,6 +85,80 @@ function withItemObservabilityDefaults(item: TaskToolItemDetails): TaskToolItemD
85
85
  };
86
86
  }
87
87
 
88
+ function toIsoTimestamp(epochMs: number | undefined): string | undefined {
89
+ if (typeof epochMs !== "number" || !Number.isFinite(epochMs)) return undefined;
90
+ const date = new Date(epochMs);
91
+ if (Number.isNaN(date.getTime())) return undefined;
92
+ return date.toISOString();
93
+ }
94
+
95
+ function extractNarrativeResult(output: string | undefined): string | undefined {
96
+ if (!output) return undefined;
97
+ const sections = parseTaskTranscriptSections(output);
98
+ if (sections.narrativeLines.length === 0) return undefined;
99
+ return sections.narrativeLines.join("\n");
100
+ }
101
+
102
+ function resolveModelResultText(details: TaskToolResultDetails): string {
103
+ const assistantText = details.assistant_text?.trim();
104
+ if (assistantText && assistantText.length > 0) return assistantText;
105
+
106
+ const outputNarrative =
107
+ details.output_available && details.output ? extractNarrativeResult(details.output) : undefined;
108
+ if (outputNarrative && outputNarrative.length > 0) return outputNarrative;
109
+
110
+ if (details.items && details.items.length === 1) {
111
+ const [item] = details.items;
112
+ if (item && item.found) {
113
+ const itemAssistantText = item.assistant_text?.trim();
114
+ if (itemAssistantText && itemAssistantText.length > 0) return itemAssistantText;
115
+
116
+ const itemNarrative =
117
+ item.output_available && item.output ? extractNarrativeResult(item.output) : undefined;
118
+ if (itemNarrative && itemNarrative.length > 0) return itemNarrative;
119
+
120
+ if (item.error_message && item.error_message.length > 0) return item.error_message;
121
+ return item.summary;
122
+ }
123
+ }
124
+
125
+ if (details.error_message && details.error_message.length > 0) return details.error_message;
126
+ return details.summary;
127
+ }
128
+
129
+ function resolveModelTaskId(details: TaskToolResultDetails): string {
130
+ if (details.task_id && details.task_id.length > 0) return details.task_id;
131
+ if (details.items && details.items.length === 1) {
132
+ const [item] = details.items;
133
+ if (item && item.id.length > 0) return item.id;
134
+ }
135
+ return "unavailable";
136
+ }
137
+
138
+ function resolveModelTimestamp(details: TaskToolResultDetails): string {
139
+ const directTimestamp =
140
+ toIsoTimestamp(details.ended_at_epoch_ms) ?? toIsoTimestamp(details.updated_at_epoch_ms);
141
+ if (directTimestamp) return directTimestamp;
142
+
143
+ if (details.items && details.items.length === 1) {
144
+ const [item] = details.items;
145
+ if (item) {
146
+ const itemTimestamp =
147
+ toIsoTimestamp(item.ended_at_epoch_ms) ?? toIsoTimestamp(item.updated_at_epoch_ms);
148
+ if (itemTimestamp) return itemTimestamp;
149
+ }
150
+ }
151
+
152
+ return "unavailable";
153
+ }
154
+
155
+ function toModelFacingContent(details: TaskToolResultDetails): string {
156
+ const taskId = resolveModelTaskId(details);
157
+ const timestamp = resolveModelTimestamp(details);
158
+ const result = resolveModelResultText(details);
159
+ return [`task_id: ${taskId}`, `timestamp: ${timestamp}`, "result:", result].join("\n");
160
+ }
161
+
88
162
  export function isOhmDebugEnabled(): boolean {
89
163
  const raw = process.env.OHM_DEBUG?.trim().toLowerCase();
90
164
  if (!raw) return false;
@@ -658,7 +732,7 @@ export function toAgentToolResult(
658
732
  );
659
733
 
660
734
  return {
661
- content: [{ type: "text", text: detailsToText(normalizedDetails, true) }],
735
+ content: [{ type: "text", text: toModelFacingContent(normalizedDetails) }],
662
736
  details: normalizedDetails,
663
737
  };
664
738
  }