@osolmaz/pi-workflows 0.5.2 → 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 +13 -3
- 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 +56 -15
- 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 +4 -2
- package/dist/extension/widget.js +111 -17
- 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-13-responsive-workflow-widget-plan.md +11 -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 +89 -23
- package/src/extension/step-message.ts +145 -0
- package/src/extension/widget.ts +158 -14
- 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
|
@@ -1,52 +1,51 @@
|
|
|
1
|
-
import { agent, compute, defineWorkflow, notify, shell } from "../workflows/definition.js";
|
|
2
|
-
import
|
|
1
|
+
import { action, agent, compute, defineWorkflow, notify, shell } from "../workflows/definition.js";
|
|
2
|
+
import {
|
|
3
|
+
estimateProgress,
|
|
4
|
+
formatProgressReport,
|
|
5
|
+
type ProgressSample,
|
|
6
|
+
type ProgressTrackState,
|
|
7
|
+
} from "../workflows/progress.js";
|
|
8
|
+
import type {
|
|
9
|
+
WorkflowDefinition,
|
|
10
|
+
WorkflowNodeContext,
|
|
11
|
+
WorkflowProgressData,
|
|
12
|
+
} from "../workflows/types.js";
|
|
13
|
+
import { validateProgressData } from "../workflows/updates.js";
|
|
3
14
|
|
|
4
15
|
const MIN_INTERVAL_MINUTES = 1;
|
|
5
16
|
const MAX_INTERVAL_MINUTES = 24 * 60;
|
|
17
|
+
const DEFAULT_INTERVAL_MINUTES = 30;
|
|
6
18
|
const MIN_CHECK_TIMEOUT_MINUTES = 5;
|
|
7
19
|
const MAX_CHECK_TIMEOUT_MINUTES = 24 * 60;
|
|
8
20
|
const DEFAULT_MIN_CHECK_TIMEOUT_MINUTES = 60;
|
|
9
21
|
const DEFAULT_MAX_CHECKS = 1_000;
|
|
10
22
|
const MAX_CHECKS = 1_000;
|
|
23
|
+
const MAX_TRACKS = 256;
|
|
11
24
|
const MAX_OBSERVATION_CHARS = 8_000;
|
|
12
25
|
const MAX_REPORT_CHARS = 4_000;
|
|
13
26
|
const MAX_REASON_CHARS = 2_000;
|
|
14
27
|
const SLEEP_TIMEOUT_MARGIN_MS = 60_000;
|
|
15
28
|
const NODE_TIMEOUT_MARGIN_MS = 2 * 60_000;
|
|
16
29
|
|
|
17
|
-
type MonitorInput = {
|
|
30
|
+
export type MonitorInput = {
|
|
18
31
|
task: string;
|
|
19
|
-
everyMinutes
|
|
20
|
-
reportWhen?: string;
|
|
32
|
+
everyMinutes?: number;
|
|
21
33
|
stopWhen?: string;
|
|
22
34
|
maxChecks?: number;
|
|
23
35
|
checkTimeoutMinutes?: number;
|
|
24
36
|
};
|
|
25
37
|
|
|
26
|
-
type MonitorConfig =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
reportWhen: string;
|
|
30
|
-
stopWhen: string;
|
|
31
|
-
maxChecks: number;
|
|
32
|
-
checkTimeoutMinutes: number;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
type MonitorRoute = "continue_quiet" | "continue_report" | "stop_quiet" | "stop_report";
|
|
36
|
-
|
|
38
|
+
type MonitorConfig = Required<MonitorInput>;
|
|
39
|
+
type MonitorRoute = "continue" | "stop";
|
|
40
|
+
type MonitorTrack = { key: string; data: WorkflowProgressData };
|
|
37
41
|
type MonitorCheck = {
|
|
38
42
|
route: MonitorRoute;
|
|
39
43
|
observation: string;
|
|
40
|
-
report
|
|
44
|
+
report: string;
|
|
45
|
+
progress?: { tracks: MonitorTrack[] };
|
|
41
46
|
reason: string;
|
|
42
47
|
};
|
|
43
|
-
|
|
44
|
-
const MONITOR_ROUTES = new Set<MonitorRoute>([
|
|
45
|
-
"continue_quiet",
|
|
46
|
-
"continue_report",
|
|
47
|
-
"stop_quiet",
|
|
48
|
-
"stop_report",
|
|
49
|
-
]);
|
|
48
|
+
type MonitorEstimate = { tracks: ProgressTrackState[] };
|
|
50
49
|
|
|
51
50
|
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
52
51
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -56,27 +55,40 @@ function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
function requireBoundedString(value: unknown, label: string, maxChars: number): string {
|
|
59
|
-
if (typeof value !== "string") {
|
|
60
|
-
throw new Error(`${label} must be a string`);
|
|
61
|
-
}
|
|
58
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
62
59
|
const trimmed = value.trim();
|
|
63
|
-
if (trimmed.length === 0) {
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
if (trimmed.length > maxChars) {
|
|
67
|
-
throw new Error(`${label} must be at most ${maxChars} characters`);
|
|
68
|
-
}
|
|
60
|
+
if (trimmed.length === 0) throw new Error(`${label} must not be empty`);
|
|
61
|
+
if (trimmed.length > maxChars) throw new Error(`${label} must be at most ${maxChars} characters`);
|
|
69
62
|
return trimmed;
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
function
|
|
65
|
+
async function waitForUpdateSlot(signal: AbortSignal): Promise<void> {
|
|
66
|
+
await new Promise<void>((resolve, reject) => {
|
|
67
|
+
const onAbort = () => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
reject(signal.reason ?? new Error("monitor progress publication was cancelled"));
|
|
70
|
+
};
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
signal.removeEventListener("abort", onAbort);
|
|
73
|
+
resolve();
|
|
74
|
+
}, 55);
|
|
75
|
+
if (signal.aborted) onAbort();
|
|
76
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
73
81
|
const value = requireRecord(input, "monitor input") as Partial<MonitorInput>;
|
|
82
|
+
const allowed = new Set(["task", "everyMinutes", "stopWhen", "maxChecks", "checkTimeoutMinutes"]);
|
|
83
|
+
for (const field of Object.keys(value)) {
|
|
84
|
+
if (!allowed.has(field)) throw new Error(`monitor input field ${field} is not supported`);
|
|
85
|
+
}
|
|
74
86
|
const task = requireBoundedString(value.task, "task", 8_000);
|
|
87
|
+
const everyMinutes = value.everyMinutes ?? DEFAULT_INTERVAL_MINUTES;
|
|
75
88
|
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
value.everyMinutes > MAX_INTERVAL_MINUTES
|
|
89
|
+
!Number.isInteger(everyMinutes) ||
|
|
90
|
+
everyMinutes < MIN_INTERVAL_MINUTES ||
|
|
91
|
+
everyMinutes > MAX_INTERVAL_MINUTES
|
|
80
92
|
) {
|
|
81
93
|
throw new Error(
|
|
82
94
|
`everyMinutes must be an integer from ${MIN_INTERVAL_MINUTES} through ${MAX_INTERVAL_MINUTES}`,
|
|
@@ -87,7 +99,7 @@ function prepareInput(input: unknown): MonitorConfig {
|
|
|
87
99
|
throw new Error(`maxChecks must be an integer from 1 through ${MAX_CHECKS}`);
|
|
88
100
|
}
|
|
89
101
|
const checkTimeoutMinutes =
|
|
90
|
-
value.checkTimeoutMinutes ?? Math.max(DEFAULT_MIN_CHECK_TIMEOUT_MINUTES,
|
|
102
|
+
value.checkTimeoutMinutes ?? Math.max(DEFAULT_MIN_CHECK_TIMEOUT_MINUTES, everyMinutes);
|
|
91
103
|
if (
|
|
92
104
|
!Number.isInteger(checkTimeoutMinutes) ||
|
|
93
105
|
checkTimeoutMinutes < MIN_CHECK_TIMEOUT_MINUTES ||
|
|
@@ -99,14 +111,10 @@ function prepareInput(input: unknown): MonitorConfig {
|
|
|
99
111
|
}
|
|
100
112
|
return {
|
|
101
113
|
task,
|
|
102
|
-
everyMinutes
|
|
103
|
-
reportWhen:
|
|
104
|
-
value.reportWhen === undefined
|
|
105
|
-
? "The observed state changes materially or needs the user's attention."
|
|
106
|
-
: requireBoundedString(value.reportWhen, "reportWhen", 4_000),
|
|
114
|
+
everyMinutes,
|
|
107
115
|
stopWhen:
|
|
108
116
|
value.stopWhen === undefined
|
|
109
|
-
? "
|
|
117
|
+
? "Stop only when the user explicitly asks to stop."
|
|
110
118
|
: requireBoundedString(value.stopWhen, "stopWhen", 4_000),
|
|
111
119
|
maxChecks,
|
|
112
120
|
checkTimeoutMinutes,
|
|
@@ -117,124 +125,198 @@ function configFrom(outputs: Record<string, unknown>): MonitorConfig {
|
|
|
117
125
|
return outputs.prepare as MonitorConfig;
|
|
118
126
|
}
|
|
119
127
|
|
|
120
|
-
function agentTimeoutMs({ outputs }: WorkflowNodeContext): number {
|
|
121
|
-
return configFrom(outputs).checkTimeoutMinutes * 60_000;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
128
|
function completedChecks(context: WorkflowNodeContext): number {
|
|
125
129
|
return context.state.steps.filter((step) => step.nodeId === "check" && step.outcome === "ok")
|
|
126
130
|
.length;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
function
|
|
133
|
+
export function validateMonitorCheck(output: unknown): MonitorCheck {
|
|
130
134
|
const value = requireRecord(output, "monitor check output");
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
value.report
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
135
|
+
const allowed = new Set(["route", "observation", "report", "progress", "reason"]);
|
|
136
|
+
for (const key of Object.keys(value))
|
|
137
|
+
if (!allowed.has(key)) throw new Error(`monitor check field ${key} is not supported`);
|
|
138
|
+
if (value.route !== "continue" && value.route !== "stop")
|
|
139
|
+
throw new Error("route must be continue or stop");
|
|
140
|
+
const check: MonitorCheck = {
|
|
141
|
+
route: value.route,
|
|
142
|
+
observation: requireBoundedString(value.observation, "observation", MAX_OBSERVATION_CHARS),
|
|
143
|
+
report: requireBoundedString(value.report, "report", MAX_REPORT_CHARS),
|
|
144
|
+
reason: requireBoundedString(value.reason, "reason", MAX_REASON_CHARS),
|
|
145
|
+
};
|
|
146
|
+
if (value.progress !== undefined) check.progress = validateMonitorProgress(value.progress);
|
|
147
|
+
return check;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateMonitorProgress(input: unknown): { tracks: MonitorTrack[] } {
|
|
151
|
+
const value = requireRecord(input, "progress");
|
|
152
|
+
if (Object.keys(value).some((key) => key !== "tracks"))
|
|
153
|
+
throw new Error("progress only supports tracks");
|
|
154
|
+
if (!Array.isArray(value.tracks) || value.tracks.length < 1 || value.tracks.length > MAX_TRACKS) {
|
|
155
|
+
throw new Error(`progress.tracks must contain 1 through ${MAX_TRACKS} entries`);
|
|
144
156
|
}
|
|
157
|
+
const keys = new Set<string>();
|
|
158
|
+
const tracks = value.tracks.map((raw, index) => {
|
|
159
|
+
const track = requireRecord(raw, `progress.tracks[${index}]`);
|
|
160
|
+
if (Object.keys(track).some((key) => key !== "key" && key !== "data")) {
|
|
161
|
+
throw new Error(`progress.tracks[${index}] has an unsupported field`);
|
|
162
|
+
}
|
|
163
|
+
const key = requireBoundedString(track.key, `progress.tracks[${index}].key`, 128);
|
|
164
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(key))
|
|
165
|
+
throw new Error(`progress.tracks[${index}].key is invalid`);
|
|
166
|
+
if (keys.has(key)) throw new Error(`progress track key ${key} is duplicated`);
|
|
167
|
+
keys.add(key);
|
|
168
|
+
return {
|
|
169
|
+
key,
|
|
170
|
+
data: validateProgressData(requireRecord(track.data, `progress.tracks[${index}].data`)),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
return { tracks };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function estimateTracks(outputs: Record<string, unknown>): MonitorEstimate {
|
|
177
|
+
const check = outputs.check as MonitorCheck;
|
|
178
|
+
if (check.progress === undefined) return { tracks: [] };
|
|
179
|
+
const previous = outputs.estimate as MonitorEstimate | undefined;
|
|
180
|
+
const previousByKey = new Map((previous?.tracks ?? []).map((track) => [track.key, track]));
|
|
181
|
+
const at = new Date().toISOString();
|
|
145
182
|
return {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
tracks: check.progress.tracks.map((track) => {
|
|
184
|
+
const samples: ProgressSample[] = [
|
|
185
|
+
...(previousByKey.get(track.key)?.samples ?? []),
|
|
186
|
+
{ at, data: track.data },
|
|
187
|
+
].slice(-9);
|
|
188
|
+
return { key: track.key, samples, estimate: estimateProgress(track.key, samples) };
|
|
189
|
+
}),
|
|
150
190
|
};
|
|
151
191
|
}
|
|
152
192
|
|
|
153
|
-
function reportMessage(
|
|
154
|
-
const check = outputs.check as MonitorCheck;
|
|
155
|
-
|
|
193
|
+
function reportMessage(context: WorkflowNodeContext): string {
|
|
194
|
+
const check = context.outputs.check as MonitorCheck;
|
|
195
|
+
const estimate = context.outputs.estimate as MonitorEstimate;
|
|
196
|
+
const config = configFrom(context.outputs);
|
|
197
|
+
const suffix: string[] = [];
|
|
198
|
+
if (estimate.tracks.length > 0) {
|
|
199
|
+
suffix.push(
|
|
200
|
+
formatProgressReport(
|
|
201
|
+
estimate.tracks.map((track) => track.estimate),
|
|
202
|
+
check.route === "continue" ? config.everyMinutes : undefined,
|
|
203
|
+
new Date(),
|
|
204
|
+
2_000,
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (check.route === "continue" && completedChecks(context) >= config.maxChecks) {
|
|
209
|
+
suffix.push(`Reached the ${config.maxChecks}-check safety limit.`);
|
|
210
|
+
}
|
|
211
|
+
const suffixText = suffix.filter(Boolean).join("\n");
|
|
212
|
+
if (suffixText.length === 0) return check.report;
|
|
213
|
+
const reportBudget = Math.max(1, MAX_REPORT_CHARS - suffixText.length - 1);
|
|
214
|
+
const report =
|
|
215
|
+
check.report.length <= reportBudget
|
|
216
|
+
? check.report
|
|
217
|
+
: `${check.report.slice(0, Math.max(0, reportBudget - 1))}…`;
|
|
218
|
+
return `${report}\n${suffixText}`;
|
|
156
219
|
}
|
|
157
220
|
|
|
158
|
-
|
|
221
|
+
const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
159
222
|
name: "monitor",
|
|
160
223
|
title: ({ input }) => {
|
|
161
224
|
try {
|
|
162
|
-
|
|
163
|
-
return `monitor: ${task.slice(0, 80)}`;
|
|
225
|
+
return `monitor: ${prepareMonitorInput(input).task.slice(0, 80)}`;
|
|
164
226
|
} catch {
|
|
165
227
|
return "monitor";
|
|
166
228
|
}
|
|
167
229
|
},
|
|
168
|
-
presentationPrompt: ({ finalOutput }) => {
|
|
169
|
-
const result = requireRecord(finalOutput, "monitor result");
|
|
170
|
-
if (result.reported === true) {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
return `Tell the user concisely why this monitor stopped: ${String(result.reason ?? "monitor ended")}`;
|
|
174
|
-
},
|
|
175
230
|
startAt: "prepare",
|
|
176
|
-
maxSteps:
|
|
231
|
+
maxSteps: 9_010,
|
|
177
232
|
nodes: {
|
|
178
|
-
prepare: compute({
|
|
179
|
-
run: ({ input }) => prepareInput(input),
|
|
180
|
-
}),
|
|
181
|
-
guard: compute({
|
|
182
|
-
run: (context) => {
|
|
183
|
-
const config = configFrom(context.outputs);
|
|
184
|
-
const checks = completedChecks(context);
|
|
185
|
-
return checks >= config.maxChecks
|
|
186
|
-
? { route: "stop", checks, reason: `Reached the ${config.maxChecks}-check limit.` }
|
|
187
|
-
: { route: "check", checks };
|
|
188
|
-
},
|
|
189
|
-
}),
|
|
190
|
-
continue_guard: compute({
|
|
191
|
-
run: (context) => {
|
|
192
|
-
const config = configFrom(context.outputs);
|
|
193
|
-
const checks = completedChecks(context);
|
|
194
|
-
return checks >= config.maxChecks
|
|
195
|
-
? { route: "stop", checks, reason: `Reached the ${config.maxChecks}-check limit.` }
|
|
196
|
-
: { route: "sleep", checks };
|
|
197
|
-
},
|
|
198
|
-
}),
|
|
233
|
+
prepare: compute({ run: ({ input }) => prepareMonitorInput(input) }),
|
|
199
234
|
check: agent({
|
|
200
235
|
statusDetail: "checking monitored target",
|
|
201
|
-
timeoutMs:
|
|
236
|
+
timeoutMs: ({ outputs }) => configFrom(outputs).checkTimeoutMinutes * 60_000,
|
|
202
237
|
prompt: (context) => {
|
|
203
238
|
const config = configFrom(context.outputs);
|
|
204
239
|
const previous = context.outputs.check as MonitorCheck | undefined;
|
|
205
|
-
const
|
|
240
|
+
const priorEstimate = context.outputs.estimate as MonitorEstimate | undefined;
|
|
206
241
|
return [
|
|
207
|
-
`Perform monitoring check ${
|
|
242
|
+
`Perform monitoring check ${completedChecks(context) + 1} of at most ${config.maxChecks}.`,
|
|
208
243
|
`Task: ${config.task}`,
|
|
209
|
-
`Report when: ${config.reportWhen}`,
|
|
210
244
|
`Stop when: ${config.stopWhen}`,
|
|
211
245
|
previous === undefined
|
|
212
|
-
? "There is no previous observation.
|
|
246
|
+
? "There is no previous observation."
|
|
213
247
|
: `Previous accepted observation: ${previous.observation}`,
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
priorEstimate?.tracks.length
|
|
249
|
+
? `Previous progress: ${formatProgressReport(priorEstimate.tracks.map((track) => track.estimate))}`
|
|
250
|
+
: "There is no previous measured progress.",
|
|
251
|
+
"Use available tools to inspect the current source of truth. Observe only unless the task explicitly authorizes a mutation.",
|
|
252
|
+
"Every accepted check must include a concise user-facing report. Add progress tracks only when the target provides measurable facts. Submit observed counts and target-provided finish times; do not invent rates or an ETA.",
|
|
253
|
+
"Choose route continue or stop.",
|
|
216
254
|
].join("\n\n");
|
|
217
255
|
},
|
|
218
256
|
expectedOutput:
|
|
219
|
-
'{ "route": "
|
|
220
|
-
validate: (output) =>
|
|
257
|
+
'{ "route": "continue" | "stop", "observation": "current factual state", "report": "concise status update", "progress": { "tracks": [{ "key": "stable-key", "data": { "schema": "pi-workflows.progress.v1", "status": "running", "completed": 1, "total": 2, "unit": "items" } }] } (optional), "reason": "short reason" }',
|
|
258
|
+
validate: (output) => validateMonitorCheck(output),
|
|
259
|
+
}),
|
|
260
|
+
estimate: compute({ run: ({ outputs }) => estimateTracks(outputs) }),
|
|
261
|
+
publish_progress: action({
|
|
262
|
+
statusDetail: "publishing monitor progress",
|
|
263
|
+
run: async ({ outputs, publishUpdate, signal }) => {
|
|
264
|
+
const check = outputs.check as MonitorCheck;
|
|
265
|
+
if (check.progress === undefined) return { published: 0 };
|
|
266
|
+
for (const track of check.progress.tracks) {
|
|
267
|
+
await waitForUpdateSlot(signal);
|
|
268
|
+
await publishUpdate({ type: "progress", key: track.key, data: track.data });
|
|
269
|
+
}
|
|
270
|
+
return { published: check.progress.tracks.length };
|
|
271
|
+
},
|
|
221
272
|
}),
|
|
222
|
-
|
|
273
|
+
report: notify({
|
|
223
274
|
statusDetail: "queueing monitor update",
|
|
224
|
-
message: (
|
|
275
|
+
message: (context) => reportMessage(context),
|
|
225
276
|
kind: "progress",
|
|
226
277
|
}),
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
278
|
+
decide: compute({
|
|
279
|
+
run: (context) => {
|
|
280
|
+
const check = context.outputs.check as MonitorCheck;
|
|
281
|
+
const config = configFrom(context.outputs);
|
|
282
|
+
const checks = completedChecks(context);
|
|
283
|
+
if (check.route === "stop") return { route: "stop", reason: check.reason, checks };
|
|
284
|
+
if (checks >= config.maxChecks) {
|
|
285
|
+
return {
|
|
286
|
+
route: "stop",
|
|
287
|
+
reason: `Reached the ${config.maxChecks}-check safety limit.`,
|
|
288
|
+
checks,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return { route: "continue", reason: check.reason, checks };
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
schedule: action({
|
|
295
|
+
statusDetail: "scheduling next monitor check",
|
|
296
|
+
run: async ({ outputs, publishUpdate }) => {
|
|
297
|
+
const config = configFrom(outputs);
|
|
298
|
+
const lastCheckAt = new Date().toISOString();
|
|
299
|
+
const nextCheckAt = new Date(
|
|
300
|
+
Date.parse(lastCheckAt) + config.everyMinutes * 60_000,
|
|
301
|
+
).toISOString();
|
|
302
|
+
await publishUpdate({
|
|
303
|
+
type: "monitor.schedule",
|
|
304
|
+
key: "next-check",
|
|
305
|
+
data: {
|
|
306
|
+
schema: "pi-workflows.monitor-schedule.v1",
|
|
307
|
+
lastCheckAt,
|
|
308
|
+
nextCheckAt,
|
|
309
|
+
everyMinutes: config.everyMinutes,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
return { lastCheckAt, nextCheckAt, everyMinutes: config.everyMinutes };
|
|
313
|
+
},
|
|
231
314
|
}),
|
|
232
315
|
sleep: shell({
|
|
233
316
|
statusDetail: "waiting for next monitor check",
|
|
234
317
|
timeoutMs: MAX_INTERVAL_MINUTES * 60_000 + NODE_TIMEOUT_MARGIN_MS,
|
|
235
318
|
exec: ({ outputs }) => {
|
|
236
|
-
const
|
|
237
|
-
const sleepMs = config.everyMinutes * 60_000;
|
|
319
|
+
const sleepMs = configFrom(outputs).everyMinutes * 60_000;
|
|
238
320
|
return {
|
|
239
321
|
command: process.execPath,
|
|
240
322
|
args: ["-e", "setTimeout(() => {}, Number(process.argv[1]))", String(sleepMs)],
|
|
@@ -246,40 +328,27 @@ export default defineWorkflow({
|
|
|
246
328
|
}),
|
|
247
329
|
finish: compute({
|
|
248
330
|
run: ({ outputs }) => {
|
|
249
|
-
const check = outputs.check as MonitorCheck
|
|
250
|
-
const
|
|
251
|
-
const continueGuard = outputs.continue_guard as { reason?: string } | undefined;
|
|
331
|
+
const check = outputs.check as MonitorCheck;
|
|
332
|
+
const decision = outputs.decide as { reason?: string; checks?: number } | undefined;
|
|
252
333
|
return {
|
|
253
|
-
reason:
|
|
254
|
-
observation: check
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
(check !== undefined && check.route === "stop_report"),
|
|
334
|
+
reason: decision?.reason ?? check.reason,
|
|
335
|
+
observation: check.observation,
|
|
336
|
+
checks: decision?.checks ?? 1,
|
|
337
|
+
reported: true,
|
|
258
338
|
};
|
|
259
339
|
},
|
|
260
340
|
}),
|
|
261
341
|
},
|
|
262
342
|
edges: [
|
|
263
|
-
{ from: "prepare", to: "
|
|
264
|
-
{ from: "
|
|
265
|
-
{
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
continue_report: "report_continue",
|
|
272
|
-
stop_quiet: "finish",
|
|
273
|
-
stop_report: "report_stop",
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
},
|
|
277
|
-
{ from: "report_continue", to: "continue_guard" },
|
|
278
|
-
{ from: "report_stop", to: "finish" },
|
|
279
|
-
{
|
|
280
|
-
from: "continue_guard",
|
|
281
|
-
switch: { on: "$.route", cases: { sleep: "sleep", stop: "finish" } },
|
|
282
|
-
},
|
|
283
|
-
{ from: "sleep", to: "guard" },
|
|
343
|
+
{ from: "prepare", to: "check" },
|
|
344
|
+
{ from: "check", to: "estimate" },
|
|
345
|
+
{ from: "estimate", to: "publish_progress" },
|
|
346
|
+
{ from: "publish_progress", to: "report" },
|
|
347
|
+
{ from: "report", to: "decide" },
|
|
348
|
+
{ from: "decide", switch: { on: "$.route", cases: { stop: "finish", continue: "schedule" } } },
|
|
349
|
+
{ from: "schedule", to: "sleep" },
|
|
350
|
+
{ from: "sleep", to: "check" },
|
|
284
351
|
],
|
|
285
352
|
});
|
|
353
|
+
|
|
354
|
+
export default monitorWorkflow;
|
|
@@ -9,8 +9,13 @@ export type SubmissionResult =
|
|
|
9
9
|
| { accepted: true; message: string }
|
|
10
10
|
| { accepted: false; message: string };
|
|
11
11
|
|
|
12
|
+
export type PromptDeliveryKind = "step" | "reminder" | "resume";
|
|
13
|
+
|
|
12
14
|
export type PromptDelivery = {
|
|
13
15
|
prompt: string;
|
|
16
|
+
contract: AgentStepRequest["contract"];
|
|
17
|
+
presentation?: AgentStepRequest["presentation"];
|
|
18
|
+
kind: PromptDeliveryKind;
|
|
14
19
|
/** True when the agent is known to be mid-run, so delivery must be queued. */
|
|
15
20
|
streaming: boolean;
|
|
16
21
|
};
|
|
@@ -54,8 +59,8 @@ const DEFAULT_MAX_NUDGES = 2;
|
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* AgentStepExecutor that runs steps inside the current pi conversation. The
|
|
57
|
-
* engine hands it a prompt; it delivers the prompt as a
|
|
58
|
-
* resolves once the model submits an accepted output through the `workflow`
|
|
62
|
+
* engine hands it a prompt; it delivers the prompt as a model-facing workflow
|
|
63
|
+
* message and resolves once the model submits an accepted output through the `workflow`
|
|
59
64
|
* tool. If the agent settles without submitting, it nudges the model a
|
|
60
65
|
* bounded number of times before failing the step.
|
|
61
66
|
*/
|
|
@@ -111,7 +116,7 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
111
116
|
}
|
|
112
117
|
try {
|
|
113
118
|
this.conversation?.beginAttempt?.(pending.request.contract);
|
|
114
|
-
this.sendPrompt(
|
|
119
|
+
this.sendPrompt(this.delivery(pending.request, pending.request.prompt, "resume"));
|
|
115
120
|
} catch (error) {
|
|
116
121
|
this.clearPending();
|
|
117
122
|
pending.reject(error);
|
|
@@ -156,7 +161,7 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
156
161
|
return;
|
|
157
162
|
}
|
|
158
163
|
try {
|
|
159
|
-
this.sendPrompt(
|
|
164
|
+
this.sendPrompt(this.delivery(request, request.prompt, "step"));
|
|
160
165
|
} catch (error) {
|
|
161
166
|
// A failed delivery must not leave the step installed, or every
|
|
162
167
|
// subsequent agent node would fail with "already awaiting output".
|
|
@@ -227,7 +232,7 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
227
232
|
accepted: true,
|
|
228
233
|
message: [
|
|
229
234
|
`Output accepted for step ${JSON.stringify(stepId)}.`,
|
|
230
|
-
"If the workflow continues, the next step arrives as a new
|
|
235
|
+
"If the workflow continues, the next step arrives as a new workflow message. End your turn now.",
|
|
231
236
|
].join(" "),
|
|
232
237
|
};
|
|
233
238
|
}
|
|
@@ -262,15 +267,18 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
262
267
|
const { nodeId, attemptId } = pending.request.contract;
|
|
263
268
|
try {
|
|
264
269
|
this.conversation?.beginAttempt?.(pending.request.contract);
|
|
265
|
-
this.sendPrompt(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
this.sendPrompt(
|
|
271
|
+
this.delivery(
|
|
272
|
+
pending.request,
|
|
273
|
+
[
|
|
274
|
+
`Reminder: workflow step ${JSON.stringify(nodeId)} is still awaiting your output.`,
|
|
275
|
+
"Complete it by calling the `workflow` tool with:",
|
|
276
|
+
`{"action": "submit", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
|
|
277
|
+
`Expected output: ${pending.request.contract.expectedOutput ?? "a JSON object with your result"}`,
|
|
278
|
+
].join("\n"),
|
|
279
|
+
"reminder",
|
|
280
|
+
),
|
|
281
|
+
);
|
|
274
282
|
} catch (error) {
|
|
275
283
|
// No reminder turn was started, so nothing would settle the step; fail
|
|
276
284
|
// it promptly instead of waiting out the node timeout.
|
|
@@ -281,6 +289,20 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
281
289
|
return true;
|
|
282
290
|
}
|
|
283
291
|
|
|
292
|
+
private delivery(
|
|
293
|
+
request: AgentStepRequest,
|
|
294
|
+
prompt: string,
|
|
295
|
+
kind: PromptDeliveryKind,
|
|
296
|
+
): PromptDelivery {
|
|
297
|
+
return {
|
|
298
|
+
prompt,
|
|
299
|
+
contract: request.contract,
|
|
300
|
+
...(request.presentation !== undefined ? { presentation: request.presentation } : {}),
|
|
301
|
+
kind,
|
|
302
|
+
streaming: this.streaming,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
284
306
|
private clearPending(): void {
|
|
285
307
|
this.pending?.cleanup();
|
|
286
308
|
this.pending?.markCleared();
|