Skip to content
Merged
457 changes: 457 additions & 0 deletions extensions/workflows/completion-projection.ts

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion extensions/workflows/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
} from "@earendil-works/pi-coding-agent";
import { type TUI, truncateToWidth } from "@earendil-works/pi-tui";
import { AgentSessionPage } from "../shared/agent-session-page.ts";
import { contextPercent } from "../shared/context-utilization.ts";
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
import { contextPercent } from "../shared/context-utilization.ts";
import {
panelFrame,
type ScreenHint,
Expand Down Expand Up @@ -428,6 +428,9 @@ export function normalizePersistedWorkflowDetails(
logs.push({
at: typeof entry.at === "number" ? entry.at : startedAt,
text: sanitizeLine(entry.text, MAX_LOG_TEXT),
...(entry.kind === "pipeline-drop"
? { kind: "pipeline-drop" as const }
: {}),
});
}

Expand Down
167 changes: 108 additions & 59 deletions extensions/workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ import {
createStatusWriter,
formatActivityStatus,
} from "../shared/activity-status.ts";
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
import { waitBounded } from "../shared/child-session.ts";
import { contextPercent } from "../shared/context-utilization.ts";
import {
registerEditorLayer,
removeEditorLayer,
} from "../shared/editor-layers.ts";
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
import { loadSetupConfig } from "../shared/setup-config.ts";
import { SPINNER_INTERVAL_MS } from "../shared/spinner.ts";
import {
Expand Down Expand Up @@ -90,6 +90,14 @@ import {
persistWorkflowJson,
persistWorkflowTerminalState,
} from "./artifacts.ts";
import {
buildExpandedWorkflowCompletion,
buildWorkflowCompletionDisplay,
isWorkflowCompletionDisplay,
workflowCompletionAlerts,
workflowCompletionResultPreview,
workflowCompletionSummary,
} from "./completion-projection.ts";
import { RunController } from "./controller.ts";
import {
resolveWorkflowLaunchPolicy,
Expand Down Expand Up @@ -125,6 +133,7 @@ import {
agentContext,
aggregateUsage,
appendLog,
compactWorkflowToolDetails,
countStates,
createUsageReader,
emptyUsage,
Expand Down Expand Up @@ -159,7 +168,7 @@ import {
import {
buildBackgroundWorkflowFollowUp,
buildBackgroundWorkflowLaunchResult,
buildProjectedWorkflowCompletionBatch,
buildProjectedWorkflowCompletionBatches,
buildProjectedWorkflowResultMessage,
buildWorkflowAgentPrompt,
buildWorkflowResultMessage,
Expand All @@ -174,15 +183,15 @@ import {
WORKFLOW_STOP_TOOL_DESCRIPTION,
WORKFLOW_TOOL_DESCRIPTION,
} from "./prompt.ts";
import {
createWorkflowResultDelivery,
type WorkflowCompletionEnvelope,
} from "./result-delivery.ts";
import {
beginProcessReplayWorkspaceLease,
createReplayIdentity,
isReplaySafeAgentCall,
} from "./replay-safety.ts";
import {
createWorkflowResultDelivery,
type WorkflowCompletionEnvelope,
} from "./result-delivery.ts";
import {
createWorkflowResources,
runAgent,
Expand All @@ -191,7 +200,7 @@ import {
type WorkflowModel,
} from "./runner.ts";
import { runWorkflowSandbox } from "./sandbox.ts";
import { safeStringify, writeFileAtomic } from "./serialization.ts";
import { writeFileAtomic } from "./serialization.ts";
import {
finalizeWorktreeHandoff,
prepareWorktreeHandoff,
Expand Down Expand Up @@ -647,21 +656,6 @@ function appendArtifactPersistenceFailure(
? `${details.error}; ${persistenceFailure}`
: persistenceFailure;
}

function compactToolDetails(details: WorkflowDetails): WorkflowDetails {
return {
...details,
...(details.result !== undefined
? {
result: JSON.parse(
safeStringify(details.result, { maxBytes: 64 * 1024 }),
),
}
: {}),
agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
};
}

export interface ActiveWorkflowRunLifecycle {
details: WorkflowDetails;
controller: Pick<RunController, "abort" | "settle">;
Expand Down Expand Up @@ -834,27 +828,28 @@ export default function workflows(pi: ExtensionAPI) {
details,
),
deliver: async (envelopes, wake) => {
const content = buildProjectedWorkflowCompletionBatch(
envelopes.map((envelope) => ({
deliveryId: envelope.deliveryId,
details: envelope.details,
runDir: path.join(getAgentDir(), "workflows", envelope.runId),
})),
const sourceEntries = envelopes.map((envelope) => ({
deliveryId: envelope.deliveryId,
details: envelope.details,
runDir: path.join(getAgentDir(), "workflows", envelope.runId),
}));
const batches = buildProjectedWorkflowCompletionBatches(
sourceEntries,
lastContext?.getContextUsage?.(),
);
pi.sendMessage(
{
customType: "workflow-result",
content,
display: true,
...(envelopes.length === 1
? { details: compactToolDetails(envelopes[0]!.details) }
: {}),
},
wake
? { deliverAs: "followUp", triggerTurn: true }
: { deliverAs: "nextTurn" },
);
for (const batch of batches) {
pi.sendMessage(
{
customType: "workflow-result",
content: batch.content,
display: true,
details: buildWorkflowCompletionDisplay(batch.entries),
},
wake
? { deliverAs: "followUp", triggerTurn: true }
: { deliverAs: "nextTurn" },
);
}
return envelopes.map((envelope) => ({
deliveryId: envelope.deliveryId,
delivered: true,
Expand Down Expand Up @@ -1295,7 +1290,7 @@ export default function workflows(pi: ExtensionAPI) {
if (background) return;
onUpdate?.({
content: [{ type: "text", text: summaryLine(details) }],
details: compactToolDetails(details),
details: compactWorkflowToolDetails(details),
});
};
const emit = (checkpoint = true) => {
Expand Down Expand Up @@ -1392,9 +1387,9 @@ export default function workflows(pi: ExtensionAPI) {

// The script's narrator. Unlike phase(), this is append-only progress
// text, so it never mutates the phase list a run is judged against.
const logFn = (text: string) => {
const logFn = (text: string, kind?: "pipeline-drop") => {
if (runSettled) return;
appendLog(details, text, Date.now());
appendLog(details, text, Date.now(), kind);
emit();
};

Expand Down Expand Up @@ -2250,7 +2245,7 @@ export default function workflows(pi: ExtensionAPI) {
}),
},
],
details: compactToolDetails(details),
details: compactWorkflowToolDetails(details),
};
}

Expand Down Expand Up @@ -2279,7 +2274,7 @@ export default function workflows(pi: ExtensionAPI) {
),
},
],
details: compactToolDetails(details),
details: compactWorkflowToolDetails(details),
};
},

Expand Down Expand Up @@ -2485,26 +2480,80 @@ export default function workflows(pi: ExtensionAPI) {
pi.registerMessageRenderer(
"workflow-result",
(message, { expanded }, theme) => {
const details = message.details as WorkflowDetails | undefined;
const body =
typeof message.content === "string"
? message.content
: (message.content
?.map((part) => (part.type === "text" ? part.text : ""))
.join("") ?? "");
const safeBody = sanitizeWorkflowDisplayText(body);
if (!details) return new Text(safeBody, 0, 0);
const headerParts = runHeader(details, theme, Date.now());
const header = headerParts.right
? `${headerParts.left} ${headerParts.right}`
: headerParts.left;
if (expanded) return new Text(`${header}\n\n${safeBody}`, 0, 0);
const preview = safeBody.split("\n").slice(0, 8).join("\n");
return new Text(
`${header}\n${preview}\n${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`,
0,
0,
);
const display = isWorkflowCompletionDisplay(message.details)
? message.details
: undefined;
const legacyDetails = isWorkflowRenderDetails(message.details)
? message.details
: undefined;
if (!display && !legacyDetails) {
return new Text(safeBody, 0, 0);
}
if (legacyDetails) {
const headerParts = runHeader(legacyDetails, theme, Date.now());
const header = headerParts.right
? `${headerParts.left} ${headerParts.right}`
: headerParts.left;
if (expanded) return new Text(`${header}\n\n${safeBody}`, 0, 0);
const preview = safeBody.split("\n").slice(0, 8).join("\n");
return new Text(
`${header}\n${preview}\n${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`,
0,
0,
);
}
if (!display) return new Text(safeBody, 0, 0);
if (expanded) {
return new Text(buildExpandedWorkflowCompletion(display), 0, 0);
}
return {
render(width: number) {
const rows: string[] = [];
for (const entry of display.entries) {
rows.push(
truncateToWidth(
`${statusGlyph(entry.status, theme, Date.now())} ${workflowCompletionSummary(entry)}`,
width,
"…",
),
);
for (const alert of workflowCompletionAlerts(entry)) {
rows.push(
truncateToWidth(` ${theme.fg("error", alert)}`, width, "…"),
);
}
const result = workflowCompletionResultPreview(entry);
if (result) {
rows.push(
truncateToWidth(
` ${theme.fg("accent", "Result:")} ${result}`,
width,
"…",
),
);
}
}
rows.push(
truncateToWidth(
theme.fg(
"muted",
`(${keyHint("app.tools.expand", "to expand")})`,
),
width,
"…",
),
);
return rows;
},
invalidate() {},
};
},
);
}
24 changes: 22 additions & 2 deletions extensions/workflows/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import * as os from "node:os";
import {
truncateHead,
type ExtensionContext,
truncateHead,
} from "@earendil-works/pi-coding-agent";
import { formatContextUtilization } from "../shared/context-utilization.ts";
import { spinnerFrame } from "../shared/spinner.ts";
Expand Down Expand Up @@ -141,6 +141,8 @@ export interface AgentRecord {
export interface WorkflowLogEntry {
at: number;
text: string;
/** Runtime-authored evidence, distinct from free-form script narration. */
kind?: "pipeline-drop";
}

export interface WorkflowDetails {
Expand Down Expand Up @@ -174,6 +176,23 @@ export interface WorkflowDetails {
error?: string;
}

/** Bounded tool-result projection; authoritative details remain in run artifacts. */
export function compactWorkflowToolDetails(
details: WorkflowDetails,
): WorkflowDetails {
return {
...details,
...(details.result !== undefined
? {
result: JSON.parse(
safeStringify(details.result, { maxBytes: 64 * 1024 }),
),
}
: {}),
agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
};
}

/**
* Bound only the current-session terminal projection. Persisted workflow
* records and side artifacts remain the canonical history.
Expand Down Expand Up @@ -335,11 +354,12 @@ export function appendLog(
details: WorkflowDetails,
text: string,
at: number,
kind?: WorkflowLogEntry["kind"],
): void {
const clean = sanitizeLine(text, MAX_LOG_TEXT);
if (!clean) return;
const logs = (details.logs ??= []);
logs.push({ at, text: clean });
logs.push({ at, text: clean, ...(kind ? { kind } : {}) });
const excess = logs.length - MAX_LOG_ENTRIES;
if (excess > 0) {
logs.splice(0, excess);
Expand Down
Loading
Loading