@aeondave/pi-persona 1.17.3 → 1.17.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/AGENTS.md +44 -36
- package/CHANGELOG.md +76 -1
- package/README.md +23 -13
- package/docs/ARCHITECTURE.md +97 -37
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +33 -8
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +65 -14
- package/package.json +12 -6
- package/src/agents/agent.ts +6 -0
- package/src/bridge.ts +25 -8
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +14 -4
- package/src/core/capabilities.ts +1 -1
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/session-runs.ts +1 -1
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +83 -19
- package/src/engine/async.ts +49 -10
- package/src/engine/child.ts +10 -2
- package/src/engine/inproc.ts +87 -46
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +6 -0
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +45 -17
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +20 -21
- package/src/extension/hooks.ts +952 -938
- package/src/extension/identity.ts +41 -75
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +17 -0
- package/src/extension.ts +2963 -2873
- package/src/loader.ts +11 -3
- package/src/orchestration/flow-journal.ts +7 -1
- package/src/orchestration/flow-run.ts +122 -19
- package/src/orchestration/flow.ts +11 -0
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +44 -6
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +1 -1
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/council.ts +40 -23
- package/src/tools/delegate-tool.ts +50 -14
- package/src/tools/delegate.ts +13 -6
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom.ts +20 -3
- package/src/tools/flow.ts +24 -8
- package/src/tools/intercom-tool.ts +98 -14
- package/src/tools/models.ts +15 -0
- package/src/tools/monitor.ts +20 -1
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +23 -2
- package/src/ui/agent-overlay.ts +11 -3
- package/src/ui/agent-tree.ts +43 -1
- package/src/ui/literal-tool-result.ts +1 -1
- package/src/ui/model-picker.ts +1 -1
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/session-cards.ts +191 -14
|
@@ -5,12 +5,13 @@ import { failureDetails } from "../extension/shared.ts";
|
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { compactInlineText, sanitizeTerminalText } from "../ui/presentation.ts";
|
|
7
7
|
import { compactVisibleText } from "../ui/presentation.ts";
|
|
8
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
8
9
|
import { createReportBody } from "../ui/report-body.ts";
|
|
9
10
|
import { coachingDisabledHint, expandDetailHint, reconcileAnsweredAsk } from "../extension/shared.ts";
|
|
10
11
|
import {
|
|
11
12
|
type AsyncRun, AsyncRunTracker, buildPeekDigest,
|
|
12
13
|
dedupeRunsById, getFullRunOutput, IdleCoalescingNotifier,
|
|
13
|
-
isAsyncRunStalled, runDisplayName, runDurationLabel,
|
|
14
|
+
isAsyncRunStalled, observedRunModel, runDisplayName, runDurationLabel,
|
|
14
15
|
} from "../engine/async.ts";
|
|
15
16
|
import type { PollGuard } from "../core/nudge.ts";
|
|
16
17
|
import { fenceUntrusted } from "../core/fence.ts";
|
|
@@ -21,11 +22,14 @@ import type { InProcessBus } from "../bus/inproc.ts";
|
|
|
21
22
|
import type { PersonaController } from "../persona/controller.ts";
|
|
22
23
|
import { emptyUsage } from "../engine/stream.ts";
|
|
23
24
|
import { toolUsageField, type ChildUsageLedger } from "../ui/usage.ts";
|
|
25
|
+
import type { TerminalResultArchive } from "../results/archive.ts";
|
|
26
|
+
import { pageLiveResult, projectResultPage, resultReceiptHint } from "../results/retrieval.ts";
|
|
24
27
|
|
|
25
28
|
export interface IntercomToolDeps {
|
|
26
29
|
get lastCtx(): ExtensionContext | undefined;
|
|
27
30
|
set lastCtx(value: ExtensionContext | undefined);
|
|
28
31
|
tracker: AsyncRunTracker;
|
|
32
|
+
archive(ctx: ExtensionContext): TerminalResultArchive;
|
|
29
33
|
completionNotifier: IdleCoalescingNotifier<AsyncRun>;
|
|
30
34
|
intercomNotifier: IdleCoalescingNotifier<PendingAsk>;
|
|
31
35
|
controller: PersonaController;
|
|
@@ -131,6 +135,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
131
135
|
[
|
|
132
136
|
Type.Literal("peek"),
|
|
133
137
|
Type.Literal("result"),
|
|
138
|
+
Type.Literal("results"),
|
|
134
139
|
Type.Literal("steer"),
|
|
135
140
|
Type.Literal("stop"),
|
|
136
141
|
Type.Literal("list"),
|
|
@@ -141,10 +146,15 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
141
146
|
],
|
|
142
147
|
{
|
|
143
148
|
description:
|
|
144
|
-
"peek = status of async sub-agents · result =
|
|
149
|
+
"peek = status of async sub-agents · results = bounded archive summaries (cursor/limit) · result = page a settled live result by to:run-id or a saved report by ref:result-UUID · steer = soft redirect into one by run id (it may ignore it) · stop = request engine cancellation by run id · message = retrieve one drained bus message by message id · list/inbox/reply/send = coaching messages (needs a coaching persona)",
|
|
145
150
|
},
|
|
146
151
|
),
|
|
147
152
|
to: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "result/steer/stop/peek: the async run id (e.g. 'run-1'); stop: \"all\" cancels every running run · send: the child handle (from `list`)" })),
|
|
153
|
+
ref: Type.Optional(Type.String({ maxLength: 43, description: "result: opaque result:<UUID> archive reference; never a run id. Mutually exclusive with to." })),
|
|
154
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "result: stored-body UTF-16 offset (default 0); use nextOffset, never split a surrogate pair." })),
|
|
155
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000, description: "result: 2..16000 UTF-16 units (default 8000). results: 1..50 summaries (default 20)." })),
|
|
156
|
+
cursor: Type.Optional(Type.Integer({ minimum: 0, description: "results: nextCursor from the prior bounded summary page; omit for the first page." })),
|
|
157
|
+
collect: Type.Optional(Type.Boolean({ description: "result with live to only: true acknowledges the whole report and suppresses its pending completion. Paging alone does not collect." })),
|
|
148
158
|
messageId: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "message: the drained bus message id shown by `inbox`" })),
|
|
149
159
|
askId: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "reply: the message id of the child's pending question" })),
|
|
150
160
|
message: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_MESSAGE_CHARS, description: "steer/reply/send: the text to deliver" })),
|
|
@@ -154,7 +164,10 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
154
164
|
label: "Intercom",
|
|
155
165
|
description: [
|
|
156
166
|
"Steer, stop and inspect your background sub-agents; a finished run or batch wakes you with a completion follow-up.",
|
|
157
|
-
"Never `peek` healthy children; use it only for status you need. `result { to: runId }`
|
|
167
|
+
"Never `peek` healthy children; use it only for status you need. `result { to: runId }` pages a live settled payload by run id (8000 units default, 16000 maximum).",
|
|
168
|
+
"`results { cursor?, limit? }` lists saved terminal summaries across this session's branches (20 default, 50 maximum); pass nextCursor for the next page.",
|
|
169
|
+
"`result { ref: 'result:<UUID>', offset?, limit? }` reads a saved report, including after reload, without collecting, billing, restarting a worker or waking the model. Memory-only references last only for this activation. Run ids route only the current runtime; never guess a historical run by id.",
|
|
170
|
+
"Use nextOffset for more UTF-16 pages. Only a whole live body returned in one read, or explicit live collect:true, suppresses a pending completion. Archive ref reads reject collect.",
|
|
158
171
|
"`steer` sends a soft course-correction to one (by run id); in-process async runs can receive it mid-run,",
|
|
159
172
|
"while child-engine (MCP/worktree) async runs receive it as a brokered follow-up. After remaining tools return, Pi can consume steering; arbitrary active tool work is not promised to stop immediately.",
|
|
160
173
|
"`message` retrieves a drained bus message by its message id; `list`/`inbox`/`reply`/`send` exchange coaching messages (a child reaches you via `contact_supervisor`)",
|
|
@@ -180,8 +193,29 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
180
193
|
if (refusal) return refusal;
|
|
181
194
|
return { content: [{ type: "text", text: buildPeekDigest(runs, { now: Date.now(), stallMs: d.STALL_FLAG_MS, ...(waiting ? { waitingForSupervisor: waiting } : {}) }) }], details: { action: "peek", ok: true, ...targetSnapshot }, isError: false };
|
|
182
195
|
}
|
|
196
|
+
if (params.action === "results" || (params.action === "result" && params.ref !== undefined)) {
|
|
197
|
+
const invalid = params.to !== undefined || params.collect !== undefined || (params.action === "results" && (params.ref !== undefined || params.offset !== undefined)) || (params.action === "result" && params.cursor !== undefined);
|
|
198
|
+
if (invalid) return { content: [{ type: "text", text: "Archive reads accept ref/offset/limit; results accepts cursor/limit. to and collect are live-only." }], details: failureDetails({ action: params.action }), isError: true };
|
|
199
|
+
const archive = d.archive(ctx);
|
|
200
|
+
if (params.action === "results") {
|
|
201
|
+
const page = archive.list({ ...(params.cursor !== undefined ? { cursor: params.cursor } : {}), ...(params.limit !== undefined ? { limit: params.limit } : {}) });
|
|
202
|
+
const text = page.ok ? ["Saved terminal reports (not live workers):", ...page.results.map((r) => `${r.ref} · ${sanitizeDisplayLabel(r.label ?? r.agent)} · ${r.status} · ${r.storage}${r.truncated ? " · truncated" : ""}`), ...(page.nextCursor !== undefined ? [`nextCursor: ${page.nextCursor}`] : []), ...(page.warnings.length ? [`Storage warnings: ${page.warnings.join(", ")}`] : [])].join("\n") : page.message;
|
|
203
|
+
return { content: [{ type: "text", text }], details: { action: "results", ...page }, isError: !page.ok };
|
|
204
|
+
}
|
|
205
|
+
const page = archive.read({ ref: params.ref!, ...(params.offset !== undefined ? { offset: params.offset } : {}), ...(params.limit !== undefined ? { limit: params.limit } : {}) });
|
|
206
|
+
if (!page.ok) {
|
|
207
|
+
return { content: [{ type: "text", text: page.message }], details: { action: "result", ...page }, isError: true };
|
|
208
|
+
}
|
|
209
|
+
// Bounded-page rule: the fenced body is delivered once, in content; details stay
|
|
210
|
+
// bounded metadata exactly like the live result path (no whole page hidden there).
|
|
211
|
+
const { body: _pageBody, ...pageMeta } = page;
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: `${page.summary.ref} · ${page.summary.status} · ${page.summary.storage}${page.summary.truncated ? " · saved body truncated" : ""}\n${projectResultPage(page, page.summary.error)}` }],
|
|
214
|
+
details: { action: "result", ...pageMeta }, isError: false,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
183
217
|
if (params.action === "result") {
|
|
184
|
-
if (!params.to) {
|
|
218
|
+
if (!params.to || params.cursor !== undefined) {
|
|
185
219
|
return { content: [{ type: "text", text: "intercom result needs { to: <run id> }." }], details: failureDetails({ action: "result", ok: false }), isError: true };
|
|
186
220
|
}
|
|
187
221
|
const run = d.tracker.peek(params.to);
|
|
@@ -199,14 +233,17 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
199
233
|
// same result cannot appear again as a follow-up a moment later. Telling the tracker too
|
|
200
234
|
// makes this retained copy the first thing retention evicts — the supervisor has read it,
|
|
201
235
|
// so it is the cheapest payload in the map to lose.
|
|
202
|
-
d.completionNotifier.discard((pending) => pending.id === run.id);
|
|
203
|
-
d.tracker.markCollected(run.id);
|
|
204
236
|
const full = getFullRunOutput(run);
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
237
|
+
const page = pageLiveResult(full, params.offset, params.limit);
|
|
238
|
+
if (!page) return { content: [{ type: "text", text: "Invalid result offset/limit; use nextOffset and 2..16000 units." }], details: failureDetails({ action: "result", ...targetSnapshot }), isError: true };
|
|
239
|
+
if (params.collect === true || (page.offset === 0 && page.endOfStoredBody)) {
|
|
240
|
+
d.completionNotifier.discard((pending) => pending.id === run.id);
|
|
241
|
+
d.tracker.markCollected(run.id);
|
|
242
|
+
}
|
|
243
|
+
const body = projectResultPage(page, run.error);
|
|
244
|
+
const receipt = resultReceiptHint(run.resultReceipt);
|
|
245
|
+
const displayRun = runDisplayName(run);
|
|
246
|
+
const modelUsed = observedRunModel(run);
|
|
210
247
|
// The wall time this leg took: an explicit collection is one of the paths a completion reaches
|
|
211
248
|
// the supervisor through, so it carries the same reading the passive completion report does.
|
|
212
249
|
const took = runDurationLabel(run);
|
|
@@ -216,8 +253,8 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
216
253
|
const billed = d.childUsage.accountMany([{ key: run.id, usage: run.result?.usage ?? emptyUsage() }]);
|
|
217
254
|
d.publishPersonaCost();
|
|
218
255
|
return {
|
|
219
|
-
content: [{ type: "text", text: `${run.id} (${displayRun}) · ${run.status}${took ? ` · ${took}` : ""}${
|
|
220
|
-
details: { action: "result", ok: true, status: run.status, ...targetSnapshot },
|
|
256
|
+
content: [{ type: "text", text: `${run.id} (${displayRun}) · ${run.status}${took ? ` · ${took}` : ""}${modelUsed ? `\nObserved model: ${modelUsed}` : ""}\n${body}${receipt ? `\n${receipt}` : ""}${note ? `\n\n${note}` : ""}` }],
|
|
257
|
+
details: { action: "result", ok: true, status: run.status, ...targetSnapshot, ...(modelUsed ? { modelUsed } : {}), page: { offset: page.offset, nextOffset: page.nextOffset, endOfStoredBody: page.endOfStoredBody }, ...(run.resultReceipt ? { resultReceipt: { ...run.resultReceipt } } : {}) },
|
|
221
258
|
isError: false,
|
|
222
259
|
...toolUsageField(billed),
|
|
223
260
|
};
|
|
@@ -317,7 +354,12 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
317
354
|
return component;
|
|
318
355
|
},
|
|
319
356
|
renderResult(result, { expanded }, theme, context) {
|
|
320
|
-
const details = (result.details ?? {}) as {
|
|
357
|
+
const details = (result.details ?? {}) as {
|
|
358
|
+
action?: string; ok?: boolean; runId?: string; target?: string; message?: string;
|
|
359
|
+
results?: Array<{ truncated?: boolean }>;
|
|
360
|
+
summary?: { ref?: string; storage?: string; storedChars?: number; truncated?: boolean };
|
|
361
|
+
nextCursor?: number; nextOffset?: number; warnings?: readonly string[];
|
|
362
|
+
};
|
|
321
363
|
const state = context?.state as IntercomRenderState | undefined;
|
|
322
364
|
const action = details.action ?? context?.args?.action;
|
|
323
365
|
const savedId = details.runId ?? context?.args?.to;
|
|
@@ -353,7 +395,20 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
353
395
|
const showTitle = !state?.callText || !preview;
|
|
354
396
|
return new Text(`${showTitle ? theme.fg("success", title) : ""}${preview ? `${showTitle ? "\n" : ""}${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}` : ""}`, 0, 0);
|
|
355
397
|
}
|
|
398
|
+
// Archive views are listings/diagnostics, never Markdown reports. A successful archive
|
|
399
|
+
// read carries no runId (live results do) and a summary (its own receipt); the invalid
|
|
400
|
+
// argument mix and failed reads fall through to the literal failure rendering below.
|
|
401
|
+
const listing = details.action === "results" && details.ok === true && Array.isArray(details.results) ? details.results : undefined;
|
|
402
|
+
const archivedPage = details.action === "result" && details.ok === true
|
|
403
|
+
&& typeof details.summary === "object" && details.summary !== null && details.runId === undefined
|
|
404
|
+
? details.summary
|
|
405
|
+
: undefined;
|
|
356
406
|
if (expanded) {
|
|
407
|
+
// Archive pages/listings stay literal via the shared renderer — complete sanitized
|
|
408
|
+
// content, no Markdown re-render of a quote-fenced page or a ref listing.
|
|
409
|
+
if (!failed && (listing || archivedPage)) {
|
|
410
|
+
return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
411
|
+
}
|
|
357
412
|
// Explicit report/message actions only; missing/running diagnostics and control
|
|
358
413
|
// receipts remain literal. Older history with no action is not guessed from prose.
|
|
359
414
|
if (!failed && ["result", "message", "inbox"].includes(action ?? "")) {
|
|
@@ -361,6 +416,35 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
361
416
|
}
|
|
362
417
|
return new Text(`${prefix}${theme.fg("toolOutput", full)}`, 0, 0);
|
|
363
418
|
}
|
|
419
|
+
if (!failed && listing) {
|
|
420
|
+
// Purpose line first (count + paging/truncation/storage honesty), then a bounded
|
|
421
|
+
// listing preview; the payload's own header line is replaced by the purpose line.
|
|
422
|
+
const truncated = listing.filter((entry) => entry?.truncated === true).length;
|
|
423
|
+
const flags = [
|
|
424
|
+
...(details.nextCursor !== undefined ? [`next page: cursor ${details.nextCursor}`] : []),
|
|
425
|
+
...(truncated > 0 ? [`${truncated} truncated`] : []),
|
|
426
|
+
...(details.warnings?.length ? [`warnings: ${details.warnings.join(", ")}`] : []),
|
|
427
|
+
];
|
|
428
|
+
const purpose = compactInlineText(`results · ${listing.length} retained${flags.length ? ` · ${flags.join(" · ")}` : ""}`, { maxChars: 100 });
|
|
429
|
+
const preview = compactVisibleText(full.split("\n").slice(1).join("\n"), { maxLines: 3, maxLineChars: 100 });
|
|
430
|
+
return new Text(`${theme.fg("success", purpose)}\n${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}`, 0, 0);
|
|
431
|
+
}
|
|
432
|
+
if (!failed && archivedPage) {
|
|
433
|
+
// Page position reads against the STORED body (originals may be truncated); honesty
|
|
434
|
+
// flags surface collapsed so a memory-only or truncated receipt is never mistaken
|
|
435
|
+
// for a durable whole report without expanding.
|
|
436
|
+
const end = details.nextOffset ?? archivedPage.storedChars ?? 0;
|
|
437
|
+
const flags = [
|
|
438
|
+
...(archivedPage.truncated ? ["truncated"] : []),
|
|
439
|
+
...(archivedPage.storage === "memory-only" ? ["memory-only"] : []),
|
|
440
|
+
...(details.warnings?.length ? [`warnings: ${details.warnings.join(", ")}`] : []),
|
|
441
|
+
];
|
|
442
|
+
const purpose = compactInlineText(`result · ${archivedPage.ref ?? "?"} · page ${end}/${archivedPage.storedChars ?? "?"}${flags.length ? ` · ${flags.join(" · ")}` : ""}`, { maxChars: 100 });
|
|
443
|
+
// The payload's receipt header and paging diagnostic line are replaced by the purpose
|
|
444
|
+
// line; the fence wrapper is dropped by the preview, leaving the page body quoted.
|
|
445
|
+
const preview = compactVisibleText(full.split("\n").slice(2).join("\n"), { maxLines: 3, maxLineChars: 100 });
|
|
446
|
+
return new Text(`${theme.fg("success", purpose)}\n${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}`, 0, 0);
|
|
447
|
+
}
|
|
364
448
|
const preview = compactVisibleText(full, { maxLines: 4, maxLineChars: 100 });
|
|
365
449
|
const hint = preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : "";
|
|
366
450
|
return new Text(`${prefix}${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
|
package/src/tools/models.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import { compactInlineText } from "../ui/presentation.ts";
|
|
6
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
6
7
|
|
|
7
8
|
export interface ModelsToolDeps {
|
|
8
9
|
get lastCtx(): ExtensionContext | undefined;
|
|
@@ -45,5 +46,19 @@ export function registerModelsTool(pi: ExtensionAPI, d: ModelsToolDeps): void {
|
|
|
45
46
|
const query = compactInlineText(args.query ?? "(shortlist)", { maxChars: 96 }) || "(shortlist)";
|
|
46
47
|
return new Text(`${theme.fg("toolTitle", theme.bold("models "))}${theme.fg("dim", query)}`, 0, 0);
|
|
47
48
|
},
|
|
49
|
+
renderResult(result, { expanded }, theme, context) {
|
|
50
|
+
// The catalog itself is literal data: the shared renderer bounds it collapsed and
|
|
51
|
+
// shows the complete sanitized listing expanded.
|
|
52
|
+
if (expanded) return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
53
|
+
const failed = context?.isError ?? result.isError ?? false;
|
|
54
|
+
if (failed) return renderLiteralToolResult(result, { expanded: false }, theme, context);
|
|
55
|
+
const details = (result.details ?? {}) as { total?: number; shown?: number; shortlist?: boolean };
|
|
56
|
+
const total = typeof details.total === "number" && Number.isFinite(details.total) ? details.total : 0;
|
|
57
|
+
const shown = typeof details.shown === "number" && Number.isFinite(details.shown) ? details.shown : 0;
|
|
58
|
+
const summary = shown === 0
|
|
59
|
+
? "no catalog matches"
|
|
60
|
+
: `${total} catalog models · showing ${shown}${details.shortlist ? " nearby candidates" : " matches"}`;
|
|
61
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("models · "))}${theme.fg("accent", compactInlineText(summary, { maxChars: 96 }))}`, 0, 0);
|
|
62
|
+
},
|
|
48
63
|
});
|
|
49
64
|
}
|
package/src/tools/monitor.ts
CHANGED
|
@@ -4,8 +4,10 @@ import { Type } from "typebox";
|
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import type { MonitorManager } from "../core/monitor.ts";
|
|
6
6
|
import { neutralizeMonitorUnicode } from "../core/monitor-output.ts";
|
|
7
|
+
import { formatDuration } from "../core/time.ts";
|
|
7
8
|
import { armWakeAdvice, failureDetails } from "../extension/shared.ts";
|
|
8
9
|
import { compactInlineText } from "../ui/presentation.ts";
|
|
10
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
9
11
|
import { fenceUntrusted, stripTerminalControls } from "../core/fence.ts";
|
|
10
12
|
import type { MonitorArchive, MonitorLogRecord } from "../monitor/archive.ts";
|
|
11
13
|
|
|
@@ -88,12 +90,29 @@ export function registerMonitorTool(pi: ExtensionAPI, d: MonitorToolDeps): void
|
|
|
88
90
|
...(params.maxEvents !== undefined ? { maxEvents: params.maxEvents } : {}),
|
|
89
91
|
});
|
|
90
92
|
if (!result.ok) return fail(result.error);
|
|
91
|
-
return { content: [{ type: "text" as const, text: `Armed ${result.entry.id} (${compactInlineText(result.entry.label, { maxChars: 120 })}); ${params.mode === "exit" ? "wakes when this job exits" : "wakes on relevant stdout lines"}. Deadline ${new Date(result.entry.expiresAtEpochMs).toISOString()}. ${armWakeAdvice(ctx, "watch", "condition", "Keep this session open. Continue independent work, or end your turn; it wakes you by itself, so never sleep, poll, or arm a `timer` to wait for it.")}` }], details: { ok: true, id: result.entry.id, expiresAtEpochMs: result.entry.expiresAtEpochMs } };
|
|
93
|
+
return { content: [{ type: "text" as const, text: `Armed ${result.entry.id} (${compactInlineText(result.entry.label, { maxChars: 120 })}); ${params.mode === "exit" ? "wakes when this job exits" : "wakes on relevant stdout lines"}. Deadline ${new Date(result.entry.expiresAtEpochMs).toISOString()}. ${armWakeAdvice(ctx, "watch", "condition", "Keep this session open. Continue independent work, or end your turn; it wakes you by itself, so never sleep, poll, or arm a `timer` to wait for it.")}` }], details: { ok: true, id: result.entry.id, expiresAtEpochMs: result.entry.expiresAtEpochMs, label: compactInlineText(result.entry.label, { maxChars: 120 }), mode: params.mode } };
|
|
92
94
|
},
|
|
93
95
|
renderCall(args, theme) {
|
|
94
96
|
const action = compactInlineText(args.action ?? "", { maxChars: 12 });
|
|
95
97
|
const label = compactInlineText(args.label ?? args.id ?? "", { maxChars: 96 });
|
|
96
98
|
return new Text(`${theme.fg("toolTitle", theme.bold(`monitor ${action}`))}${label ? ` ${theme.fg("dim", label)}` : ""}`, 0, 0);
|
|
97
99
|
},
|
|
100
|
+
renderResult(result, { expanded }, theme, context) {
|
|
101
|
+
const details = (result.details ?? {}) as { ok?: boolean; label?: string; mode?: string; expiresAtEpochMs?: number };
|
|
102
|
+
const failed = context?.isError ?? result.isError ?? false;
|
|
103
|
+
const isArm = !failed && typeof details.label === "string" && (details.mode === "output" || details.mode === "exit");
|
|
104
|
+
if (!isArm) {
|
|
105
|
+
// Failures and every list/cancel/result receipt share the one literal renderer:
|
|
106
|
+
// error heading, bounded sanitized preview, expansion hint.
|
|
107
|
+
return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
108
|
+
}
|
|
109
|
+
if (expanded) return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
110
|
+
const left = typeof details.expiresAtEpochMs === "number" && Number.isFinite(details.expiresAtEpochMs)
|
|
111
|
+
? details.expiresAtEpochMs - Date.now()
|
|
112
|
+
: undefined;
|
|
113
|
+
const deadline = left === undefined ? "" : left > 0 ? ` · deadline in ${formatDuration(left)}` : " · deadline reached";
|
|
114
|
+
const summary = compactInlineText(`${details.label} · ${details.mode}${deadline}`, { maxChars: 140 });
|
|
115
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("armed "))}${theme.fg("accent", summary)}`, 0, 0);
|
|
116
|
+
},
|
|
98
117
|
});
|
|
99
118
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { fenceUntrusted } from "../core/fence.ts";
|
|
5
|
+
import { RESULT_READ_DEFAULT, RESULT_READ_MAX } from "../results/types.ts";
|
|
6
|
+
import { isReadRequest, isReadReplyFor, readFailure, type ReadReply, type ScopedReader } from "../bus/result-reader.ts";
|
|
7
|
+
import { compactInlineText } from "../ui/presentation.ts";
|
|
8
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
9
|
+
|
|
10
|
+
const Params = Type.Object({
|
|
11
|
+
ref: Type.String({ minLength: 43, maxLength: 43, description: "Granted opaque result:<UUID> reference, not a file path or run id." }),
|
|
12
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "UTF-16 offset; use nextOffset from the previous page." })),
|
|
13
|
+
limit: Type.Optional(Type.Integer({ minimum: 2, maximum: RESULT_READ_MAX, default: RESULT_READ_DEFAULT, description: "Maximum UTF-16 characters to read." })),
|
|
14
|
+
}, { additionalProperties: false });
|
|
15
|
+
type PageDetails = Omit<Extract<ReadReply, { ok: true }>, "body">;
|
|
16
|
+
|
|
17
|
+
/** Available only when the parent explicitly grants a scoped reader. No archive browsing,
|
|
18
|
+
* implicit fallback, polling, messages or model wakes. Fencing happens exactly once here.
|
|
19
|
+
* Wrapped in `defineTool` so callers that store the definition in a wider-typed variable
|
|
20
|
+
* (engine registration sites) keep renderer parameter inference. */
|
|
21
|
+
export function makeReadResultTool(reader: Pick<ScopedReader, "read">) {
|
|
22
|
+
return defineTool<typeof Params, PageDetails>({
|
|
23
|
+
name: "read_result",
|
|
24
|
+
label: "Read result",
|
|
25
|
+
description: "Read one bounded page of a terminal result explicitly granted by your supervisor. Use only a supplied result:<UUID> reference. Follow nextOffset for another page; endOfStoredBody ends the retained report, which may be truncated. No search, paths, live runs or polling.",
|
|
26
|
+
parameters: Params,
|
|
27
|
+
async execute(_id, request, signal) {
|
|
28
|
+
if (signal?.aborted) throw new Error(readFailure("cancelled").message);
|
|
29
|
+
if (!isReadRequest(request)) throw new Error(readFailure("invalid").message);
|
|
30
|
+
let result: ReadReply;
|
|
31
|
+
try { result = await reader.read(request, signal); }
|
|
32
|
+
catch { throw new Error(readFailure(signal?.aborted ? "cancelled" : "unavailable").message); }
|
|
33
|
+
if (signal?.aborted) throw new Error(readFailure("cancelled").message);
|
|
34
|
+
try {
|
|
35
|
+
if (!isReadReplyFor(result, request)) throw new Error();
|
|
36
|
+
} catch { throw new Error(readFailure("protocol").message); }
|
|
37
|
+
if (!result.ok) throw new Error(result.message);
|
|
38
|
+
const { body, ...details } = result;
|
|
39
|
+
return { content: [{ type: "text", text: `${JSON.stringify(details)}\n${fenceUntrusted(body)}` }], details };
|
|
40
|
+
},
|
|
41
|
+
renderCall(args, theme) {
|
|
42
|
+
const ref = compactInlineText(args.ref ?? "", { maxChars: 48 });
|
|
43
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("read_result"))}${ref ? ` ${theme.fg("dim", ref)}` : ""}`, 0, 0);
|
|
44
|
+
},
|
|
45
|
+
renderResult(result, { expanded }, theme, context) {
|
|
46
|
+
// The granted page IS literal content: the shared renderer bounds it collapsed with an
|
|
47
|
+
// expansion hint and shows the complete sanitized receipt (details line + fenced body)
|
|
48
|
+
// expanded; native context.isError drives the failure heading.
|
|
49
|
+
return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
package/src/tools/timer.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/** Session timer tool — arm a wall-clock wake. */
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
5
|
import { armWakeAdvice, failureDetails } from "../extension/shared.ts";
|
|
5
6
|
import { formatRemaining, type TimerScheduler } from "../core/timer.ts";
|
|
6
|
-
import { parseInstant } from "../core/time.ts";
|
|
7
|
+
import { formatDuration, parseInstant } from "../core/time.ts";
|
|
8
|
+
import { compactInlineText } from "../ui/presentation.ts";
|
|
9
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
7
10
|
import { readClockSnapshot } from "../extension/clock.ts";
|
|
8
11
|
|
|
9
12
|
export interface TimerToolDeps {
|
|
@@ -97,7 +100,25 @@ export function registerTimerTool(pi: ExtensionAPI, d: TimerToolDeps): void {
|
|
|
97
100
|
}
|
|
98
101
|
const e = r.entry;
|
|
99
102
|
const text = `Armed ${e.id} (${e.label}) — fires in ${formatRemaining(r.remainingMs ?? e.fireAtEpochMs - e.armedAtEpochMs)} [${new Date(e.fireAtEpochMs).toISOString()}]. ${armWakeAdvice(ctx, "alarm", "time-gated step", "You can end this turn now; keep the session open for the alarm.")}`;
|
|
100
|
-
return { content: [{ type: "text", text }], details: { action: "arm", id: e.id, fireAtEpochMs: e.fireAtEpochMs, ok: true }, isError: false };
|
|
103
|
+
return { content: [{ type: "text", text }], details: { action: "arm", id: e.id, fireAtEpochMs: e.fireAtEpochMs, label: compactInlineText(e.label, { maxChars: 120 }), ok: true }, isError: false };
|
|
104
|
+
},
|
|
105
|
+
renderCall(args, theme) {
|
|
106
|
+
const action = compactInlineText(args.action ?? "", { maxChars: 12 });
|
|
107
|
+
const label = compactInlineText(args.label ?? args.id ?? "", { maxChars: 96 });
|
|
108
|
+
return new Text(`${theme.fg("toolTitle", theme.bold(`timer ${action}`))}${label ? ` ${theme.fg("dim", label)}` : ""}`, 0, 0);
|
|
109
|
+
},
|
|
110
|
+
renderResult(result, { expanded }, theme, context) {
|
|
111
|
+
const details = (result.details ?? {}) as { action?: string; ok?: boolean; id?: string; label?: string; fireAtEpochMs?: number };
|
|
112
|
+
const failed = context?.isError ?? result.isError ?? false;
|
|
113
|
+
const fireAt = typeof details.fireAtEpochMs === "number" ? details.fireAtEpochMs : undefined;
|
|
114
|
+
const isArm = !failed && details.action === "arm" && details.ok === true && fireAt !== undefined;
|
|
115
|
+
// Failures and every now/list/cancel receipt share the one literal renderer:
|
|
116
|
+
// error heading, bounded sanitized preview, expansion hint.
|
|
117
|
+
if (!isArm || expanded) return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
118
|
+
const left = fireAt - Date.now();
|
|
119
|
+
const due = left > 0 ? `fires in ${formatDuration(left)}` : "due now";
|
|
120
|
+
const summary = compactInlineText(`${details.label ?? details.id ?? "alarm"} · ${due}`, { maxChars: 140 });
|
|
121
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("armed "))}${theme.fg("accent", summary)}`, 0, 0);
|
|
101
122
|
},
|
|
102
123
|
});
|
|
103
124
|
}
|
package/src/ui/agent-overlay.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
wrapTextWithAnsi,
|
|
24
24
|
} from "@earendil-works/pi-tui";
|
|
25
25
|
|
|
26
|
-
import { type AgentNode, type AgentTree, type FlatRow, deriveWaitingNodeIds, flattenTree, isWakeSource, nodeGlyph, runningAnnotation } from "./agent-tree.ts";
|
|
26
|
+
import { type AgentNode, type AgentTree, type FlatRow, deriveWaitingNodeIds, effortTag, flattenTree, isWakeSource, nodeGlyph, runningAnnotation, thinkingDetailLine } from "./agent-tree.ts";
|
|
27
27
|
import { LiveClock } from "./live-clock.ts";
|
|
28
28
|
import { visibleWindow } from "./model-picker.ts";
|
|
29
29
|
import { compactInlineText, OPEN_SEQUENCE_TAIL, sanitizeTerminalText } from "./presentation.ts";
|
|
@@ -77,8 +77,12 @@ export function composeAgentRow(input: AgentRowInput): string {
|
|
|
77
77
|
|
|
78
78
|
const prefixCols = 2 + visibleWidth(indent); // "▸ " or " ", then the indent
|
|
79
79
|
const avail = Math.max(0, bodyMax - prefixCols - 2); // 2 = glyph + space
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// The compact effort sigla rides the label (` · xh` / ` · xh→h`); its width comes out of the
|
|
81
|
+
// label budget so the appended field never becomes the part a tight row eats first. The final
|
|
82
|
+
// clamp below still guarantees the row fits either way.
|
|
83
|
+
const effort = effortTag(node.thinking, node.thinkingEffective);
|
|
84
|
+
const labelMax = Math.max(8, Math.floor(avail * 0.62) - visibleWidth(effort));
|
|
85
|
+
const label = (safeInline(node.label, labelMax) || "agent") + effort;
|
|
82
86
|
const detailMax = node.detail ? Math.max(0, avail - labelMax - 2) : 0;
|
|
83
87
|
const detail = detailMax > 0 ? safeInline(node.detail ?? "", detailMax) : "";
|
|
84
88
|
const detailText = detail ? t.fg("dim", ` ${detail}`) : "";
|
|
@@ -383,6 +387,10 @@ export class AgentOverlay extends Container {
|
|
|
383
387
|
new Text(`${nodeGlyph(node)} ${t.fg("accent", t.bold(safeInline(node.label) || "agent"))}${live ? t.fg("success", wake ? " ● armed" : " ● live") : ""}`, 1, 0),
|
|
384
388
|
);
|
|
385
389
|
if (node.detail) this.addChild(new Text(t.fg("dim", safeInline(node.detail)), 1, 0));
|
|
390
|
+
// The drill-in has room for FULL effort names (the rows stay compact): an observed level
|
|
391
|
+
// appears only once a terminal result actually reported one.
|
|
392
|
+
const effortLine = thinkingDetailLine(node.thinking, node.thinkingEffective);
|
|
393
|
+
if (effortLine) this.addChild(new Text(t.fg("dim", effortLine), 1, 0));
|
|
386
394
|
this.addChild(new Spacer(1));
|
|
387
395
|
|
|
388
396
|
const raw = node.output?.trim()
|
package/src/ui/agent-tree.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { compactTokens } from "../core/format.ts";
|
|
2
|
+
import { effortFullName, effortSigla } from "../core/leg-thinking.ts";
|
|
2
3
|
import { formatDuration } from "../core/time.ts";
|
|
4
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
3
5
|
import type { ChildUsage } from "../engine/stream.ts";
|
|
4
6
|
import { compactInlineText } from "./presentation.ts";
|
|
5
7
|
import { formatUsage } from "./usage.ts";
|
|
@@ -25,6 +27,13 @@ export interface AgentNode {
|
|
|
25
27
|
/** Configured agent definition (operator/reviewer/…), if known. */
|
|
26
28
|
agent?: string;
|
|
27
29
|
model?: string;
|
|
30
|
+
/** Requested per-leg thinking effort, known at launch (the spec-level value the launch row
|
|
31
|
+
* carried). Rendered as the compact sigla right after the model; absent/off render nothing. */
|
|
32
|
+
thinking?: ThinkingLevel;
|
|
33
|
+
/** Observed effective Pi effort from the terminal result. Set ONLY from a real
|
|
34
|
+
* AgentResult.thinkingEffective (never invented); when it diverges from `thinking`, the row
|
|
35
|
+
* renders the honest `xh→h` form. */
|
|
36
|
+
thinkingEffective?: ThinkingLevel;
|
|
28
37
|
/** A short trailing annotation (usage, current activity), shown after the label. */
|
|
29
38
|
detail: string | undefined;
|
|
30
39
|
/** The agent's output buffer, shown when the user drills into the node. */
|
|
@@ -88,6 +97,28 @@ export interface RenderOptions {
|
|
|
88
97
|
waitingForSupervisor?: ReadonlySet<string>;
|
|
89
98
|
}
|
|
90
99
|
|
|
100
|
+
/** The frozen compact effort tag for a tree/overlay row: ` · xh` after the model, or ` · xh→h`
|
|
101
|
+
* when the terminal result reported a DIFFERENT observed effective level. `off`/absent render
|
|
102
|
+
* nothing — absence means off, and an observation is never invented: the divergence form shows
|
|
103
|
+
* only when both sides are known and genuinely differ (an observed `off` never leaves a dangling
|
|
104
|
+
* `→`). Pure. */
|
|
105
|
+
export function effortTag(requested: ThinkingLevel | undefined, effective?: ThinkingLevel): string {
|
|
106
|
+
const req = requested !== undefined ? effortSigla(requested) : "";
|
|
107
|
+
if (!req) return "";
|
|
108
|
+
const obs = effective !== undefined ? effortSigla(effective) : "";
|
|
109
|
+
return obs && obs !== req ? ` · ${req}→${obs}` : ` · ${req}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The F9 drill-in line for a leg's effort — FULL names, the drill-in has the room (the compact
|
|
113
|
+
* siglas live only on the rows): `thinking: requested xhigh → observed high`. Same honesty rules
|
|
114
|
+
* as {@link effortTag}; the line is omitted entirely when nothing was requested. Pure. */
|
|
115
|
+
export function thinkingDetailLine(requested: ThinkingLevel | undefined, effective?: ThinkingLevel): string {
|
|
116
|
+
const req = requested !== undefined ? effortFullName(requested) : "";
|
|
117
|
+
if (!req) return "";
|
|
118
|
+
const obs = effective !== undefined && effective !== requested ? effortFullName(effective) : "";
|
|
119
|
+
return obs ? `thinking: requested ${req} → observed ${obs}` : `thinking: requested ${req}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
91
122
|
/** Whether `detail` is the literal "queued" marker a seeded-but-not-yet-live leg carries — shared
|
|
92
123
|
* by `runningAnnotation` (suppresses the clock) and `telemetryStatus` (`extension.ts`, reports the
|
|
93
124
|
* `queued` state). One predicate so the two readings of the marker cannot drift apart. */
|
|
@@ -179,7 +210,10 @@ export function renderAgentTree(nodes: AgentNode[], opts: RenderOptions = {}): s
|
|
|
179
210
|
const isRoot = parentId === undefined;
|
|
180
211
|
const isLast = i === kids.length - 1;
|
|
181
212
|
const branch = isRoot ? "" : isLast ? "└─ " : "├─ ";
|
|
182
|
-
const
|
|
213
|
+
const base = safeInline(node.label) || safeInline(node.id) || "agent";
|
|
214
|
+
// The compact effort sigla rides AFTER the label clamp — the existing per-part clipping is
|
|
215
|
+
// untouched; the appended field is at most ` · xh→h` wide.
|
|
216
|
+
const label = `${base}${effortTag(node.thinking, node.thinkingEffective)}`;
|
|
183
217
|
const detailText = node.detail ? safeInline(node.detail) : "";
|
|
184
218
|
const clock = opts.now !== undefined ? runningAnnotation(node, opts.now, opts.stallMs ?? 0, opts.waitingForSupervisor?.has(node.id) ?? false) : undefined;
|
|
185
219
|
const tail = [detailText, clock].filter((s): s is string => Boolean(s)).join(" · ");
|
|
@@ -267,6 +301,8 @@ export interface AddNodeInput {
|
|
|
267
301
|
kind?: AgentNodeKind;
|
|
268
302
|
agent?: string;
|
|
269
303
|
model?: string;
|
|
304
|
+
thinking?: ThinkingLevel;
|
|
305
|
+
thinkingEffective?: ThinkingLevel;
|
|
270
306
|
detail?: string;
|
|
271
307
|
output?: string;
|
|
272
308
|
startedAt?: number;
|
|
@@ -279,6 +315,8 @@ export interface AgentNodePatch {
|
|
|
279
315
|
kind?: AgentNodeKind;
|
|
280
316
|
agent?: string;
|
|
281
317
|
model?: string;
|
|
318
|
+
thinking?: ThinkingLevel;
|
|
319
|
+
thinkingEffective?: ThinkingLevel;
|
|
282
320
|
detail?: string;
|
|
283
321
|
output?: string;
|
|
284
322
|
startedAt?: number;
|
|
@@ -293,6 +331,8 @@ function applyPatch(node: AgentNode, patch: AgentNodePatch): void {
|
|
|
293
331
|
if (patch.kind !== undefined) node.kind = patch.kind;
|
|
294
332
|
if (patch.agent !== undefined) node.agent = patch.agent;
|
|
295
333
|
if (patch.model !== undefined) node.model = patch.model;
|
|
334
|
+
if (patch.thinking !== undefined) node.thinking = patch.thinking;
|
|
335
|
+
if (patch.thinkingEffective !== undefined) node.thinkingEffective = patch.thinkingEffective;
|
|
296
336
|
if (patch.detail !== undefined) node.detail = patch.detail;
|
|
297
337
|
if (patch.output !== undefined) node.output = patch.output;
|
|
298
338
|
if (patch.startedAt !== undefined) node.startedAt = patch.startedAt;
|
|
@@ -333,6 +373,8 @@ export class AgentTree {
|
|
|
333
373
|
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
334
374
|
...(input.agent !== undefined ? { agent: input.agent } : {}),
|
|
335
375
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
376
|
+
...(input.thinking !== undefined ? { thinking: input.thinking } : {}),
|
|
377
|
+
...(input.thinkingEffective !== undefined ? { thinkingEffective: input.thinkingEffective } : {}),
|
|
336
378
|
...(input.output !== undefined ? { output: input.output } : {}),
|
|
337
379
|
...(input.dueAt !== undefined ? { dueAt: input.dueAt } : {}),
|
|
338
380
|
};
|
|
@@ -5,7 +5,7 @@ import { compactVisibleText, sanitizeTerminalText } from "./presentation.ts";
|
|
|
5
5
|
|
|
6
6
|
interface LiteralResult {
|
|
7
7
|
content: readonly { type: string; text?: string }[];
|
|
8
|
-
/**
|
|
8
|
+
/** Direct internal renderer calls carry this; Pi passes it in the render context. */
|
|
9
9
|
isError?: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
package/src/ui/model-picker.ts
CHANGED
|
@@ -217,7 +217,7 @@ export class ModelPicker extends Container implements Focusable {
|
|
|
217
217
|
return;
|
|
218
218
|
} else {
|
|
219
219
|
const before = this.input.getValue();
|
|
220
|
-
// Input has no per-instance keybindings API (Pi 1.
|
|
220
|
+
// Input has no per-instance keybindings API (Pi 1.4). Scope its global
|
|
221
221
|
// lookup to this synchronous call and restore it even if handling throws.
|
|
222
222
|
const global = getKeybindings();
|
|
223
223
|
try {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Persisted orchestration launch requests. Rendering never consults live persona or model settings. */
|
|
2
|
+
import { resolveLegThinking } from "../core/leg-thinking.ts";
|
|
3
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
4
|
+
import type { ModelParticipant, ModelSource } from "../persona/model-participants.ts";
|
|
5
|
+
import { compactInlineText, sanitizeTerminalText } from "./presentation.ts";
|
|
6
|
+
|
|
7
|
+
export interface OrchestrationLaunchParticipant {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
agent: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
modelSource: ModelSource;
|
|
13
|
+
thinkingRequested?: ThinkingLevel;
|
|
14
|
+
setupPending: boolean;
|
|
15
|
+
param?: string;
|
|
16
|
+
phase?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OrchestrationLaunchSnapshot {
|
|
20
|
+
participants: OrchestrationLaunchParticipant[];
|
|
21
|
+
unavailable?: true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function snapshotOrchestrationParticipants(
|
|
25
|
+
participants: readonly ModelParticipant[],
|
|
26
|
+
opts: { agentThinking(agent: string): ThinkingLevel | undefined; childThinking: unknown; pendingKeys: ReadonlySet<string> },
|
|
27
|
+
): OrchestrationLaunchSnapshot {
|
|
28
|
+
return { participants: participants.map((p) => {
|
|
29
|
+
const agentThinking = opts.agentThinking(p.agent);
|
|
30
|
+
const thinkingRequested = resolveLegThinking(p, agentThinking ? { thinking: agentThinking } : {}, opts.childThinking);
|
|
31
|
+
return { key: p.key, label: p.label, agent: p.agent, modelSource: p.source, setupPending: !p.assigned && opts.pendingKeys.has(p.key),
|
|
32
|
+
...(p.model ? { model: p.model } : {}), ...(thinkingRequested ? { thinkingRequested } : {}), ...(p.param ? { param: p.param } : {}) };
|
|
33
|
+
}) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Three individually bounded rows collapsed; expansion retains every participant and full model ref. */
|
|
37
|
+
export function orchestrationLaunchLines(snapshot: OrchestrationLaunchSnapshot | undefined, expanded: boolean): string[] {
|
|
38
|
+
if (!snapshot || snapshot.unavailable) return ["Launch metadata unavailable"];
|
|
39
|
+
const rows = expanded ? snapshot.participants : snapshot.participants.slice(0, 3);
|
|
40
|
+
const lines = rows.map((p) => {
|
|
41
|
+
const clean = (value: string, maxChars: number): string => expanded ? sanitizeTerminalText(value) : compactInlineText(value, { maxChars });
|
|
42
|
+
const label = clean(`${p.phase ? `${p.phase} / ` : ""}${p.label}${p.param ? ` [${p.param}]` : ""}`, 48);
|
|
43
|
+
const model = p.model ? clean(p.model, 64) : "unknown";
|
|
44
|
+
const modelText = p.setupPending ? `model pending (current default ${model}; setup may change)` : `requested model ${model}`;
|
|
45
|
+
return ` ${label} · ${modelText} · requested effort ${p.thinkingRequested ?? "unknown"}`;
|
|
46
|
+
});
|
|
47
|
+
if (!expanded && snapshot.participants.length > rows.length) lines.push(` +${snapshot.participants.length - rows.length} more · expand for launch details`);
|
|
48
|
+
return lines;
|
|
49
|
+
}
|