@osolmaz/pi-workflows 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +65 -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 +58 -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 +95 -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({
|
|
@@ -301,6 +320,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
301
320
|
};
|
|
302
321
|
let activeRun: ActiveRun | null = null;
|
|
303
322
|
let systemTurnAbort: AgentStepContract | null = null;
|
|
323
|
+
let suppressWorkflowAssistantTail = false;
|
|
304
324
|
let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
|
|
305
325
|
let pendingToolLaunch: {
|
|
306
326
|
ctx: ExtensionContext;
|
|
@@ -387,6 +407,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
387
407
|
runHeld(),
|
|
388
408
|
width,
|
|
389
409
|
theme,
|
|
410
|
+
widgetSource.updateHistory,
|
|
390
411
|
);
|
|
391
412
|
widgetShownScroll = view.scroll;
|
|
392
413
|
widgetMaxScroll = view.maxScroll;
|
|
@@ -411,13 +432,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
411
432
|
ctx: ExtensionContext,
|
|
412
433
|
state: WorkflowRunState,
|
|
413
434
|
snapshot: WorkflowDefinitionSnapshot,
|
|
435
|
+
updateHistory?: WorkflowUpdateRecord[],
|
|
414
436
|
) => {
|
|
415
437
|
if (state.steps.length !== widgetStepCount) {
|
|
416
438
|
widgetStepCount = state.steps.length;
|
|
417
439
|
// The workflow moved on; resume following the active node.
|
|
418
440
|
widgetScroll = null;
|
|
419
441
|
}
|
|
420
|
-
widgetSource = {
|
|
442
|
+
widgetSource = {
|
|
443
|
+
state,
|
|
444
|
+
snapshot,
|
|
445
|
+
...(updateHistory !== undefined ? { updateHistory: [...updateHistory] } : {}),
|
|
446
|
+
};
|
|
421
447
|
renderWidget(ctx);
|
|
422
448
|
};
|
|
423
449
|
|
|
@@ -505,6 +531,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
505
531
|
return;
|
|
506
532
|
}
|
|
507
533
|
presentationPending = run.generation;
|
|
534
|
+
suppressWorkflowAssistantTail = false;
|
|
508
535
|
pi.sendMessage(
|
|
509
536
|
{
|
|
510
537
|
customType: PRESENTATION_MESSAGE_TYPE,
|
|
@@ -596,7 +623,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
596
623
|
void run.recorder?.stop();
|
|
597
624
|
stopWidgetTicker();
|
|
598
625
|
const { state } = result;
|
|
599
|
-
updateWidget(ctx, state, run.snapshot);
|
|
626
|
+
updateWidget(ctx, state, run.snapshot, run.updateHistory);
|
|
600
627
|
clearWidgetTimer();
|
|
601
628
|
// A waiting run is parked at a checkpoint for a human; keep its widget up
|
|
602
629
|
// until a new workflow replaces it. Terminal runs fade after a grace TTL.
|
|
@@ -734,8 +761,25 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
734
761
|
}
|
|
735
762
|
|
|
736
763
|
const executor = new ConversationStepExecutor({
|
|
737
|
-
sendPrompt: ({ prompt, streaming }) => {
|
|
738
|
-
|
|
764
|
+
sendPrompt: ({ prompt, contract, presentation, kind, streaming }) => {
|
|
765
|
+
const details: WorkflowAgentStepMessageDetails = {
|
|
766
|
+
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
767
|
+
kind,
|
|
768
|
+
contract,
|
|
769
|
+
...(presentation !== undefined ? { presentation } : {}),
|
|
770
|
+
};
|
|
771
|
+
pi.sendMessage(
|
|
772
|
+
{
|
|
773
|
+
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
774
|
+
content: prompt,
|
|
775
|
+
display: true,
|
|
776
|
+
details,
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
triggerTurn: true,
|
|
780
|
+
deliverAs: streaming ? "steer" : "followUp",
|
|
781
|
+
},
|
|
782
|
+
);
|
|
739
783
|
},
|
|
740
784
|
onAbort: (contract, reason) => {
|
|
741
785
|
lastExpiredAttempt = {
|
|
@@ -799,9 +843,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
799
843
|
onRunFinishing: async () => {
|
|
800
844
|
await run.recorder?.finish();
|
|
801
845
|
},
|
|
802
|
-
onEvent: (
|
|
846
|
+
onEvent: (event, state: WorkflowRunState) => {
|
|
803
847
|
run.lastState = state;
|
|
804
|
-
|
|
848
|
+
run.updateHistory = appendProgressHistory(
|
|
849
|
+
run.updateHistory,
|
|
850
|
+
progressRecordsFromTrace([event]),
|
|
851
|
+
);
|
|
852
|
+
updateWidget(ctx, state, snapshot, run.updateHistory);
|
|
805
853
|
},
|
|
806
854
|
});
|
|
807
855
|
const run: ActiveRun = {
|
|
@@ -814,6 +862,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
814
862
|
presentationPrompt: options.presentation === false ? undefined : workflow.presentationPrompt,
|
|
815
863
|
generation,
|
|
816
864
|
lastState: null,
|
|
865
|
+
updateHistory: [],
|
|
817
866
|
...(options.childKey !== undefined ? { childKey: options.childKey } : {}),
|
|
818
867
|
...(options.onFinish !== undefined ? { onFinish: options.onFinish } : {}),
|
|
819
868
|
...(claimToken !== undefined ? { claimToken } : {}),
|
|
@@ -1562,13 +1611,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1562
1611
|
name: "workflow",
|
|
1563
1612
|
label: "Workflow",
|
|
1564
1613
|
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.",
|
|
1614
|
+
"List, start, inspect, pause, resume, cancel, answer, update, or complete Pi Workflows runs.",
|
|
1615
|
+
"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.",
|
|
1616
|
+
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
1568
1617
|
"Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
|
|
1569
1618
|
].join(" "),
|
|
1570
1619
|
parameters: WorkflowToolParameters,
|
|
1571
|
-
async execute(
|
|
1620
|
+
async execute(toolCallId, params: WorkflowToolInput, _signal, _onUpdate, ctx) {
|
|
1572
1621
|
let control: WorkflowControlResult;
|
|
1573
1622
|
switch (params.action) {
|
|
1574
1623
|
case "list":
|
|
@@ -1596,6 +1645,21 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1596
1645
|
});
|
|
1597
1646
|
break;
|
|
1598
1647
|
}
|
|
1648
|
+
case "update": {
|
|
1649
|
+
if (!activeRun) throw new Error("No workflow step is active.");
|
|
1650
|
+
const receipt = await activeRun.engine.publishUpdate(
|
|
1651
|
+
params.step,
|
|
1652
|
+
params.attempt,
|
|
1653
|
+
params.update,
|
|
1654
|
+
toolCallId,
|
|
1655
|
+
);
|
|
1656
|
+
return {
|
|
1657
|
+
content: [
|
|
1658
|
+
{ type: "text", text: "Workflow update published; the step remains active." },
|
|
1659
|
+
],
|
|
1660
|
+
details: { action: "update", ...receipt },
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1599
1663
|
case "submit": {
|
|
1600
1664
|
if (!activeRun) {
|
|
1601
1665
|
if (
|
|
@@ -1620,6 +1684,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1620
1684
|
if (!result.accepted) {
|
|
1621
1685
|
throw new Error(result.message);
|
|
1622
1686
|
}
|
|
1687
|
+
suppressWorkflowAssistantTail = true;
|
|
1623
1688
|
return {
|
|
1624
1689
|
content: [{ type: "text", text: result.message }],
|
|
1625
1690
|
details: { action: "submit", step: params.step, accepted: true },
|
|
@@ -1683,6 +1748,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1683
1748
|
});
|
|
1684
1749
|
|
|
1685
1750
|
pi.on("agent_start", () => {
|
|
1751
|
+
suppressWorkflowAssistantTail = false;
|
|
1686
1752
|
if (!activeRun && presentationPending === null && presentationAbort) {
|
|
1687
1753
|
// A normal user turn started while an async presentation prompt was
|
|
1688
1754
|
// still resolving. The user's new request supersedes that old result.
|
|
@@ -1693,6 +1759,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1693
1759
|
});
|
|
1694
1760
|
|
|
1695
1761
|
pi.on("agent_end", (event, ctx) => {
|
|
1762
|
+
suppressWorkflowAssistantTail = false;
|
|
1696
1763
|
const aborted = event.messages.some(
|
|
1697
1764
|
(message) =>
|
|
1698
1765
|
typeof message === "object" &&
|
|
@@ -1740,6 +1807,14 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1740
1807
|
});
|
|
1741
1808
|
|
|
1742
1809
|
pi.on("message_end", (event) => {
|
|
1810
|
+
if (suppressWorkflowAssistantTail && event.message.role === "assistant") {
|
|
1811
|
+
const message = {
|
|
1812
|
+
...event.message,
|
|
1813
|
+
content: event.message.content.filter((part) => part.type !== "text"),
|
|
1814
|
+
};
|
|
1815
|
+
activeRun?.recorder?.handleMessageEnd({ ...event, message });
|
|
1816
|
+
return { message };
|
|
1817
|
+
}
|
|
1743
1818
|
activeRun?.recorder?.handleMessageEnd(event);
|
|
1744
1819
|
});
|
|
1745
1820
|
|
|
@@ -1785,6 +1860,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1785
1860
|
pi.on("session_shutdown", async () => {
|
|
1786
1861
|
sessionClosed = true;
|
|
1787
1862
|
systemTurnAbort = null;
|
|
1863
|
+
suppressWorkflowAssistantTail = false;
|
|
1788
1864
|
supersedePresentation();
|
|
1789
1865
|
const run = activeRun;
|
|
1790
1866
|
if (run !== null && run.claimToken !== undefined) {
|
|
@@ -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: {},
|