@osolmaz/pi-workflows 0.5.3 → 0.6.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/README.md +10 -2
- package/dist/builtins/catalog.js +1 -1
- package/dist/builtins/monitor.workflow.d.ts +25 -69
- package/dist/builtins/monitor.workflow.js +194 -123
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/extension/executor.d.ts +7 -2
- package/dist/extension/executor.js +20 -14
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.js +51 -13
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/step-message.d.ts +24 -0
- package/dist/extension/step-message.js +106 -0
- package/dist/extension/step-message.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -2
- package/dist/extension/widget.js +64 -5
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +9 -0
- package/dist/extension/workflow-tool.js +10 -0
- package/dist/extension/workflow-tool.js.map +1 -1
- package/dist/host/rpc-bridge.js +25 -11
- package/dist/host/rpc-bridge.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +2 -2
- package/dist/host/rpc-executor.js +23 -12
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/viewer/cli.js +1 -1
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +14 -0
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.js +1 -1
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -1
- package/dist/workflows/engine.js +88 -6
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +4 -2
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +34 -0
- package/dist/workflows/progress.js +268 -0
- package/dist/workflows/progress.js.map +1 -0
- package/dist/workflows/schema.js +21 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +2 -2
- package/dist/workflows/shell.js +103 -25
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.d.ts +15 -2
- package/dist/workflows/store.js +44 -2
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +52 -1
- package/dist/workflows/updates.d.ts +15 -0
- package/dist/workflows/updates.js +188 -0
- package/dist/workflows/updates.js.map +1 -0
- package/docs/DESIGN_PHILOSOPHY.md +51 -0
- package/docs/MONITOR.md +282 -0
- package/docs/WORKFLOW_STEP_MESSAGES.md +141 -0
- package/docs/WORKFLOW_UPDATES.md +416 -0
- package/docs/development.md +7 -3
- package/docs/plans/2026-08-16-workflow-updates-plan.md +494 -0
- package/docs/run-bundles.md +10 -2
- package/docs/workflows.md +57 -17
- package/package.json +1 -1
- package/src/builtins/catalog.ts +1 -1
- package/src/builtins/monitor.workflow.ts +217 -148
- package/src/extension/executor.ts +36 -14
- package/src/extension/index.ts +81 -19
- package/src/extension/step-message.ts +145 -0
- package/src/extension/widget.ts +93 -4
- package/src/extension/workflow-tool.ts +22 -0
- package/src/host/rpc-bridge.ts +37 -14
- package/src/host/rpc-executor.ts +35 -14
- package/src/viewer/cli.ts +1 -1
- package/src/viewer/render.ts +27 -0
- package/src/viewer/tui.ts +1 -1
- package/src/workflows/engine.ts +117 -4
- package/src/workflows/index.ts +32 -0
- package/src/workflows/progress.ts +326 -0
- package/src/workflows/schema.ts +23 -1
- package/src/workflows/shell.ts +109 -26
- package/src/workflows/store.ts +67 -2
- package/src/workflows/types.ts +78 -1
- package/src/workflows/updates.ts +208 -0
package/src/extension/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../workflows/errors.js";
|
|
15
15
|
import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
|
|
16
16
|
import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
|
|
17
|
+
import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/progress.js";
|
|
17
18
|
import {
|
|
18
19
|
createRunId,
|
|
19
20
|
listRunBundles,
|
|
@@ -28,6 +29,7 @@ import type {
|
|
|
28
29
|
WorkflowDefinitionSnapshot,
|
|
29
30
|
WorkflowRunResult,
|
|
30
31
|
WorkflowRunState,
|
|
32
|
+
WorkflowUpdateRecord,
|
|
31
33
|
} from "../workflows/types.js";
|
|
32
34
|
import {
|
|
33
35
|
PiControllerHost,
|
|
@@ -36,6 +38,12 @@ import {
|
|
|
36
38
|
} from "./controller-host.js";
|
|
37
39
|
import { ConversationStepExecutor } from "./executor.js";
|
|
38
40
|
import { SessionRecorder } from "./recorder.js";
|
|
41
|
+
import {
|
|
42
|
+
registerWorkflowAgentStepMessageRenderer,
|
|
43
|
+
WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
44
|
+
WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
45
|
+
type WorkflowAgentStepMessageDetails,
|
|
46
|
+
} from "./step-message.js";
|
|
39
47
|
import { buildWidgetView } from "./widget.js";
|
|
40
48
|
import { WorkflowToolParameters, type WorkflowToolInput } from "./workflow-tool.js";
|
|
41
49
|
|
|
@@ -71,6 +79,7 @@ type ActiveRun = {
|
|
|
71
79
|
presentationPrompt: WorkflowDefinition["presentationPrompt"];
|
|
72
80
|
generation: number;
|
|
73
81
|
lastState: WorkflowRunState | null;
|
|
82
|
+
updateHistory: WorkflowUpdateRecord[];
|
|
74
83
|
childKey?: string;
|
|
75
84
|
onFinish?: (result: WorkflowSchedulerResult) => void;
|
|
76
85
|
completion?: Promise<void>;
|
|
@@ -191,6 +200,10 @@ function workflowStateSummary(state: WorkflowRunState): JsonObject {
|
|
|
191
200
|
workflowName: state.workflowName,
|
|
192
201
|
status: state.status,
|
|
193
202
|
steps: state.steps.length,
|
|
203
|
+
updates: (state.updates ?? []).map(({ data, ...record }) => ({
|
|
204
|
+
...record,
|
|
205
|
+
data: data as JsonObject,
|
|
206
|
+
})),
|
|
194
207
|
...(state.currentNode !== undefined ? { currentNode: state.currentNode } : {}),
|
|
195
208
|
...(state.waitingOn !== undefined ? { waitingOn: state.waitingOn } : {}),
|
|
196
209
|
...(state.startedAt !== undefined ? { startedAt: state.startedAt } : {}),
|
|
@@ -202,6 +215,7 @@ function workflowStateSummary(state: WorkflowRunState): JsonObject {
|
|
|
202
215
|
type WidgetSource = {
|
|
203
216
|
state: WorkflowRunState;
|
|
204
217
|
snapshot: WorkflowDefinitionSnapshot;
|
|
218
|
+
updateHistory?: WorkflowUpdateRecord[];
|
|
205
219
|
};
|
|
206
220
|
|
|
207
221
|
type WorkflowWidgetComponent = {
|
|
@@ -213,6 +227,8 @@ type WorkflowWidgetFactory = (_tui: unknown, theme: Theme) => WorkflowWidgetComp
|
|
|
213
227
|
type WorkflowWidgetContent = string[] | WorkflowWidgetFactory;
|
|
214
228
|
|
|
215
229
|
export default function piWorkflows(pi: ExtensionAPI) {
|
|
230
|
+
registerWorkflowAgentStepMessageRenderer(pi);
|
|
231
|
+
|
|
216
232
|
// One runner identity per session; it names this session in run claims.
|
|
217
233
|
const runnerId = randomUUID();
|
|
218
234
|
let runQueueStore: SqliteControllerStore | null = null;
|
|
@@ -267,16 +283,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
267
283
|
leaseMs: NOTIFICATION_DELIVERY_LEASE_MS,
|
|
268
284
|
})) {
|
|
269
285
|
if (!alreadyDelivered.has(notification.notificationId)) {
|
|
270
|
-
pi.sendMessage(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
286
|
+
pi.sendMessage(
|
|
287
|
+
{
|
|
288
|
+
customType: "pi-workflows-notification",
|
|
289
|
+
content: notification.content,
|
|
290
|
+
display: true,
|
|
291
|
+
details: {
|
|
292
|
+
notificationId: notification.notificationId,
|
|
293
|
+
runId: notification.runId,
|
|
294
|
+
kind: notification.kind,
|
|
295
|
+
},
|
|
278
296
|
},
|
|
279
|
-
|
|
297
|
+
{ triggerTurn: false },
|
|
298
|
+
);
|
|
280
299
|
alreadyDelivered.add(notification.notificationId);
|
|
281
300
|
}
|
|
282
301
|
runQueueStore.markWorkflowNotificationDelivered({
|
|
@@ -387,6 +406,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
387
406
|
runHeld(),
|
|
388
407
|
width,
|
|
389
408
|
theme,
|
|
409
|
+
widgetSource.updateHistory,
|
|
390
410
|
);
|
|
391
411
|
widgetShownScroll = view.scroll;
|
|
392
412
|
widgetMaxScroll = view.maxScroll;
|
|
@@ -411,13 +431,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
411
431
|
ctx: ExtensionContext,
|
|
412
432
|
state: WorkflowRunState,
|
|
413
433
|
snapshot: WorkflowDefinitionSnapshot,
|
|
434
|
+
updateHistory?: WorkflowUpdateRecord[],
|
|
414
435
|
) => {
|
|
415
436
|
if (state.steps.length !== widgetStepCount) {
|
|
416
437
|
widgetStepCount = state.steps.length;
|
|
417
438
|
// The workflow moved on; resume following the active node.
|
|
418
439
|
widgetScroll = null;
|
|
419
440
|
}
|
|
420
|
-
widgetSource = {
|
|
441
|
+
widgetSource = {
|
|
442
|
+
state,
|
|
443
|
+
snapshot,
|
|
444
|
+
...(updateHistory !== undefined ? { updateHistory: [...updateHistory] } : {}),
|
|
445
|
+
};
|
|
421
446
|
renderWidget(ctx);
|
|
422
447
|
};
|
|
423
448
|
|
|
@@ -596,7 +621,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
596
621
|
void run.recorder?.stop();
|
|
597
622
|
stopWidgetTicker();
|
|
598
623
|
const { state } = result;
|
|
599
|
-
updateWidget(ctx, state, run.snapshot);
|
|
624
|
+
updateWidget(ctx, state, run.snapshot, run.updateHistory);
|
|
600
625
|
clearWidgetTimer();
|
|
601
626
|
// A waiting run is parked at a checkpoint for a human; keep its widget up
|
|
602
627
|
// until a new workflow replaces it. Terminal runs fade after a grace TTL.
|
|
@@ -734,8 +759,25 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
734
759
|
}
|
|
735
760
|
|
|
736
761
|
const executor = new ConversationStepExecutor({
|
|
737
|
-
sendPrompt: ({ prompt, streaming }) => {
|
|
738
|
-
|
|
762
|
+
sendPrompt: ({ prompt, contract, presentation, kind, streaming }) => {
|
|
763
|
+
const details: WorkflowAgentStepMessageDetails = {
|
|
764
|
+
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
765
|
+
kind,
|
|
766
|
+
contract,
|
|
767
|
+
...(presentation !== undefined ? { presentation } : {}),
|
|
768
|
+
};
|
|
769
|
+
pi.sendMessage(
|
|
770
|
+
{
|
|
771
|
+
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
772
|
+
content: prompt,
|
|
773
|
+
display: true,
|
|
774
|
+
details,
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
triggerTurn: true,
|
|
778
|
+
deliverAs: streaming ? "steer" : "followUp",
|
|
779
|
+
},
|
|
780
|
+
);
|
|
739
781
|
},
|
|
740
782
|
onAbort: (contract, reason) => {
|
|
741
783
|
lastExpiredAttempt = {
|
|
@@ -799,9 +841,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
799
841
|
onRunFinishing: async () => {
|
|
800
842
|
await run.recorder?.finish();
|
|
801
843
|
},
|
|
802
|
-
onEvent: (
|
|
844
|
+
onEvent: (event, state: WorkflowRunState) => {
|
|
803
845
|
run.lastState = state;
|
|
804
|
-
|
|
846
|
+
run.updateHistory = appendProgressHistory(
|
|
847
|
+
run.updateHistory,
|
|
848
|
+
progressRecordsFromTrace([event]),
|
|
849
|
+
);
|
|
850
|
+
updateWidget(ctx, state, snapshot, run.updateHistory);
|
|
805
851
|
},
|
|
806
852
|
});
|
|
807
853
|
const run: ActiveRun = {
|
|
@@ -814,6 +860,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
814
860
|
presentationPrompt: options.presentation === false ? undefined : workflow.presentationPrompt,
|
|
815
861
|
generation,
|
|
816
862
|
lastState: null,
|
|
863
|
+
updateHistory: [],
|
|
817
864
|
...(options.childKey !== undefined ? { childKey: options.childKey } : {}),
|
|
818
865
|
...(options.onFinish !== undefined ? { onFinish: options.onFinish } : {}),
|
|
819
866
|
...(claimToken !== undefined ? { claimToken } : {}),
|
|
@@ -1562,13 +1609,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1562
1609
|
name: "workflow",
|
|
1563
1610
|
label: "Workflow",
|
|
1564
1611
|
description: [
|
|
1565
|
-
"List, start, inspect, pause, resume, cancel, answer, or complete Pi Workflows runs.",
|
|
1566
|
-
"When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, everyMinutes,
|
|
1567
|
-
"Use submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
1612
|
+
"List, start, inspect, pause, resume, cancel, answer, update, or complete Pi Workflows runs.",
|
|
1613
|
+
"When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, everyMinutes, stopWhen, and optional maxChecks.",
|
|
1614
|
+
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
1568
1615
|
"Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
|
|
1569
1616
|
].join(" "),
|
|
1570
1617
|
parameters: WorkflowToolParameters,
|
|
1571
|
-
async execute(
|
|
1618
|
+
async execute(toolCallId, params: WorkflowToolInput, _signal, _onUpdate, ctx) {
|
|
1572
1619
|
let control: WorkflowControlResult;
|
|
1573
1620
|
switch (params.action) {
|
|
1574
1621
|
case "list":
|
|
@@ -1596,6 +1643,21 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1596
1643
|
});
|
|
1597
1644
|
break;
|
|
1598
1645
|
}
|
|
1646
|
+
case "update": {
|
|
1647
|
+
if (!activeRun) throw new Error("No workflow step is active.");
|
|
1648
|
+
const receipt = await activeRun.engine.publishUpdate(
|
|
1649
|
+
params.step,
|
|
1650
|
+
params.attempt,
|
|
1651
|
+
params.update,
|
|
1652
|
+
toolCallId,
|
|
1653
|
+
);
|
|
1654
|
+
return {
|
|
1655
|
+
content: [
|
|
1656
|
+
{ type: "text", text: "Workflow update published; the step remains active." },
|
|
1657
|
+
],
|
|
1658
|
+
details: { action: "update", ...receipt },
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1599
1661
|
case "submit": {
|
|
1600
1662
|
if (!activeRun) {
|
|
1601
1663
|
if (
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Box, Text, TruncatedText } from "@earendil-works/pi-tui";
|
|
3
|
+
import { sanitizeText } from "../workflows/text.js";
|
|
4
|
+
import type { AgentStepContract, AgentStepPresentation } from "../workflows/types.js";
|
|
5
|
+
import type { PromptDeliveryKind } from "./executor.js";
|
|
6
|
+
|
|
7
|
+
export const WORKFLOW_AGENT_STEP_MESSAGE_TYPE = "pi-workflows-agent-step";
|
|
8
|
+
export const WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA = "pi-workflows.agent-step-message.v1";
|
|
9
|
+
|
|
10
|
+
export type WorkflowAgentStepMessageDetails = {
|
|
11
|
+
schema: typeof WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA;
|
|
12
|
+
kind: PromptDeliveryKind;
|
|
13
|
+
contract: AgentStepContract;
|
|
14
|
+
presentation?: AgentStepPresentation;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type WorkflowAgentStepMessage = {
|
|
18
|
+
content: unknown;
|
|
19
|
+
details?: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type WorkflowAgentStepView = {
|
|
23
|
+
title: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
expandedText?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function registerWorkflowAgentStepMessageRenderer(pi: ExtensionAPI): void {
|
|
29
|
+
pi.registerMessageRenderer<WorkflowAgentStepMessageDetails>(
|
|
30
|
+
WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
31
|
+
(message, { expanded }, theme) => {
|
|
32
|
+
const view = buildWorkflowAgentStepView(message, expanded, theme);
|
|
33
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
34
|
+
box.addChild(new TruncatedText(view.title));
|
|
35
|
+
if (view.status !== undefined) {
|
|
36
|
+
box.addChild(new TruncatedText(view.status));
|
|
37
|
+
}
|
|
38
|
+
if (view.expandedText !== undefined) {
|
|
39
|
+
box.addChild(new Text(`\n${view.expandedText}`));
|
|
40
|
+
}
|
|
41
|
+
return box;
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Build renderer text without depending on Pi's session or TUI state. */
|
|
47
|
+
export function buildWorkflowAgentStepView(
|
|
48
|
+
message: WorkflowAgentStepMessage,
|
|
49
|
+
expanded: boolean,
|
|
50
|
+
theme?: Pick<Theme, "fg">,
|
|
51
|
+
): WorkflowAgentStepView {
|
|
52
|
+
const details = parseDetails(message.details);
|
|
53
|
+
const contract = details?.contract;
|
|
54
|
+
const kind = details?.kind ?? "step";
|
|
55
|
+
const workflowName = cleanSingleLine(contract?.workflowName ?? "Workflow");
|
|
56
|
+
const nodeId = cleanSingleLine(contract?.nodeId ?? "step");
|
|
57
|
+
const runTitle = cleanOptionalSingleLine(details?.presentation?.runTitle);
|
|
58
|
+
const statusDetail = cleanOptionalSingleLine(details?.presentation?.statusDetail);
|
|
59
|
+
const label = runTitle ?? workflowName;
|
|
60
|
+
const suffix = kind === "step" ? "" : ` · ${kind}`;
|
|
61
|
+
const glyph = kind === "step" ? "▶" : "↻";
|
|
62
|
+
const title = paint(theme, "accent", `${glyph} ${label} › ${nodeId}${suffix}`);
|
|
63
|
+
|
|
64
|
+
if (!expanded) {
|
|
65
|
+
return {
|
|
66
|
+
title,
|
|
67
|
+
...(statusDetail !== undefined ? { status: paint(theme, "dim", statusDetail) } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const metadata = [
|
|
72
|
+
`Workflow: ${workflowName}`,
|
|
73
|
+
...(runTitle !== undefined ? [`Run title: ${runTitle}`] : []),
|
|
74
|
+
`Run id: ${cleanSingleLine(contract?.runId ?? "unknown")}`,
|
|
75
|
+
`Node id: ${nodeId}`,
|
|
76
|
+
`Attempt id: ${cleanSingleLine(contract?.attemptId ?? "unknown")}`,
|
|
77
|
+
`Delivery: ${kind}`,
|
|
78
|
+
`Expected output: ${cleanSingleLine(contract?.expectedOutput ?? "a JSON object with your result")}`,
|
|
79
|
+
];
|
|
80
|
+
const prompt = contentText(message.content);
|
|
81
|
+
return {
|
|
82
|
+
title,
|
|
83
|
+
...(statusDetail !== undefined ? { status: paint(theme, "dim", statusDetail) } : {}),
|
|
84
|
+
expandedText: `${metadata.map((line) => paint(theme, "dim", line)).join("\n")}\n\n${prompt}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseDetails(value: unknown): WorkflowAgentStepMessageDetails | undefined {
|
|
89
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
90
|
+
const candidate = value as Partial<WorkflowAgentStepMessageDetails>;
|
|
91
|
+
if (candidate.schema !== WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA) return undefined;
|
|
92
|
+
if (candidate.kind !== "step" && candidate.kind !== "reminder" && candidate.kind !== "resume") {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const contract = candidate.contract;
|
|
96
|
+
if (
|
|
97
|
+
contract === null ||
|
|
98
|
+
typeof contract !== "object" ||
|
|
99
|
+
typeof contract.runId !== "string" ||
|
|
100
|
+
typeof contract.workflowName !== "string" ||
|
|
101
|
+
typeof contract.nodeId !== "string" ||
|
|
102
|
+
typeof contract.attemptId !== "string"
|
|
103
|
+
) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return candidate as WorkflowAgentStepMessageDetails;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function contentText(content: unknown): string {
|
|
110
|
+
if (typeof content === "string") return cleanMultiline(content);
|
|
111
|
+
if (!Array.isArray(content)) return "";
|
|
112
|
+
return content
|
|
113
|
+
.map((part) => {
|
|
114
|
+
if (part === null || typeof part !== "object" || !("text" in part)) return "";
|
|
115
|
+
const text = (part as { text?: unknown }).text;
|
|
116
|
+
return typeof text === "string" ? cleanMultiline(text) : "";
|
|
117
|
+
})
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cleanOptionalSingleLine(value: unknown): string | undefined {
|
|
123
|
+
return typeof value === "string" && value.length > 0 ? cleanSingleLine(value) : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cleanSingleLine(value: string): string {
|
|
127
|
+
return sanitizeText(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function cleanMultiline(value: string): string {
|
|
131
|
+
return value
|
|
132
|
+
.replaceAll("\r\n", "\n")
|
|
133
|
+
.replaceAll("\r", "\n")
|
|
134
|
+
.split("\n")
|
|
135
|
+
.map((line) => sanitizeText(line))
|
|
136
|
+
.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function paint(
|
|
140
|
+
theme: Pick<Theme, "fg"> | undefined,
|
|
141
|
+
color: "accent" | "dim",
|
|
142
|
+
text: string,
|
|
143
|
+
): string {
|
|
144
|
+
return theme?.fg(color, text) ?? text;
|
|
145
|
+
}
|
package/src/extension/widget.ts
CHANGED
|
@@ -2,11 +2,20 @@ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { formatDuration } from "../render/format.js";
|
|
4
4
|
import { nodeTypeGlyph } from "../render/node-type.js";
|
|
5
|
+
import {
|
|
6
|
+
estimateProgress,
|
|
7
|
+
formatProgressLine,
|
|
8
|
+
formatRemaining,
|
|
9
|
+
prioritizeProgressEstimates,
|
|
10
|
+
progressTracksFromRecords,
|
|
11
|
+
type ProgressEstimate,
|
|
12
|
+
} from "../workflows/progress.js";
|
|
5
13
|
import { sanitizeText } from "../workflows/text.js";
|
|
6
14
|
import type {
|
|
7
15
|
WorkflowDefinitionSnapshot,
|
|
8
16
|
WorkflowRunState,
|
|
9
17
|
WorkflowRunStatus,
|
|
18
|
+
WorkflowUpdateRecord,
|
|
10
19
|
} from "../workflows/types.js";
|
|
11
20
|
|
|
12
21
|
const STATUS_GLYPHS: Record<WorkflowRunStatus, string> = {
|
|
@@ -68,6 +77,7 @@ export function buildWidgetView(
|
|
|
68
77
|
held = false,
|
|
69
78
|
width = Number.POSITIVE_INFINITY,
|
|
70
79
|
theme?: WidgetTheme,
|
|
80
|
+
updateHistory?: WorkflowUpdateRecord[],
|
|
71
81
|
): WidgetView {
|
|
72
82
|
const availableWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : width;
|
|
73
83
|
if (availableWidth === 0) return { lines: [], scroll: 0, maxScroll: 0 };
|
|
@@ -93,13 +103,17 @@ export function buildWidgetView(
|
|
|
93
103
|
);
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
const
|
|
106
|
+
const progress = progressLines(state, now, updateHistory).slice(0, 4);
|
|
107
|
+
const budget = PI_MAX_WIDGET_LINES - 1 - footer.length - progress.length;
|
|
97
108
|
const nodes = displayNodeIds(snapshot).map((nodeId) =>
|
|
98
109
|
compactNodeLine(state, snapshot, nodeId, now, paused, theme),
|
|
99
110
|
);
|
|
100
|
-
if (nodes.length === 0) {
|
|
111
|
+
if (nodes.length === 0 || budget <= 0) {
|
|
101
112
|
return {
|
|
102
|
-
lines: fitLines(
|
|
113
|
+
lines: fitLines(
|
|
114
|
+
[header, ...progress, ...footer].slice(0, PI_MAX_WIDGET_LINES),
|
|
115
|
+
availableWidth,
|
|
116
|
+
),
|
|
103
117
|
scroll: 0,
|
|
104
118
|
maxScroll: 0,
|
|
105
119
|
};
|
|
@@ -110,7 +124,12 @@ export function buildWidgetView(
|
|
|
110
124
|
const indentation = availableWidth >= 3 ? " " : "";
|
|
111
125
|
return {
|
|
112
126
|
lines: fitLines(
|
|
113
|
-
[
|
|
127
|
+
[
|
|
128
|
+
header,
|
|
129
|
+
...windowed.lines.map((line) => `${indentation}${line}`),
|
|
130
|
+
...progress.map((line) => `${indentation}${line}`),
|
|
131
|
+
...footer,
|
|
132
|
+
],
|
|
114
133
|
availableWidth,
|
|
115
134
|
),
|
|
116
135
|
scroll: windowed.scroll,
|
|
@@ -118,6 +137,76 @@ export function buildWidgetView(
|
|
|
118
137
|
};
|
|
119
138
|
}
|
|
120
139
|
|
|
140
|
+
function progressLines(
|
|
141
|
+
state: WorkflowRunState,
|
|
142
|
+
now: Date,
|
|
143
|
+
updateHistory?: WorkflowUpdateRecord[],
|
|
144
|
+
): string[] {
|
|
145
|
+
const measured =
|
|
146
|
+
updateHistory === undefined
|
|
147
|
+
? undefined
|
|
148
|
+
: progressTracksFromRecords(updateHistory, now).map((track) => track.estimate);
|
|
149
|
+
const projected = mergeProgressEstimates(latestProgressEstimates(state, now), measured ?? []);
|
|
150
|
+
const estimates = mergeProgressEstimates(projected, monitorEstimates(state) ?? []);
|
|
151
|
+
const lines = prioritizeProgressEstimates(estimates).map((estimate) =>
|
|
152
|
+
formatProgressLine(estimate, now),
|
|
153
|
+
);
|
|
154
|
+
const schedule = (state.updates ?? []).find(
|
|
155
|
+
(record) => record.type === "monitor.schedule" && record.key === "next-check",
|
|
156
|
+
);
|
|
157
|
+
if (schedule !== undefined && typeof schedule.data.nextCheckAt === "string") {
|
|
158
|
+
const next = Date.parse(schedule.data.nextCheckAt);
|
|
159
|
+
if (Number.isFinite(next)) {
|
|
160
|
+
const age = Math.max(0, now.getTime() - Date.parse(schedule.at));
|
|
161
|
+
lines.push(
|
|
162
|
+
`Last update ${formatRemaining(age)} ago next check ${formatRemaining(next - now.getTime())}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function monitorEstimates(state: WorkflowRunState): ProgressEstimate[] | undefined {
|
|
170
|
+
const output = state.outputs.estimate;
|
|
171
|
+
if (output === null || typeof output !== "object" || Array.isArray(output)) return undefined;
|
|
172
|
+
const tracks = (output as { tracks?: unknown }).tracks;
|
|
173
|
+
if (!Array.isArray(tracks)) return undefined;
|
|
174
|
+
const estimates = tracks
|
|
175
|
+
.map((track) =>
|
|
176
|
+
track !== null && typeof track === "object" && !Array.isArray(track)
|
|
177
|
+
? (track as { estimate?: ProgressEstimate }).estimate
|
|
178
|
+
: undefined,
|
|
179
|
+
)
|
|
180
|
+
.filter((estimate): estimate is ProgressEstimate => estimate !== undefined);
|
|
181
|
+
return estimates.length > 0 ? estimates : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function mergeProgressEstimates(
|
|
185
|
+
latest: ProgressEstimate[],
|
|
186
|
+
measured: ProgressEstimate[],
|
|
187
|
+
): ProgressEstimate[] {
|
|
188
|
+
const measuredByKey = new Map(measured.map((estimate) => [estimate.key, estimate]));
|
|
189
|
+
const merged = latest.map((estimate) => measuredByKey.get(estimate.key) ?? estimate);
|
|
190
|
+
const latestKeys = new Set(latest.map((estimate) => estimate.key));
|
|
191
|
+
merged.push(...measured.filter((estimate) => !latestKeys.has(estimate.key)));
|
|
192
|
+
return merged;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function latestProgressEstimates(state: WorkflowRunState, now: Date): ProgressEstimate[] {
|
|
196
|
+
const estimates: ProgressEstimate[] = [];
|
|
197
|
+
for (const record of state.updates ?? []) {
|
|
198
|
+
if (record.type !== "progress") continue;
|
|
199
|
+
try {
|
|
200
|
+
estimates.push(
|
|
201
|
+
estimateProgress(record.key, [{ at: record.at, data: record.data as never }], now),
|
|
202
|
+
);
|
|
203
|
+
} catch {
|
|
204
|
+
// A malformed historical update must not break the workflow widget.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return estimates;
|
|
208
|
+
}
|
|
209
|
+
|
|
121
210
|
function compactFocusIndex(state: WorkflowRunState, snapshot: WorkflowDefinitionSnapshot): number {
|
|
122
211
|
const nodeIds = displayNodeIds(snapshot);
|
|
123
212
|
const focused = state.currentNode ?? state.waitingOn;
|
|
@@ -37,6 +37,22 @@ export const WorkflowToolParameters: TSchema = Type.Union([
|
|
|
37
37
|
},
|
|
38
38
|
noExtraProperties,
|
|
39
39
|
),
|
|
40
|
+
Type.Object(
|
|
41
|
+
{
|
|
42
|
+
action: StringEnum(["update"] as const),
|
|
43
|
+
step: Type.String({ description: "Active step id" }),
|
|
44
|
+
attempt: Type.String({ description: "Active attempt id" }),
|
|
45
|
+
update: Type.Object(
|
|
46
|
+
{
|
|
47
|
+
type: Type.String(),
|
|
48
|
+
key: Type.String(),
|
|
49
|
+
data: Type.Record(Type.String(), Type.Unknown()),
|
|
50
|
+
},
|
|
51
|
+
noExtraProperties,
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
noExtraProperties,
|
|
55
|
+
),
|
|
40
56
|
Type.Object(
|
|
41
57
|
{
|
|
42
58
|
action: StringEnum(["submit"] as const),
|
|
@@ -56,4 +72,10 @@ export type WorkflowToolInput =
|
|
|
56
72
|
| { action: "resume" }
|
|
57
73
|
| { action: "cancel" }
|
|
58
74
|
| { action: "answer"; input: unknown; runId?: string }
|
|
75
|
+
| {
|
|
76
|
+
action: "update";
|
|
77
|
+
step: string;
|
|
78
|
+
attempt: string;
|
|
79
|
+
update: { type: string; key: string; data: Record<string, unknown> };
|
|
80
|
+
}
|
|
59
81
|
| { action: "submit"; step: string; attempt: string; output: unknown };
|
package/src/host/rpc-bridge.ts
CHANGED
|
@@ -15,26 +15,49 @@ export default function piWorkflowsRpcBridge(pi: ExtensionAPI) {
|
|
|
15
15
|
name: "workflow",
|
|
16
16
|
label: "Workflow",
|
|
17
17
|
description: [
|
|
18
|
-
"
|
|
18
|
+
"Publish an update or submit the output for the pending workflow step.",
|
|
19
19
|
"Only call this tool when a workflow step contract in the conversation asks you to.",
|
|
20
|
-
"Pass the exact step
|
|
20
|
+
"Pass the exact step and attempt ids from the contract.",
|
|
21
21
|
].join(" "),
|
|
22
|
-
parameters: Type.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
parameters: Type.Union([
|
|
23
|
+
Type.Object(
|
|
24
|
+
{
|
|
25
|
+
action: StringEnum(["update"] as const),
|
|
26
|
+
step: Type.String(),
|
|
27
|
+
attempt: Type.String(),
|
|
28
|
+
update: Type.Object(
|
|
29
|
+
{
|
|
30
|
+
type: Type.String(),
|
|
31
|
+
key: Type.String(),
|
|
32
|
+
data: Type.Record(Type.String(), Type.Unknown()),
|
|
33
|
+
},
|
|
34
|
+
{ additionalProperties: false },
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
{ additionalProperties: false },
|
|
38
|
+
),
|
|
39
|
+
Type.Object(
|
|
40
|
+
{
|
|
41
|
+
action: StringEnum(["submit"] as const),
|
|
42
|
+
step: Type.String({ description: "Step id from the workflow step contract" }),
|
|
43
|
+
attempt: Type.String({ description: "Attempt id from the workflow step contract" }),
|
|
44
|
+
output: Type.Unknown({ description: "Step output matching the expected shape" }),
|
|
45
|
+
},
|
|
46
|
+
{ additionalProperties: false },
|
|
47
|
+
),
|
|
48
|
+
]),
|
|
49
|
+
async execute(toolCallId, params) {
|
|
50
|
+
process.stderr.write(
|
|
51
|
+
`${RPC_SUBMISSION_PREFIX}${JSON.stringify({ ...params, idempotencyKey: toolCallId })}\n`,
|
|
52
|
+
);
|
|
33
53
|
return {
|
|
34
54
|
content: [
|
|
35
55
|
{
|
|
36
56
|
type: "text",
|
|
37
|
-
text:
|
|
57
|
+
text:
|
|
58
|
+
params.action === "update"
|
|
59
|
+
? "Workflow update recorded; continue the current step."
|
|
60
|
+
: "Submission recorded. Continue only when the workflow sends the next step.",
|
|
38
61
|
},
|
|
39
62
|
],
|
|
40
63
|
details: {},
|