@tranhoangnguyen0310/pi-flow-external 2.9.0-external.0 → 2.10.0-external.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 +13 -0
- package/package.json +1 -1
- package/src/core/run-inspection.ts +18 -4
- package/src/core/spawn.ts +38 -3
- package/src/core/subagent-render.ts +10 -5
- package/src/external-command.ts +15 -5
- package/src/external-runs.ts +6 -4
- package/src/pi-subagent.ts +8 -1
- package/src/types.ts +3 -0
- package/src/workflow/journal.ts +8 -3
- package/src/workflow/runtime.ts +1 -1
- package/src/workflow/source.ts +10 -0
- package/src/workflow/tool.ts +42 -9
- package/src/workflow/types.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,20 @@ All notable changes to pi-flow external are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [2.10.0-external.0] - 2026-09-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Add paged Launch inspection through `external_runs` and `/external runs` for queued, active, and historical assignments, including recorded prompts, context, and execution configuration.
|
|
11
|
+
- Record requested settings and applied Pi SDK tools/thinking/context, and preserve workflow source and arguments in redacted launch evidence.
|
|
12
|
+
- Document the delegation experience north star and visual guidelines.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Make background Agent and workflow cards explicit launch receipts, with inspection routes and clearer workflow purpose, workspace, and access disclosure.
|
|
16
|
+
- Improve card legibility with consistent label/value contrast, bounded source paths and run references, and expanded-only usage accounting.
|
|
17
|
+
- Add run-detail refresh and recover from stale inspection pages without closing navigation.
|
|
18
|
+
|
|
7
19
|
## [2.9.0-external.0] - 2026-09-25
|
|
20
|
+
|
|
8
21
|
### Added
|
|
9
22
|
|
|
10
23
|
- OpenCode (`opencode`) as a sixth CLI backend, with JSONL progress, verified-final-step completion, session resume, native root-step usage, cancellation, and durable receipts. Targets OpenCode 2 only (verified against `@opencode/cli` 2.0.16); OpenCode 1.x is not supported. Every run uses `--standalone`, a private server the run owns, and never touches the shared background service. Success is verified from the persisted session export: a new user turn for this prompt, a `succeeded` idle outcome, and a clean final assistant message whose text is the result. It never relies on the zero exit code or streamed narration alone. Restricted tiers use native deny-by-default tool rules, not an OS sandbox, and select their agent with `--agent` on every run, including resumes. Unsafe resumes (a restricted resume of a session with its own permission rules, or a danger resume of a restricted session) are refused. A pinned thinking level is passed as the pinned model's `#variant`, which OpenCode validates. Structured schemas are rejected. Nested-subagent total cost is unknown, and budgets are unenforceable.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tranhoangnguyen0310/pi-flow-external",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0-external.0",
|
|
4
4
|
"description": "External Claude Code, Codex CLI, Antigravity, Grok Build CLI, Muse Code, and OpenCode delegation for pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -17,7 +17,7 @@ const MAX_CURSOR_CHARS = 4096;
|
|
|
17
17
|
const MAX_LIST_SCAN = 200;
|
|
18
18
|
const RUN_ID_PATTERN = /^run_[A-Za-z0-9_-]{1,128}$/;
|
|
19
19
|
|
|
20
|
-
export type RunInspectionView = "output" | "diagnostics" | "summary" | "final";
|
|
20
|
+
export type RunInspectionView = "output" | "diagnostics" | "summary" | "final" | "launch";
|
|
21
21
|
export type RunRecordIntegrity = "complete" | "incomplete" | "damaged";
|
|
22
22
|
export type RunOutputStatus = "preliminary" | "final" | "interrupted";
|
|
23
23
|
|
|
@@ -98,6 +98,8 @@ interface EvidenceEvent { runId?: string; sequence?: number; timestamp?: string;
|
|
|
98
98
|
interface CompleteLine { start: number; end: number; text: string }
|
|
99
99
|
interface ScanResult { missing: boolean; stopped: boolean; truncatedTail: boolean; malformed: boolean; endPosition: number }
|
|
100
100
|
interface RunObservation {
|
|
101
|
+
launch?: Record<string, unknown>;
|
|
102
|
+
applied?: Record<string, unknown>;
|
|
101
103
|
metadata?: Record<string, unknown>;
|
|
102
104
|
queuedAt?: string;
|
|
103
105
|
executionStartedAt?: string;
|
|
@@ -115,10 +117,12 @@ export async function inspectRun({
|
|
|
115
117
|
view,
|
|
116
118
|
limitBytes = DEFAULT_PAGE_BYTES,
|
|
117
119
|
cursor,
|
|
120
|
+
live = false,
|
|
118
121
|
}: {
|
|
119
122
|
runsDirectory: string;
|
|
120
123
|
runId: string;
|
|
121
124
|
view: RunInspectionView;
|
|
125
|
+
live?: boolean;
|
|
122
126
|
limitBytes?: number;
|
|
123
127
|
cursor?: string;
|
|
124
128
|
}): Promise<RunInspectionPage> {
|
|
@@ -136,10 +140,16 @@ export async function inspectRun({
|
|
|
136
140
|
const terminalStatus = asString(terminal?.status);
|
|
137
141
|
const finalAvailable = isFinalAvailable(terminal);
|
|
138
142
|
|
|
139
|
-
if (view === "summary") {
|
|
143
|
+
if (view === "summary" || view === "launch") {
|
|
140
144
|
if (decoded?.kind === "inspect" && decoded.source !== "summary") throw new Error("Cursor source does not match summary view");
|
|
141
145
|
const observation = await readObservation(eventsPath, runId);
|
|
142
|
-
const text = JSON.stringify(
|
|
146
|
+
const text = JSON.stringify(view === "launch" ? {
|
|
147
|
+
runId,
|
|
148
|
+
intent: asRecord(summary.document?.metadata) ?? observation.metadata ?? "Evidence pending or unavailable",
|
|
149
|
+
execution: observation.launch ?? (terminal?.backendStarted === false ? "Never started" : summary.document ? "Not recorded" : live ? "Pending — execution settings not recorded yet" : "Interrupted or uncertain — launch settings unavailable"),
|
|
150
|
+
applied: observation.applied ?? "Not recorded; see execution for CLI settings",
|
|
151
|
+
backendInternalContext: "Not observable by this extension",
|
|
152
|
+
} : summaryProjection(runId, summary, observation), null, view === "launch" ? 2 : undefined);
|
|
143
153
|
const revision = contentRevision(text);
|
|
144
154
|
if (decoded?.kind === "inspect" && decoded.revision !== revision) throw staleCursorError();
|
|
145
155
|
const offset = decoded?.kind === "inspect" ? decoded.textOffset : 0;
|
|
@@ -402,6 +412,10 @@ async function readObservation(eventsPath: string, runId: string): Promise<RunOb
|
|
|
402
412
|
if (event.type === "run_started") {
|
|
403
413
|
observation.metadata = asRecord(event.data);
|
|
404
414
|
observation.queuedAt = asString(observation.metadata?.queuedAt) ?? event.timestamp;
|
|
415
|
+
} else if (event.type === "launch_resolved") {
|
|
416
|
+
observation.launch ??= asRecord(event.data);
|
|
417
|
+
} else if (event.type === "launch_applied") {
|
|
418
|
+
observation.applied ??= asRecord(event.data);
|
|
405
419
|
} else if (event.type === "execution_started") {
|
|
406
420
|
observation.executionStartedAt ??= event.timestamp;
|
|
407
421
|
} else if (event.type === "process_started") {
|
|
@@ -839,7 +853,7 @@ function hasKeys(value: Record<string, unknown>, keys: string[]): boolean {
|
|
|
839
853
|
}
|
|
840
854
|
|
|
841
855
|
function isView(value: unknown): value is RunInspectionView {
|
|
842
|
-
return value === "output" || value === "diagnostics" || value === "summary" || value === "final";
|
|
856
|
+
return value === "output" || value === "diagnostics" || value === "summary" || value === "final" || value === "launch";
|
|
843
857
|
}
|
|
844
858
|
|
|
845
859
|
function isNonNegativeInteger(value: unknown): value is number {
|
package/src/core/spawn.ts
CHANGED
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
} from "./progress.ts";
|
|
23
23
|
import { spawnClaudeSubagent } from "./claude.ts";
|
|
24
24
|
import { spawnCodexSubagent } from "./codex.ts";
|
|
25
|
-
import { spawnAgySubagent, isTransientAgyFailure } from "./agy.ts";
|
|
26
|
-
import { spawnGrokSubagent } from "./grok.ts";
|
|
27
|
-
import { museHasNestedAgentActivity, spawnMuseSubagent } from "./muse.ts";
|
|
25
|
+
import { spawnAgySubagent, isTransientAgyFailure, normalizeAgyEffort } from "./agy.ts";
|
|
26
|
+
import { spawnGrokSubagent, normalizeGrokReasoningEffort } from "./grok.ts";
|
|
27
|
+
import { museHasNestedAgentActivity, spawnMuseSubagent, normalizeMuseReasoningEffort } from "./muse.ts";
|
|
28
28
|
import { opencodeHasNestedAgentActivity, spawnOpencodeSubagent } from "./opencode.ts";
|
|
29
29
|
import type {
|
|
30
30
|
PermissionTier,
|
|
@@ -392,6 +392,34 @@ export async function spawnSubagent(params: SpawnSubagentParams): Promise<AgentT
|
|
|
392
392
|
}
|
|
393
393
|
return result;
|
|
394
394
|
}
|
|
395
|
+
await record?.event("launch_resolved", {
|
|
396
|
+
roleInstructions: params.profile.systemPrompt,
|
|
397
|
+
appendedInstructions: params.appendInstructions ?? null,
|
|
398
|
+
outputSchema: params.outputSchema ?? params.customTools?.find(tool => tool.name === "structured_output")?.parameters ?? null,
|
|
399
|
+
context: params.context ?? { mode: "none" },
|
|
400
|
+
configuration: {
|
|
401
|
+
harness: selectorHarness(params.profile),
|
|
402
|
+
backend: params.profile.backend,
|
|
403
|
+
model: params.model ? `${params.model.provider}/${params.model.id}` : params.profile.model ?? "Backend default (not resolved by extension)",
|
|
404
|
+
thinkingRequested: params.thinkingLevel,
|
|
405
|
+
thinkingApplied: params.profile.backend === "pi" ? "Pending SDK initialization" :
|
|
406
|
+
params.profile.backend === "agy" ? normalizeAgyEffort(params.thinkingLevel) ?? "Backend default" :
|
|
407
|
+
params.profile.backend === "grok" ? normalizeGrokReasoningEffort(params.thinkingLevel) ?? "Backend default" :
|
|
408
|
+
params.profile.backend === "muse" ? normalizeMuseReasoningEffort(params.thinkingLevel) ?? "Backend default" : params.thinkingLevel ?? "Backend default",
|
|
409
|
+
thinkingApplication: "CLI effort argument, or SDK setting; not a claim about provider-internal reasoning",
|
|
410
|
+
requestedTools: params.profile.tools ?? "Backend defaults",
|
|
411
|
+
resourceLoading: params.profile.backend === "pi" ? { preset: effectivePiResourcePreset(params.profile.preset), projectTrusted: params.projectTrusted === true, contextFiles: "SDK-loaded; recorded after initialization", skills: effectivePiResourcePreset(params.profile.preset) === "skills" ? "Enabled; project skills require trust" : "Disabled", extensions: false, promptTemplates: false, themes: false } : "Backend-controlled; not fully observable",
|
|
412
|
+
tools: params.profile.backend === "pi" ? "Pending SDK initialization" : "Backend-controlled; requested tools recorded separately",
|
|
413
|
+
excludedTools: params.excludeTools,
|
|
414
|
+
workspace: params.ctx.cwd,
|
|
415
|
+
permissionRequested: params.permission ?? "default",
|
|
416
|
+
permissionEffective: permission,
|
|
417
|
+
timeoutMs: params.timeoutMs,
|
|
418
|
+
maxBudgetUsd: params.maxBudgetUsd ?? null,
|
|
419
|
+
budgetEnforced: (params.maxBudgetUsd ?? 0) > 0 && params.profile.backend === "claude",
|
|
420
|
+
resumeRunId: params.resumeRunId ?? null,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
395
423
|
let resumeSession: Awaited<ReturnType<typeof resolveResume>>["session"];
|
|
396
424
|
if (params.resumeRunId) {
|
|
397
425
|
const resolved = await resolveResume(runRecordsDirectory(), params.resumeRunId, params.profile.backend);
|
|
@@ -439,6 +467,7 @@ export async function spawnSubagent(params: SpawnSubagentParams): Promise<AgentT
|
|
|
439
467
|
let result = await spawnSubagentRuntime({
|
|
440
468
|
...params,
|
|
441
469
|
permission: effectiveTier,
|
|
470
|
+
runRecord: record,
|
|
442
471
|
signal: timeout.signal,
|
|
443
472
|
onBackendEvent,
|
|
444
473
|
onProcessStart,
|
|
@@ -906,6 +935,12 @@ async function spawnSubagentRuntime(params: SpawnSubagentRuntimeParams): Promise
|
|
|
906
935
|
thinkingClamped = { requested: thinkingLevel, effective: session.thinkingLevel };
|
|
907
936
|
}
|
|
908
937
|
|
|
938
|
+
await params.runRecord?.event("launch_applied", {
|
|
939
|
+
thinkingApplied: session.thinkingLevel,
|
|
940
|
+
tools: session.getActiveToolNames(),
|
|
941
|
+
contextFiles: resourceLoader.getAgentsFiles(),
|
|
942
|
+
systemPrompt: session.systemPrompt,
|
|
943
|
+
});
|
|
909
944
|
if (signal) {
|
|
910
945
|
abortHandler = () => {
|
|
911
946
|
void session?.abort();
|
|
@@ -253,7 +253,7 @@ export function renderCompactSubagentNode(
|
|
|
253
253
|
? ` ${theme.fg("muted", `${status === "running" ? "--" : "->"} ${formatActivityLineForDisplay(summary)}`)}`
|
|
254
254
|
: "";
|
|
255
255
|
return new Text(
|
|
256
|
-
`${indent}${subagentMarker(status, theme, frame, node.timedOut)} ${theme.fg(bodyColor, compactTitle(node))}${meta ? ` ${theme.fg("dim", meta)}` : ""}${preview}`,
|
|
256
|
+
`${indent}${subagentMarker(status, theme, frame, node.timedOut)} ${status === "error" || status === "aborted" ? theme.fg(bodyColor, compactTitle(node)) : compactTitle(node)}${meta ? ` ${theme.fg("dim", meta)}` : ""}${preview}`,
|
|
257
257
|
0,
|
|
258
258
|
0,
|
|
259
259
|
);
|
|
@@ -305,13 +305,18 @@ export function renderSubagentNode(
|
|
|
305
305
|
expanded = false,
|
|
306
306
|
showAccess = true,
|
|
307
307
|
): Text | Container {
|
|
308
|
+
const displayNode = expanded ? node : { ...node, usage: undefined };
|
|
308
309
|
const rendered = shouldRenderRichSubagent(node, runningCount)
|
|
309
|
-
? renderRichSubagentNode(
|
|
310
|
-
: renderCompactSubagentNode(
|
|
311
|
-
if (
|
|
312
|
-
|
|
310
|
+
? renderRichSubagentNode(displayNode, theme, frame, indent, now, showAccess)
|
|
311
|
+
: renderCompactSubagentNode(displayNode, theme, frame, indent, now, showAccess);
|
|
312
|
+
if (isActiveSubagentStatus(node.status)) {
|
|
313
|
+
const active = new Container();
|
|
314
|
+
active.addChild(rendered);
|
|
315
|
+
active.addChild(new Text(`${indent} ${theme.fg("muted", "Inspect")} ${expanded ? node.runId ?? node.externalRunId ?? "pending" : (node.runId ?? node.externalRunId)?.slice(-8) ?? "pending"} · ${theme.fg("accent", "/external runs")}`, 0, 0));
|
|
316
|
+
return active;
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
if (!expanded) return rendered;
|
|
315
320
|
const container = new Container();
|
|
316
321
|
container.addChild(rendered);
|
|
317
322
|
const output = node.result ?? node.assistantOutput?.messages.map((message) => message.text).join("\n\n");
|
package/src/external-command.ts
CHANGED
|
@@ -696,10 +696,18 @@ async function readSummary(options: ExternalCommandOptions, runId: string, ctx:
|
|
|
696
696
|
return object(JSON.parse(text));
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
-
async function showPages(options: ExternalCommandOptions, runId: string, view: "output" | "diagnostics" | "final", ctx: ExtensionCommandContext): Promise<void> {
|
|
699
|
+
async function showPages(options: ExternalCommandOptions, runId: string, view: "output" | "diagnostics" | "final" | "launch", ctx: ExtensionCommandContext): Promise<void> {
|
|
700
700
|
let cursor: string | undefined;
|
|
701
701
|
do {
|
|
702
|
-
|
|
702
|
+
let page: ExternalRunsResult;
|
|
703
|
+
try {
|
|
704
|
+
page = await runAction(options, { action: "inspect", runId, view, ...(cursor ? { cursor } : {}) }, ctx);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (!cursor || !(error instanceof Error) || !error.message.includes("Run changed while paging")) throw error;
|
|
707
|
+
ctx.ui.notify("Run changed while reading. Reopening the latest snapshot from page 1.", "info");
|
|
708
|
+
cursor = undefined;
|
|
709
|
+
page = await runAction(options, { action: "inspect", runId, view }, ctx);
|
|
710
|
+
}
|
|
703
711
|
// The tool's `final` projection is deliberately a clean, narration-free
|
|
704
712
|
// canonical-answer surface: unavailable is a bounded EMPTY page with
|
|
705
713
|
// finalAvailable:false (true for both an agent run and a workflow — see
|
|
@@ -738,15 +746,17 @@ function formatSummaryHeader(summary: JsonObject): string {
|
|
|
738
746
|
|
|
739
747
|
async function navigateRun(options: ExternalCommandOptions, runId: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
740
748
|
const summary = await readSummary(options, runId, ctx);
|
|
749
|
+
const observedAt = new Date().toISOString();
|
|
741
750
|
const children = Array.isArray(summary.children) ? summary.children.map(object).filter((child) => typeof child.runId === "string") : [];
|
|
742
751
|
const state = object(summary.state);
|
|
743
752
|
const childChoices = children.map((child) => `Child ${String(child.runId)}${child.label ? ` · ${String(child.label)}` : ""}`);
|
|
744
|
-
const actions = ["Summary", "Output", "Final", "Diagnostics", ...childChoices, ...(state.status === "running" || state.status === "queued" ? ["Cancel run"] : []), "Back"];
|
|
753
|
+
const actions = ["Refresh", "Launch", "Summary", "Output", "Final", "Diagnostics", ...childChoices, ...(state.status === "running" || state.status === "queued" ? ["Cancel run"] : []), "Back"];
|
|
745
754
|
while (true) {
|
|
746
755
|
const choice = await ctx.ui.select(`Run ${runId}`, actions);
|
|
747
756
|
if (!choice || choice === "Back") return;
|
|
748
|
-
if (choice === "
|
|
749
|
-
|
|
757
|
+
if (choice === "Refresh") return navigateRun(options, runId, ctx);
|
|
758
|
+
if (choice === "Summary") await ctx.ui.editor(`summary ${runId}`, `Snapshot ${observedAt} · Refresh for current state\n${formatSummaryHeader(summary)}\n\n${JSON.stringify(summary, null, 2)}`);
|
|
759
|
+
else if (choice === "Launch" || choice === "Output" || choice === "Final" || choice === "Diagnostics") await showPages(options, runId, choice.toLowerCase() as "output" | "final" | "diagnostics" | "launch", ctx);
|
|
750
760
|
else if (choice === "Cancel run") {
|
|
751
761
|
if (await ctx.ui.confirm("Cancel external run?", `${runId}\n\nStopping execution does not roll back side effects.`)) {
|
|
752
762
|
const result = await runAction(options, { action: "cancel", runId, reason: "cancelled from /external runs" }, ctx);
|
package/src/external-runs.ts
CHANGED
|
@@ -32,8 +32,8 @@ const externalRunsParameters = Type.Object({
|
|
|
32
32
|
maxItems: MAX_TARGETS,
|
|
33
33
|
description: "Target run IDs (run_... agent, wf_... workflow). For inspect: a single-entry list [\"run_...\"] inspects that run with any view (summary, output, diagnostics, final); multiple entries (1-20 targets) batch summary inspect. For cancel: a single-entry list [\"run_...\"]. For wait: any|all of up to 100 targets.",
|
|
34
34
|
})),
|
|
35
|
-
view: Type.Optional(StringEnum(["summary", "output", "diagnostics", "final"] as const, {
|
|
36
|
-
description: "inspect view: summary (state/timing/freshness/refs, default), output (assistant text plus canonical result, partial or final), diagnostics (tool activity/errors), final (only the verified canonical terminal answer, empty until a successful terminal boundary exists). Batch inspect (runIds) only supports summary.",
|
|
35
|
+
view: Type.Optional(StringEnum(["summary", "output", "diagnostics", "final", "launch"] as const, {
|
|
36
|
+
description: "inspect view: launch (recorded prompt, context and execution configuration; sensitive, explicit inspection only), summary (state/timing/freshness/refs, default), output (assistant text plus canonical result, partial or final), diagnostics (tool activity/errors), final (only the verified canonical terminal answer, empty until a successful terminal boundary exists). Batch inspect (runIds) only supports summary.",
|
|
37
37
|
})),
|
|
38
38
|
mode: Type.Optional(StringEnum(["any", "all"] as const, {
|
|
39
39
|
description: "wait mode: any returns on the first terminal outcome; all waits for every target. Neither cancels pending work; an unsuccessful selected workflow returns early even in all mode.",
|
|
@@ -797,7 +797,9 @@ export function createExternalRunsTool(
|
|
|
797
797
|
const nextCursor = page.nextOffset === undefined ? undefined : encodeProjectionCursor(params.runId, view, page.nextOffset, text);
|
|
798
798
|
return result(page.text, { runId: params.runId, view, text: page.text, finalAvailable: true, nextCursor });
|
|
799
799
|
}
|
|
800
|
-
const source = view === "
|
|
800
|
+
const source = view === "launch"
|
|
801
|
+
? { runId: params.runId, launch: (entry?.observation as WorkflowToolDetails | undefined)?.launch ?? historical?.launch ?? "Not recorded" }
|
|
802
|
+
: view === "summary"
|
|
801
803
|
? entry ? liveSummary(entry, true) : journalSummary(historical!, true)
|
|
802
804
|
: view === "output"
|
|
803
805
|
// Ternary, not ??: entry and historical are mutually exclusive, but a
|
|
@@ -835,7 +837,7 @@ export function createExternalRunsTool(
|
|
|
835
837
|
return result(portion.text, { runId: params.runId, view, text: portion.text, outputStatus: entry.state === "running" ? "preliminary" : entry.outcome?.outcome === "succeeded" ? "final" : "interrupted", nextCursor });
|
|
836
838
|
}
|
|
837
839
|
}
|
|
838
|
-
const page = await inspectRun({ runsDirectory, runId: params.runId, view, limitBytes: params.limitBytes, cursor: params.cursor });
|
|
840
|
+
const page = await inspectRun({ runsDirectory, runId: params.runId, view, limitBytes: params.limitBytes, cursor: params.cursor, live: entry?.state === "running" });
|
|
839
841
|
const text = page.items.map((item) => item.text).join("\n");
|
|
840
842
|
// view: "final" stays a clean, narration-free canonical-answer
|
|
841
843
|
// surface — a bounded empty page (finalAvailable:false) when
|
package/src/pi-subagent.ts
CHANGED
|
@@ -448,12 +448,16 @@ function createAgentTool(
|
|
|
448
448
|
project,
|
|
449
449
|
description: params.description,
|
|
450
450
|
prompt: briefing.prompt,
|
|
451
|
+
authoredPrompt: params.prompt,
|
|
452
|
+
plannedConfiguration: { roleInstructions: profile.systemPrompt, model: profile.model, thinking: thinkingLevel, tools: profile.tools, permissionRequested: params.permission ?? "default", timeoutMs, maxBudgetUsd },
|
|
453
|
+
context: briefing.context ?? { mode: "none" },
|
|
451
454
|
profile: profile.name,
|
|
452
455
|
backend: profile.backend,
|
|
453
456
|
harness: selectorHarness(profile),
|
|
454
457
|
queuedAt: new Date(queuedAt).toISOString(),
|
|
455
458
|
},
|
|
456
459
|
});
|
|
460
|
+
await runRecord.event("intent_ready");
|
|
457
461
|
const progress = createProgressNode(toolCallId, params.description, subagentType, "queued", profile.backend, selectorHarness(profile));
|
|
458
462
|
progress.context = briefing.context;
|
|
459
463
|
progress.queuedAt = queuedAt;
|
|
@@ -589,6 +593,7 @@ function createAgentTool(
|
|
|
589
593
|
runId: runRecord.runId,
|
|
590
594
|
recordPath: runRecord.directory,
|
|
591
595
|
progress,
|
|
596
|
+
backgroundReceipt: true,
|
|
592
597
|
},
|
|
593
598
|
);
|
|
594
599
|
},
|
|
@@ -639,14 +644,16 @@ function createAgentTool(
|
|
|
639
644
|
const lines = [
|
|
640
645
|
`${theme.bold("Delegating")} ${theme.bold(getBackendAgentLabel(backend))} ${theme.fg("muted", `→ ${subagentType}`)} · ${theme.fg("warning", tierLabel)}`,
|
|
641
646
|
description ? `${theme.fg("muted", "Task")} ${description}` : "",
|
|
642
|
-
profile?.description ? `${theme.fg("muted", "
|
|
647
|
+
profile?.description ? `${theme.fg("muted", "Role")} ${profile.description}` : "",
|
|
643
648
|
formatSharedContext(args.context) ? `${theme.fg("muted", "Context")} ${formatSharedContext(args.context)}` : "",
|
|
644
649
|
context.cwd ? `${theme.fg("muted", "Workspace")} ${context.cwd}` : "",
|
|
650
|
+
`${theme.fg("muted", "Mode")} ${args.background ? "Background" : "Foreground"}`,
|
|
645
651
|
].filter(Boolean);
|
|
646
652
|
return new Text(lines.join("\n"), 0, 0);
|
|
647
653
|
},
|
|
648
654
|
renderResult(result, { expanded }, theme) {
|
|
649
655
|
const details = result.details as SubagentToolDetails;
|
|
656
|
+
if (details.backgroundReceipt) return new Text(`${theme.bold("Started in background")}\n${theme.fg("muted", "Launch receipt — not a live monitor")}\n${theme.fg("muted", "Run")} ${expanded ? details.runId : details.runId?.slice(-8)} · ${theme.fg("accent", "/external runs")} ${theme.fg("muted", "to inspect or follow")}`, 0, 0);
|
|
650
657
|
return renderSubagentNode(
|
|
651
658
|
details.progress ?? details,
|
|
652
659
|
theme,
|
package/src/types.ts
CHANGED
|
@@ -143,6 +143,8 @@ export interface WorkflowAgentSnapshot {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
export interface WorkflowToolDetails {
|
|
146
|
+
launch?: unknown;
|
|
147
|
+
backgroundReceipt?: boolean;
|
|
146
148
|
name: string;
|
|
147
149
|
status: "running" | "completed" | "error" | "aborted";
|
|
148
150
|
/**
|
|
@@ -243,6 +245,7 @@ export interface SubagentProgressNode {
|
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
export interface SubagentToolDetails {
|
|
248
|
+
backgroundReceipt?: boolean;
|
|
246
249
|
context?: ParentContextReceipt;
|
|
247
250
|
description: string;
|
|
248
251
|
subagentType: SubagentType | "unknown";
|
package/src/workflow/journal.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface WorkflowRunIdentity {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface LoadedWorkflowJournal {
|
|
30
|
+
launch?: unknown;
|
|
30
31
|
runId: string;
|
|
31
32
|
path: string;
|
|
32
33
|
agentResults: WorkflowCachedAgentResult[];
|
|
@@ -106,6 +107,7 @@ export async function loadWorkflowJournal(dir: string, runId: string): Promise<L
|
|
|
106
107
|
|
|
107
108
|
const agentResults: WorkflowCachedAgentResult[] = [];
|
|
108
109
|
const children: LoadedWorkflowJournal["children"] = [];
|
|
110
|
+
let launch: unknown;
|
|
109
111
|
let seenRunStart = false;
|
|
110
112
|
let name: string | undefined;
|
|
111
113
|
let source: string | undefined;
|
|
@@ -131,6 +133,7 @@ export async function loadWorkflowJournal(dir: string, runId: string): Promise<L
|
|
|
131
133
|
);
|
|
132
134
|
}
|
|
133
135
|
seenRunStart = entry.runId === runId;
|
|
136
|
+
launch = entry.launch;
|
|
134
137
|
name = typeof entry.name === "string" ? entry.name : undefined;
|
|
135
138
|
source = typeof entry.source === "string" ? entry.source : undefined;
|
|
136
139
|
project = typeof entry.project === "string" ? entry.project : undefined;
|
|
@@ -189,7 +192,7 @@ export async function loadWorkflowJournal(dir: string, runId: string): Promise<L
|
|
|
189
192
|
if (!seenRunStart) {
|
|
190
193
|
throw new Error(`Workflow journal ${path} does not match run id ${runId}`);
|
|
191
194
|
}
|
|
192
|
-
return { runId, path, agentResults, name, source, project, status, outcome, result, error: terminalError, children: children.filter(Boolean) };
|
|
195
|
+
return { launch, runId, path, agentResults, name, source, project, status, outcome, result, error: terminalError, children: children.filter(Boolean) };
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
export interface WorkflowJournalPage {
|
|
@@ -243,6 +246,7 @@ export async function listWorkflowJournals(dir: string, project: string, limit =
|
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
export async function createWorkflowJournalWriter(params: {
|
|
249
|
+
launch?: unknown;
|
|
246
250
|
dir: string;
|
|
247
251
|
identity: WorkflowRunIdentity;
|
|
248
252
|
name: string;
|
|
@@ -251,12 +255,13 @@ export async function createWorkflowJournalWriter(params: {
|
|
|
251
255
|
scriptPath?: string;
|
|
252
256
|
resumeFromRunId?: string;
|
|
253
257
|
}): Promise<WorkflowJournalWriter> {
|
|
254
|
-
await mkdir(params.dir, { recursive: true });
|
|
258
|
+
await mkdir(params.dir, { recursive: true, mode: 0o700 });
|
|
255
259
|
const path = workflowJournalPath(params.dir, params.identity.runId);
|
|
256
260
|
await writeFile(
|
|
257
261
|
path,
|
|
258
262
|
`${JSON.stringify({
|
|
259
263
|
type: "run_start",
|
|
264
|
+
launch: params.launch,
|
|
260
265
|
version: JOURNAL_VERSION,
|
|
261
266
|
apiVersion: params.identity.apiVersion,
|
|
262
267
|
runId: params.identity.runId,
|
|
@@ -268,7 +273,7 @@ export async function createWorkflowJournalWriter(params: {
|
|
|
268
273
|
scriptHash: params.identity.scriptHash,
|
|
269
274
|
argsHash: params.identity.argsHash,
|
|
270
275
|
})}\n`,
|
|
271
|
-
"utf8",
|
|
276
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
272
277
|
);
|
|
273
278
|
|
|
274
279
|
let appendQueue = Promise.resolve();
|
package/src/workflow/runtime.ts
CHANGED
|
@@ -235,7 +235,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
235
235
|
state.resumePrefixActive = false;
|
|
236
236
|
|
|
237
237
|
// Queue on the shared global cap. May reject if aborted while waiting.
|
|
238
|
-
const queuedEvent: WorkflowAgentQueuedEvent = { index, label, phase: assignedPhase, subagentType, prompt: taskPrompt, context: briefing.context };
|
|
238
|
+
const queuedEvent: WorkflowAgentQueuedEvent = { permission: call.permission, maxBudgetUsd: call.maxBudgetUsd, schema: call.schema, authoredPrompt: originalPrompt, index, label, phase: assignedPhase, subagentType, prompt: taskPrompt, context: briefing.context };
|
|
239
239
|
await options.onAgentQueued?.(queuedEvent);
|
|
240
240
|
const runRecord = queuedEvent.runRecord;
|
|
241
241
|
if (runRecord) call.runRecord = runRecord;
|
package/src/workflow/source.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type WorkflowRunIdentity,
|
|
13
13
|
} from "./journal.ts";
|
|
14
14
|
import { loadSavedWorkflowRegistry, loadWorkflowScriptPath } from "./registry.ts";
|
|
15
|
+
import { redactSecrets } from "../core/run-record.ts";
|
|
15
16
|
import { parseWorkflowScript } from "./script-validation.ts";
|
|
16
17
|
import type { WorkflowCachedAgentResult, WorkflowMetaPhase } from "./types.ts";
|
|
17
18
|
|
|
@@ -56,6 +57,7 @@ export type WorkflowToolParams = Static<typeof workflowToolParameters>;
|
|
|
56
57
|
|
|
57
58
|
export type PreparedWorkflowToolSource = {
|
|
58
59
|
script: string;
|
|
60
|
+
launch: unknown;
|
|
59
61
|
metaName: string;
|
|
60
62
|
plannedPhases?: WorkflowMetaPhase[];
|
|
61
63
|
source: "inline" | "saved" | "path";
|
|
@@ -309,10 +311,17 @@ export async function prepareWorkflowToolSource(
|
|
|
309
311
|
resumeAgentResults = journal.agentResults;
|
|
310
312
|
}
|
|
311
313
|
|
|
314
|
+
const launch = redactSecrets({
|
|
315
|
+
name: metaName, description: parseWorkflowScript(script).meta.description,
|
|
316
|
+
source: source.source, sourcePath: source.sourcePath, script, args: params.args,
|
|
317
|
+
workspace: project, mode: params.background ? "background" : "foreground",
|
|
318
|
+
plannedPhases, resumeFromRunId,
|
|
319
|
+
});
|
|
312
320
|
let journalWriter: WorkflowJournalWriter | undefined;
|
|
313
321
|
if (sessionWorkflowDir) {
|
|
314
322
|
try {
|
|
315
323
|
journalWriter = await createWorkflowJournalWriter({
|
|
324
|
+
launch,
|
|
316
325
|
dir: sessionWorkflowDir,
|
|
317
326
|
identity,
|
|
318
327
|
name: metaName,
|
|
@@ -339,6 +348,7 @@ export async function prepareWorkflowToolSource(
|
|
|
339
348
|
return {
|
|
340
349
|
ok: true,
|
|
341
350
|
value: {
|
|
351
|
+
launch,
|
|
342
352
|
script,
|
|
343
353
|
metaName,
|
|
344
354
|
plannedPhases,
|
package/src/workflow/tool.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ToolDefinition,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Container, Text } from "@earendil-works/pi-tui";
|
|
10
|
-
import { resolve } from "node:path";
|
|
10
|
+
import { basename, resolve } from "node:path";
|
|
11
11
|
import type { ConcurrencyLimiter } from "../core/concurrency.ts";
|
|
12
12
|
import { isActiveSubagentStatus, isCompletedSubagentStatus, renderOutputText, renderSubagentNode } from "../core/subagent-render.ts";
|
|
13
13
|
import { SPINNER_INTERVAL_MS } from "../core/spinner.ts";
|
|
@@ -24,6 +24,7 @@ import { loadExternalCatalog, resolveExternalProfile, selectorHarness, unknownEx
|
|
|
24
24
|
import { effectivePiResourcePreset, loadHarnessConfigs } from "../harnesses.ts";
|
|
25
25
|
import { WORKFLOW_PROMPT_SNIPPET } from "../prompts.ts";
|
|
26
26
|
import { EXTERNAL_HARNESSES, type PermissionTier, type SubagentProfile, type SubagentToolDetails, type SubagentUsage, type WorkflowAgentSnapshot, type WorkflowToolDetails } from "../types.ts";
|
|
27
|
+
import { parseWorkflowScript } from "./script-validation.ts";
|
|
27
28
|
import { isWorkflowAbortError, runWorkflow } from "./runtime.ts";
|
|
28
29
|
import { prepareWorkflowToolSource, workflowToolParameters } from "./source.ts";
|
|
29
30
|
import { ChildRunError, type ChildRunOutcome, type WorkflowAgentRunner, type WorkflowSubagentDescriptor } from "./types.ts";
|
|
@@ -173,6 +174,7 @@ export function createWorkflowTool(
|
|
|
173
174
|
} = prepared.value;
|
|
174
175
|
|
|
175
176
|
const snapshot: WorkflowToolDetails = {
|
|
177
|
+
launch: prepared.value.launch,
|
|
176
178
|
name: metaName,
|
|
177
179
|
status: "running",
|
|
178
180
|
agentCount: 0,
|
|
@@ -428,12 +430,16 @@ export function createWorkflowTool(
|
|
|
428
430
|
workflowRunId: identity.runId,
|
|
429
431
|
description: event.label,
|
|
430
432
|
prompt: event.prompt,
|
|
433
|
+
authoredPrompt: event.authoredPrompt,
|
|
434
|
+
plannedConfiguration: { roleInstructions: profile?.systemPrompt, model: profile?.model, thinking: profile?.thinking ?? thinkingLevel, tools: profile?.tools, timeoutMs, permissionRequested: event.permission ?? "default", maxBudgetUsd: event.maxBudgetUsd ?? profile?.maxBudgetUsd ?? defaultMaxBudgetUsd, outputSchema: event.schema ?? null, outputInstructions: event.schema ? "Schema-constrained output; backend-specific instructions resolved at start" : WORKFLOW_PLAIN_TEXT_OUTPUT_NOTE },
|
|
435
|
+
context: event.context ?? { mode: "none" },
|
|
431
436
|
profile: event.subagentType,
|
|
432
437
|
backend: profile?.backend,
|
|
433
438
|
harness: profile ? selectorHarness(profile) : undefined,
|
|
434
439
|
queuedAt: new Date(queuedAt).toISOString(),
|
|
435
440
|
},
|
|
436
441
|
});
|
|
442
|
+
await runRecord.event("intent_ready");
|
|
437
443
|
event.runRecord = runRecord;
|
|
438
444
|
snapshot.agents.push({
|
|
439
445
|
index: event.index,
|
|
@@ -588,23 +594,41 @@ export function createWorkflowTool(
|
|
|
588
594
|
: result.details.error ? { error: result.details.error } : {}),
|
|
589
595
|
}),
|
|
590
596
|
});
|
|
597
|
+
emit();
|
|
591
598
|
if (!background) return await registered.result;
|
|
592
599
|
return workflowResult(
|
|
593
600
|
`Workflow "${metaName}" queued as ${identity.runId}. Use external_runs to inspect, wait, or cancel it.`,
|
|
594
|
-
cloneSnapshot(snapshot),
|
|
601
|
+
{ ...cloneSnapshot(snapshot), backgroundReceipt: true },
|
|
595
602
|
);
|
|
596
603
|
},
|
|
597
|
-
renderCall(args, theme,
|
|
598
|
-
|
|
599
|
-
|
|
604
|
+
renderCall(args, theme, context) {
|
|
605
|
+
let name = args.name || (args.scriptPath ? basename(args.scriptPath) : "Preparing workflow");
|
|
606
|
+
let purpose = "";
|
|
607
|
+
const state = context.state as { script?: string; meta?: { name: string; description: string } };
|
|
608
|
+
if (typeof args.script === "string") {
|
|
609
|
+
if (state.script !== args.script) {
|
|
610
|
+
state.script = args.script;
|
|
611
|
+
state.meta = undefined;
|
|
612
|
+
try { state.meta = parseWorkflowScript(args.script).meta; } catch { /* Streamed arguments may be incomplete. */ }
|
|
613
|
+
}
|
|
614
|
+
if (state.meta) { name = state.meta.name; purpose = state.meta.description; }
|
|
615
|
+
}
|
|
616
|
+
return new Text([
|
|
617
|
+
`${theme.bold("Workflow")} ${name}`,
|
|
618
|
+
purpose ? `${theme.fg("muted", "Purpose")} ${purpose}` : "",
|
|
619
|
+
`${theme.fg("muted", "Source")} ${args.scriptPath ? (context.expanded ? args.scriptPath : basename(args.scriptPath)) : args.name ?? "Ad-hoc workflow"}`,
|
|
620
|
+
`${theme.fg("muted", "Workspace")} ${context.cwd}`,
|
|
621
|
+
`${theme.fg("muted", "Mode")} ${args.background ? "Background" : "Foreground"}`,
|
|
622
|
+
theme.fg("warning", "Host access · children may execute unsandboxed; inspect effective permissions"),
|
|
623
|
+
].filter(Boolean).join("\n"), 0, 0);
|
|
600
624
|
},
|
|
601
|
-
renderResult(result, { expanded }, theme) {
|
|
625
|
+
renderResult(result, { expanded }, theme, context) {
|
|
602
626
|
// Pure function of the snapshot: the spinner frame is carried on the
|
|
603
627
|
// snapshot itself and advanced by the runtime heartbeat in execute(), so
|
|
604
628
|
// there is no UI-side timer to leak when a row is torn down or rendered in
|
|
605
629
|
// a non-live context (e.g. HTML export).
|
|
606
630
|
const details = result.details as WorkflowToolDetails;
|
|
607
|
-
return renderWorkflowSnapshot(details, theme, details.frame ?? 0, expanded);
|
|
631
|
+
return renderWorkflowSnapshot(details, theme, details.frame ?? 0, expanded, Boolean((context.state as { meta?: unknown } | undefined)?.meta));
|
|
608
632
|
},
|
|
609
633
|
});
|
|
610
634
|
}
|
|
@@ -733,14 +757,23 @@ function renderFlatAgents(container: Container, details: WorkflowToolDetails, th
|
|
|
733
757
|
}
|
|
734
758
|
}
|
|
735
759
|
|
|
736
|
-
function renderWorkflowSnapshot(details: WorkflowToolDetails, theme: Theme, frame: number, expanded: boolean): Container {
|
|
760
|
+
function renderWorkflowSnapshot(details: WorkflowToolDetails, theme: Theme, frame: number, expanded: boolean, purposeShown = false): Container {
|
|
737
761
|
const container = new Container();
|
|
762
|
+
const launch = details.launch as { description?: string } | undefined;
|
|
763
|
+
if (!purposeShown && launch?.description) container.addChild(new Text(`${theme.fg("muted", "Purpose")} ${launch.description}`, 0, 0));
|
|
764
|
+
const inspect = `${theme.fg("muted", "Run")} ${expanded ? details.runId : details.runId?.slice(-8)} · ${theme.fg("accent", "/external runs")} ${theme.fg("muted", "to inspect or follow")}`;
|
|
765
|
+
if (details.backgroundReceipt) {
|
|
766
|
+
container.addChild(new Text(`${theme.bold("Started in background")}\n${theme.fg("muted", "Launch receipt — not a live monitor")}\n${inspect}`, 0, 0));
|
|
767
|
+
return container;
|
|
768
|
+
}
|
|
769
|
+
if (details.runId) container.addChild(new Text(inspect, 0, 0));
|
|
770
|
+
if (!details.agents.length && details.status === "running") container.addChild(new Text("Preparing workflow · No assignments launched yet", 0, 0));
|
|
738
771
|
const done = details.agents.filter((agent) => isCompletedSubagentStatus(agent.status)).length;
|
|
739
772
|
const active = workflowRunningCount(details);
|
|
740
773
|
const queued = details.agents.filter((agent) => agent.status === "queued").length;
|
|
741
774
|
const failed = details.agents.filter(isFailedWorkflowAgent).length;
|
|
742
775
|
const counts = formatAgentCounts(done, active, queued, failed, details.agents.length);
|
|
743
|
-
const access =
|
|
776
|
+
const access = "";
|
|
744
777
|
container.addChild(
|
|
745
778
|
new Text(
|
|
746
779
|
`${theme.bold(`Workflow(${details.name})`)} ${theme.fg("dim", `${normalizeRunStatus(details.status)} · ${access}${counts}`)}`,
|
package/src/workflow/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ParentContextMessages, ParentContextReceipt } from "../core/parent-context.ts";
|
|
2
2
|
import type { ConcurrencyLimiter } from "../core/concurrency.ts";
|
|
3
3
|
import type { RunRecord } from "../core/run-record.ts";
|
|
4
|
-
import type { SubagentAssistantOutput } from "../types.ts";
|
|
4
|
+
import type { PermissionTier, SubagentAssistantOutput } from "../types.ts";
|
|
5
5
|
|
|
6
6
|
export type ChildRunOutcome = "failed" | "cancelled" | "timed_out";
|
|
7
7
|
|
|
@@ -112,6 +112,10 @@ export interface WorkflowAgentResultEvent extends WorkflowCachedAgentResult {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export interface WorkflowAgentQueuedEvent {
|
|
115
|
+
permission?: PermissionTier;
|
|
116
|
+
maxBudgetUsd?: number;
|
|
117
|
+
schema?: unknown;
|
|
118
|
+
authoredPrompt?: string;
|
|
115
119
|
index: number;
|
|
116
120
|
label: string;
|
|
117
121
|
phase?: string;
|