@tshu-w/pi-agents 0.1.1 → 0.2.1
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 +27 -20
- package/bin/daemon.mjs +32 -14
- package/bin/host.mjs +9 -1
- package/bin/pd.mjs +87 -42
- package/docs/HOSTS.md +111 -0
- package/docs/PROGRAM.md +39 -114
- package/docs/SPEC.md +109 -161
- package/package.json +7 -5
- package/src/agents/agents.ts +129 -55
- package/src/agents/registry.ts +62 -10
- package/src/agents/search.ts +21 -4
- package/src/agents/tool.ts +41 -13
- package/src/daemon/client.mjs +4 -1
- package/src/host/client.mjs +30 -5
- package/src/host/extension.ts +38 -14
- package/src/host/rpc-client.mjs +380 -0
- package/src/index.ts +44 -16
- package/src/output.ts +5 -1
- package/src/programs/codemode.ts +2 -2
- package/src/programs/execute.ts +150 -34
- package/src/programs/host-patches.ts +56 -27
- package/src/programs/loadout.ts +18 -3
- package/src/programs/programs.ts +128 -20
- package/src/programs/run-output.mjs +11 -0
- package/src/programs/sandbox.ts +17 -4
- package/src/programs/scope.ts +15 -7
- package/src/programs/tool.ts +172 -16
- package/src/render-call.ts +113 -15
- package/src/roots/guard.mjs +8 -2
- package/src/roots/paths.mjs +4 -4
- package/src/roots/queue.ts +52 -0
- package/src/roots/report.ts +12 -6
- package/src/roots/rpc.ts +618 -0
- package/src/roots/runtime.ts +17 -2
- package/src/roots/transport.mjs +41 -23
- package/src/ui/message.ts +24 -0
- package/src/ui/panel.ts +39 -7
- package/src/ui/queue.ts +47 -0
- package/src/ui/viewer.ts +182 -38
- package/src/workbench.mjs +91 -51
- package/docs/WORKBENCH.md +0 -167
package/src/programs/execute.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { loadCodemode, loadPiCodemode } from "./codemode.ts";
|
|
|
10
10
|
import type { DetachedCallOptions } from "./host-patches.ts";
|
|
11
11
|
|
|
12
12
|
export const PROGRAM_TOOL_NAME = "program";
|
|
13
|
+
/** How long a Program's work may take to stop once it ends. */
|
|
14
|
+
export const CLEANUP_TIMEOUT_MS = 15_000;
|
|
13
15
|
|
|
14
16
|
export type ProgramOutcome = "completed" | "failed" | "stopped";
|
|
15
17
|
|
|
@@ -20,28 +22,70 @@ export interface ProgramFiles {
|
|
|
20
22
|
edited: string[];
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/** A call in codemode's `details.calls`. */
|
|
26
|
+
interface CallRecord {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
args: string;
|
|
30
|
+
status: "running" | "ok" | "error" | "cancelled";
|
|
31
|
+
durationMs?: number;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Records calls that a Program's own globals make, which codemode does not see, beside its tool calls.
|
|
37
|
+
* `start` returns the function that ends the call; a call still running when the script ends is cancelled.
|
|
38
|
+
*/
|
|
39
|
+
export type CallTracker = (name: string, args: string) => (status: "ok" | "error" | "cancelled", error?: string) => void;
|
|
40
|
+
|
|
41
|
+
const ARGS_PREVIEW_CHARS = 200;
|
|
42
|
+
const ERROR_PREVIEW_CHARS = 500;
|
|
43
|
+
const NOTE_NAMES = 10;
|
|
44
|
+
const NOTE_NAME_CHARS = 60;
|
|
45
|
+
|
|
46
|
+
const preview = (text: string, max: number) => text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
47
|
+
|
|
48
|
+
/** Names of calls for a note: at most `NOTE_NAMES` of them, each cut to `NOTE_NAME_CHARS`, since scripts name Agents. */
|
|
49
|
+
function callNames(calls: CallRecord[], status: boolean): string {
|
|
50
|
+
const shown = calls.slice(0, NOTE_NAMES).map((call) => {
|
|
51
|
+
const name = preview(call.name, NOTE_NAME_CHARS);
|
|
52
|
+
return status ? `${name} (${call.status})` : name;
|
|
53
|
+
});
|
|
54
|
+
return calls.length > NOTE_NAMES ? `${shown.join(", ")}, and ${calls.length - NOTE_NAMES} more` : shown.join(", ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Adds a note after a blank line to the last text item, or as a new item after images. */
|
|
58
|
+
function appendNote(content: AgentToolResult<unknown>["content"], note: string): void {
|
|
59
|
+
const last = content.at(-1);
|
|
60
|
+
if (last?.type === "text") last.text += `\n\n${note}`;
|
|
61
|
+
else content.push({ type: "text", text: note });
|
|
62
|
+
}
|
|
63
|
+
|
|
23
64
|
export interface ProgramRunResult {
|
|
24
65
|
outcome: ProgramOutcome;
|
|
25
66
|
result: AgentToolResult<unknown> & { isError?: boolean };
|
|
26
67
|
files: ProgramFiles;
|
|
27
68
|
/** Whether some of the Program's work did not stop during cleanup. */
|
|
28
69
|
abandoned?: boolean;
|
|
70
|
+
/** Settles once the tool calls the script started have ended, which may be after it ends. */
|
|
71
|
+
toolCalls: Promise<unknown>;
|
|
29
72
|
}
|
|
30
73
|
|
|
31
74
|
export interface ProgramRunOptions {
|
|
32
75
|
/** Seconds until the Program stops; the script's `timeout_ms` option applies as well. */
|
|
33
76
|
timeout?: number;
|
|
34
|
-
/** Globals added to the script, such as the host side of `agent()
|
|
35
|
-
globals?: CodemodeTool[];
|
|
77
|
+
/** Globals added to the script, such as the host side of `agent()`; `track` records their calls. */
|
|
78
|
+
globals?: (track: CallTracker) => CodemodeTool[];
|
|
36
79
|
/** Rewrites the code before it runs, such as prepending the `agent()` prelude. */
|
|
37
80
|
prepare?: (code: string) => string;
|
|
38
81
|
onUpdate?: AgentToolUpdateCallback<unknown>;
|
|
39
82
|
/**
|
|
40
|
-
* Whether the Program runs
|
|
41
|
-
* since Pi records nested calls only until the calling tool
|
|
83
|
+
* Whether the Program runs outside a tool call: after its tool call returns, or from `/program`.
|
|
84
|
+
* Its calls then get their own IDs, since Pi records nested calls only until the calling tool
|
|
85
|
+
* call returns.
|
|
42
86
|
*/
|
|
43
|
-
|
|
44
|
-
/** Stops a
|
|
87
|
+
detached?: boolean;
|
|
88
|
+
/** Stops a detached Program; `ctx.abort()` of its calls' hooks and tools calls it. */
|
|
45
89
|
abort?: () => void;
|
|
46
90
|
appendEntry(customType: string, data: unknown): void;
|
|
47
91
|
getToolNamespace(name: string): { name: string } | undefined;
|
|
@@ -53,10 +97,32 @@ interface SandboxExtras {
|
|
|
53
97
|
timeoutMs: number;
|
|
54
98
|
result?: CodemodeResult;
|
|
55
99
|
used: boolean;
|
|
100
|
+
/** Called when the script ends, before the sandbox closes. */
|
|
101
|
+
ended(): void;
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
const sandboxExtras = new AsyncLocalStorage<SandboxExtras>();
|
|
59
|
-
let
|
|
105
|
+
let sandboxPatch: Promise<void> | undefined;
|
|
106
|
+
|
|
107
|
+
async function patchSandbox(): Promise<void> {
|
|
108
|
+
const proto = (await loadCodemode()).CodemodeSandbox.prototype as unknown as Record<string, any>;
|
|
109
|
+
const execute = proto.execute;
|
|
110
|
+
proto.execute = function (this: Record<string, any>, code: string, options: Record<string, unknown> = {}) {
|
|
111
|
+
const current = sandboxExtras.getStore();
|
|
112
|
+
if (!current || current.used) return execute.call(this, code, options);
|
|
113
|
+
current.used = true;
|
|
114
|
+
for (const global of current.globals) this.globalsByName.set(global.name, global);
|
|
115
|
+
const timeoutMs = Math.min(current.timeoutMs, (options.timeoutMs as number | undefined) ?? this.timeoutMs);
|
|
116
|
+
return execute.call(this, current.prepare(code), { ...options, timeoutMs }).then((result: CodemodeResult) => {
|
|
117
|
+
current.result = result;
|
|
118
|
+
current.ended();
|
|
119
|
+
return result;
|
|
120
|
+
}, (error: unknown) => {
|
|
121
|
+
current.ended();
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}
|
|
60
126
|
|
|
61
127
|
/**
|
|
62
128
|
* Pi's executor creates and runs one sandbox per script. Its first execution in `extras`' async
|
|
@@ -64,22 +130,8 @@ let sandboxPatched = false;
|
|
|
64
130
|
* do not.
|
|
65
131
|
*/
|
|
66
132
|
async function withSandboxExtras<T>(extras: SandboxExtras, run: () => Promise<T>): Promise<T> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const proto = (await loadCodemode()).CodemodeSandbox.prototype as unknown as Record<string, any>;
|
|
70
|
-
const execute = proto.execute;
|
|
71
|
-
proto.execute = function (this: Record<string, any>, code: string, options: Record<string, unknown> = {}) {
|
|
72
|
-
const current = sandboxExtras.getStore();
|
|
73
|
-
if (!current || current.used) return execute.call(this, code, options);
|
|
74
|
-
current.used = true;
|
|
75
|
-
for (const global of current.globals) this.globalsByName.set(global.name, global);
|
|
76
|
-
const timeoutMs = Math.min(current.timeoutMs, (options.timeoutMs as number | undefined) ?? this.timeoutMs);
|
|
77
|
-
return execute.call(this, current.prepare(code), { ...options, timeoutMs }).then((result: CodemodeResult) => {
|
|
78
|
-
current.result = result;
|
|
79
|
-
return result;
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
}
|
|
133
|
+
// Programs started together wait for the same patch.
|
|
134
|
+
await (sandboxPatch ??= patchSandbox());
|
|
83
135
|
return sandboxExtras.run(extras, run);
|
|
84
136
|
}
|
|
85
137
|
|
|
@@ -102,30 +154,94 @@ export async function executeProgram(
|
|
|
102
154
|
if (typeof path === "string" && list && !list.includes(path)) list.push(path);
|
|
103
155
|
};
|
|
104
156
|
let detachedCalls = 0;
|
|
157
|
+
const toolCalls: Promise<unknown>[] = [];
|
|
158
|
+
const executeTool = (name: string, args: unknown, callOptions: { signal?: AbortSignal }) => {
|
|
159
|
+
if (!options.detached) return ctx.executeTool(name, args, callOptions);
|
|
160
|
+
addFile(name, args);
|
|
161
|
+
const detachedOptions = { ...callOptions, detachedCallerId: `${toolCallId}:${++detachedCalls}`, detachedAbort: () => options.abort?.() } satisfies DetachedCallOptions;
|
|
162
|
+
return ctx.executeTool(name, args, detachedOptions as never);
|
|
163
|
+
};
|
|
105
164
|
const programCtx = Object.create(ctx, {
|
|
106
165
|
tools: { get: () => ctx.tools.filter((tool) => tool.name !== PROGRAM_TOOL_NAME) },
|
|
107
166
|
executeTool: {
|
|
108
167
|
value: (name: string, args: unknown, callOptions: { signal?: AbortSignal } = {}) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return ctx.executeTool(name, args, detachedOptions as never);
|
|
168
|
+
const call = executeTool(name, args, callOptions);
|
|
169
|
+
toolCalls.push(call);
|
|
170
|
+
return call;
|
|
113
171
|
},
|
|
114
172
|
},
|
|
115
173
|
}) as ExtensionToolContext;
|
|
174
|
+
// The globals' calls, each placed after the codemode calls seen when it started.
|
|
175
|
+
const own: Array<{ record: CallRecord; after: number; startedAt: number }> = [];
|
|
176
|
+
let seen: CallRecord[] = [];
|
|
177
|
+
let last: Parameters<AgentToolUpdateCallback<unknown>>[0] = { content: [], details: undefined };
|
|
178
|
+
let scriptEnded = false;
|
|
179
|
+
const merged = (): CallRecord[] => {
|
|
180
|
+
const calls: CallRecord[] = [];
|
|
181
|
+
let next = 0;
|
|
182
|
+
for (const { record, after } of own) {
|
|
183
|
+
while (next < after && next < seen.length) calls.push({ ...seen[next++]! });
|
|
184
|
+
calls.push({ ...record });
|
|
185
|
+
}
|
|
186
|
+
while (next < seen.length) calls.push({ ...seen[next++]! });
|
|
187
|
+
return calls;
|
|
188
|
+
};
|
|
189
|
+
const publish = () => options.onUpdate?.({ ...last, details: { ...(last.details as object | undefined), calls: merged() } });
|
|
190
|
+
const onUpdate: AgentToolUpdateCallback<unknown> = (update) => {
|
|
191
|
+
last = update;
|
|
192
|
+
seen = (update.details as { calls?: CallRecord[] } | undefined)?.calls ?? seen;
|
|
193
|
+
publish();
|
|
194
|
+
};
|
|
195
|
+
const finish = (entry: (typeof own)[number], status: "ok" | "error" | "cancelled", error?: string) => {
|
|
196
|
+
if (entry.record.status !== "running") return;
|
|
197
|
+
entry.record.status = status;
|
|
198
|
+
entry.record.durationMs = performance.now() - entry.startedAt;
|
|
199
|
+
if (error !== undefined) entry.record.error = preview(error, ERROR_PREVIEW_CHARS);
|
|
200
|
+
publish();
|
|
201
|
+
};
|
|
202
|
+
const track: CallTracker = (name, args) => {
|
|
203
|
+
const entry = { record: { id: `${toolCallId}/${name}-${own.length + 1}`, name, args: preview(args, ARGS_PREVIEW_CHARS), status: "running" as const }, after: seen.length, startedAt: performance.now() };
|
|
204
|
+
own.push(entry);
|
|
205
|
+
if (scriptEnded) finish(entry, "cancelled");
|
|
206
|
+
else publish();
|
|
207
|
+
return (status, error) => finish(entry, status, error);
|
|
208
|
+
};
|
|
209
|
+
// Calls still running when the script ends are cut off by it.
|
|
210
|
+
let cutOff: CallRecord[] = [];
|
|
116
211
|
const extras: SandboxExtras = {
|
|
117
|
-
globals: options.globals ?? [],
|
|
212
|
+
globals: options.globals?.(track) ?? [],
|
|
118
213
|
prepare: options.prepare ?? ((code) => code),
|
|
119
214
|
timeoutMs: options.timeout === undefined ? Number.POSITIVE_INFINITY : options.timeout * 1000,
|
|
120
215
|
used: false,
|
|
216
|
+
ended: () => {
|
|
217
|
+
scriptEnded = true;
|
|
218
|
+
const running = own.filter((entry) => entry.record.status === "running");
|
|
219
|
+
for (const entry of running) finish(entry, "cancelled");
|
|
220
|
+
cutOff = running.map((entry) => entry.record);
|
|
221
|
+
},
|
|
121
222
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
223
|
+
let result: Awaited<ReturnType<typeof executeCodemode>>;
|
|
224
|
+
try {
|
|
225
|
+
result = await withSandboxExtras(extras, () => executeCodemode(toolCallId, { code: source }, signal, onUpdate, programCtx, {
|
|
226
|
+
models: true,
|
|
227
|
+
appendEntry: options.appendEntry,
|
|
228
|
+
getToolNamespace: options.getToolNamespace,
|
|
229
|
+
}));
|
|
230
|
+
} finally {
|
|
231
|
+
if (!scriptEnded) extras.ended();
|
|
232
|
+
}
|
|
127
233
|
const outcome: ProgramOutcome = extras.result?.ok
|
|
128
234
|
? "completed"
|
|
129
235
|
: extras.result?.error.kind === "timeout" || extras.result?.error.kind === "aborted" ? "stopped" : "failed";
|
|
130
|
-
|
|
236
|
+
// codemode marks calls cut off by the script ending only in its result, after the last update.
|
|
237
|
+
seen = (result.details as { calls?: CallRecord[] } | undefined)?.calls ?? seen;
|
|
238
|
+
if (own.length > 0) result.details = { ...(result.details as object | undefined), calls: merged() };
|
|
239
|
+
// codemode lists the tool calls made before a failure; the globals' calls get a line of their own.
|
|
240
|
+
if (outcome !== "completed" && own.length > 0) {
|
|
241
|
+
appendNote(result.content, `Agent calls made before the failure (they are not undone): ${callNames(own.map((entry) => entry.record), true)}`);
|
|
242
|
+
} else if (outcome === "completed") {
|
|
243
|
+
const stopped = [...seen.filter((call) => call.status === "cancelled"), ...cutOff];
|
|
244
|
+
if (stopped.length > 0) appendNote(result.content, `Calls still running when the script returned were cancelled; their effects are not undone: ${callNames(stopped, false)}`);
|
|
245
|
+
}
|
|
246
|
+
return { outcome, result, files, toolCalls: Promise.allSettled(toolCalls) };
|
|
131
247
|
}
|
|
@@ -16,7 +16,10 @@ interface DetachedCall {
|
|
|
16
16
|
active: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// These two are shared with later copies of this module, such as after a reload, since the patches stay those of the first copy.
|
|
20
|
+
const detached = ((globalThis as Record<symbol, unknown>)[Symbol.for("pi-agents:detached")] ??= new AsyncLocalStorage<DetachedCall>()) as AsyncLocalStorage<DetachedCall>;
|
|
21
|
+
/** The Session whose extension command is running, including work the command started. */
|
|
22
|
+
const inCommand = ((globalThis as Record<symbol, unknown>)[Symbol.for("pi-agents:in-command")] ??= new AsyncLocalStorage<object>()) as AsyncLocalStorage<object>;
|
|
20
23
|
/** Set while the built-in MCP extension runs, including work it started. */
|
|
21
24
|
const inMcp = new AsyncLocalStorage<true>();
|
|
22
25
|
|
|
@@ -30,11 +33,54 @@ export interface DetachedCallOptions {
|
|
|
30
33
|
|
|
31
34
|
type Patchable = Record<PropertyKey, any>;
|
|
32
35
|
|
|
36
|
+
/** The Session running the current extension command. */
|
|
37
|
+
export function commandSession(): Patchable | undefined {
|
|
38
|
+
return inCommand.getStore() as Patchable | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Runs `work` as a detached call: `ctx.signal` and `ctx.abort()` of the Session's hooks and tools act on `signal` and `abort`. */
|
|
42
|
+
export async function runDetached<T>(session: Patchable, signal: AbortSignal | undefined, abort: (() => void) | undefined, work: () => Promise<T>): Promise<T> {
|
|
43
|
+
const extensions = session._extensionRunner as Patchable;
|
|
44
|
+
const getSignal = extensions.getSignalFn;
|
|
45
|
+
if (!getSignal[PATCHED]) {
|
|
46
|
+
const patched = () => {
|
|
47
|
+
const call = detached.getStore();
|
|
48
|
+
return call?.active ? call.signal : getSignal();
|
|
49
|
+
};
|
|
50
|
+
(patched as Patchable)[PATCHED] = true;
|
|
51
|
+
extensions.getSignalFn = patched;
|
|
52
|
+
}
|
|
53
|
+
const abortTurn = extensions.abortFn;
|
|
54
|
+
if (!abortTurn[PATCHED]) {
|
|
55
|
+
const patched = () => {
|
|
56
|
+
const call = detached.getStore();
|
|
57
|
+
return call?.active ? call.abort?.() : abortTurn();
|
|
58
|
+
};
|
|
59
|
+
(patched as Patchable)[PATCHED] = true;
|
|
60
|
+
extensions.abortFn = patched;
|
|
61
|
+
}
|
|
62
|
+
const call: DetachedCall = { signal, abort, active: true };
|
|
63
|
+
try {
|
|
64
|
+
return await detached.run(call, work);
|
|
65
|
+
} finally {
|
|
66
|
+
// Work the call started, such as a turn it queued, keeps the store but not the signal.
|
|
67
|
+
call.active = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Stands in for the assistant message Pi passes to hooks of nested calls, which a command's Session may lack. */
|
|
72
|
+
function commandMessage() {
|
|
73
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
|
74
|
+
return { role: "assistant", content: [], api: "none", provider: "none", model: "none", usage, stopReason: "stop", timestamp: Date.now() };
|
|
75
|
+
}
|
|
76
|
+
|
|
33
77
|
/**
|
|
34
78
|
* - A call with `detachedCallerId` runs under that ID: Pi numbers nested calls per caller and
|
|
35
79
|
* resets the numbers when the calling tool call returns and when the turn ends, which would
|
|
36
80
|
* repeat IDs across the calls of a background Program. While it runs, `ctx.signal` and
|
|
37
81
|
* `ctx.abort()` of hooks and tools act on the call instead of the caller's turn.
|
|
82
|
+
* - An extension command knows its Session through `commandSession()`, and its detached calls run
|
|
83
|
+
* before the Session has an assistant message, which Pi requires of nested calls.
|
|
38
84
|
* - The built-in MCP extension sees `program` as `codemode`: it activates `program` when its tools
|
|
39
85
|
* need a script tool and neither is active, counts an active `program` as reaching its tools,
|
|
40
86
|
* and makes a Program wait for the servers its code names, as it does for `codemode` scripts.
|
|
@@ -47,36 +93,19 @@ export function installHostPatches(
|
|
|
47
93
|
const sessionProto = session.prototype as unknown as Patchable;
|
|
48
94
|
if (!sessionProto[PATCHED]) {
|
|
49
95
|
sessionProto[PATCHED] = true;
|
|
96
|
+
const tryCommand = sessionProto._tryExecuteExtensionCommand;
|
|
97
|
+
sessionProto._tryExecuteExtensionCommand = function (this: Patchable, text: string) {
|
|
98
|
+
return inCommand.run(this, () => tryCommand.call(this, text));
|
|
99
|
+
};
|
|
100
|
+
const findAssistant = sessionProto._findLastAssistantMessage;
|
|
101
|
+
sessionProto._findLastAssistantMessage = function (this: Patchable) {
|
|
102
|
+
return findAssistant.call(this) ?? (inCommand.getStore() === this && detached.getStore()?.active ? commandMessage() : undefined);
|
|
103
|
+
};
|
|
50
104
|
const execute = sessionProto._executeNestedToolCall;
|
|
51
105
|
sessionProto._executeNestedToolCall = async function (this: Patchable, callerId: string, name: string, args: unknown, options: Record<string, unknown> = {}) {
|
|
52
106
|
const { detachedCallerId, detachedAbort, ...rest } = options;
|
|
53
107
|
if (typeof detachedCallerId !== "string") return execute.call(this, callerId, name, args, options);
|
|
54
|
-
|
|
55
|
-
const getSignal = extensions.getSignalFn;
|
|
56
|
-
if (!getSignal[PATCHED]) {
|
|
57
|
-
const patched = () => {
|
|
58
|
-
const call = detached.getStore();
|
|
59
|
-
return call?.active ? call.signal : getSignal();
|
|
60
|
-
};
|
|
61
|
-
(patched as Patchable)[PATCHED] = true;
|
|
62
|
-
extensions.getSignalFn = patched;
|
|
63
|
-
}
|
|
64
|
-
const abort = extensions.abortFn;
|
|
65
|
-
if (!abort[PATCHED]) {
|
|
66
|
-
const patched = () => {
|
|
67
|
-
const call = detached.getStore();
|
|
68
|
-
return call?.active ? call.abort?.() : abort();
|
|
69
|
-
};
|
|
70
|
-
(patched as Patchable)[PATCHED] = true;
|
|
71
|
-
extensions.abortFn = patched;
|
|
72
|
-
}
|
|
73
|
-
const call: DetachedCall = { signal: rest.signal as AbortSignal | undefined, abort: detachedAbort as (() => void) | undefined, active: true };
|
|
74
|
-
try {
|
|
75
|
-
return await detached.run(call, () => execute.call(this, detachedCallerId, name, args, rest));
|
|
76
|
-
} finally {
|
|
77
|
-
// Work the call started, such as a turn it queued, keeps the store but not the signal.
|
|
78
|
-
call.active = false;
|
|
79
|
-
}
|
|
108
|
+
return runDetached(this, rest.signal as AbortSignal | undefined, detachedAbort as (() => void) | undefined, () => execute.call(this, detachedCallerId, name, args, rest));
|
|
80
109
|
};
|
|
81
110
|
}
|
|
82
111
|
const runnerProto = runner.prototype as unknown as Patchable;
|
package/src/programs/loadout.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { highlightCode, type ExtensionAPI, type ToolDefinition } from "@earendil
|
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { CODEMODE_TOOL_NAME, type PiCodemode } from "./codemode.ts";
|
|
4
4
|
import { PROGRAM_TOOL_NAME } from "./execute.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { shortId } from "../agents/registry.ts";
|
|
6
|
+
import { formatDuration, formatToolCall, moreLines, renderResultList, renderTextResult, startDuration, type DurationState } from "../render-call.ts";
|
|
7
|
+
import type { Truncation } from "../output.ts";
|
|
8
|
+
import { omittedLine, runningLine, type ProgramResultDetails } from "./programs.ts";
|
|
6
9
|
|
|
7
10
|
/** The first line of the `codemode` description, which the `program` intro replaces. */
|
|
8
11
|
const CODEMODE_FIRST_LINE = "Run JavaScript code to orchestrate/compose tool calls\n";
|
|
@@ -91,9 +94,21 @@ export function programRenderers(piCodemode: PiCodemode): Renderers {
|
|
|
91
94
|
component.setText(text);
|
|
92
95
|
return component;
|
|
93
96
|
},
|
|
94
|
-
// `wait` shows how long it has waited
|
|
97
|
+
// `wait` shows how long it has waited, and when collapsed, each result as a line with the first lines of its output;
|
|
98
|
+
// the other results render like `codemode`.
|
|
95
99
|
renderResult: (result, options, theme, context) => (context.args as { action?: string }).action === "wait"
|
|
96
|
-
? renderTextResult(result, options, theme, context)
|
|
100
|
+
? renderTextResult(result, options, theme, context, () => {
|
|
101
|
+
const { results = [], running = [], interrupted, ...truncated } = (result.details ?? {}) as { results?: ProgramResultDetails[]; running?: string[]; interrupted?: boolean } & Partial<Truncation>;
|
|
102
|
+
const only = results[0];
|
|
103
|
+
// As `renderProgramWait` returns them, no results, or a single completed result alone, show as they are.
|
|
104
|
+
if (!only || (results.length === 1 && running.length === 0 && only.state === "completed")) return undefined;
|
|
105
|
+
const ids = [...results.map((entry) => entry.id), ...running];
|
|
106
|
+
const notes = [omittedLine(results.flatMap((entry) => entry.output === undefined ? [entry.id] : []), ids), runningLine(running, interrupted, ids)];
|
|
107
|
+
return renderResultList(results.flatMap(({ id, state, wallTimeSeconds, output }) => output === undefined ? [] : [{
|
|
108
|
+
line: [`Program ${shortId(id, ids)}`, state, ...(wallTimeSeconds === undefined ? [] : [formatDuration(wallTimeSeconds * 1000)])].join(" "),
|
|
109
|
+
body: output,
|
|
110
|
+
}]), notes, truncated, theme);
|
|
111
|
+
})
|
|
97
112
|
: piCodemode.renderResult(result, options, theme, context) as Text,
|
|
98
113
|
};
|
|
99
114
|
}
|