pi-squire 0.9.2 → 0.9.4
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 +36 -1
- package/README.md +16 -0
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/render.ts +197 -0
- package/src/results.ts +127 -42
- package/src/tracking.ts +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,11 +6,44 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.4] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The `delegate` tool's row is drawn in the same style as the result message:
|
|
14
|
+
a call line naming the agent and label, then a ◐ header with the delegate's
|
|
15
|
+
name and model, a task preview and any skipped fallback models; expanded,
|
|
16
|
+
the whole task and the delegation id, window, thinking and auto-exit. The
|
|
17
|
+
text the model reads is unchanged.
|
|
18
|
+
|
|
19
|
+
## [0.9.3] - 2026-10-23
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- A collapsed delegate result previews its first 3 non-blank lines, in a
|
|
24
|
+
brighter color, with heading marks, backticks and bold stripped. Expanded,
|
|
25
|
+
the result comes right after the task under a `Result` label, and the ids
|
|
26
|
+
and files move to the end with the home directory shown as `~`. The
|
|
27
|
+
renderer tests change with it.
|
|
28
|
+
|
|
9
29
|
## [0.9.2] - 2026-10-22
|
|
10
30
|
|
|
11
31
|
### Changed
|
|
12
32
|
|
|
13
33
|
- A delegate's footer shows its agent name in the theme's accent color.
|
|
34
|
+
- Delegate results and closed-without-a-result messages are drawn with a
|
|
35
|
+
header (status icon, delegate name, model) and, collapsed, the first 3
|
|
36
|
+
lines of the result; expanded, the ids and files, the quoted task and the
|
|
37
|
+
result as Markdown. The text the parent model reads is unchanged apart from
|
|
38
|
+
the model. Messages from before this change show as plain text. The
|
|
39
|
+
renderer tests change with it.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- A delegate that finished on a different model than it launched on (a
|
|
44
|
+
fallback, or `/model` in its window) is now shown as `launched → finished`
|
|
45
|
+
in the result, the closed-without-a-result message and `delegation_status`,
|
|
46
|
+
read from the delegate's session file. It used to name the launch model.
|
|
14
47
|
|
|
15
48
|
## [0.9.1] - 2026-10-22
|
|
16
49
|
|
|
@@ -331,7 +364,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
331
364
|
|
|
332
365
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
333
366
|
|
|
334
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.9.
|
|
367
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.9.4...HEAD
|
|
368
|
+
[0.9.4]: https://github.com/gvanderclay/pi-squire/compare/v0.9.3...v0.9.4
|
|
369
|
+
[0.9.3]: https://github.com/gvanderclay/pi-squire/compare/v0.9.2...v0.9.3
|
|
335
370
|
[0.9.2]: https://github.com/gvanderclay/pi-squire/compare/v0.9.1...v0.9.2
|
|
336
371
|
[0.9.1]: https://github.com/gvanderclay/pi-squire/compare/v0.9.0...v0.9.1
|
|
337
372
|
[0.9.0]: https://github.com/gvanderclay/pi-squire/compare/v0.8.1...v0.9.0
|
package/README.md
CHANGED
|
@@ -443,6 +443,22 @@ Delegate session: <session file or id>
|
|
|
443
443
|
- The header carries the agent, the model, the delegation id and the request
|
|
444
444
|
id; the status is the envelope's (`done`, `failed` or
|
|
445
445
|
`stopped`).
|
|
446
|
+
- The model is the one the delegation was launched on. When the delegate's
|
|
447
|
+
session file shows it finished on another (a fallback after a usage limit,
|
|
448
|
+
or `/model` in its window), it reads `launched → finished`, for example
|
|
449
|
+
`(anthropic/claude-haiku-5-5 → openai/gpt-6-luna)`. `delegation_status`
|
|
450
|
+
reports the same.
|
|
451
|
+
- That text is what the parent model reads. The user sees it drawn instead:
|
|
452
|
+
collapsed, a header with a status icon (✓ done, ✗ failed, ■ stopped), the
|
|
453
|
+
delegate's name and the model, then the first 3 non-blank lines of the
|
|
454
|
+
result with heading marks, backticks and bold stripped; expanded (Ctrl+O),
|
|
455
|
+
the quoted task, the result as Markdown, then the ids and files, with the
|
|
456
|
+
home directory shown as `~`.
|
|
457
|
+
- The `delegate` tool's own row is drawn the same way, so a launch and its
|
|
458
|
+
result match: a call line naming the agent and label, then a header with
|
|
459
|
+
a ◐ started icon, the delegate's name and the model it started on, a preview of the task and any skipped fallback models;
|
|
460
|
+
expanded, the whole task, then the delegation id, window, thinking and
|
|
461
|
+
auto-exit. A refused call keeps its plain error text. The closed-without-a-result message gets the same header with ⊘.
|
|
446
462
|
- Stopping a delegate's run (Esc) sends no result: the provider holds the
|
|
447
463
|
task, so the delegation stays running while the user steers it. The next
|
|
448
464
|
run that completes sends the result, `done`, and its body opens with a note
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { Type } from "typebox";
|
|
|
49
49
|
import { type Agent, isThinking, type Roster, readRoster, THINKING_LEVELS } from "./agents.ts";
|
|
50
50
|
import { AGENT_ENV, AUTO_EXIT_ENV, FALLBACK_ENV, registerChild } from "./child.ts";
|
|
51
51
|
import { activeMark, clearMarks, isFree, readMarks } from "./limits.ts";
|
|
52
|
+
import { renderDelegateCall, renderDelegateResult } from "./render.ts";
|
|
52
53
|
import { createResults, delegateName } from "./results.ts";
|
|
53
54
|
import { createTmuxClient, type TmuxClient } from "./tmux.ts";
|
|
54
55
|
import { createTracking, toolResult } from "./tracking.ts";
|
|
@@ -596,6 +597,8 @@ export default function delegate(
|
|
|
596
597
|
),
|
|
597
598
|
}),
|
|
598
599
|
outputSchema: DelegateSchema,
|
|
600
|
+
renderCall: renderDelegateCall,
|
|
601
|
+
renderResult: renderDelegateResult,
|
|
599
602
|
// Opens a new window each call and removes nothing. Open world: the delegate calls a remote model and runs its agent's tools unattended.
|
|
600
603
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
601
604
|
async execute(
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// How delegations look in the parent: the `delegate` tool's row and the
|
|
2
|
+
// result or gone message that arrives later share one header and preview
|
|
3
|
+
// style. Both are drawn from structured details, while their text stays what
|
|
4
|
+
// the parent model reads.
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getMarkdownTheme,
|
|
9
|
+
keyText,
|
|
10
|
+
type MessageRenderer,
|
|
11
|
+
type MessageRenderOptions,
|
|
12
|
+
type Theme,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { Box, type Component, Markdown, Spacer, Text, TruncatedText } from "@earendil-works/pi-tui";
|
|
15
|
+
import type { Static } from "typebox";
|
|
16
|
+
|
|
17
|
+
import type { DelegateSchema } from "./index.ts";
|
|
18
|
+
import type { MessageDetails } from "./results.ts";
|
|
19
|
+
|
|
20
|
+
/** The launch model, or `launch → finished` when the delegate finished on another one. */
|
|
21
|
+
export function modelLabel(model: string, finished: string | undefined): string {
|
|
22
|
+
return finished === undefined || finished === model ? model : `${model} → ${finished}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Lines of the body the collapsed view previews. */
|
|
26
|
+
const PREVIEW_LINES = 3;
|
|
27
|
+
|
|
28
|
+
/** The icon and theme color for each status; an unknown status gets a neutral icon. */
|
|
29
|
+
const STATUS_STYLE: Record<string, { icon: string; color: "accent" | "success" | "error" | "warning" | "muted" }> = {
|
|
30
|
+
started: { icon: "◐", color: "accent" },
|
|
31
|
+
done: { icon: "✓", color: "success" },
|
|
32
|
+
failed: { icon: "✗", color: "error" },
|
|
33
|
+
stopped: { icon: "■", color: "warning" },
|
|
34
|
+
gone: { icon: "⊘", color: "warning" },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The header line: status icon, the delegate's name and the model it ran on. */
|
|
38
|
+
function header(details: MessageDetails, theme: Theme): string {
|
|
39
|
+
const status = details.kind === "gone" ? "gone" : details.status;
|
|
40
|
+
const known = Object.hasOwn(STATUS_STYLE, status);
|
|
41
|
+
const style = known ? STATUS_STYLE[status] : { icon: "•", color: "muted" as const };
|
|
42
|
+
let trailer = "";
|
|
43
|
+
if (details.kind === "gone") trailer = " closed without a result";
|
|
44
|
+
else if (!known) trailer = ` ${details.status}`;
|
|
45
|
+
return headerLine(theme, style, details.name, modelLabel(details.model, details.finishedModel), trailer);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function headerLine(
|
|
49
|
+
theme: Theme,
|
|
50
|
+
style: { icon: string; color: "accent" | "success" | "error" | "warning" | "muted" },
|
|
51
|
+
name: string,
|
|
52
|
+
model: string,
|
|
53
|
+
trailer: string,
|
|
54
|
+
): string {
|
|
55
|
+
return `${theme.fg(style.color, style.icon)} ${theme.fg("accent", name)} ${theme.fg("muted", model)}${
|
|
56
|
+
trailer === "" ? "" : theme.fg(style.color, trailer)
|
|
57
|
+
}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function expandHint(theme: Theme): Text {
|
|
61
|
+
return new Text(`${theme.fg("dim", keyText("app.tools.expand"))}${theme.fg("muted", " to expand")}`, 0, 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `path` with the home directory shown as `~`. */
|
|
65
|
+
function tilde(path: string): string {
|
|
66
|
+
const home = homedir();
|
|
67
|
+
return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function metadata(details: MessageDetails): string[] {
|
|
71
|
+
const lines = [`delegation ${details.id}`, `request ${details.requestId}`];
|
|
72
|
+
if (details.kind === "result") lines.push(`envelope ${tilde(details.envelopePath)}`);
|
|
73
|
+
else lines.push(`window ${details.windowName}`);
|
|
74
|
+
lines.push(
|
|
75
|
+
`session ${details.sessionPath === undefined ? `${details.id} (no session file found)` : tilde(details.sessionPath)}`,
|
|
76
|
+
);
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The first non-blank lines of a body, with heading marks, backticks and bold
|
|
82
|
+
* markers stripped. Plain text, not Markdown, because a cut-off fragment can
|
|
83
|
+
* break mid-list or mid-code-block.
|
|
84
|
+
*/
|
|
85
|
+
function preview(body: string): string[] {
|
|
86
|
+
return body
|
|
87
|
+
.split("\n")
|
|
88
|
+
.filter((line) => line.trim() !== "")
|
|
89
|
+
.slice(0, PREVIEW_LINES)
|
|
90
|
+
.map((line) =>
|
|
91
|
+
line
|
|
92
|
+
.replace(/^#+\s+/, "")
|
|
93
|
+
.replaceAll("`", "")
|
|
94
|
+
.replaceAll("**", ""),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A delegate message drawn from its details. Collapsed: a header, a short
|
|
100
|
+
* preview of the result body and an expand hint. Expanded: the header, the
|
|
101
|
+
* quoted task, the body as Markdown, then the ids and files. Ctrl+O toggles it
|
|
102
|
+
* with tool output; the model always gets the full content.
|
|
103
|
+
*/
|
|
104
|
+
export function renderMessage(
|
|
105
|
+
message: Parameters<MessageRenderer>[0],
|
|
106
|
+
{ expanded, outputPad }: MessageRenderOptions,
|
|
107
|
+
theme: Theme,
|
|
108
|
+
): Component | undefined {
|
|
109
|
+
const details = message.details as MessageDetails | undefined;
|
|
110
|
+
if (details?.kind !== "result" && details?.kind !== "gone") return undefined;
|
|
111
|
+
const box = new Box(outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
112
|
+
if (!expanded) {
|
|
113
|
+
box.addChild(new TruncatedText(header(details, theme)));
|
|
114
|
+
if (details.kind === "result") {
|
|
115
|
+
for (const line of preview(details.body)) box.addChild(new TruncatedText(theme.fg("muted", line)));
|
|
116
|
+
}
|
|
117
|
+
box.addChild(expandHint(theme));
|
|
118
|
+
return box;
|
|
119
|
+
}
|
|
120
|
+
box.addChild(new Text(header(details, theme), 0, 0));
|
|
121
|
+
const dim = metadata(details);
|
|
122
|
+
if (details.kind === "gone") dim.push("A reply that arrives later is still delivered.");
|
|
123
|
+
else if (details.limit !== undefined) dim.push(details.limit);
|
|
124
|
+
const metadataText = new Text(dim.map((line) => theme.fg("dim", line)).join("\n"), 0, 0);
|
|
125
|
+
if (details.kind === "gone") {
|
|
126
|
+
box.addChild(metadataText);
|
|
127
|
+
return box;
|
|
128
|
+
}
|
|
129
|
+
const { task } = details;
|
|
130
|
+
const quote =
|
|
131
|
+
"note" in task
|
|
132
|
+
? theme.fg("muted", task.note)
|
|
133
|
+
: `${theme.fg("muted", `Task (${tilde(task.path)}):`)}\n${task.text
|
|
134
|
+
.split("\n")
|
|
135
|
+
.map((line) => theme.fg("muted", `> ${line}`))
|
|
136
|
+
.join("\n")}`;
|
|
137
|
+
box.addChild(new Text(quote, 0, 0));
|
|
138
|
+
box.addChild(new Spacer(1));
|
|
139
|
+
box.addChild(new Text(theme.fg("dim", "Result"), 0, 0));
|
|
140
|
+
box.addChild(new Markdown(details.body, 0, 0, getMarkdownTheme()));
|
|
141
|
+
box.addChild(new Spacer(1));
|
|
142
|
+
box.addChild(metadataText);
|
|
143
|
+
return box;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The `delegate` tool's call line: the tool, the agent and any label. Arguments may still be streaming. */
|
|
147
|
+
export function renderDelegateCall(args: { agent?: unknown; label?: unknown }, theme: Theme): Component {
|
|
148
|
+
let line = theme.bold("delegate");
|
|
149
|
+
if (typeof args.agent === "string") line += ` ${theme.fg("accent", args.agent)}`;
|
|
150
|
+
if (typeof args.label === "string" && args.label !== "" && args.label !== "null")
|
|
151
|
+
line += ` ${theme.fg("muted", `· ${args.label}`)}`;
|
|
152
|
+
return new Text(line, 0, 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The `delegate` tool's result, in the result message's style: a started
|
|
157
|
+
* header, then a task preview and the hint, or, expanded, the whole task and
|
|
158
|
+
* the ids. A refused or blocked call has no details and keeps its plain text.
|
|
159
|
+
*/
|
|
160
|
+
export function renderDelegateResult(
|
|
161
|
+
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
162
|
+
{ expanded }: { expanded: boolean },
|
|
163
|
+
theme: Theme,
|
|
164
|
+
context: { args: { task?: unknown }; isError: boolean },
|
|
165
|
+
): Component {
|
|
166
|
+
const details = result.details as Partial<Static<typeof DelegateSchema>> | undefined;
|
|
167
|
+
if (context.isError || details?.outcome !== "started") {
|
|
168
|
+
const text = result.content.map((part) => part.text ?? "").join("\n");
|
|
169
|
+
return new Text(context.isError ? theme.fg("error", text) : text, 0, 0);
|
|
170
|
+
}
|
|
171
|
+
const started = details as Static<typeof DelegateSchema>;
|
|
172
|
+
const task = typeof context.args.task === "string" ? context.args.task : "";
|
|
173
|
+
const box = new Box(0, 0);
|
|
174
|
+
box.addChild(new TruncatedText(headerLine(theme, STATUS_STYLE.started, started.windowName, started.model, "")));
|
|
175
|
+
const skipped = started.skipped.map(({ model, reason }) => theme.fg("warning", `skipped ${model}: ${reason}`));
|
|
176
|
+
if (!expanded) {
|
|
177
|
+
for (const line of preview(task)) box.addChild(new TruncatedText(theme.fg("muted", line)));
|
|
178
|
+
for (const line of skipped) box.addChild(new TruncatedText(line));
|
|
179
|
+
box.addChild(expandHint(theme));
|
|
180
|
+
return box;
|
|
181
|
+
}
|
|
182
|
+
const quote = task
|
|
183
|
+
.split("\n")
|
|
184
|
+
.map((line) => theme.fg("muted", `> ${line}`))
|
|
185
|
+
.join("\n");
|
|
186
|
+
box.addChild(new Text(quote, 0, 0));
|
|
187
|
+
if (skipped.length + started.notices.length > 0)
|
|
188
|
+
box.addChild(new Text([...skipped, ...started.notices.map((line) => theme.fg("dim", line))].join("\n"), 0, 0));
|
|
189
|
+
box.addChild(new Spacer(1));
|
|
190
|
+
const ids = [
|
|
191
|
+
`delegation ${started.id}`,
|
|
192
|
+
`window ${started.windowName} (${started.windowId})`,
|
|
193
|
+
`thinking ${started.thinking}, auto-exit ${started.autoExit ? "on" : "off"}`,
|
|
194
|
+
];
|
|
195
|
+
box.addChild(new Text(ids.map((line) => theme.fg("dim", line)).join("\n"), 0, 0));
|
|
196
|
+
return box;
|
|
197
|
+
}
|
package/src/results.ts
CHANGED
|
@@ -4,18 +4,13 @@
|
|
|
4
4
|
// the message over, shows the parent one delegate result and records the
|
|
5
5
|
// result. The records are the one source of truth for a delegation's state,
|
|
6
6
|
// for the footer, `delegation_status` and `delegation_close`.
|
|
7
|
-
import { readdirSync } from "node:fs";
|
|
7
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
type ExtensionAPI,
|
|
12
|
-
type ExtensionContext,
|
|
13
|
-
keyText,
|
|
14
|
-
type MessageRenderer,
|
|
15
|
-
} from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { Box, Text } from "@earendil-works/pi-tui";
|
|
10
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
11
|
|
|
18
12
|
import { readMarks } from "./limits.ts";
|
|
13
|
+
import { modelLabel, renderMessage } from "./render.ts";
|
|
19
14
|
|
|
20
15
|
/** The session entry type both delegation records and results use. */
|
|
21
16
|
export const CUSTOM_TYPE = "delegate";
|
|
@@ -70,8 +65,41 @@ export type Result = {
|
|
|
70
65
|
envelopePath: string;
|
|
71
66
|
/** The delegate's session file, when it could be found. */
|
|
72
67
|
sessionPath?: string;
|
|
68
|
+
/** The `provider/model` of the delegate's last assistant message, when its session file holds one. */
|
|
69
|
+
finishedModel?: string;
|
|
73
70
|
};
|
|
74
71
|
|
|
72
|
+
/** What both delegate messages carry for their view, beside the text the model reads. */
|
|
73
|
+
type CommonDetails = {
|
|
74
|
+
name: string;
|
|
75
|
+
agent: string;
|
|
76
|
+
/** The model the delegation was launched on. */
|
|
77
|
+
model: string;
|
|
78
|
+
finishedModel?: string;
|
|
79
|
+
id: string;
|
|
80
|
+
requestId: string;
|
|
81
|
+
sessionPath?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** The task as the result quotes it: its text and copy path, or why there is none. */
|
|
85
|
+
export type TaskQuote = { text: string; path: string } | { note: string };
|
|
86
|
+
|
|
87
|
+
/** The details of a delegate message; the renderer draws from these. */
|
|
88
|
+
export type MessageDetails =
|
|
89
|
+
| (CommonDetails & {
|
|
90
|
+
kind: "result";
|
|
91
|
+
/** The envelope's status: `done`, `failed` or `stopped`. */
|
|
92
|
+
status: string;
|
|
93
|
+
envelopePath: string;
|
|
94
|
+
limit?: string;
|
|
95
|
+
task: TaskQuote;
|
|
96
|
+
/** The reply body, capped. */
|
|
97
|
+
body: string;
|
|
98
|
+
})
|
|
99
|
+
| (CommonDetails & { kind: "gone"; windowName: string });
|
|
100
|
+
|
|
101
|
+
type ResultDetails = Extract<MessageDetails, { kind: "result" }>;
|
|
102
|
+
|
|
75
103
|
/** One delegation as it stands: what was recorded at its start, plus what came after. */
|
|
76
104
|
export type Recorded = Delegation & {
|
|
77
105
|
/** Set when a result arrived. */
|
|
@@ -165,6 +193,34 @@ export function findSession(dir: string | undefined, id: string): string | undef
|
|
|
165
193
|
return name === undefined ? undefined : join(dir, name);
|
|
166
194
|
}
|
|
167
195
|
|
|
196
|
+
/**
|
|
197
|
+
* The `provider/model` of the last assistant message in a delegate's session
|
|
198
|
+
* file: the model that actually answered, after any fallback or `/model`.
|
|
199
|
+
* Undefined when the file is missing or holds no assistant message.
|
|
200
|
+
*/
|
|
201
|
+
export function finishedModel(sessionPath: string | undefined): string | undefined {
|
|
202
|
+
if (sessionPath === undefined) return undefined;
|
|
203
|
+
let lines: string[];
|
|
204
|
+
try {
|
|
205
|
+
lines = readFileSync(sessionPath, "utf8").split("\n");
|
|
206
|
+
} catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
210
|
+
let entry: { type?: unknown; message?: { role?: unknown; provider?: unknown; model?: unknown } };
|
|
211
|
+
try {
|
|
212
|
+
entry = JSON.parse(lines[i]);
|
|
213
|
+
} catch {
|
|
214
|
+
continue; // a line still being written, or a blank one
|
|
215
|
+
}
|
|
216
|
+
const message = entry?.message;
|
|
217
|
+
if (entry?.type !== "message" || message?.role !== "assistant") continue;
|
|
218
|
+
if (typeof message.provider === "string" && typeof message.model === "string")
|
|
219
|
+
return `${message.provider}/${message.model}`;
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
168
224
|
/** The line naming a usage-limit mark this delegation recorded, if any; a `done` delegation finished on a fallback. */
|
|
169
225
|
function limitLine(id: string, status: string): string | undefined {
|
|
170
226
|
const now = Date.now();
|
|
@@ -177,54 +233,67 @@ function limitLine(id: string, status: string): string | undefined {
|
|
|
177
233
|
}${status === "done" ? " It finished on a fallback model; its session names which." : ""}`;
|
|
178
234
|
}
|
|
179
235
|
|
|
180
|
-
/** The
|
|
181
|
-
function
|
|
236
|
+
/** The details of a settled delegation's result message. */
|
|
237
|
+
function resultDetails(
|
|
238
|
+
delegation: Delegation,
|
|
239
|
+
payload: Inbound,
|
|
240
|
+
session: string | undefined,
|
|
241
|
+
finished: string | undefined,
|
|
242
|
+
): ResultDetails {
|
|
182
243
|
const reply = payload.envelope;
|
|
183
244
|
const copy = payload.requests.find((request) => request.envelope?.id === delegation.requestId);
|
|
184
|
-
let
|
|
245
|
+
let task: TaskQuote;
|
|
185
246
|
if (copy === undefined) {
|
|
186
|
-
|
|
247
|
+
task = { note: `[delegate] The task ${delegation.requestId} has no copy in sent/; only its id is known.` };
|
|
187
248
|
} else {
|
|
188
249
|
const cut = cap(copy.envelope.body, TASK_CAP);
|
|
189
|
-
const
|
|
250
|
+
const text =
|
|
190
251
|
cut === undefined ? copy.envelope.body : `${cut}\n[delegate] Task cut at 2 KiB; the full copy is ${copy.path}`;
|
|
191
|
-
|
|
192
|
-
.split("\n")
|
|
193
|
-
.map((line) => `> ${line}`)
|
|
194
|
-
.join("\n")}`;
|
|
252
|
+
task = { text, path: copy.path };
|
|
195
253
|
}
|
|
196
254
|
const cut = cap(reply.body, BODY_CAP);
|
|
197
|
-
const
|
|
255
|
+
const body =
|
|
198
256
|
cut === undefined ? reply.body : `${cut}\n[delegate] Body cut at 32 KiB; the full envelope is ${payload.path}`;
|
|
199
257
|
const limit = reply.status === "stopped" ? undefined : limitLine(delegation.id, reply.status);
|
|
258
|
+
return {
|
|
259
|
+
kind: "result",
|
|
260
|
+
status: reply.status,
|
|
261
|
+
name: recordedName(delegation),
|
|
262
|
+
agent: delegation.agent,
|
|
263
|
+
model: delegation.model,
|
|
264
|
+
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
265
|
+
id: delegation.id,
|
|
266
|
+
requestId: delegation.requestId,
|
|
267
|
+
envelopePath: payload.path,
|
|
268
|
+
...(session === undefined ? {} : { sessionPath: session }),
|
|
269
|
+
...(limit === undefined ? {} : { limit }),
|
|
270
|
+
task,
|
|
271
|
+
body,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The one message the parent model reads for a settled delegation. */
|
|
276
|
+
function resultText(details: ResultDetails): string {
|
|
277
|
+
const { task } = details;
|
|
278
|
+
const quote =
|
|
279
|
+
"note" in task
|
|
280
|
+
? task.note
|
|
281
|
+
: `Task, quoted from ${task.path}:\n${task.text
|
|
282
|
+
.split("\n")
|
|
283
|
+
.map((line) => `> ${line}`)
|
|
284
|
+
.join("\n")}`;
|
|
200
285
|
return [
|
|
201
|
-
`[delegate] Result from ${
|
|
202
|
-
`Status: ${
|
|
203
|
-
...(limit === undefined ? [] : [limit]),
|
|
286
|
+
`[delegate] Result from ${details.agent} (${modelLabel(details.model, details.finishedModel)}), delegation ${details.id}, request ${details.requestId}.`,
|
|
287
|
+
`Status: ${details.status}`,
|
|
288
|
+
...(details.limit === undefined ? [] : [details.limit]),
|
|
204
289
|
quote,
|
|
205
|
-
`Envelope: ${
|
|
206
|
-
`Delegate session: ${
|
|
290
|
+
`Envelope: ${details.envelopePath}`,
|
|
291
|
+
`Delegate session: ${details.sessionPath ?? details.id}`,
|
|
207
292
|
"",
|
|
208
|
-
|
|
293
|
+
details.body,
|
|
209
294
|
].join("\n");
|
|
210
295
|
}
|
|
211
296
|
|
|
212
|
-
/**
|
|
213
|
-
* Collapsed, a delegate message shows its first two lines (who answered and the
|
|
214
|
-
* status) and a hint; expanded, Pi's default rendering of the whole message.
|
|
215
|
-
* The model always gets the full content. Ctrl+O toggles it with tool output.
|
|
216
|
-
*/
|
|
217
|
-
export const renderMessage: MessageRenderer = (message, { expanded, outputPad }, theme) => {
|
|
218
|
-
if (expanded || typeof message.content !== "string") return undefined;
|
|
219
|
-
const lines = message.content.split("\n");
|
|
220
|
-
const shown = lines.slice(0, 2).join("\n");
|
|
221
|
-
const hint =
|
|
222
|
-
lines.length > 2 ? `\n${theme.fg("dim", keyText("app.tools.expand"))}${theme.fg("muted", " to expand")}` : "";
|
|
223
|
-
const box = new Box(outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
224
|
-
box.addChild(new Text(`${theme.fg("customMessageText", shown)}${hint}`, 0, 0));
|
|
225
|
-
return box;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
297
|
export function createResults(pi: ExtensionAPI): Results {
|
|
229
298
|
pi.registerMessageRenderer(CUSTOM_TYPE, renderMessage);
|
|
230
299
|
/** Every recorded delegation by its id, in the order recorded. */
|
|
@@ -306,13 +375,26 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
306
375
|
if (record === undefined || record.result !== undefined || record.closed === true || record.gone === true)
|
|
307
376
|
return false;
|
|
308
377
|
const session = findSession(ctx?.sessionManager.getSessionDir(), record.id);
|
|
378
|
+
const finished = finishedModel(session);
|
|
309
379
|
pi.appendEntry(CUSTOM_TYPE, { id, gone: true });
|
|
310
380
|
record.gone = true;
|
|
381
|
+
const details: MessageDetails = {
|
|
382
|
+
kind: "gone",
|
|
383
|
+
name: recordedName(record),
|
|
384
|
+
agent: record.agent,
|
|
385
|
+
model: record.model,
|
|
386
|
+
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
387
|
+
id: record.id,
|
|
388
|
+
requestId: record.requestId,
|
|
389
|
+
...(session === undefined ? {} : { sessionPath: session }),
|
|
390
|
+
windowName: record.windowName,
|
|
391
|
+
};
|
|
311
392
|
pi.sendMessage(
|
|
312
393
|
{
|
|
313
394
|
customType: CUSTOM_TYPE,
|
|
395
|
+
details,
|
|
314
396
|
content: [
|
|
315
|
-
`[delegate] Delegation ${record.id} (${record.agent}, ${record.model}), request ${record.requestId}, closed without a result: its window ${record.windowName} is gone and no reply arrived.`,
|
|
397
|
+
`[delegate] Delegation ${record.id} (${record.agent}, ${modelLabel(record.model, finished)}), request ${record.requestId}, closed without a result: its window ${record.windowName} is gone and no reply arrived.`,
|
|
316
398
|
`Delegate session: ${session ?? record.id}`,
|
|
317
399
|
"A reply that arrives later is still delivered.",
|
|
318
400
|
].join("\n"),
|
|
@@ -336,9 +418,11 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
336
418
|
const delegation = byRequest.get(requestId) as Recorded;
|
|
337
419
|
shown.add(reply.id);
|
|
338
420
|
const session = findSession(ctx?.sessionManager.getSessionDir(), delegation.id);
|
|
421
|
+
const finished = finishedModel(session);
|
|
422
|
+
const details = resultDetails(delegation, payload, session, finished);
|
|
339
423
|
// Steer, not follow-up: a busy parent gets the result before its next model call, not after it has answered.
|
|
340
424
|
pi.sendMessage(
|
|
341
|
-
{ customType: CUSTOM_TYPE, content: resultText(
|
|
425
|
+
{ customType: CUSTOM_TYPE, content: resultText(details), details, display: true },
|
|
342
426
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
343
427
|
);
|
|
344
428
|
const result: Result = {
|
|
@@ -346,6 +430,7 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
346
430
|
replyId: reply.id,
|
|
347
431
|
envelopePath: payload.path,
|
|
348
432
|
...(session === undefined ? {} : { sessionPath: session }),
|
|
433
|
+
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
349
434
|
};
|
|
350
435
|
pi.appendEntry(CUSTOM_TYPE, { id: delegation.id, result });
|
|
351
436
|
delegation.result = result;
|
package/src/tracking.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
8
8
|
import { Type } from "typebox";
|
|
9
9
|
|
|
10
10
|
import { type Mark, readMarks } from "./limits.ts";
|
|
11
|
+
import { modelLabel } from "./render.ts";
|
|
11
12
|
import {
|
|
12
13
|
type DelegationState,
|
|
13
14
|
deriveState,
|
|
@@ -55,7 +56,10 @@ const DelegationSchema = Type.Object({
|
|
|
55
56
|
id: Type.String(),
|
|
56
57
|
name: Type.String(),
|
|
57
58
|
agent: Type.String(),
|
|
58
|
-
model: Type.String(),
|
|
59
|
+
model: Type.String({ description: "The model it was launched on." }),
|
|
60
|
+
finishedModel: Type.Optional(
|
|
61
|
+
Type.String({ description: "The model its result came from, read from its session file; done only." }),
|
|
62
|
+
),
|
|
59
63
|
thinking: Type.String(),
|
|
60
64
|
autoExit: Type.Boolean(),
|
|
61
65
|
windowId: Type.String(),
|
|
@@ -193,7 +197,7 @@ function view(record: Recorded, windows: TmuxWindow[] | undefined, ctx: Extensio
|
|
|
193
197
|
function viewText({ record, state, name, sessionPath, lastActive }: View): string {
|
|
194
198
|
const seen = lastActive === undefined ? "" : ` (last active ${ago(Date.now() - lastActive.getTime())})`;
|
|
195
199
|
const lines = [
|
|
196
|
-
`${record.id} ${record.agent} (${record.model}, thinking ${record.thinking})`,
|
|
200
|
+
`${record.id} ${record.agent} (${modelLabel(record.model, record.result?.finishedModel)}, thinking ${record.thinking})`,
|
|
197
201
|
` name: ${name}`,
|
|
198
202
|
` state: ${stateLine(state, record)}${seen}`,
|
|
199
203
|
` window: ${record.windowName} (${record.windowId})`,
|
|
@@ -211,6 +215,7 @@ function viewDetails({ record, state, name, sessionPath, lastActive }: View): Re
|
|
|
211
215
|
name,
|
|
212
216
|
agent: record.agent,
|
|
213
217
|
model: record.model,
|
|
218
|
+
...(record.result?.finishedModel === undefined ? {} : { finishedModel: record.result.finishedModel }),
|
|
214
219
|
thinking: record.thinking,
|
|
215
220
|
autoExit: record.autoExit === true,
|
|
216
221
|
windowId: record.windowId,
|