@howarewoo/pi-subagents 0.0.0-stage → 0.2.0
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 +63 -0
- package/LICENSE +21 -0
- package/README.md +499 -2
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/docs/advanced-policies.md +78 -0
- package/docs/delegation-contract.md +297 -0
- package/docs/images/subagent-hub.svg +1 -0
- package/docs/images/subagent-profiles.svg +1 -0
- package/docs/images/subagent-widget.svg +1 -0
- package/docs/parity-inventory.md +330 -0
- package/docs/provenance.md +13 -0
- package/docs/releases.md +236 -0
- package/docs/verification.md +430 -0
- package/package.json +64 -4
- package/src/capabilities.ts +39 -0
- package/src/child-execution.ts +459 -0
- package/src/coordinator.ts +1376 -0
- package/src/delegation-types.ts +111 -0
- package/src/hub.ts +612 -0
- package/src/model-adapter.ts +77 -0
- package/src/model-policy.ts +100 -0
- package/src/outcomes.ts +110 -0
- package/src/pi-extended-subagents.ts +673 -0
- package/src/profile-viewer.ts +246 -0
- package/src/profiles.ts +225 -0
- package/src/scheduling.ts +73 -0
- package/src/subagent-widget.ts +115 -0
- package/src/supported-host.ts +26 -0
- package/src/task-input.ts +74 -0
- package/src/task-presentation.ts +165 -0
- package/src/thread.ts +268 -0
- package/src/tool-schemas.ts +165 -0
- package/src/workspace.ts +46 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { stat, realpath } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import type { ExtensionToolContext, ReadToolDetails } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
function hasDeniedTaskComponent(path: string): boolean {
|
|
6
|
+
return path.split(/[\\/]/).some((part) => part.startsWith(".") || part === "node_modules");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Content is obtained only through the effective public read dispatcher, never raw filesystem reads. */
|
|
10
|
+
export async function readTaskInput(
|
|
11
|
+
path: string,
|
|
12
|
+
ctx: ExtensionToolContext,
|
|
13
|
+
signal?: AbortSignal,
|
|
14
|
+
beforeRead?: () => void | Promise<void>,
|
|
15
|
+
cwd = ctx.cwd,
|
|
16
|
+
): Promise<string> {
|
|
17
|
+
if (!path || isAbsolute(path) || path.includes("\0") || hasDeniedTaskComponent(path))
|
|
18
|
+
throw new Error(
|
|
19
|
+
"taskFile must be an explicit non-hidden workspace-relative path without traversal",
|
|
20
|
+
);
|
|
21
|
+
if (!ctx.tools.some((tool) => tool.name === "read"))
|
|
22
|
+
throw new Error("taskFile requires a callable parent read tool");
|
|
23
|
+
const base = await realpath(cwd);
|
|
24
|
+
const target = resolve(base, path);
|
|
25
|
+
const checkedPath = await realpath(target);
|
|
26
|
+
const inside = relative(base, checkedPath);
|
|
27
|
+
if (!inside || inside === ".." || inside.startsWith(".." + sep) || isAbsolute(inside))
|
|
28
|
+
throw new Error("taskFile escapes workspace");
|
|
29
|
+
if (hasDeniedTaskComponent(inside))
|
|
30
|
+
throw new Error("taskFile resolved target must be non-hidden and outside node_modules");
|
|
31
|
+
const before = await stat(target);
|
|
32
|
+
if (!before.isFile() || before.size === 0 || before.size > 50000)
|
|
33
|
+
throw new Error("taskFile must be a nonempty regular text file of at most 50000 bytes");
|
|
34
|
+
signal?.throwIfAborted();
|
|
35
|
+
await beforeRead?.();
|
|
36
|
+
signal?.throwIfAborted();
|
|
37
|
+
const outcome = await ctx.executeTool("read", { path: target }, { signal });
|
|
38
|
+
if (outcome.isError)
|
|
39
|
+
throw new Error(
|
|
40
|
+
"taskFile read denied or failed: " +
|
|
41
|
+
outcome.result.content
|
|
42
|
+
.filter((block) => block.type === "text")
|
|
43
|
+
.map((block) => block.text)
|
|
44
|
+
.join("\n"),
|
|
45
|
+
);
|
|
46
|
+
const details = outcome.result.details as ReadToolDetails | undefined;
|
|
47
|
+
if (
|
|
48
|
+
details?.truncation?.truncated ||
|
|
49
|
+
outcome.result.content.length !== 1 ||
|
|
50
|
+
outcome.result.content[0].type !== "text"
|
|
51
|
+
)
|
|
52
|
+
throw new Error("taskFile must be complete untruncated text");
|
|
53
|
+
const text = outcome.result.content[0].text;
|
|
54
|
+
if (
|
|
55
|
+
!text.trim() ||
|
|
56
|
+
text.includes("\0") ||
|
|
57
|
+
text.includes("\ufffd") ||
|
|
58
|
+
Buffer.byteLength(text) > 50000
|
|
59
|
+
)
|
|
60
|
+
throw new Error(
|
|
61
|
+
"taskFile must contain nonempty UTF-8 text without replacement characters or NUL",
|
|
62
|
+
);
|
|
63
|
+
const after = await stat(target);
|
|
64
|
+
if (
|
|
65
|
+
(await realpath(target)) !== checkedPath ||
|
|
66
|
+
before.dev !== after.dev ||
|
|
67
|
+
before.ino !== after.ino ||
|
|
68
|
+
before.size !== after.size ||
|
|
69
|
+
before.mtimeMs !== after.mtimeMs ||
|
|
70
|
+
before.ctimeMs !== after.ctimeMs
|
|
71
|
+
)
|
|
72
|
+
throw new Error("taskFile changed during preflight; retry");
|
|
73
|
+
return text;
|
|
74
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { ChildSnapshot, DelegationCoordinator } from "./coordinator.ts";
|
|
3
|
+
import { safe } from "./thread.ts";
|
|
4
|
+
|
|
5
|
+
export const lineText = (text: string) => safe(text).replaceAll("\n", " ");
|
|
6
|
+
export const assignmentHeadline = (child: ChildSnapshot) =>
|
|
7
|
+
safe(child.assignment)
|
|
8
|
+
.split("\n")
|
|
9
|
+
.map((line) => line.trim())
|
|
10
|
+
.find(Boolean)
|
|
11
|
+
?.replace(/^#{1,6}\s+/, "") || "Assignment";
|
|
12
|
+
export const agentLabel = (child: ChildSnapshot) =>
|
|
13
|
+
`${assignmentHeadline(child)} · ${lineText(child.profile)}`;
|
|
14
|
+
export const active = (child: ChildSnapshot) => !child.disposed && child.status !== "paused";
|
|
15
|
+
export const activity = (child: ChildSnapshot) =>
|
|
16
|
+
(child.status === "paused"
|
|
17
|
+
? `Paused: ${child.pauseReason ?? "deadline"} · context ${child.retainedContext ? "retained" : "not yet available"} · resume via model or stop in Hub`
|
|
18
|
+
: child.error) ||
|
|
19
|
+
(active(child) ? child.activity || child.progress : child.result) ||
|
|
20
|
+
child.progress ||
|
|
21
|
+
"Waiting for SDK activity";
|
|
22
|
+
export const writerWarning = "Model-tool writer only: no conflicting manual !/!! or external edits";
|
|
23
|
+
export const statuses = [
|
|
24
|
+
"queued",
|
|
25
|
+
"setup",
|
|
26
|
+
"running",
|
|
27
|
+
"draining",
|
|
28
|
+
"pausing",
|
|
29
|
+
"paused",
|
|
30
|
+
"settled",
|
|
31
|
+
"failed",
|
|
32
|
+
"cancelled",
|
|
33
|
+
] as const;
|
|
34
|
+
export function fleetSummary(children: ChildSnapshot[]): string {
|
|
35
|
+
return children.length
|
|
36
|
+
? statuses
|
|
37
|
+
.map((status) => `${status} ${children.filter((child) => child.status === status).length}`)
|
|
38
|
+
.join(" · ")
|
|
39
|
+
: "No tasks · ask the model to delegate_task";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function taskCard(child: ChildSnapshot | undefined, expanded = false): string[] {
|
|
43
|
+
if (!child)
|
|
44
|
+
return [
|
|
45
|
+
"Task identity unavailable",
|
|
46
|
+
"See tool error; /subagent-hub lists current runtime tasks",
|
|
47
|
+
];
|
|
48
|
+
const headline = `${truncateToWidth(assignmentHeadline(child), 240, "… [preview truncated]")} · ${lineText(child.profile)} #${child.generation} · ${child.status}`;
|
|
49
|
+
if (!expanded)
|
|
50
|
+
return [
|
|
51
|
+
headline,
|
|
52
|
+
...(child.status === "paused"
|
|
53
|
+
? [
|
|
54
|
+
`Pause: ${truncateToWidth(lineText(child.pauseReason ?? "deadline"), 240, "… [preview truncated]")}`,
|
|
55
|
+
]
|
|
56
|
+
: child.error
|
|
57
|
+
? [`Error: ${truncateToWidth(lineText(child.error), 240, "… [preview truncated]")}`]
|
|
58
|
+
: []),
|
|
59
|
+
].map(lineText);
|
|
60
|
+
return [
|
|
61
|
+
headline,
|
|
62
|
+
`Identity ${child.id} #${child.generation}`,
|
|
63
|
+
`${child.model.provider}/${child.model.id} · effort ${child.effort ?? "unavailable"}`,
|
|
64
|
+
truncateToWidth(lineText(activity(child)), 240, "… [preview truncated]"),
|
|
65
|
+
`Prewalk ${child.prewalk ? JSON.stringify(child.prewalk) : "off"} · advisor unavailable`,
|
|
66
|
+
`Parent ${child.parentId}${child.parentGeneration ? ` #${child.parentGeneration}` : ""} · depth ${child.depth} · writer lease ${child.writerLease}`,
|
|
67
|
+
...(child.outcome
|
|
68
|
+
? [
|
|
69
|
+
`Structured ${child.outcome.validation} · ${child.outcome.finalized ? "finalized" : "provisional"} · attempts ${child.outcome.attempts}`,
|
|
70
|
+
]
|
|
71
|
+
: []),
|
|
72
|
+
...(child.limitReason ? [`Limit: ${child.limitReason}`] : []),
|
|
73
|
+
...(child.pauseReason
|
|
74
|
+
? [
|
|
75
|
+
`Pause: ${child.pauseReason} · context ${child.retainedContext ? "retained" : "not yet available"} · resume via model control after drain`,
|
|
76
|
+
]
|
|
77
|
+
: []),
|
|
78
|
+
`Window ${child.window ?? 1} · cumulative requests ${child.telemetry.requests ?? 0}/${child.limits.requests} · tools ${child.toolCalls}/${child.limits.tools} · spawns ${child.spawned ?? 0}/${child.limits.spawns}`,
|
|
79
|
+
...(child.artifacts ? [`Artifacts: ${JSON.stringify(child.artifacts)}`] : []),
|
|
80
|
+
"Assignment: " + truncateToWidth(lineText(child.assignment), 480, "… [preview truncated]"),
|
|
81
|
+
"Result: " +
|
|
82
|
+
truncateToWidth(
|
|
83
|
+
lineText(child.result || child.error || "Not available yet"),
|
|
84
|
+
480,
|
|
85
|
+
"… [preview truncated]",
|
|
86
|
+
),
|
|
87
|
+
child.transcript
|
|
88
|
+
? `Full transcript: ${child.transcript} · /subagent-hub`
|
|
89
|
+
: "Transcript unavailable (setup) · /subagent-hub",
|
|
90
|
+
].map(lineText);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Read-only content projection: never dumps provider/auth objects, image payloads or opaque signatures. */
|
|
94
|
+
export function transcriptLines(
|
|
95
|
+
messages: ReturnType<DelegationCoordinator["messages"]>,
|
|
96
|
+
toolsOnly = false,
|
|
97
|
+
): string[] {
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
for (const message of messages) {
|
|
100
|
+
if (message.role === "system") continue;
|
|
101
|
+
const start = lines.length;
|
|
102
|
+
const blocks = "content" in message ? message.content : undefined;
|
|
103
|
+
if (!toolsOnly || message.role === "toolResult") {
|
|
104
|
+
lines.push(
|
|
105
|
+
`[${message.role}${message.role === "toolResult" ? ` ${message.toolName} ${message.toolCallId} ${message.isError ? "ERROR" : "result"}` : ""}]`,
|
|
106
|
+
);
|
|
107
|
+
if (typeof blocks === "string") lines.push(safe(blocks));
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(blocks))
|
|
110
|
+
for (const block of blocks) {
|
|
111
|
+
if (block.type === "toolCall")
|
|
112
|
+
lines.push(
|
|
113
|
+
`Tool ${block.name} ${block.id}\nArguments: ${safe(JSON.stringify(block.arguments, null, 2))}`,
|
|
114
|
+
);
|
|
115
|
+
else if (!toolsOnly || message.role === "toolResult") {
|
|
116
|
+
if (block.type === "text") lines.push(safe(block.text));
|
|
117
|
+
else if (block.type === "thinking") lines.push("Thinking: " + safe(block.thinking));
|
|
118
|
+
else if (block.type === "image")
|
|
119
|
+
lines.push(`[image ${safe(block.mimeType)}; payload omitted]`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (message.role === "assistant" && message.errorMessage)
|
|
123
|
+
lines.push("Error: " + safe(message.errorMessage));
|
|
124
|
+
if (start && lines.length > start) lines.splice(start, 0, "");
|
|
125
|
+
}
|
|
126
|
+
return lines.length
|
|
127
|
+
? lines.map(safe)
|
|
128
|
+
: [toolsOnly ? "No tool calls yet" : "Waiting for SDK messages"];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function taskDetails(
|
|
132
|
+
child: ChildSnapshot,
|
|
133
|
+
presence: ReturnType<DelegationCoordinator["agents"]>[number]["presence"],
|
|
134
|
+
): string[] {
|
|
135
|
+
const metric = (value: unknown) =>
|
|
136
|
+
value === null || value === undefined ? "unavailable" : JSON.stringify(value);
|
|
137
|
+
return [
|
|
138
|
+
`Identity ${child.id} #${child.generation}`,
|
|
139
|
+
"",
|
|
140
|
+
"Assignment",
|
|
141
|
+
safe(child.assignment),
|
|
142
|
+
"",
|
|
143
|
+
"Result",
|
|
144
|
+
safe(child.result || "Not available yet"),
|
|
145
|
+
"",
|
|
146
|
+
...(child.error ? ["Error", safe(child.error), ""] : []),
|
|
147
|
+
...(child.outcome ? ["Structured outcome", JSON.stringify(child.outcome, null, 2), ""] : []),
|
|
148
|
+
...(child.artifacts ? ["Saved artifacts", JSON.stringify(child.artifacts, null, 2), ""] : []),
|
|
149
|
+
`Workspace: ${safe(child.cwd ?? "parent workspace")}`,
|
|
150
|
+
`Effective builtin tools: ${child.tools.join(", ")}`,
|
|
151
|
+
`${child.model.provider}/${child.model.id} · effort ${child.effort ?? "unavailable"}`,
|
|
152
|
+
`Prewalk ${child.prewalk ? JSON.stringify(child.prewalk) : "off"} · advisor unavailable`,
|
|
153
|
+
`Presence ${presence ?? "historical"} · resources ${child.disposed ? "disposed" : child.status === "paused" ? "idle SDK; context retained; model resume required" : "live"}`,
|
|
154
|
+
`Elapsed ${Math.max(0, Math.floor(((child.endedAt ?? Date.now()) - child.createdAt) / 1000))}s`,
|
|
155
|
+
`Direct requests ${metric(child.telemetry.requests)} · cost ${metric(child.telemetry.cost)}`,
|
|
156
|
+
`Direct tokens ${metric(child.telemetry.tokens)}`,
|
|
157
|
+
`Context ${metric(child.telemetry.context)}`,
|
|
158
|
+
`Parent ${child.parentId} #${child.parentGeneration ?? "root"} · depth ${child.depth} · direct generation only`,
|
|
159
|
+
`Limits ${JSON.stringify(child.limits)} · calls ${child.toolCalls} · reason ${child.limitReason ?? "none"}`,
|
|
160
|
+
"Activity: " + safe(activity(child)),
|
|
161
|
+
"Full transcript: " + safe(child.transcript ?? "unavailable (queued/setup)"),
|
|
162
|
+
"Followup/revive: native delegate_coordinate only (Hub has no tool dispatcher)",
|
|
163
|
+
...(child.writerLease ? [writerWarning] : []),
|
|
164
|
+
];
|
|
165
|
+
}
|
package/src/thread.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssistantMessageComponent,
|
|
3
|
+
UserMessageComponent,
|
|
4
|
+
ToolExecutionComponent,
|
|
5
|
+
getMarkdownTheme,
|
|
6
|
+
createReadToolDefinition,
|
|
7
|
+
createBashToolDefinition,
|
|
8
|
+
createEditToolDefinition,
|
|
9
|
+
createWriteToolDefinition,
|
|
10
|
+
createGrepToolDefinition,
|
|
11
|
+
createFindToolDefinition,
|
|
12
|
+
createLsToolDefinition,
|
|
13
|
+
type ToolRenderers,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Text, wrapTextWithAnsi, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
16
|
+
import type { DelegationCoordinator } from "./coordinator.ts";
|
|
17
|
+
|
|
18
|
+
export function safe(text: string): string {
|
|
19
|
+
return text
|
|
20
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
21
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
22
|
+
.replace(
|
|
23
|
+
/[\x00-\x08\x0b-\x1f\x7f-\x9f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g,
|
|
24
|
+
" ",
|
|
25
|
+
)
|
|
26
|
+
.replaceAll("\t", " ");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Inputs are sanitized before native rendering. Only native SGR survives the output boundary,
|
|
30
|
+
// not OSC links/shell zones, image protocols or cursor-control sequences.
|
|
31
|
+
export function nativeText(text: string): string {
|
|
32
|
+
return text
|
|
33
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
34
|
+
.split(/(\x1b\[[0-9;:]*m)/g)
|
|
35
|
+
.map((part, index) => (index % 2 ? part : safe(part)))
|
|
36
|
+
.join("");
|
|
37
|
+
}
|
|
38
|
+
function safeValue(value: unknown): unknown {
|
|
39
|
+
if (typeof value === "string") return safe(value);
|
|
40
|
+
if (Array.isArray(value)) return value.map(safeValue);
|
|
41
|
+
if (value && typeof value === "object")
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries(value).map(([key, item]) => [safe(key), safeValue(item)]),
|
|
44
|
+
);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Messages = ReturnType<DelegationCoordinator["messages"]>;
|
|
49
|
+
export type ThreadDependencies = Readonly<{
|
|
50
|
+
tui: TUI;
|
|
51
|
+
cwd: string;
|
|
52
|
+
toolRenderers?: Readonly<Record<string, ToolRenderers>>;
|
|
53
|
+
}>;
|
|
54
|
+
export type LiveTool = {
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
toolName: string;
|
|
57
|
+
args: unknown;
|
|
58
|
+
content?: readonly { type: string; text?: string; mimeType?: string }[];
|
|
59
|
+
};
|
|
60
|
+
const textContent = (content: readonly { type: string; text?: string; mimeType?: string }[]) =>
|
|
61
|
+
content.flatMap((block) =>
|
|
62
|
+
block.type === "text"
|
|
63
|
+
? [{ type: "text", text: safe(block.text ?? "") }]
|
|
64
|
+
: block.type === "image"
|
|
65
|
+
? [{ type: "text", text: `[image ${safe(block.mimeType ?? "unknown")}; payload omitted]` }]
|
|
66
|
+
: [],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/** Selected-generation readonly projection through the same public classes as Pi's main chat. */
|
|
70
|
+
export function createNativeThread({ tui, cwd, toolRenderers }: ThreadDependencies) {
|
|
71
|
+
const renderers = new Map<string, ToolRenderers>(
|
|
72
|
+
[
|
|
73
|
+
createReadToolDefinition(cwd),
|
|
74
|
+
createBashToolDefinition(cwd),
|
|
75
|
+
createEditToolDefinition(cwd),
|
|
76
|
+
createWriteToolDefinition(cwd),
|
|
77
|
+
createGrepToolDefinition(cwd),
|
|
78
|
+
createFindToolDefinition(cwd),
|
|
79
|
+
createLsToolDefinition(cwd),
|
|
80
|
+
].map(({ name, renderShell, renderCall, renderResult }) => [
|
|
81
|
+
name,
|
|
82
|
+
{ renderShell, renderCall, renderResult } as ToolRenderers,
|
|
83
|
+
]),
|
|
84
|
+
);
|
|
85
|
+
for (const [name, renderersForTool] of Object.entries(toolRenderers ?? {}))
|
|
86
|
+
renderers.set(name, renderersForTool);
|
|
87
|
+
const cache = new Map<string, { signature: string; component: Component }>();
|
|
88
|
+
let generation: string | undefined;
|
|
89
|
+
return {
|
|
90
|
+
render(
|
|
91
|
+
ref: string,
|
|
92
|
+
messages: Messages,
|
|
93
|
+
partial: Messages[number] | undefined,
|
|
94
|
+
live: LiveTool | undefined,
|
|
95
|
+
width: number,
|
|
96
|
+
): string[] {
|
|
97
|
+
if (generation !== ref) {
|
|
98
|
+
cache.clear();
|
|
99
|
+
generation = ref;
|
|
100
|
+
}
|
|
101
|
+
const used = new Set<string>();
|
|
102
|
+
const components: Component[] = [];
|
|
103
|
+
const append = (
|
|
104
|
+
key: string,
|
|
105
|
+
value: unknown,
|
|
106
|
+
build: () => Component,
|
|
107
|
+
update?: (component: Component) => void,
|
|
108
|
+
) => {
|
|
109
|
+
const signature = JSON.stringify(value);
|
|
110
|
+
let entry = cache.get(key);
|
|
111
|
+
if (!entry || entry.signature !== signature) {
|
|
112
|
+
if (entry && update) {
|
|
113
|
+
update(entry.component);
|
|
114
|
+
entry.signature = signature;
|
|
115
|
+
} else {
|
|
116
|
+
entry = { signature, component: build() };
|
|
117
|
+
cache.set(key, entry);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
used.add(key);
|
|
121
|
+
components.push(entry.component);
|
|
122
|
+
};
|
|
123
|
+
const all = partial ? [...messages, partial] : messages;
|
|
124
|
+
const results = new Map(
|
|
125
|
+
all.flatMap((message) =>
|
|
126
|
+
message.role === "toolResult" ? [[message.toolCallId, message] as const] : [],
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
const toolIds = new Set<string>();
|
|
130
|
+
const tool = (name: string, id: string, args: unknown, failure?: string) => {
|
|
131
|
+
if (toolIds.has(id)) return;
|
|
132
|
+
toolIds.add(id);
|
|
133
|
+
const result = results.get(id);
|
|
134
|
+
const active = live?.toolCallId === id ? live : undefined;
|
|
135
|
+
const content = result
|
|
136
|
+
? textContent(result.content)
|
|
137
|
+
: active?.content
|
|
138
|
+
? textContent(active.content)
|
|
139
|
+
: failure
|
|
140
|
+
? [{ type: "text", text: safe(failure) }]
|
|
141
|
+
: undefined;
|
|
142
|
+
// Only the documented edit diff is renderer input. Never pass arbitrary tool details.
|
|
143
|
+
const details = result?.details;
|
|
144
|
+
const diff =
|
|
145
|
+
name === "edit" &&
|
|
146
|
+
details &&
|
|
147
|
+
typeof details === "object" &&
|
|
148
|
+
"diff" in details &&
|
|
149
|
+
typeof details.diff === "string"
|
|
150
|
+
? {
|
|
151
|
+
diff: safe(details.diff),
|
|
152
|
+
...("firstChangedLine" in details && typeof details.firstChangedLine === "number"
|
|
153
|
+
? { firstChangedLine: details.firstChangedLine }
|
|
154
|
+
: {}),
|
|
155
|
+
}
|
|
156
|
+
: undefined;
|
|
157
|
+
const toolArgs = safeValue(active?.args ?? args);
|
|
158
|
+
const output = content
|
|
159
|
+
? { content, details: diff, isError: result?.isError ?? !!failure }
|
|
160
|
+
: undefined;
|
|
161
|
+
append("tool:" + id, [name, toolArgs, output, !!active], () => {
|
|
162
|
+
const component = new ToolExecutionComponent(
|
|
163
|
+
safe(name),
|
|
164
|
+
safe(id),
|
|
165
|
+
toolArgs,
|
|
166
|
+
{ showImages: false },
|
|
167
|
+
renderers.get(name),
|
|
168
|
+
tui,
|
|
169
|
+
cwd,
|
|
170
|
+
);
|
|
171
|
+
component.setExpanded(true);
|
|
172
|
+
// Native edit's completed-args preview reads cwd files. Recorded result.diff is sufficient.
|
|
173
|
+
if (name !== "edit") component.setArgsComplete();
|
|
174
|
+
// Do not mark execution started: this observer must not invent elapsed clocks/timers.
|
|
175
|
+
if (output) component.updateResult(output, !!active && !result);
|
|
176
|
+
return component;
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
all.forEach((message, index) => {
|
|
180
|
+
if (message.role === "system") return;
|
|
181
|
+
if (message.role === "assistant") {
|
|
182
|
+
const content: typeof message.content = message.content.map((block) =>
|
|
183
|
+
block.type === "text"
|
|
184
|
+
? { type: "text", text: safe(block.text) }
|
|
185
|
+
: block.type === "thinking"
|
|
186
|
+
? { type: "thinking", thinking: safe(block.thinking) }
|
|
187
|
+
: {
|
|
188
|
+
type: "toolCall",
|
|
189
|
+
id: safe(block.id),
|
|
190
|
+
name: safe(block.name),
|
|
191
|
+
arguments: safeValue(block.arguments) as typeof block.arguments,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
const projected = {
|
|
195
|
+
role: message.role,
|
|
196
|
+
api: message.api,
|
|
197
|
+
provider: message.provider,
|
|
198
|
+
model: message.model,
|
|
199
|
+
timestamp: message.timestamp,
|
|
200
|
+
usage: message.usage,
|
|
201
|
+
stopReason: message.stopReason,
|
|
202
|
+
content,
|
|
203
|
+
errorMessage: message.errorMessage ? safe(message.errorMessage) : undefined,
|
|
204
|
+
};
|
|
205
|
+
const streaming = message === partial;
|
|
206
|
+
append(
|
|
207
|
+
"assistant:" + index,
|
|
208
|
+
[projected, streaming],
|
|
209
|
+
() => {
|
|
210
|
+
const component = new AssistantMessageComponent(projected, false, getMarkdownTheme());
|
|
211
|
+
component.updateContent(projected, streaming);
|
|
212
|
+
return component;
|
|
213
|
+
},
|
|
214
|
+
(component) =>
|
|
215
|
+
(component as AssistantMessageComponent).updateContent(projected, streaming),
|
|
216
|
+
);
|
|
217
|
+
for (const block of message.content)
|
|
218
|
+
if (block.type === "toolCall")
|
|
219
|
+
tool(
|
|
220
|
+
block.name,
|
|
221
|
+
block.id,
|
|
222
|
+
block.arguments,
|
|
223
|
+
message.stopReason === "error" || message.stopReason === "aborted"
|
|
224
|
+
? message.errorMessage || "Operation aborted"
|
|
225
|
+
: undefined,
|
|
226
|
+
);
|
|
227
|
+
if (
|
|
228
|
+
message.errorMessage &&
|
|
229
|
+
message.stopReason !== "error" &&
|
|
230
|
+
message.stopReason !== "aborted"
|
|
231
|
+
) {
|
|
232
|
+
append(
|
|
233
|
+
"error:" + index,
|
|
234
|
+
message.errorMessage,
|
|
235
|
+
() => new Text("Error: " + safe(message.errorMessage!), 1, 0),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
} else if (message.role === "toolResult") {
|
|
239
|
+
if (!toolIds.has(message.toolCallId)) tool(message.toolName, message.toolCallId, {});
|
|
240
|
+
} else if ("content" in message) {
|
|
241
|
+
const text =
|
|
242
|
+
typeof message.content === "string"
|
|
243
|
+
? safe(message.content)
|
|
244
|
+
: Array.isArray(message.content)
|
|
245
|
+
? textContent(message.content)
|
|
246
|
+
.map((block) => block.text)
|
|
247
|
+
.join("\n")
|
|
248
|
+
: "";
|
|
249
|
+
append("user:" + index, text, () => new UserMessageComponent(text, getMarkdownTheme()));
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
if (live && !toolIds.has(live.toolCallId)) tool(live.toolName, live.toolCallId, live.args);
|
|
253
|
+
for (const key of cache.keys()) if (!used.has(key)) cache.delete(key);
|
|
254
|
+
return components.flatMap((component) =>
|
|
255
|
+
component
|
|
256
|
+
.render(Math.max(12, width))
|
|
257
|
+
.flatMap((line) => wrapTextWithAnsi(nativeText(line), Math.max(1, width))),
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
invalidate() {
|
|
261
|
+
for (const entry of cache.values()) entry.component.invalidate();
|
|
262
|
+
},
|
|
263
|
+
clear() {
|
|
264
|
+
cache.clear();
|
|
265
|
+
generation = undefined;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { EFFORTS } from "./model-policy.ts";
|
|
3
|
+
import { COORDINATION_ACTIONS } from "./profiles.ts";
|
|
4
|
+
|
|
5
|
+
const role = Type.Union([
|
|
6
|
+
Type.Literal("general"),
|
|
7
|
+
Type.Literal("scout"),
|
|
8
|
+
Type.Literal("review"),
|
|
9
|
+
Type.Literal("implementation"),
|
|
10
|
+
]);
|
|
11
|
+
const limits = Type.Object(
|
|
12
|
+
{
|
|
13
|
+
depth: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
14
|
+
spawns: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
15
|
+
deadlineMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
16
|
+
requests: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
17
|
+
tools: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
18
|
+
},
|
|
19
|
+
{ additionalProperties: false },
|
|
20
|
+
);
|
|
21
|
+
const modelSelection = Type.Object(
|
|
22
|
+
{
|
|
23
|
+
provider: Type.String({ minLength: 1 }),
|
|
24
|
+
id: Type.String({ minLength: 1 }),
|
|
25
|
+
effort: Type.Enum(EFFORTS),
|
|
26
|
+
},
|
|
27
|
+
{ additionalProperties: false },
|
|
28
|
+
);
|
|
29
|
+
const assignment = {
|
|
30
|
+
cwd: Type.Optional(
|
|
31
|
+
Type.String({
|
|
32
|
+
minLength: 1,
|
|
33
|
+
description:
|
|
34
|
+
"Existing workspace; absolute or relative to root parent cwd. No new permissions or sandbox.",
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
models: Type.Optional(Type.Array(modelSelection, { maxItems: 64 })),
|
|
38
|
+
model: Type.Optional(modelSelection),
|
|
39
|
+
assignment: Type.Optional(Type.String({ minLength: 1 })),
|
|
40
|
+
taskFile: Type.Optional(Type.String({ minLength: 1 })),
|
|
41
|
+
resultSchema: Type.Optional(Type.Unknown()),
|
|
42
|
+
profile: role,
|
|
43
|
+
spawn: Type.Optional(Type.Array(role)),
|
|
44
|
+
coordination: Type.Optional(
|
|
45
|
+
Type.Array(Type.Union(COORDINATION_ACTIONS.map((action) => Type.Literal(action)))),
|
|
46
|
+
),
|
|
47
|
+
limits: Type.Optional(limits),
|
|
48
|
+
};
|
|
49
|
+
const item = Type.Object(assignment, { additionalProperties: false });
|
|
50
|
+
export const parameters = Type.Union([
|
|
51
|
+
Type.Object(
|
|
52
|
+
{ ...assignment, background: Type.Optional(Type.Boolean()) },
|
|
53
|
+
{ additionalProperties: false },
|
|
54
|
+
),
|
|
55
|
+
Type.Object(
|
|
56
|
+
{
|
|
57
|
+
tasks: Type.Array(item, { minItems: 1, maxItems: 64 }),
|
|
58
|
+
background: Type.Optional(Type.Boolean()),
|
|
59
|
+
},
|
|
60
|
+
{ additionalProperties: false },
|
|
61
|
+
),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const identity = Type.Object(
|
|
65
|
+
{ id: Type.String({ minLength: 1 }), generation: Type.Integer({ minimum: 1 }) },
|
|
66
|
+
{ additionalProperties: false },
|
|
67
|
+
);
|
|
68
|
+
export const coordinationParameters = Type.Union([
|
|
69
|
+
Type.Object(
|
|
70
|
+
{
|
|
71
|
+
action: Type.Literal("message"),
|
|
72
|
+
target: Type.Optional(identity),
|
|
73
|
+
text: Type.String({ minLength: 1, maxLength: 4096 }),
|
|
74
|
+
mode: Type.Optional(
|
|
75
|
+
Type.Union([Type.Literal("inbox"), Type.Literal("steer"), Type.Literal("followUp")]),
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
{ additionalProperties: false },
|
|
79
|
+
),
|
|
80
|
+
Type.Object({ action: Type.Literal("inbox") }, { additionalProperties: false }),
|
|
81
|
+
Type.Object(
|
|
82
|
+
{
|
|
83
|
+
action: Type.Literal("wait"),
|
|
84
|
+
target: Type.Optional(identity),
|
|
85
|
+
messageId: Type.Optional(Type.String({ minLength: 1 })),
|
|
86
|
+
timeoutMs: Type.Integer({ minimum: 1, maximum: 60000 }),
|
|
87
|
+
},
|
|
88
|
+
{ additionalProperties: false },
|
|
89
|
+
),
|
|
90
|
+
Type.Object(
|
|
91
|
+
{
|
|
92
|
+
action: Type.Union([Type.Literal("park"), Type.Literal("stop"), Type.Literal("status")]),
|
|
93
|
+
target: identity,
|
|
94
|
+
},
|
|
95
|
+
{ additionalProperties: false },
|
|
96
|
+
),
|
|
97
|
+
Type.Object(
|
|
98
|
+
{
|
|
99
|
+
action: Type.Union([Type.Literal("followup"), Type.Literal("revive")]),
|
|
100
|
+
target: identity,
|
|
101
|
+
assignment: Type.String({ minLength: 1 }),
|
|
102
|
+
resultSchema: Type.Optional(Type.Unknown()),
|
|
103
|
+
},
|
|
104
|
+
{ additionalProperties: false },
|
|
105
|
+
),
|
|
106
|
+
Type.Object(
|
|
107
|
+
{
|
|
108
|
+
action: Type.Literal("resume"),
|
|
109
|
+
target: identity,
|
|
110
|
+
assignment: Type.String({ minLength: 1 }),
|
|
111
|
+
},
|
|
112
|
+
{ additionalProperties: false },
|
|
113
|
+
),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const worker = Type.Union([role, Type.Literal("worker")]);
|
|
117
|
+
const compatibleItem = Type.Object(
|
|
118
|
+
{
|
|
119
|
+
agent: worker,
|
|
120
|
+
task: Type.String({ minLength: 1 }),
|
|
121
|
+
cwd: Type.Optional(
|
|
122
|
+
Type.String({
|
|
123
|
+
minLength: 1,
|
|
124
|
+
description:
|
|
125
|
+
"Existing workspace; absolute or relative to root parent cwd. Omit to inherit the call default.",
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
model: Type.Optional(Type.Union([modelSelection, Type.String({ minLength: 1 })])),
|
|
129
|
+
limits: Type.Optional(limits),
|
|
130
|
+
},
|
|
131
|
+
{ additionalProperties: false },
|
|
132
|
+
);
|
|
133
|
+
export const compatibleParameters = Type.Union([
|
|
134
|
+
Type.Object(
|
|
135
|
+
{ ...compatibleItem.properties, async: Type.Optional(Type.Boolean()) },
|
|
136
|
+
{ additionalProperties: false },
|
|
137
|
+
),
|
|
138
|
+
Type.Object(
|
|
139
|
+
{
|
|
140
|
+
tasks: Type.Array(compatibleItem, { minItems: 1, maxItems: 64 }),
|
|
141
|
+
async: Type.Optional(Type.Boolean()),
|
|
142
|
+
cwd: Type.Optional(Type.String({ minLength: 1 })),
|
|
143
|
+
},
|
|
144
|
+
{ additionalProperties: false },
|
|
145
|
+
),
|
|
146
|
+
Type.Object(
|
|
147
|
+
{ action: Type.Union([Type.Literal("list"), Type.Literal("agents")]) },
|
|
148
|
+
{ additionalProperties: false },
|
|
149
|
+
),
|
|
150
|
+
Type.Object(
|
|
151
|
+
{
|
|
152
|
+
action: Type.Union([Type.Literal("status"), Type.Literal("stop")]),
|
|
153
|
+
id: Type.String({ minLength: 1 }),
|
|
154
|
+
},
|
|
155
|
+
{ additionalProperties: false },
|
|
156
|
+
),
|
|
157
|
+
Type.Object(
|
|
158
|
+
{
|
|
159
|
+
action: Type.Union([Type.Literal("steer"), Type.Literal("resume")]),
|
|
160
|
+
id: Type.String({ minLength: 1 }),
|
|
161
|
+
message: Type.String({ minLength: 1 }),
|
|
162
|
+
},
|
|
163
|
+
{ additionalProperties: false },
|
|
164
|
+
),
|
|
165
|
+
]);
|