@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 +2 -1
- package/package.json +6 -6
- package/src/tools/primary.ts +4 -4
- package/src/tools/task/contracts.ts +2 -0
- package/src/tools/task/defaults.test.ts +64 -0
- package/src/tools/task/defaults.ts +20 -7
- package/src/tools/task/execution/projection.ts +2 -0
- package/src/tools/task/operations.lifecycle.test.ts +16 -5
- package/src/tools/task/render.ts +75 -1
package/README.md
CHANGED
|
@@ -360,7 +360,8 @@ Additional hardening behaviors:
|
|
|
360
360
|
|
|
361
361
|
Persistence details:
|
|
362
362
|
|
|
363
|
-
- default snapshot path: `${
|
|
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.
|
|
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": "
|
|
23
|
-
"@pi-ohm/config": "0.6.4-dev.
|
|
24
|
-
"@pi-ohm/tui": "0.6.4-dev.
|
|
25
|
-
"better-result": "
|
|
26
|
-
"zod": "
|
|
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": "*",
|
package/src/tools/primary.ts
CHANGED
|
@@ -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
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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,
|
|
1341
|
-
assert.match(plainText,
|
|
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
|
-
|
|
1395
|
-
assert.match(
|
|
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
|
-
|
|
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 () => {
|
package/src/tools/task/render.ts
CHANGED
|
@@ -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:
|
|
735
|
+
content: [{ type: "text", text: toModelFacingContent(normalizedDetails) }],
|
|
662
736
|
details: normalizedDetails,
|
|
663
737
|
};
|
|
664
738
|
}
|