@heyhuynhgiabuu/pi-task 0.1.4 → 0.1.6
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 +52 -4
- package/README.md +33 -1
- package/dist/conversation.d.ts +86 -0
- package/dist/conversation.js +238 -0
- package/dist/helpers.d.ts +6 -6
- package/dist/helpers.js +30 -6
- package/dist/index.d.ts +17 -0
- package/dist/index.js +427 -175
- package/dist/session-text.d.ts +2 -2
- package/dist/session-text.js +28 -2
- package/dist/subagent/buildArgv.d.ts +1 -0
- package/dist/subagent/buildArgv.js +1 -1
- package/dist/subagent/waitCompletion.d.ts +1 -0
- package/dist/subagent/waitCompletion.js +27 -20
- package/dist/task-widget.d.ts +21 -0
- package/dist/task-widget.js +122 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ All notable changes to `@heyhuynhgiabuu/pi-task` are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.1.6] — 2026-06-25
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Per-task data is now in flat files at the top of `.pi/artifacts/`.
|
|
12
|
+
No per-task subdirs, no `<task-id>` paths. The pikit canonical
|
|
13
|
+
files (TODO.md, PLAN.md, PROGRESS.md, DECISIONS.md) are flat at the
|
|
14
|
+
same level; pi-task files now sit alongside them.
|
|
15
|
+
- Refined the task TUI widget and background completion rendering:
|
|
16
|
+
foreground/background task stats now use consistent colors, background
|
|
17
|
+
completion summaries use a padded themed result block, completed
|
|
18
|
+
background widgets no longer duplicate the main-pane completion, and
|
|
19
|
+
final tool-call counts now match the live widget count.
|
|
20
|
+
|
|
21
|
+
### Layout
|
|
22
|
+
|
|
23
|
+
- `.pi/artifacts/TASKS.md` — one `### <task-id>` block per task, with
|
|
24
|
+
H4 subsections for `#### Metadata` (JSON) and `#### Result`.
|
|
25
|
+
- `.pi/artifacts/task-sessions.json` — registry mapping
|
|
26
|
+
`conversation_id` to `{ task_id, session_file }`. Renamed from
|
|
27
|
+
the v0.1.5 `task-conversations.json`.
|
|
28
|
+
- The subagent's session is auto-saved by pi at
|
|
29
|
+
`~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task reads
|
|
30
|
+
the last assistant message from there to populate `#### Result`
|
|
31
|
+
in `TASKS.md`. The subagent's final assistant message IS the
|
|
32
|
+
result; no separate result file is required.
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
|
|
36
|
+
- `.pi/artifacts/task-<id>/` per-task subdirs (and the
|
|
37
|
+
`metadata.json` + `SESSION.md` + `sessions/` files inside them).
|
|
38
|
+
All per-task data lives in `TASKS.md` blocks now.
|
|
39
|
+
- `.pi/artifacts/task-conversations.json` — replaced by
|
|
40
|
+
`task-sessions.json`.
|
|
41
|
+
- The `taskArtifactName(taskId)` / `taskIdFromArtifactName(name)`
|
|
42
|
+
helpers and the `getArtifactsDir(piDir)` / `getTaskDir(piDir)` /
|
|
43
|
+
`getTaskRunsDir(piDir)` helpers.
|
|
44
|
+
|
|
45
|
+
### Verified
|
|
46
|
+
|
|
47
|
+
- `npm test` passes
|
|
48
|
+
- `npm run typecheck` passes
|
|
49
|
+
- `npm run build` passes
|
|
50
|
+
- `npm run smoke` passes
|
|
51
|
+
|
|
7
52
|
## [0.1.4] — 2026-06-21
|
|
8
53
|
|
|
9
54
|
### Fixed
|
|
@@ -76,7 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
76
121
|
- `npm view @heyhuynhgiabuu/pi-task@0.1.2 pi` returns
|
|
77
122
|
`{ extensions: [ './dist/index.js' ] }`
|
|
78
123
|
|
|
79
|
-
[0.1.2]: https://github.com/
|
|
124
|
+
[0.1.2]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.2
|
|
80
125
|
|
|
81
126
|
## [0.1.1] — 2025
|
|
82
127
|
|
|
@@ -116,6 +161,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
116
161
|
|
|
117
162
|
See the git history: `git log --oneline -- CHANGELOG.md`.
|
|
118
163
|
|
|
119
|
-
[0.1.1]: https://github.com/
|
|
120
|
-
[
|
|
121
|
-
[
|
|
164
|
+
[0.1.1]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.1
|
|
165
|
+
[0.1.4]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.4
|
|
166
|
+
[0.1.5]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.5
|
|
167
|
+
[0.1.6]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.6
|
|
168
|
+
[Keep a Changelog]: https://keepachangelog.com/
|
|
169
|
+
[Semantic Versioning]: https://semver.org/
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Foreground task:
|
|
|
45
45
|
|
|
46
46
|
Background task:
|
|
47
47
|
|
|
48
|
-
```
|
|
48
|
+
```
|
|
49
49
|
{
|
|
50
50
|
"agent_type": "scout",
|
|
51
51
|
"description": "Research SDK docs",
|
|
@@ -54,6 +54,38 @@ Background task:
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
Durable specialist conversation:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
{
|
|
61
|
+
"agent_type": "scout",
|
|
62
|
+
"conversation_id": "research-ai",
|
|
63
|
+
"description": "Ask research assistant",
|
|
64
|
+
"background": false,
|
|
65
|
+
"prompt": "Continue our prior research thread. What did we conclude about retrieval evaluation?"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`conversation_id` maps to a durable subagent run. Reused across calls
|
|
70
|
+
to keep specialist memory, e.g. a reusable research assistant.
|
|
71
|
+
Use `/task-sessions` to list known durable conversations.
|
|
72
|
+
|
|
73
|
+
Stored files (all flat at the top of `.pi/artifacts/`, no
|
|
74
|
+
per-task subdirs):
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
.pi/artifacts/TASKS.md # one ### <task-id> block per task
|
|
78
|
+
.pi/artifacts/task-sessions.json # conversation_id -> { task_id, session_file }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The subagent's session is auto-saved by pi at
|
|
82
|
+
`~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task reads
|
|
83
|
+
the last assistant message from there to populate
|
|
84
|
+
`#### Result` in `TASKS.md`. The subagent's final message IS
|
|
85
|
+
the result; no separate result file is required.
|
|
86
|
+
|
|
87
|
+
Note: true conversation resume requires the tmux/CLI backend so Pi can reopen the saved subagent session. SDK fallback can run one-shot tasks, but it cannot resume a prior Pi session.
|
|
88
|
+
|
|
57
89
|
## Agent precedence
|
|
58
90
|
|
|
59
91
|
When two agents have the same name, later sources override earlier ones:
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversational subagent helpers.
|
|
3
|
+
*
|
|
4
|
+
* Per-task data lives in `.pi/artifacts/TASKS.md` as `### <task-id>` blocks.
|
|
5
|
+
* A small `task-sessions.json` registry in the same directory maps
|
|
6
|
+
* `conversation_id` to the auto-saved session file path so the
|
|
7
|
+
* subagent can be resumed later.
|
|
8
|
+
*
|
|
9
|
+
* The subagent's session is auto-saved by pi at
|
|
10
|
+
* `~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task does not
|
|
11
|
+
* maintain its own session storage.
|
|
12
|
+
*
|
|
13
|
+
* All artifacts live flat at the top of `.pi/artifacts/`, alongside the
|
|
14
|
+
* pikit canonical files (TODO.md, PLAN.md, PROGRESS.md, DECISIONS.md).
|
|
15
|
+
* No subdirs. No per-task paths.
|
|
16
|
+
*/
|
|
17
|
+
export declare const TASKS_FILE = "TASKS.md";
|
|
18
|
+
export declare const TASK_SESSIONS_REGISTRY_FILE = "task-sessions.json";
|
|
19
|
+
export interface ConversationMetadata {
|
|
20
|
+
conversation_id: string;
|
|
21
|
+
task_id: string;
|
|
22
|
+
agent_type: string;
|
|
23
|
+
session_file: string;
|
|
24
|
+
created_at: string;
|
|
25
|
+
last_used_at: string;
|
|
26
|
+
last_prompt?: string;
|
|
27
|
+
}
|
|
28
|
+
export type TaskSessionsRegistry = Record<string, {
|
|
29
|
+
task_id: string;
|
|
30
|
+
session_file: string;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function getTasksFilePath(piDir: string): string;
|
|
33
|
+
export declare function getTaskSessionsRegistryPath(piDir: string): string;
|
|
34
|
+
export declare function normalizeConversationId(value: unknown): string | undefined;
|
|
35
|
+
export declare function readTaskSessionsRegistry(piDir: string): TaskSessionsRegistry;
|
|
36
|
+
export declare function writeTaskSessionsRegistry(piDir: string, registry: TaskSessionsRegistry): void;
|
|
37
|
+
/**
|
|
38
|
+
* Find a `### <task-id>` block in TASKS.md. Returns the block content
|
|
39
|
+
* (everything between the heading and the next H3 or EOF) plus the
|
|
40
|
+
* status line if present. Returns undefined if no block exists.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readTaskBlock(piDir: string, taskId: string): {
|
|
43
|
+
status: string | null;
|
|
44
|
+
body: string;
|
|
45
|
+
} | undefined;
|
|
46
|
+
export declare function listTaskBlocks(piDir: string): Map<string, {
|
|
47
|
+
status: string | null;
|
|
48
|
+
body: string;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Append or update a `### <task-id>` block in TASKS.md. If the block
|
|
52
|
+
* already exists, its body is replaced. Otherwise, the block is
|
|
53
|
+
* appended at the end of the file.
|
|
54
|
+
*/
|
|
55
|
+
export declare function writeTaskBlock(options: {
|
|
56
|
+
piDir: string;
|
|
57
|
+
taskId: string;
|
|
58
|
+
status: "active" | "done" | "abandoned";
|
|
59
|
+
updated: string;
|
|
60
|
+
body: string;
|
|
61
|
+
}): void;
|
|
62
|
+
export declare function parseMetadataFromBody(body: string | undefined): {
|
|
63
|
+
created_at?: string;
|
|
64
|
+
last_used_at?: string;
|
|
65
|
+
agent_type?: string;
|
|
66
|
+
session_file?: string;
|
|
67
|
+
conversation_id?: string;
|
|
68
|
+
last_prompt?: string;
|
|
69
|
+
} | undefined;
|
|
70
|
+
export interface WriteTaskBlockInput {
|
|
71
|
+
piDir: string;
|
|
72
|
+
taskId: string;
|
|
73
|
+
conversationId: string;
|
|
74
|
+
agentType: string;
|
|
75
|
+
sessionFile: string;
|
|
76
|
+
prompt: string;
|
|
77
|
+
result: string;
|
|
78
|
+
resultLabel?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Persist a completed task: write (or update) the `### <task-id>` block
|
|
82
|
+
* in TASKS.md with metadata and result as H4 subsections. Also updates
|
|
83
|
+
* the task-sessions registry.
|
|
84
|
+
*/
|
|
85
|
+
export declare function writeConversationArtifacts(input: WriteTaskBlockInput): ConversationMetadata;
|
|
86
|
+
export declare function renderConversationSessions(piDir: string): string;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Conversational subagent helpers.
|
|
5
|
+
*
|
|
6
|
+
* Per-task data lives in `.pi/artifacts/TASKS.md` as `### <task-id>` blocks.
|
|
7
|
+
* A small `task-sessions.json` registry in the same directory maps
|
|
8
|
+
* `conversation_id` to the auto-saved session file path so the
|
|
9
|
+
* subagent can be resumed later.
|
|
10
|
+
*
|
|
11
|
+
* The subagent's session is auto-saved by pi at
|
|
12
|
+
* `~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task does not
|
|
13
|
+
* maintain its own session storage.
|
|
14
|
+
*
|
|
15
|
+
* All artifacts live flat at the top of `.pi/artifacts/`, alongside the
|
|
16
|
+
* pikit canonical files (TODO.md, PLAN.md, PROGRESS.md, DECISIONS.md).
|
|
17
|
+
* No subdirs. No per-task paths.
|
|
18
|
+
*/
|
|
19
|
+
export const TASKS_FILE = "TASKS.md";
|
|
20
|
+
export const TASK_SESSIONS_REGISTRY_FILE = "task-sessions.json";
|
|
21
|
+
export function getTasksFilePath(piDir) {
|
|
22
|
+
return join(piDir, "artifacts", TASKS_FILE);
|
|
23
|
+
}
|
|
24
|
+
export function getTaskSessionsRegistryPath(piDir) {
|
|
25
|
+
return join(piDir, "artifacts", TASK_SESSIONS_REGISTRY_FILE);
|
|
26
|
+
}
|
|
27
|
+
export function normalizeConversationId(value) {
|
|
28
|
+
if (typeof value !== "string")
|
|
29
|
+
return undefined;
|
|
30
|
+
const conversationId = value.trim();
|
|
31
|
+
if (!conversationId)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!/^[A-Za-z0-9._-]{1,80}$/.test(conversationId)) {
|
|
34
|
+
throw new Error("conversation_id must be 1-80 chars and contain only letters, numbers, '.', '_' or '-'");
|
|
35
|
+
}
|
|
36
|
+
return conversationId;
|
|
37
|
+
}
|
|
38
|
+
export function readTaskSessionsRegistry(piDir) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(getTaskSessionsRegistryPath(piDir), "utf-8"));
|
|
41
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
const registry = {};
|
|
45
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
46
|
+
if (value &&
|
|
47
|
+
typeof value === "object" &&
|
|
48
|
+
typeof value.task_id === "string" &&
|
|
49
|
+
typeof value.session_file === "string") {
|
|
50
|
+
const v = value;
|
|
51
|
+
registry[key] = { task_id: v.task_id, session_file: v.session_file };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return registry;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function writeTaskSessionsRegistry(piDir, registry) {
|
|
61
|
+
mkdirSync(join(piDir, "artifacts"), { recursive: true });
|
|
62
|
+
writeFileSync(getTaskSessionsRegistryPath(piDir), `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Find a `### <task-id>` block in TASKS.md. Returns the block content
|
|
66
|
+
* (everything between the heading and the next H3 or EOF) plus the
|
|
67
|
+
* status line if present. Returns undefined if no block exists.
|
|
68
|
+
*/
|
|
69
|
+
export function readTaskBlock(piDir, taskId) {
|
|
70
|
+
let content;
|
|
71
|
+
try {
|
|
72
|
+
content = readFileSync(getTasksFilePath(piDir), "utf-8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return parseTaskBlocks(content).get(taskId);
|
|
78
|
+
}
|
|
79
|
+
export function listTaskBlocks(piDir) {
|
|
80
|
+
let content;
|
|
81
|
+
try {
|
|
82
|
+
content = readFileSync(getTasksFilePath(piDir), "utf-8");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return new Map();
|
|
86
|
+
}
|
|
87
|
+
return parseTaskBlocks(content);
|
|
88
|
+
}
|
|
89
|
+
function parseTaskBlocks(content) {
|
|
90
|
+
const blocks = new Map();
|
|
91
|
+
const lines = content.split("\n");
|
|
92
|
+
let currentTaskId = null;
|
|
93
|
+
let currentStatus = null;
|
|
94
|
+
let currentBody = [];
|
|
95
|
+
const flush = () => {
|
|
96
|
+
if (currentTaskId !== null) {
|
|
97
|
+
blocks.set(currentTaskId, {
|
|
98
|
+
status: currentStatus,
|
|
99
|
+
body: currentBody.join("\n"),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
currentTaskId = null;
|
|
103
|
+
currentStatus = null;
|
|
104
|
+
currentBody = [];
|
|
105
|
+
};
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const heading = line.match(/^###\s+(\S+)\s*$/);
|
|
108
|
+
if (heading) {
|
|
109
|
+
flush();
|
|
110
|
+
currentTaskId = heading[1];
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (currentTaskId === null)
|
|
114
|
+
continue;
|
|
115
|
+
const statusMatch = line.match(/^status:\s*(\S+)/);
|
|
116
|
+
if (statusMatch) {
|
|
117
|
+
currentStatus = statusMatch[1].toLowerCase();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
currentBody.push(line);
|
|
121
|
+
}
|
|
122
|
+
flush();
|
|
123
|
+
return blocks;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Append or update a `### <task-id>` block in TASKS.md. If the block
|
|
127
|
+
* already exists, its body is replaced. Otherwise, the block is
|
|
128
|
+
* appended at the end of the file.
|
|
129
|
+
*/
|
|
130
|
+
export function writeTaskBlock(options) {
|
|
131
|
+
const path = getTasksFilePath(options.piDir);
|
|
132
|
+
let content = "";
|
|
133
|
+
try {
|
|
134
|
+
content = readFileSync(path, "utf-8");
|
|
135
|
+
if (!content.endsWith("\n"))
|
|
136
|
+
content += "\n";
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
content = "";
|
|
140
|
+
}
|
|
141
|
+
const heading = `### ${options.taskId}`;
|
|
142
|
+
const statusLine = `status: ${options.status} | updated: ${options.updated}`;
|
|
143
|
+
const block = `${heading}\n${statusLine}\n\n${options.body}\n`;
|
|
144
|
+
const headingRe = new RegExp(`^### ${escapeRegExp(options.taskId)}\\s*$`, "m");
|
|
145
|
+
const match = content.match(headingRe);
|
|
146
|
+
if (match && match.index !== undefined) {
|
|
147
|
+
const start = match.index;
|
|
148
|
+
const after = content.slice(start);
|
|
149
|
+
const nextHeading = after.search(/^###\s+\S+/m);
|
|
150
|
+
const end = nextHeading > 0 ? start + nextHeading : content.length;
|
|
151
|
+
content = content.slice(0, start) + block + content.slice(end);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
if (content.length > 0 && !content.endsWith("\n\n")) {
|
|
155
|
+
content += "\n";
|
|
156
|
+
}
|
|
157
|
+
content += block;
|
|
158
|
+
}
|
|
159
|
+
mkdirSync(join(options.piDir, "artifacts"), { recursive: true });
|
|
160
|
+
writeFileSync(path, content, "utf-8");
|
|
161
|
+
}
|
|
162
|
+
function escapeRegExp(value) {
|
|
163
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
164
|
+
}
|
|
165
|
+
export function parseMetadataFromBody(body) {
|
|
166
|
+
if (!body)
|
|
167
|
+
return undefined;
|
|
168
|
+
const match = body.match(/```json\n([\s\S]*?)\n```/);
|
|
169
|
+
if (!match)
|
|
170
|
+
return undefined;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(match[1]);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Persist a completed task: write (or update) the `### <task-id>` block
|
|
180
|
+
* in TASKS.md with metadata and result as H4 subsections. Also updates
|
|
181
|
+
* the task-sessions registry.
|
|
182
|
+
*/
|
|
183
|
+
export function writeConversationArtifacts(input) {
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const existing = readTaskBlock(input.piDir, input.taskId);
|
|
186
|
+
const previous = parseMetadataFromBody(existing?.body);
|
|
187
|
+
const metadata = {
|
|
188
|
+
conversation_id: input.conversationId,
|
|
189
|
+
task_id: input.taskId,
|
|
190
|
+
agent_type: input.agentType,
|
|
191
|
+
session_file: input.sessionFile,
|
|
192
|
+
created_at: previous?.created_at ?? now,
|
|
193
|
+
last_used_at: now,
|
|
194
|
+
last_prompt: input.prompt,
|
|
195
|
+
};
|
|
196
|
+
const body = [
|
|
197
|
+
"#### Metadata",
|
|
198
|
+
"",
|
|
199
|
+
"```json",
|
|
200
|
+
JSON.stringify(metadata, null, 2),
|
|
201
|
+
"```",
|
|
202
|
+
"",
|
|
203
|
+
"#### Result",
|
|
204
|
+
"",
|
|
205
|
+
input.result.trim(),
|
|
206
|
+
"",
|
|
207
|
+
].join("\n");
|
|
208
|
+
writeTaskBlock({
|
|
209
|
+
piDir: input.piDir,
|
|
210
|
+
taskId: input.taskId,
|
|
211
|
+
status: "done",
|
|
212
|
+
updated: now,
|
|
213
|
+
body,
|
|
214
|
+
});
|
|
215
|
+
const registry = readTaskSessionsRegistry(input.piDir);
|
|
216
|
+
registry[input.conversationId] = {
|
|
217
|
+
task_id: input.taskId,
|
|
218
|
+
session_file: input.sessionFile,
|
|
219
|
+
};
|
|
220
|
+
writeTaskSessionsRegistry(input.piDir, registry);
|
|
221
|
+
return metadata;
|
|
222
|
+
}
|
|
223
|
+
export function renderConversationSessions(piDir) {
|
|
224
|
+
const blocks = listTaskBlocks(piDir);
|
|
225
|
+
if (blocks.size === 0) {
|
|
226
|
+
return 'No durable task conversations found. Start one with task({ conversation_id: "research-ai", ... }).';
|
|
227
|
+
}
|
|
228
|
+
const entries = Array.from(blocks.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
229
|
+
const lines = ["Durable task conversations:"];
|
|
230
|
+
for (const [taskId, block] of entries) {
|
|
231
|
+
const metadata = parseMetadataFromBody(block.body);
|
|
232
|
+
const agent = metadata?.agent_type ?? "unknown";
|
|
233
|
+
const last = metadata?.last_used_at ?? "unknown";
|
|
234
|
+
const conv = metadata?.conversation_id ?? "(no conversation_id)";
|
|
235
|
+
lines.push(`${conv} -> ${taskId} — ${agent}, last used ${last}`);
|
|
236
|
+
}
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
package/dist/helpers.d.ts
CHANGED
|
@@ -72,12 +72,12 @@ export declare function formatAgentList(agents: AgentConfig[]): string;
|
|
|
72
72
|
* Build pi CLI arguments for spawning or resuming a sub-agent session.
|
|
73
73
|
*
|
|
74
74
|
* - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
export declare function buildPiArgs(agent: AgentConfig, sessionName: string, sessionDir: string, promptContent: string, resume?: boolean, parentToolNames?: string[]): string[];
|
|
75
|
+
* - Resume: pass `resume=true` and optionally `resumeSessionRef` —
|
|
76
|
+
* `--session <ref>` is included so pi continues an existing session.
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildPiArgs(agent: AgentConfig, sessionName: string, sessionDir: string, promptContent: string, resume?: boolean, parentToolNames?: string[], resumeSessionRef?: string): string[];
|
|
79
79
|
/** Count tool uses and turns from pi JSONL session files. */
|
|
80
|
-
export declare function countToolUses(sessionDir: string): {
|
|
80
|
+
export declare function countToolUses(sessionDir: string, sessionName?: string): {
|
|
81
81
|
toolUses: number;
|
|
82
82
|
turns: number;
|
|
83
83
|
};
|
|
@@ -94,7 +94,7 @@ export declare function summarizeArgs(toolName: string, args: unknown): string;
|
|
|
94
94
|
* Returns total counts plus the last `limit` records in chronological order.
|
|
95
95
|
* Safe against malformed lines and missing fields.
|
|
96
96
|
*/
|
|
97
|
-
export declare function readRecentToolCalls(sessionDir: string, limit?: number): {
|
|
97
|
+
export declare function readRecentToolCalls(sessionDir: string, limit?: number, sessionName?: string): {
|
|
98
98
|
toolUses: number;
|
|
99
99
|
turns: number;
|
|
100
100
|
recent: ToolCallRecord[];
|
package/dist/helpers.js
CHANGED
|
@@ -256,22 +256,42 @@ export function formatAgentList(agents) {
|
|
|
256
256
|
* Build pi CLI arguments for spawning or resuming a sub-agent session.
|
|
257
257
|
*
|
|
258
258
|
* - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
export function buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames) {
|
|
259
|
+
* - Resume: pass `resume=true` and optionally `resumeSessionRef` —
|
|
260
|
+
* `--session <ref>` is included so pi continues an existing session.
|
|
261
|
+
*/
|
|
262
|
+
export function buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames, resumeSessionRef) {
|
|
263
263
|
return buildPiArgv({
|
|
264
264
|
agent,
|
|
265
265
|
sessionName,
|
|
266
266
|
sessionDir,
|
|
267
267
|
promptContent,
|
|
268
268
|
resume,
|
|
269
|
+
resumeSessionRef,
|
|
269
270
|
parentToolNames,
|
|
270
271
|
});
|
|
271
272
|
}
|
|
272
273
|
// ─── JSONL Session Helpers ───────────────────────────────────────────────────
|
|
274
|
+
function matchesJsonlSessionName(content, sessionName) {
|
|
275
|
+
if (!sessionName)
|
|
276
|
+
return true;
|
|
277
|
+
for (const rawLine of content.split("\n")) {
|
|
278
|
+
const line = rawLine.trim();
|
|
279
|
+
if (!line)
|
|
280
|
+
continue;
|
|
281
|
+
try {
|
|
282
|
+
const entry = JSON.parse(line);
|
|
283
|
+
if (entry.type === "session_info") {
|
|
284
|
+
return (entry.name ?? entry.session_info?.name) === sessionName;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Skip malformed lines
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
273
293
|
/** Count tool uses and turns from pi JSONL session files. */
|
|
274
|
-
export function countToolUses(sessionDir) {
|
|
294
|
+
export function countToolUses(sessionDir, sessionName) {
|
|
275
295
|
let toolUses = 0;
|
|
276
296
|
let turns = 0;
|
|
277
297
|
try {
|
|
@@ -280,6 +300,8 @@ export function countToolUses(sessionDir) {
|
|
|
280
300
|
const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
|
|
281
301
|
for (const file of files) {
|
|
282
302
|
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
303
|
+
if (!matchesJsonlSessionName(content, sessionName))
|
|
304
|
+
continue;
|
|
283
305
|
for (const rawLine of content.split("\n")) {
|
|
284
306
|
const line = rawLine.trim();
|
|
285
307
|
if (!line)
|
|
@@ -368,7 +390,7 @@ export function summarizeArgs(toolName, args) {
|
|
|
368
390
|
* Returns total counts plus the last `limit` records in chronological order.
|
|
369
391
|
* Safe against malformed lines and missing fields.
|
|
370
392
|
*/
|
|
371
|
-
export function readRecentToolCalls(sessionDir, limit = 12) {
|
|
393
|
+
export function readRecentToolCalls(sessionDir, limit = 12, sessionName) {
|
|
372
394
|
let toolUses = 0;
|
|
373
395
|
let turns = 0;
|
|
374
396
|
const calls = [];
|
|
@@ -379,6 +401,8 @@ export function readRecentToolCalls(sessionDir, limit = 12) {
|
|
|
379
401
|
const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
|
|
380
402
|
for (const file of files) {
|
|
381
403
|
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
404
|
+
if (!matchesJsonlSessionName(content, sessionName))
|
|
405
|
+
continue;
|
|
382
406
|
for (const rawLine of content.split("\n")) {
|
|
383
407
|
const line = rawLine.trim();
|
|
384
408
|
if (!line)
|
package/dist/index.d.ts
CHANGED
|
@@ -15,4 +15,21 @@
|
|
|
15
15
|
* detection, 30-minute timeout.
|
|
16
16
|
*/
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
export /** Details attached to tool result for rendering. */ interface TaskDetails {
|
|
19
|
+
task_id: string;
|
|
20
|
+
agent_type: string;
|
|
21
|
+
description: string;
|
|
22
|
+
conversation_id?: string;
|
|
23
|
+
phase: "done" | "timeout" | "aborted" | "failed";
|
|
24
|
+
status?: string;
|
|
25
|
+
summary?: string;
|
|
26
|
+
findings?: string;
|
|
27
|
+
evidence?: string;
|
|
28
|
+
confidence?: string;
|
|
29
|
+
duration_ms?: number;
|
|
30
|
+
turn_count?: number;
|
|
31
|
+
tool_uses?: number;
|
|
32
|
+
background?: boolean;
|
|
33
|
+
tmux_session?: string;
|
|
34
|
+
}
|
|
18
35
|
export default function (pi: ExtensionAPI): void;
|