@osolmaz/pi-workflows 0.12.1 → 0.13.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 +52 -28
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +369 -73
- package/dist/builtins/autoplan.workflow.d.ts +126 -13
- package/dist/builtins/autoplan.workflow.js +200 -29
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +6 -4
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -1
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -21
- package/dist/builtins/monitor.workflow.js +497 -216
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +32 -0
- package/dist/builtins/plain-summary.workflow.js +134 -0
- package/dist/builtins/plain-summary.workflow.js.map +1 -0
- package/dist/builtins/plan-change.workflow.d.ts +170 -22
- package/dist/builtins/sanity-check.workflow.d.ts +45 -3
- package/dist/builtins/sanity-check.workflow.js +45 -7
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/controllers/effects.d.ts +3 -2
- package/dist/controllers/effects.js +8 -1
- package/dist/controllers/effects.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/manager.d.ts +1 -0
- package/dist/controllers/manager.js +41 -17
- package/dist/controllers/manager.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +107 -85
- package/dist/controllers/sqlite.js +1611 -1486
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +9 -8
- package/dist/controllers/store.js +1 -31
- package/dist/controllers/store.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
- package/dist/controllers/workflow-engine-scheduler.js +3 -5
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +2 -2
- package/dist/controllers/workflows.js +11 -10
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.js +2 -2
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -0
- package/dist/extension/decision-channels.js +311 -118
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.d.ts +9 -2
- package/dist/extension/executor.js +89 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/herdr-viewer.d.ts +0 -1
- package/dist/extension/herdr-viewer.js +0 -4
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/extension/index.js +156 -103
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +3 -10
- package/dist/extension/recorder.js +13 -22
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +7 -2
- package/dist/extension/step-message.js +94 -2
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +6 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +3 -0
- package/dist/host/rpc-executor.js +2 -0
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +3 -6
- package/dist/host/runner.js +83 -32
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +14 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.d.ts +35 -0
- package/dist/state/database.js +287 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/json.d.ts +6 -0
- package/dist/state/json.js +38 -0
- package/dist/state/json.js.map +1 -0
- package/dist/state/mutation.d.ts +57 -0
- package/dist/state/mutation.js +255 -0
- package/dist/state/mutation.js.map +1 -0
- package/dist/state/schema.d.ts +6 -0
- package/dist/state/schema.js +473 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/viewer/cli.d.ts +2 -2
- package/dist/viewer/cli.js +142 -122
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.d.ts +4 -4
- package/dist/viewer/render.js +2 -17
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +1 -1
- package/dist/viewer/session-reducer.js +1 -1
- package/dist/viewer/tui.d.ts +3 -3
- package/dist/viewer/tui.js +15 -13
- package/dist/viewer/tui.js.map +1 -1
- package/dist/viewer/watch.d.ts +2 -5
- package/dist/viewer/watch.js +13 -13
- package/dist/viewer/watch.js.map +1 -1
- package/dist/workflows/composition.js +12 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -2
- package/dist/workflows/definition.js +17 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +183 -103
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -3
- package/dist/workflows/errors.js +3 -3
- package/dist/workflows/human-decision.d.ts +29 -4
- package/dist/workflows/human-decision.js +547 -283
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +4 -5
- package/dist/workflows/index.js +3 -4
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +902 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +55 -81
- package/docs/CONTROLLERS.md +10 -8
- package/docs/DEFERRED_TURNS.md +2 -2
- package/docs/DESIGN_PHILOSOPHY.md +4 -2
- package/docs/HUMAN_DECISIONS.md +13 -14
- package/docs/MONITOR.md +179 -231
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +19 -19
- package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
- package/docs/WORKFLOW_UPDATES.md +10 -9
- package/docs/development.md +9 -9
- package/docs/live-replay-protocol.md +28 -37
- package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
- package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
- package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
- package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
- package/docs/plans/session-event-replay-implementation-plan.md +1 -1
- package/docs/plans/tui-viewer-implementation-plan.md +1 -1
- package/docs/session-event-journal.md +63 -434
- package/docs/tui-viewer.md +13 -15
- package/docs/workflows.md +104 -50
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +1 -11
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -163
- package/skills/pi-workflows/SKILL.md +7 -7
- package/skills/sanity-check/SKILL.md +1 -1
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +6 -4
- package/src/builtins/index.ts +7 -1
- package/src/builtins/monitor.workflow.ts +663 -249
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/sanity-check.workflow.ts +62 -7
- package/src/controllers/effects.ts +7 -0
- package/src/controllers/index.ts +0 -6
- package/src/controllers/manager.ts +72 -32
- package/src/controllers/sqlite.ts +2420 -1948
- package/src/controllers/store.ts +17 -38
- package/src/controllers/types.ts +3 -0
- package/src/controllers/workflow-engine-scheduler.ts +4 -8
- package/src/controllers/workflows.ts +45 -21
- package/src/extension/controller-host.ts +1 -2
- package/src/extension/decision-channels.ts +427 -144
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +180 -118
- package/src/extension/recorder.ts +13 -22
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +8 -0
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +11 -4
- package/src/state/database.ts +375 -0
- package/src/state/index.ts +29 -0
- package/src/state/json.ts +42 -0
- package/src/state/mutation.ts +435 -0
- package/src/state/schema.ts +475 -0
- package/src/viewer/cli.ts +151 -144
- package/src/viewer/render.ts +12 -24
- package/src/viewer/session-reducer.ts +1 -1
- package/src/viewer/tui.ts +18 -17
- package/src/viewer/watch.ts +13 -17
- package/src/workflows/composition.ts +12 -3
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +212 -116
- package/src/workflows/errors.ts +3 -3
- package/src/workflows/human-decision.ts +795 -340
- package/src/workflows/index.ts +15 -19
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/store.ts +1295 -1163
- package/src/workflows/types.ts +61 -81
- package/dist/workflows/artifacts.d.ts +0 -40
- package/dist/workflows/artifacts.js +0 -155
- package/dist/workflows/artifacts.js.map +0 -1
- package/dist/workflows/migrate-sources.d.ts +0 -42
- package/dist/workflows/migrate-sources.js +0 -133
- package/dist/workflows/migrate-sources.js.map +0 -1
- package/docs/run-bundles.md +0 -481
- package/src/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
|
@@ -21,41 +21,32 @@ import type {
|
|
|
21
21
|
} from "../workflows/types.js";
|
|
22
22
|
import { validateProgressData } from "../workflows/updates.js";
|
|
23
23
|
import autoimplementWorkflow, { type AutoimplementInput } from "./autoimplement.workflow.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
parsePlanApprovalPolicy,
|
|
26
|
+
type ResolvedPlanApprovalPolicy,
|
|
27
|
+
} from "./plan-approval.workflow.js";
|
|
25
28
|
import planChangeWorkflow, { type NormalizedPlanChangeInput } from "./plan-change.workflow.js";
|
|
26
29
|
|
|
27
30
|
const MIN_INTERVAL_MINUTES = 1;
|
|
28
31
|
const MAX_INTERVAL_MINUTES = 24 * 60;
|
|
29
32
|
const DEFAULT_INTERVAL_MINUTES = 30;
|
|
30
|
-
const
|
|
31
|
-
const MAX_CHECK_TIMEOUT_MINUTES = 24 * 60;
|
|
32
|
-
const DEFAULT_MIN_CHECK_TIMEOUT_MINUTES = 60;
|
|
33
|
+
const DEFAULT_CHECK_TIMEOUT_MINUTES = 60;
|
|
33
34
|
const DEFAULT_MAX_CHECKS = 1_000;
|
|
34
35
|
const MAX_CHECKS = 1_000;
|
|
35
36
|
const MAX_TRACKS = 256;
|
|
36
37
|
const MAX_OBSERVATION_CHARS = 8_000;
|
|
37
38
|
const MAX_REPORT_CHARS = 4_000;
|
|
38
39
|
const MAX_REASON_CHARS = 2_000;
|
|
40
|
+
const MAX_ACTION_TEXT_CHARS = 8_000;
|
|
41
|
+
const MAX_ID_CHARS = 256;
|
|
39
42
|
const SLEEP_TIMEOUT_MARGIN_MS = 60_000;
|
|
40
43
|
const NODE_TIMEOUT_MARGIN_MS = 2 * 60_000;
|
|
41
44
|
|
|
42
|
-
export type MonitorRepairPolicy = {
|
|
43
|
-
authorized: true;
|
|
44
|
-
scope?: string;
|
|
45
|
-
constraints?: string[];
|
|
46
|
-
repository?: string;
|
|
47
|
-
baseBranch?: string;
|
|
48
|
-
merge?: boolean;
|
|
49
|
-
approval?: PlanApprovalPolicy;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
45
|
export type MonitorInput = {
|
|
53
46
|
task: string;
|
|
54
47
|
everyMinutes?: number;
|
|
55
48
|
stopWhen?: string;
|
|
56
49
|
maxChecks?: number;
|
|
57
|
-
checkTimeoutMinutes?: number;
|
|
58
|
-
repair?: MonitorRepairPolicy;
|
|
59
50
|
};
|
|
60
51
|
|
|
61
52
|
type MonitorConfig = {
|
|
@@ -63,25 +54,78 @@ type MonitorConfig = {
|
|
|
63
54
|
everyMinutes: number;
|
|
64
55
|
stopWhen: string;
|
|
65
56
|
maxChecks: number;
|
|
66
|
-
checkTimeoutMinutes: number;
|
|
67
|
-
repair?: MonitorRepairPolicy;
|
|
68
57
|
};
|
|
69
|
-
type MonitorRoute = "
|
|
58
|
+
type MonitorRoute = "wait" | "act" | "stop";
|
|
59
|
+
type MonitorGoalState = "complete" | "incomplete" | "blocked";
|
|
60
|
+
type MonitorWorkState = "running" | "waiting" | "idle" | "failed" | "stopped" | "unknown";
|
|
61
|
+
type MonitorActionKind = "advance" | "recover" | "repair";
|
|
70
62
|
type MonitorTrack = { key: string; data: WorkflowProgressData };
|
|
71
|
-
type
|
|
72
|
-
|
|
63
|
+
type MonitorAuthority = {
|
|
64
|
+
status: "authorized" | "outside";
|
|
65
|
+
basis: string;
|
|
66
|
+
allowedMutations: string[];
|
|
67
|
+
forbiddenMutations: string[];
|
|
68
|
+
costLimit: string;
|
|
69
|
+
providerRuntime: string;
|
|
70
|
+
requiredChecks: string[];
|
|
71
|
+
stopConditions: string[];
|
|
72
|
+
allowedRecoveryActions: string[];
|
|
73
|
+
repository?: string;
|
|
74
|
+
baseBranch?: string;
|
|
75
|
+
merge: boolean;
|
|
76
|
+
repairApproval: ResolvedPlanApprovalPolicy;
|
|
77
|
+
};
|
|
78
|
+
type MonitorCostSafety = {
|
|
79
|
+
paidAction: boolean;
|
|
80
|
+
status: "not-applicable" | "within-limit" | "missing" | "exceeded";
|
|
81
|
+
evidence: string;
|
|
82
|
+
};
|
|
83
|
+
type MonitorDefectSafety = {
|
|
84
|
+
sharedCodeOrDataDefect: boolean;
|
|
85
|
+
paidWorkers: "not-applicable" | "stopped" | "running";
|
|
86
|
+
evidence: string;
|
|
87
|
+
};
|
|
88
|
+
type MonitorActionRequest = {
|
|
89
|
+
kind: MonitorActionKind;
|
|
90
|
+
incomplete: string;
|
|
73
91
|
evidence: unknown;
|
|
74
|
-
|
|
92
|
+
nextAction: string;
|
|
93
|
+
authority: MonitorAuthority;
|
|
94
|
+
cost: MonitorCostSafety;
|
|
95
|
+
defect: MonitorDefectSafety;
|
|
96
|
+
verification: string;
|
|
97
|
+
failureId: string;
|
|
98
|
+
targetStateId: string;
|
|
75
99
|
};
|
|
76
|
-
type
|
|
100
|
+
export type MonitorObservation = {
|
|
77
101
|
route: MonitorRoute;
|
|
102
|
+
goalState: MonitorGoalState;
|
|
103
|
+
workState: MonitorWorkState;
|
|
78
104
|
observation: string;
|
|
79
105
|
report: string;
|
|
106
|
+
targetStateId: string;
|
|
107
|
+
authorizedActions: string[];
|
|
80
108
|
progress?: { tracks: MonitorTrack[] };
|
|
81
|
-
|
|
109
|
+
action?: MonitorActionRequest;
|
|
82
110
|
reason: string;
|
|
83
111
|
};
|
|
112
|
+
type MonitorActionResult = {
|
|
113
|
+
status: "succeeded" | "failed" | "blocked";
|
|
114
|
+
summary: string;
|
|
115
|
+
evidence: unknown;
|
|
116
|
+
verification: string;
|
|
117
|
+
failureId: string;
|
|
118
|
+
targetStateId: string;
|
|
119
|
+
};
|
|
84
120
|
type MonitorEstimate = { tracks: ProgressTrackState[] };
|
|
121
|
+
type RecordedAction = {
|
|
122
|
+
status: "succeeded" | "failed" | "blocked";
|
|
123
|
+
summary: string;
|
|
124
|
+
evidence: unknown;
|
|
125
|
+
verification: string;
|
|
126
|
+
failureId: string;
|
|
127
|
+
targetStateId: string;
|
|
128
|
+
};
|
|
85
129
|
|
|
86
130
|
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
87
131
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -90,6 +134,15 @@ function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
|
90
134
|
return value as Record<string, unknown>;
|
|
91
135
|
}
|
|
92
136
|
|
|
137
|
+
function requireExactKeys(
|
|
138
|
+
value: Record<string, unknown>,
|
|
139
|
+
allowed: readonly string[],
|
|
140
|
+
label: string,
|
|
141
|
+
): void {
|
|
142
|
+
const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
|
|
143
|
+
if (unexpected !== undefined) throw new Error(`${label} field ${unexpected} is not supported`);
|
|
144
|
+
}
|
|
145
|
+
|
|
93
146
|
function requireBoundedString(value: unknown, label: string, maxChars: number): string {
|
|
94
147
|
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
95
148
|
const trimmed = value.trim();
|
|
@@ -98,6 +151,15 @@ function requireBoundedString(value: unknown, label: string, maxChars: number):
|
|
|
98
151
|
return trimmed;
|
|
99
152
|
}
|
|
100
153
|
|
|
154
|
+
function requireStringArray(value: unknown, label: string): string[] {
|
|
155
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
156
|
+
throw new Error(`${label} must be an array of strings`);
|
|
157
|
+
}
|
|
158
|
+
return value.map((item, index) =>
|
|
159
|
+
requireBoundedString(item, `${label}[${index}]`, MAX_ACTION_TEXT_CHARS),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
101
163
|
async function waitForUpdateSlot(signal: AbortSignal): Promise<void> {
|
|
102
164
|
await new Promise<void>((resolve, reject) => {
|
|
103
165
|
const onAbort = () => {
|
|
@@ -115,17 +177,8 @@ async function waitForUpdateSlot(signal: AbortSignal): Promise<void> {
|
|
|
115
177
|
|
|
116
178
|
export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
117
179
|
const value = requireRecord(input, "monitor input") as Partial<MonitorInput>;
|
|
118
|
-
const allowed =
|
|
119
|
-
|
|
120
|
-
"everyMinutes",
|
|
121
|
-
"stopWhen",
|
|
122
|
-
"maxChecks",
|
|
123
|
-
"checkTimeoutMinutes",
|
|
124
|
-
"repair",
|
|
125
|
-
]);
|
|
126
|
-
for (const field of Object.keys(value)) {
|
|
127
|
-
if (!allowed.has(field)) throw new Error(`monitor input field ${field} is not supported`);
|
|
128
|
-
}
|
|
180
|
+
const allowed = ["task", "everyMinutes", "stopWhen", "maxChecks"] as const;
|
|
181
|
+
requireExactKeys(value, allowed, "monitor input");
|
|
129
182
|
const task = requireBoundedString(value.task, "task", 8_000);
|
|
130
183
|
const everyMinutes = value.everyMinutes ?? DEFAULT_INTERVAL_MINUTES;
|
|
131
184
|
if (
|
|
@@ -141,47 +194,6 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
|
141
194
|
if (!Number.isInteger(maxChecks) || maxChecks <= 0 || maxChecks > MAX_CHECKS) {
|
|
142
195
|
throw new Error(`maxChecks must be an integer from 1 through ${MAX_CHECKS}`);
|
|
143
196
|
}
|
|
144
|
-
const checkTimeoutMinutes =
|
|
145
|
-
value.checkTimeoutMinutes ?? Math.max(DEFAULT_MIN_CHECK_TIMEOUT_MINUTES, everyMinutes);
|
|
146
|
-
if (
|
|
147
|
-
!Number.isInteger(checkTimeoutMinutes) ||
|
|
148
|
-
checkTimeoutMinutes < MIN_CHECK_TIMEOUT_MINUTES ||
|
|
149
|
-
checkTimeoutMinutes > MAX_CHECK_TIMEOUT_MINUTES
|
|
150
|
-
) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
`checkTimeoutMinutes must be an integer from ${MIN_CHECK_TIMEOUT_MINUTES} through ${MAX_CHECK_TIMEOUT_MINUTES}`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
let repair: MonitorRepairPolicy | undefined;
|
|
156
|
-
if (value.repair !== undefined) {
|
|
157
|
-
const raw = requireRecord(value.repair, "repair policy");
|
|
158
|
-
if (raw.authorized !== true) throw new Error("repair policy must set authorized to true");
|
|
159
|
-
if (
|
|
160
|
-
raw.constraints !== undefined &&
|
|
161
|
-
(!Array.isArray(raw.constraints) || raw.constraints.some((item) => typeof item !== "string"))
|
|
162
|
-
) {
|
|
163
|
-
throw new Error("repair constraints must be an array of strings");
|
|
164
|
-
}
|
|
165
|
-
if (raw.merge !== undefined && typeof raw.merge !== "boolean") {
|
|
166
|
-
throw new Error("repair merge must be a boolean");
|
|
167
|
-
}
|
|
168
|
-
const approval = parsePlanApprovalPolicy(raw.approval);
|
|
169
|
-
repair = {
|
|
170
|
-
authorized: true,
|
|
171
|
-
...(raw.scope !== undefined
|
|
172
|
-
? { scope: requireBoundedString(raw.scope, "repair scope", 4_000) }
|
|
173
|
-
: {}),
|
|
174
|
-
...(raw.constraints !== undefined ? { constraints: [...raw.constraints] as string[] } : {}),
|
|
175
|
-
...(raw.repository !== undefined
|
|
176
|
-
? { repository: requireBoundedString(raw.repository, "repair repository", 4_000) }
|
|
177
|
-
: {}),
|
|
178
|
-
...(raw.baseBranch !== undefined
|
|
179
|
-
? { baseBranch: requireBoundedString(raw.baseBranch, "repair base branch", 256) }
|
|
180
|
-
: {}),
|
|
181
|
-
...(raw.merge !== undefined ? { merge: raw.merge !== false } : {}),
|
|
182
|
-
approval,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
197
|
return {
|
|
186
198
|
task,
|
|
187
199
|
everyMinutes,
|
|
@@ -190,8 +202,6 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
|
190
202
|
? "Stop only when the user explicitly asks to stop."
|
|
191
203
|
: requireBoundedString(value.stopWhen, "stopWhen", 4_000),
|
|
192
204
|
maxChecks,
|
|
193
|
-
checkTimeoutMinutes,
|
|
194
|
-
...(repair !== undefined ? { repair } : {}),
|
|
195
205
|
};
|
|
196
206
|
}
|
|
197
207
|
|
|
@@ -199,63 +209,305 @@ function configFrom(outputs: Record<string, unknown>): MonitorConfig {
|
|
|
199
209
|
return outputs.prepare as MonitorConfig;
|
|
200
210
|
}
|
|
201
211
|
|
|
202
|
-
function
|
|
203
|
-
return context.state.steps.filter((step) => step.nodeId === "
|
|
212
|
+
function completedObservations(context: WorkflowNodeContext): number {
|
|
213
|
+
return context.state.steps.filter((step) => step.nodeId === "observe" && step.outcome === "ok")
|
|
204
214
|
.length;
|
|
205
215
|
}
|
|
206
216
|
|
|
207
|
-
|
|
208
|
-
const value = requireRecord(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
function parseAuthority(input: unknown): MonitorAuthority {
|
|
218
|
+
const value = requireRecord(input, "action authority");
|
|
219
|
+
requireExactKeys(
|
|
220
|
+
value,
|
|
221
|
+
[
|
|
222
|
+
"status",
|
|
223
|
+
"basis",
|
|
224
|
+
"allowedMutations",
|
|
225
|
+
"forbiddenMutations",
|
|
226
|
+
"costLimit",
|
|
227
|
+
"providerRuntime",
|
|
228
|
+
"requiredChecks",
|
|
229
|
+
"stopConditions",
|
|
230
|
+
"allowedRecoveryActions",
|
|
231
|
+
"repository",
|
|
232
|
+
"baseBranch",
|
|
233
|
+
"merge",
|
|
234
|
+
"repairApproval",
|
|
235
|
+
],
|
|
236
|
+
"action authority",
|
|
237
|
+
);
|
|
238
|
+
if (value.status !== "authorized" && value.status !== "outside") {
|
|
239
|
+
throw new Error("action authority status must be authorized or outside");
|
|
240
|
+
}
|
|
241
|
+
if (typeof value.merge !== "boolean") throw new Error("action authority merge must be boolean");
|
|
242
|
+
const approval =
|
|
243
|
+
value.repairApproval === undefined
|
|
244
|
+
? undefined
|
|
245
|
+
: requireRecord(value.repairApproval, "repair approval");
|
|
246
|
+
return {
|
|
247
|
+
status: value.status,
|
|
248
|
+
basis: requireBoundedString(value.basis, "action authority basis", MAX_ACTION_TEXT_CHARS),
|
|
249
|
+
allowedMutations: requireStringArray(
|
|
250
|
+
value.allowedMutations,
|
|
251
|
+
"action authority allowedMutations",
|
|
252
|
+
),
|
|
253
|
+
forbiddenMutations: requireStringArray(
|
|
254
|
+
value.forbiddenMutations,
|
|
255
|
+
"action authority forbiddenMutations",
|
|
256
|
+
),
|
|
257
|
+
costLimit: requireBoundedString(
|
|
258
|
+
value.costLimit,
|
|
259
|
+
"action authority costLimit",
|
|
260
|
+
MAX_ACTION_TEXT_CHARS,
|
|
261
|
+
),
|
|
262
|
+
providerRuntime: requireBoundedString(
|
|
263
|
+
value.providerRuntime,
|
|
264
|
+
"action authority providerRuntime",
|
|
265
|
+
MAX_ACTION_TEXT_CHARS,
|
|
266
|
+
),
|
|
267
|
+
requiredChecks: requireStringArray(value.requiredChecks, "action authority requiredChecks"),
|
|
268
|
+
stopConditions: requireStringArray(value.stopConditions, "action authority stopConditions"),
|
|
269
|
+
allowedRecoveryActions: requireStringArray(
|
|
270
|
+
value.allowedRecoveryActions,
|
|
271
|
+
"action authority allowedRecoveryActions",
|
|
272
|
+
),
|
|
273
|
+
...(value.repository !== undefined
|
|
274
|
+
? {
|
|
275
|
+
repository: requireBoundedString(value.repository, "action authority repository", 4_000),
|
|
276
|
+
}
|
|
277
|
+
: {}),
|
|
278
|
+
...(value.baseBranch !== undefined
|
|
279
|
+
? {
|
|
280
|
+
baseBranch: requireBoundedString(value.baseBranch, "action authority baseBranch", 256),
|
|
281
|
+
}
|
|
282
|
+
: {}),
|
|
283
|
+
merge: value.merge,
|
|
284
|
+
repairApproval: parsePlanApprovalPolicy(approval),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseCostSafety(input: unknown): MonitorCostSafety {
|
|
289
|
+
const value = requireRecord(input, "action cost");
|
|
290
|
+
requireExactKeys(value, ["paidAction", "status", "evidence"], "action cost");
|
|
291
|
+
if (typeof value.paidAction !== "boolean")
|
|
292
|
+
throw new Error("action cost paidAction must be boolean");
|
|
293
|
+
if (
|
|
294
|
+
value.status !== "not-applicable" &&
|
|
295
|
+
value.status !== "within-limit" &&
|
|
296
|
+
value.status !== "missing" &&
|
|
297
|
+
value.status !== "exceeded"
|
|
298
|
+
) {
|
|
299
|
+
throw new Error("action cost status is invalid");
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
paidAction: value.paidAction,
|
|
303
|
+
status: value.status,
|
|
304
|
+
evidence: requireBoundedString(value.evidence, "action cost evidence", MAX_ACTION_TEXT_CHARS),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseDefectSafety(input: unknown): MonitorDefectSafety {
|
|
309
|
+
const value = requireRecord(input, "action defect");
|
|
310
|
+
requireExactKeys(value, ["sharedCodeOrDataDefect", "paidWorkers", "evidence"], "action defect");
|
|
311
|
+
if (typeof value.sharedCodeOrDataDefect !== "boolean") {
|
|
312
|
+
throw new Error("action defect sharedCodeOrDataDefect must be boolean");
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
value.paidWorkers !== "not-applicable" &&
|
|
316
|
+
value.paidWorkers !== "stopped" &&
|
|
317
|
+
value.paidWorkers !== "running"
|
|
318
|
+
) {
|
|
319
|
+
throw new Error("action defect paidWorkers is invalid");
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
sharedCodeOrDataDefect: value.sharedCodeOrDataDefect,
|
|
323
|
+
paidWorkers: value.paidWorkers,
|
|
324
|
+
evidence: requireBoundedString(value.evidence, "action defect evidence", MAX_ACTION_TEXT_CHARS),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function parseActionRequest(input: unknown): MonitorActionRequest {
|
|
329
|
+
const value = requireRecord(input, "monitor action request");
|
|
330
|
+
requireExactKeys(
|
|
331
|
+
value,
|
|
332
|
+
[
|
|
333
|
+
"kind",
|
|
334
|
+
"incomplete",
|
|
335
|
+
"evidence",
|
|
336
|
+
"nextAction",
|
|
337
|
+
"authority",
|
|
338
|
+
"cost",
|
|
339
|
+
"defect",
|
|
340
|
+
"verification",
|
|
341
|
+
"failureId",
|
|
342
|
+
"targetStateId",
|
|
343
|
+
],
|
|
344
|
+
"monitor action request",
|
|
345
|
+
);
|
|
346
|
+
if (value.kind !== "advance" && value.kind !== "recover" && value.kind !== "repair") {
|
|
347
|
+
throw new Error("monitor action kind must be advance, recover, or repair");
|
|
348
|
+
}
|
|
349
|
+
const request: MonitorActionRequest = {
|
|
350
|
+
kind: value.kind,
|
|
351
|
+
incomplete: requireBoundedString(
|
|
352
|
+
value.incomplete,
|
|
353
|
+
"monitor action incomplete work",
|
|
354
|
+
MAX_ACTION_TEXT_CHARS,
|
|
355
|
+
),
|
|
356
|
+
evidence: value.evidence ?? null,
|
|
357
|
+
nextAction: requireBoundedString(
|
|
358
|
+
value.nextAction,
|
|
359
|
+
"monitor action nextAction",
|
|
360
|
+
MAX_ACTION_TEXT_CHARS,
|
|
361
|
+
),
|
|
362
|
+
authority: parseAuthority(value.authority),
|
|
363
|
+
cost: parseCostSafety(value.cost),
|
|
364
|
+
defect: parseDefectSafety(value.defect),
|
|
365
|
+
verification: requireBoundedString(
|
|
366
|
+
value.verification,
|
|
367
|
+
"monitor action verification",
|
|
368
|
+
MAX_ACTION_TEXT_CHARS,
|
|
369
|
+
),
|
|
370
|
+
failureId: requireBoundedString(value.failureId, "monitor action failureId", MAX_ID_CHARS),
|
|
371
|
+
targetStateId: requireBoundedString(
|
|
372
|
+
value.targetStateId,
|
|
373
|
+
"monitor action targetStateId",
|
|
374
|
+
MAX_ID_CHARS,
|
|
375
|
+
),
|
|
376
|
+
};
|
|
377
|
+
if (request.authority.status !== "authorized") {
|
|
378
|
+
throw new Error("route act requires action authority status authorized");
|
|
379
|
+
}
|
|
380
|
+
if (request.authority.allowedMutations.length === 0) {
|
|
381
|
+
throw new Error("route act requires at least one allowed mutation");
|
|
382
|
+
}
|
|
383
|
+
if (
|
|
384
|
+
request.cost.paidAction &&
|
|
385
|
+
(request.cost.status === "missing" || request.cost.status === "exceeded")
|
|
386
|
+
) {
|
|
387
|
+
throw new Error("route act cannot launch paid work without verified remaining authority");
|
|
388
|
+
}
|
|
389
|
+
if (request.cost.paidAction && request.cost.status !== "within-limit") {
|
|
390
|
+
throw new Error("paid route act requires cost status within-limit");
|
|
391
|
+
}
|
|
392
|
+
if (!request.cost.paidAction && request.cost.status !== "not-applicable") {
|
|
393
|
+
throw new Error("unpaid route act requires cost status not-applicable");
|
|
394
|
+
}
|
|
395
|
+
if (
|
|
396
|
+
request.kind === "repair" &&
|
|
397
|
+
request.defect.sharedCodeOrDataDefect &&
|
|
398
|
+
request.defect.paidWorkers === "running"
|
|
399
|
+
) {
|
|
400
|
+
throw new Error("paid workers must stop before a shared code or data repair");
|
|
401
|
+
}
|
|
402
|
+
return request;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function validateMonitorObservation(output: unknown): MonitorObservation {
|
|
406
|
+
const value = requireRecord(output, "monitor observation output");
|
|
407
|
+
requireExactKeys(
|
|
408
|
+
value,
|
|
409
|
+
[
|
|
410
|
+
"route",
|
|
411
|
+
"goalState",
|
|
412
|
+
"workState",
|
|
413
|
+
"observation",
|
|
414
|
+
"report",
|
|
415
|
+
"targetStateId",
|
|
416
|
+
"authorizedActions",
|
|
417
|
+
"progress",
|
|
418
|
+
"action",
|
|
419
|
+
"reason",
|
|
420
|
+
],
|
|
421
|
+
"monitor observation",
|
|
422
|
+
);
|
|
423
|
+
if (value.route !== "wait" && value.route !== "act" && value.route !== "stop") {
|
|
424
|
+
throw new Error("route must be wait, act, or stop");
|
|
425
|
+
}
|
|
426
|
+
if (
|
|
427
|
+
value.goalState !== "complete" &&
|
|
428
|
+
value.goalState !== "incomplete" &&
|
|
429
|
+
value.goalState !== "blocked"
|
|
430
|
+
) {
|
|
431
|
+
throw new Error("goalState must be complete, incomplete, or blocked");
|
|
214
432
|
}
|
|
215
|
-
if (
|
|
216
|
-
|
|
433
|
+
if (
|
|
434
|
+
value.workState !== "running" &&
|
|
435
|
+
value.workState !== "waiting" &&
|
|
436
|
+
value.workState !== "idle" &&
|
|
437
|
+
value.workState !== "failed" &&
|
|
438
|
+
value.workState !== "stopped" &&
|
|
439
|
+
value.workState !== "unknown"
|
|
440
|
+
) {
|
|
441
|
+
throw new Error("workState is invalid");
|
|
217
442
|
}
|
|
218
|
-
const
|
|
443
|
+
const observation: MonitorObservation = {
|
|
219
444
|
route: value.route,
|
|
445
|
+
goalState: value.goalState,
|
|
446
|
+
workState: value.workState,
|
|
220
447
|
observation: requireBoundedString(value.observation, "observation", MAX_OBSERVATION_CHARS),
|
|
221
448
|
report: requireBoundedString(value.report, "report", MAX_REPORT_CHARS),
|
|
449
|
+
targetStateId: requireBoundedString(value.targetStateId, "targetStateId", MAX_ID_CHARS),
|
|
450
|
+
authorizedActions: requireStringArray(value.authorizedActions, "authorizedActions"),
|
|
222
451
|
reason: requireBoundedString(value.reason, "reason", MAX_REASON_CHARS),
|
|
223
452
|
};
|
|
224
|
-
if (value.progress !== undefined)
|
|
225
|
-
if (value.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
453
|
+
if (value.progress !== undefined) observation.progress = validateMonitorProgress(value.progress);
|
|
454
|
+
if (value.action !== undefined) observation.action = parseActionRequest(value.action);
|
|
455
|
+
if (observation.route === "act" && observation.action === undefined) {
|
|
456
|
+
throw new Error("route act requires action details");
|
|
457
|
+
}
|
|
458
|
+
if (
|
|
459
|
+
observation.action !== undefined &&
|
|
460
|
+
observation.action.targetStateId !== observation.targetStateId
|
|
461
|
+
) {
|
|
462
|
+
throw new Error("monitor action targetStateId must match the observed targetStateId");
|
|
463
|
+
}
|
|
464
|
+
if (observation.route !== "act" && observation.action !== undefined) {
|
|
465
|
+
throw new Error("action details are only valid for route act");
|
|
236
466
|
}
|
|
237
|
-
if (
|
|
238
|
-
throw new Error("route
|
|
467
|
+
if (observation.route === "act" && observation.goalState !== "incomplete") {
|
|
468
|
+
throw new Error("route act requires goalState incomplete");
|
|
239
469
|
}
|
|
240
|
-
|
|
470
|
+
if (
|
|
471
|
+
observation.route === "act" &&
|
|
472
|
+
observation.workState !== "idle" &&
|
|
473
|
+
observation.workState !== "failed" &&
|
|
474
|
+
observation.workState !== "stopped"
|
|
475
|
+
) {
|
|
476
|
+
throw new Error("route act requires idle, failed, or stopped work");
|
|
477
|
+
}
|
|
478
|
+
if (observation.route === "wait" && observation.goalState !== "incomplete") {
|
|
479
|
+
throw new Error("route wait requires goalState incomplete");
|
|
480
|
+
}
|
|
481
|
+
if (
|
|
482
|
+
observation.route === "wait" &&
|
|
483
|
+
observation.workState !== "running" &&
|
|
484
|
+
observation.workState !== "waiting"
|
|
485
|
+
) {
|
|
486
|
+
throw new Error("route wait requires running work or an external wait");
|
|
487
|
+
}
|
|
488
|
+
if (observation.goalState === "complete" && observation.route !== "stop") {
|
|
489
|
+
throw new Error("goalState complete requires route stop");
|
|
490
|
+
}
|
|
491
|
+
if (observation.goalState === "blocked" && observation.route !== "stop") {
|
|
492
|
+
throw new Error("goalState blocked requires route stop");
|
|
493
|
+
}
|
|
494
|
+
return observation;
|
|
241
495
|
}
|
|
242
496
|
|
|
243
497
|
function validateMonitorProgress(input: unknown): { tracks: MonitorTrack[] } {
|
|
244
498
|
const value = requireRecord(input, "progress");
|
|
245
|
-
|
|
246
|
-
throw new Error("progress only supports tracks");
|
|
499
|
+
requireExactKeys(value, ["tracks"], "progress");
|
|
247
500
|
if (!Array.isArray(value.tracks) || value.tracks.length < 1 || value.tracks.length > MAX_TRACKS) {
|
|
248
501
|
throw new Error(`progress.tracks must contain 1 through ${MAX_TRACKS} entries`);
|
|
249
502
|
}
|
|
250
503
|
const keys = new Set<string>();
|
|
251
504
|
const tracks = value.tracks.map((raw, index) => {
|
|
252
505
|
const track = requireRecord(raw, `progress.tracks[${index}]`);
|
|
253
|
-
|
|
254
|
-
throw new Error(`progress.tracks[${index}] has an unsupported field`);
|
|
255
|
-
}
|
|
506
|
+
requireExactKeys(track, ["key", "data"], `progress.tracks[${index}]`);
|
|
256
507
|
const key = requireBoundedString(track.key, `progress.tracks[${index}].key`, 128);
|
|
257
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(key))
|
|
508
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(key)) {
|
|
258
509
|
throw new Error(`progress.tracks[${index}].key is invalid`);
|
|
510
|
+
}
|
|
259
511
|
if (keys.has(key)) throw new Error(`progress track key ${key} is duplicated`);
|
|
260
512
|
keys.add(key);
|
|
261
513
|
return {
|
|
@@ -266,14 +518,18 @@ function validateMonitorProgress(input: unknown): { tracks: MonitorTrack[] } {
|
|
|
266
518
|
return { tracks };
|
|
267
519
|
}
|
|
268
520
|
|
|
521
|
+
function currentObservation(outputs: Record<string, unknown>): MonitorObservation {
|
|
522
|
+
return (outputs.guard ?? outputs.observe) as MonitorObservation;
|
|
523
|
+
}
|
|
524
|
+
|
|
269
525
|
function estimateTracks(outputs: Record<string, unknown>): MonitorEstimate {
|
|
270
|
-
const
|
|
271
|
-
if (
|
|
526
|
+
const observation = currentObservation(outputs);
|
|
527
|
+
if (observation.progress === undefined) return { tracks: [] };
|
|
272
528
|
const previous = outputs.estimate as MonitorEstimate | undefined;
|
|
273
529
|
const previousByKey = new Map((previous?.tracks ?? []).map((track) => [track.key, track]));
|
|
274
530
|
const at = new Date().toISOString();
|
|
275
531
|
return {
|
|
276
|
-
tracks:
|
|
532
|
+
tracks: observation.progress.tracks.map((track) => {
|
|
277
533
|
const samples: ProgressSample[] = [
|
|
278
534
|
...(previousByKey.get(track.key)?.samples ?? []),
|
|
279
535
|
{ at, data: track.data },
|
|
@@ -283,24 +539,93 @@ function estimateTracks(outputs: Record<string, unknown>): MonitorEstimate {
|
|
|
283
539
|
};
|
|
284
540
|
}
|
|
285
541
|
|
|
286
|
-
function
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
542
|
+
function actionFrom(outputs: Record<string, unknown>): MonitorActionRequest {
|
|
543
|
+
const request = currentObservation(outputs).action;
|
|
544
|
+
if (request === undefined) throw new Error("monitor action details are missing");
|
|
545
|
+
return request;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function repeatedCompletedRepair(context: WorkflowNodeContext): boolean {
|
|
549
|
+
const action = (context.outputs.observe as MonitorObservation).action;
|
|
550
|
+
if (action?.kind !== "repair") return false;
|
|
551
|
+
const currentIndex = context.state.steps.findLastIndex((step) => step.nodeId === "observe");
|
|
552
|
+
for (let index = currentIndex - 1; index >= 0; index -= 1) {
|
|
553
|
+
const step = context.state.steps[index];
|
|
554
|
+
if (step?.nodeId !== "observe") continue;
|
|
555
|
+
const prior = step.output as MonitorObservation;
|
|
556
|
+
if (
|
|
557
|
+
prior.action?.kind !== "repair" ||
|
|
558
|
+
prior.action.failureId !== action.failureId ||
|
|
559
|
+
prior.action.targetStateId !== action.targetStateId
|
|
560
|
+
) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
return context.state.steps
|
|
564
|
+
.slice(index + 1, currentIndex)
|
|
565
|
+
.some((candidate) => candidate.nodeId === "repairComplete" && candidate.outcome === "ok");
|
|
300
566
|
}
|
|
301
567
|
return false;
|
|
302
568
|
}
|
|
303
569
|
|
|
570
|
+
function guardObservation(context: WorkflowNodeContext): MonitorObservation {
|
|
571
|
+
const observation = context.outputs.observe as MonitorObservation;
|
|
572
|
+
if (!repeatedCompletedRepair(context)) return observation;
|
|
573
|
+
const action = observation.action;
|
|
574
|
+
if (action === undefined) return observation;
|
|
575
|
+
return {
|
|
576
|
+
route: "stop",
|
|
577
|
+
goalState: "blocked",
|
|
578
|
+
workState: observation.workState,
|
|
579
|
+
observation: observation.observation,
|
|
580
|
+
report: "The same failure and target state returned after one completed repair.",
|
|
581
|
+
targetStateId: observation.targetStateId,
|
|
582
|
+
authorizedActions: observation.authorizedActions,
|
|
583
|
+
...(observation.progress !== undefined ? { progress: observation.progress } : {}),
|
|
584
|
+
reason: `Repair stopped because ${action.failureId} returned in target state ${action.targetStateId}.`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function validateMonitorActionResult(
|
|
589
|
+
output: unknown,
|
|
590
|
+
expected: Pick<MonitorActionRequest, "failureId" | "targetStateId">,
|
|
591
|
+
): MonitorActionResult {
|
|
592
|
+
const value = requireRecord(output, "monitor action result");
|
|
593
|
+
requireExactKeys(
|
|
594
|
+
value,
|
|
595
|
+
["status", "summary", "evidence", "verification", "failureId", "targetStateId"],
|
|
596
|
+
"monitor action result",
|
|
597
|
+
);
|
|
598
|
+
if (value.status !== "succeeded" && value.status !== "failed" && value.status !== "blocked") {
|
|
599
|
+
throw new Error("monitor action result status must be succeeded, failed, or blocked");
|
|
600
|
+
}
|
|
601
|
+
const result: MonitorActionResult = {
|
|
602
|
+
status: value.status,
|
|
603
|
+
summary: requireBoundedString(value.summary, "monitor action result summary", MAX_REPORT_CHARS),
|
|
604
|
+
evidence: value.evidence ?? null,
|
|
605
|
+
verification: requireBoundedString(
|
|
606
|
+
value.verification,
|
|
607
|
+
"monitor action result verification",
|
|
608
|
+
MAX_ACTION_TEXT_CHARS,
|
|
609
|
+
),
|
|
610
|
+
failureId: requireBoundedString(
|
|
611
|
+
value.failureId,
|
|
612
|
+
"monitor action result failureId",
|
|
613
|
+
MAX_ID_CHARS,
|
|
614
|
+
),
|
|
615
|
+
targetStateId: requireBoundedString(
|
|
616
|
+
value.targetStateId,
|
|
617
|
+
"monitor action result targetStateId",
|
|
618
|
+
MAX_ID_CHARS,
|
|
619
|
+
),
|
|
620
|
+
};
|
|
621
|
+
if (result.failureId !== expected.failureId || result.targetStateId !== expected.targetStateId) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
"monitor action result must preserve the requested failure and target-state IDs",
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
|
|
304
629
|
function currentRepairPlan(outputs: Record<string, unknown>): {
|
|
305
630
|
plan: unknown;
|
|
306
631
|
planDigest: string;
|
|
@@ -315,9 +640,31 @@ function currentRepairPlan(outputs: Record<string, unknown>): {
|
|
|
315
640
|
};
|
|
316
641
|
}
|
|
317
642
|
|
|
643
|
+
function authorityScope(actionRequest: MonitorActionRequest): string {
|
|
644
|
+
const authority = actionRequest.authority;
|
|
645
|
+
return [
|
|
646
|
+
`Authorization basis: ${authority.basis}`,
|
|
647
|
+
`Allowed mutations: ${authority.allowedMutations.join("; ")}`,
|
|
648
|
+
`Forbidden mutations: ${authority.forbiddenMutations.join("; ") || "none recorded"}`,
|
|
649
|
+
`Cost limit: ${authority.costLimit}`,
|
|
650
|
+
`Provider/runtime contract: ${authority.providerRuntime}`,
|
|
651
|
+
`Allowed recovery actions: ${authority.allowedRecoveryActions.join("; ") || "none recorded"}`,
|
|
652
|
+
].join("\n");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function authorityConstraints(actionRequest: MonitorActionRequest): string[] {
|
|
656
|
+
const authority = actionRequest.authority;
|
|
657
|
+
return [
|
|
658
|
+
...authority.forbiddenMutations.map((item) => `Forbidden: ${item}`),
|
|
659
|
+
`Cost limit: ${authority.costLimit}`,
|
|
660
|
+
`Provider/runtime contract: ${authority.providerRuntime}`,
|
|
661
|
+
...authority.requiredChecks.map((item) => `Required check: ${item}`),
|
|
662
|
+
...authority.stopConditions.map((item) => `Stop condition: ${item}`),
|
|
663
|
+
`Repair verification: ${actionRequest.verification}`,
|
|
664
|
+
];
|
|
665
|
+
}
|
|
666
|
+
|
|
318
667
|
function repairBlockedReason(outputs: Record<string, unknown>): string {
|
|
319
|
-
const guard = outputs.repairGuard as { reason?: string; route?: string } | undefined;
|
|
320
|
-
if (guard?.route === "blocked" && guard.reason !== undefined) return guard.reason;
|
|
321
668
|
const planChange = outputs.planChange as
|
|
322
669
|
| { exit?: string; output?: { reason?: string } }
|
|
323
670
|
| undefined;
|
|
@@ -327,36 +674,81 @@ function repairBlockedReason(outputs: Record<string, unknown>): string {
|
|
|
327
674
|
return (
|
|
328
675
|
implementation?.output?.reason ??
|
|
329
676
|
planChange?.output?.reason ??
|
|
330
|
-
"The repair did not produce
|
|
677
|
+
"The repair did not produce a verified result."
|
|
331
678
|
);
|
|
332
679
|
}
|
|
333
680
|
|
|
681
|
+
function recordedActionFromStep(
|
|
682
|
+
step: WorkflowNodeContext["state"]["steps"][number],
|
|
683
|
+
): RecordedAction | undefined {
|
|
684
|
+
if (step.outcome !== "ok") return undefined;
|
|
685
|
+
if (step.nodeId === "act") {
|
|
686
|
+
const result = step.output as MonitorActionResult;
|
|
687
|
+
return {
|
|
688
|
+
status: result.status,
|
|
689
|
+
summary: result.summary,
|
|
690
|
+
evidence: result.evidence,
|
|
691
|
+
verification: result.verification,
|
|
692
|
+
failureId: result.failureId,
|
|
693
|
+
targetStateId: result.targetStateId,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
if (step.nodeId === "repairComplete") return step.output as RecordedAction;
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function latestRecordedAction(context: WorkflowNodeContext): RecordedAction | undefined {
|
|
701
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
702
|
+
const step = context.state.steps[index];
|
|
703
|
+
if (step === undefined) continue;
|
|
704
|
+
const recorded = recordedActionFromStep(step);
|
|
705
|
+
if (recorded !== undefined) return recorded;
|
|
706
|
+
}
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
709
|
+
|
|
334
710
|
function reportMessage(context: WorkflowNodeContext): string {
|
|
335
|
-
const
|
|
711
|
+
const observation = currentObservation(context.outputs);
|
|
336
712
|
const estimate = context.outputs.estimate as MonitorEstimate;
|
|
337
713
|
const config = configFrom(context.outputs);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
formatProgressReport(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
714
|
+
const progress =
|
|
715
|
+
estimate.tracks.length === 0
|
|
716
|
+
? "Progress: unavailable"
|
|
717
|
+
: formatProgressReport(
|
|
718
|
+
estimate.tracks.map((track) => track.estimate),
|
|
719
|
+
undefined,
|
|
720
|
+
new Date(),
|
|
721
|
+
2_000,
|
|
722
|
+
);
|
|
723
|
+
const lines = [
|
|
724
|
+
`Monitor: ${observation.route === "stop" ? "stopping" : "active"}`,
|
|
725
|
+
`Goal: ${observation.goalState}`,
|
|
726
|
+
`Work: ${observation.workState}`,
|
|
727
|
+
progress,
|
|
728
|
+
];
|
|
729
|
+
const lastAction = latestRecordedAction(context);
|
|
730
|
+
if (lastAction !== undefined) lines.push(`Last action: ${lastAction.summary}`);
|
|
731
|
+
if (observation.action !== undefined && completedObservations(context) < config.maxChecks) {
|
|
732
|
+
lines.push(`Next action: ${observation.action.nextAction}`);
|
|
348
733
|
}
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
734
|
+
if (observation.route === "wait" && completedObservations(context) < config.maxChecks) {
|
|
735
|
+
lines.push(`Next check: ${config.everyMinutes} minutes`);
|
|
736
|
+
}
|
|
737
|
+
if (completedObservations(context) >= config.maxChecks && observation.route !== "stop") {
|
|
738
|
+
lines.push(`Safety limit: reached ${config.maxChecks} observations`);
|
|
739
|
+
}
|
|
740
|
+
lines.push(`Status: ${observation.report}`);
|
|
741
|
+
const message = lines.join("\n");
|
|
742
|
+
return message.length <= MAX_REPORT_CHARS
|
|
743
|
+
? message
|
|
744
|
+
: `${message.slice(0, MAX_REPORT_CHARS - 1)}…`;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function previousActionPrompt(context: WorkflowNodeContext): string {
|
|
748
|
+
const actionRecord = latestRecordedAction(context);
|
|
749
|
+
return actionRecord === undefined
|
|
750
|
+
? "There is no previous completed action."
|
|
751
|
+
: `Previous action result: ${JSON.stringify(actionRecord)}`;
|
|
360
752
|
}
|
|
361
753
|
|
|
362
754
|
const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
@@ -376,26 +768,23 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
376
768
|
includes: {
|
|
377
769
|
planChange: includeWorkflow(planChangeWorkflow, {
|
|
378
770
|
input: ({ outputs }): NormalizedPlanChangeInput => {
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
if (repair === undefined) throw new Error("monitor repair details are missing");
|
|
382
|
-
const prior = outputs.planChange as
|
|
383
|
-
| { exit?: string; output?: { plan?: unknown } }
|
|
384
|
-
| undefined;
|
|
771
|
+
const request = actionFrom(outputs);
|
|
772
|
+
if (request.kind !== "repair") throw new Error("monitor repair action is missing");
|
|
385
773
|
return {
|
|
386
|
-
task:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
:
|
|
391
|
-
...(config.repair?.repository !== undefined
|
|
392
|
-
? { repository: config.repair.repository }
|
|
393
|
-
: {}),
|
|
394
|
-
...(prior?.exit === "ready" && prior.output?.plan !== undefined
|
|
395
|
-
? { previousPlan: prior.output.plan }
|
|
774
|
+
task: request.nextAction,
|
|
775
|
+
scope: authorityScope(request),
|
|
776
|
+
constraints: authorityConstraints(request),
|
|
777
|
+
...(request.authority.repository !== undefined
|
|
778
|
+
? { repository: request.authority.repository }
|
|
396
779
|
: {}),
|
|
397
|
-
newEvidence:
|
|
398
|
-
|
|
780
|
+
newEvidence: {
|
|
781
|
+
incomplete: request.incomplete,
|
|
782
|
+
evidence: request.evidence,
|
|
783
|
+
failureId: request.failureId,
|
|
784
|
+
targetStateId: request.targetStateId,
|
|
785
|
+
defect: request.defect,
|
|
786
|
+
},
|
|
787
|
+
approval: request.authority.repairApproval,
|
|
399
788
|
};
|
|
400
789
|
},
|
|
401
790
|
}),
|
|
@@ -403,120 +792,143 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
403
792
|
workflow: "autoimplement",
|
|
404
793
|
contract: autoimplementWorkflow,
|
|
405
794
|
input: ({ outputs }) => {
|
|
406
|
-
const
|
|
407
|
-
const repair = (outputs.check as MonitorCheck).repair;
|
|
795
|
+
const request = actionFrom(outputs);
|
|
408
796
|
const documented = currentRepairPlan(outputs);
|
|
409
|
-
if (
|
|
410
|
-
const
|
|
411
|
-
task:
|
|
797
|
+
if (request.kind !== "repair") throw new Error("monitor repair action is missing");
|
|
798
|
+
const implementationInput: AutoimplementInput = {
|
|
799
|
+
task: request.nextAction,
|
|
412
800
|
plan: documented.plan,
|
|
413
801
|
documentation: {
|
|
414
802
|
status: "current",
|
|
415
803
|
planDigest: documented.planDigest,
|
|
416
804
|
documents: documented.documents,
|
|
417
805
|
},
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
806
|
+
scope: authorityScope(request),
|
|
807
|
+
constraints: authorityConstraints(request),
|
|
808
|
+
...(request.authority.repository !== undefined
|
|
809
|
+
? { repository: request.authority.repository }
|
|
421
810
|
: {}),
|
|
422
|
-
...(
|
|
423
|
-
? {
|
|
811
|
+
...(request.authority.baseBranch !== undefined
|
|
812
|
+
? { baseBranch: request.authority.baseBranch }
|
|
424
813
|
: {}),
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
: {}),
|
|
428
|
-
approval: parsePlanApprovalPolicy(config.repair?.approval),
|
|
429
|
-
merge: config.repair?.merge === true,
|
|
814
|
+
approval: request.authority.repairApproval,
|
|
815
|
+
merge: request.authority.merge,
|
|
430
816
|
};
|
|
431
|
-
return
|
|
817
|
+
return implementationInput;
|
|
432
818
|
},
|
|
433
819
|
}),
|
|
434
820
|
},
|
|
435
821
|
nodes: {
|
|
436
822
|
prepare: compute({ run: ({ input }) => prepareMonitorInput(input) }),
|
|
437
|
-
|
|
438
|
-
statusDetail: "
|
|
439
|
-
timeoutMs:
|
|
823
|
+
observe: agent({
|
|
824
|
+
statusDetail: "observing monitored goal",
|
|
825
|
+
timeoutMs: DEFAULT_CHECK_TIMEOUT_MINUTES * 60_000,
|
|
440
826
|
prompt: (context) => {
|
|
441
827
|
const config = configFrom(context.outputs);
|
|
442
|
-
const previous = context.outputs.
|
|
828
|
+
const previous = context.outputs.observe as MonitorObservation | undefined;
|
|
443
829
|
const priorEstimate = context.outputs.estimate as MonitorEstimate | undefined;
|
|
444
830
|
return [
|
|
445
|
-
`Perform
|
|
446
|
-
`
|
|
831
|
+
`Perform read-only observation ${completedObservations(context) + 1} of at most ${config.maxChecks}.`,
|
|
832
|
+
`Goal and authority: ${config.task}`,
|
|
447
833
|
`Stop when: ${config.stopWhen}`,
|
|
834
|
+
"Use normal read-only tools to inspect authoritative target state, durable outputs, failures, checkpoints, active work, and applicable authority. Do not mutate any file, process, Job, service, remote resource, or configuration during this step.",
|
|
835
|
+
"Preserve the exact goal, allowed files and systems, forbidden changes, cost ceiling, provider/runtime contract, required checks, stop conditions, and allowed recovery actions from the task, conversation, and repository instructions. Do not invent permission.",
|
|
836
|
+
"List the safe actions the user has already authorized, even when no action is needed now. Choose wait only when useful target work is moving or an external event must finish. Choose act only when the goal is incomplete, work is idle, failed, or stopped, and one safe action is fully authorized. Choose stop when the goal is complete or safe continuation is blocked.",
|
|
837
|
+
"For act, describe one exact action. Use advance for normal next work, recover for an operational restart or resume, and repair only for a code or configuration defect. A normal start, resume, or restart must not become repair. If a paid action lacks verified remaining authority or exceeds its limit, choose stop. Before repair of a shared code or data defect, affected paid workers must already be stopped.",
|
|
838
|
+
"You are the regular Pi model and observation adapter. Publish measured facts with workflow update when useful. Do not require the target to implement a Pi-specific API, file, store, schema, command, service, transport, or dependency. Do not invent counts, rates, or ETA values.",
|
|
448
839
|
previous === undefined
|
|
449
|
-
? "There is no previous observation."
|
|
450
|
-
: `Previous accepted observation: ${previous
|
|
840
|
+
? "There is no previous accepted observation."
|
|
841
|
+
: `Previous accepted observation: ${JSON.stringify(previous)}`,
|
|
842
|
+
previousActionPrompt(context),
|
|
451
843
|
priorEstimate?.tracks.length
|
|
452
844
|
? `Previous progress: ${formatProgressReport(priorEstimate.tracks.map((track) => track.estimate))}`
|
|
453
845
|
: "There is no previous measured progress.",
|
|
454
|
-
config.repair === undefined
|
|
455
|
-
? "Observe only. This monitor has no mutation authorization."
|
|
456
|
-
: "Repair is explicitly authorized within the supplied repair policy. Choose repair only for a concrete issue that can be changed within that scope. Include a stable issue fingerprint based on the issue and observed target state. Do not change protected model, benchmark, credential, hardware, spending, or scope decisions.",
|
|
457
|
-
"Use available tools to inspect the current source of truth.",
|
|
458
|
-
"You are the regular Pi model running this check and the observation adapter. When useful measurable facts appear during the check, publish them with workflow action update. Include the latest tracks in the final submission. Do not require the monitored target to implement a Pi-specific progress API, file, store, schema, or command.",
|
|
459
|
-
"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.",
|
|
460
|
-
config.repair === undefined
|
|
461
|
-
? "Choose route continue or stop."
|
|
462
|
-
: "Choose route continue, repair, or stop.",
|
|
463
846
|
].join("\n\n");
|
|
464
847
|
},
|
|
465
848
|
expectedOutput:
|
|
466
|
-
'{ "route": "
|
|
467
|
-
validate:
|
|
468
|
-
validateMonitorCheck(output, configFrom(context.outputs).repair !== undefined),
|
|
849
|
+
'{ "route": "wait" | "act" | "stop", "goalState": "complete" | "incomplete" | "blocked", "workState": "running" | "waiting" | "idle" | "failed" | "stopped" | "unknown", "observation": "factual state", "report": "concise factual summary", "targetStateId": "stable observed target-state ID", "authorizedActions": ["safe action already authorized by the user"], "progress": { "tracks": [{ "key": "stable-key", "data": { "schema": "pi-workflows.progress.v1", "status": "running", "completed": 1, "total": 2, "unit": "items" } }] } (optional), "action": { "kind": "advance" | "recover" | "repair", "incomplete": "what remains", "evidence": {}, "nextAction": "one exact action", "authority": { "status": "authorized", "basis": "existing authority", "allowedMutations": ["allowed file, system, or resource"], "forbiddenMutations": [], "costLimit": "recorded limit or not applicable", "providerRuntime": "recorded contract or not applicable", "requiredChecks": [], "stopConditions": [], "allowedRecoveryActions": [], "repository": "optional absolute path", "baseBranch": "optional branch", "merge": false, "repairApproval": { "mode": "auto" | "required" | "skip" } }, "cost": { "paidAction": false, "status": "not-applicable" | "within-limit", "evidence": "cost evidence" }, "defect": { "sharedCodeOrDataDefect": false, "paidWorkers": "not-applicable" | "stopped" | "running", "evidence": "worker evidence" }, "verification": "how to prove success", "failureId": "stable failure ID", "targetStateId": "stable target-state ID" } (required only for act), "reason": "short reason" }',
|
|
850
|
+
validate: validateMonitorObservation,
|
|
469
851
|
}),
|
|
852
|
+
guard: compute({ run: guardObservation }),
|
|
470
853
|
estimate: compute({ run: ({ outputs }) => estimateTracks(outputs) }),
|
|
471
854
|
publish_progress: action({
|
|
472
855
|
statusDetail: "publishing monitor progress",
|
|
473
856
|
run: async ({ outputs, publishUpdate, signal }) => {
|
|
474
|
-
const
|
|
475
|
-
if (
|
|
476
|
-
for (const track of
|
|
857
|
+
const observation = currentObservation(outputs);
|
|
858
|
+
if (observation.progress === undefined) return { published: 0 };
|
|
859
|
+
for (const track of observation.progress.tracks) {
|
|
477
860
|
await waitForUpdateSlot(signal);
|
|
478
861
|
await publishUpdate({ type: "progress", key: track.key, data: track.data });
|
|
479
862
|
}
|
|
480
|
-
return { published:
|
|
863
|
+
return { published: observation.progress.tracks.length };
|
|
481
864
|
},
|
|
482
865
|
}),
|
|
483
866
|
report: notify({
|
|
484
867
|
statusDetail: "queueing monitor update",
|
|
485
|
-
message:
|
|
868
|
+
message: reportMessage,
|
|
486
869
|
kind: "progress",
|
|
487
870
|
}),
|
|
488
871
|
decide: compute({
|
|
489
872
|
run: (context) => {
|
|
490
|
-
const
|
|
873
|
+
const observation = currentObservation(context.outputs);
|
|
491
874
|
const config = configFrom(context.outputs);
|
|
492
|
-
const checks =
|
|
493
|
-
if (
|
|
875
|
+
const checks = completedObservations(context);
|
|
876
|
+
if (observation.route === "stop")
|
|
877
|
+
return { route: "stop", reason: observation.reason, checks };
|
|
494
878
|
if (checks >= config.maxChecks) {
|
|
495
879
|
return {
|
|
496
880
|
route: "stop",
|
|
497
|
-
reason: `Reached the ${config.maxChecks}-
|
|
881
|
+
reason: `Reached the ${config.maxChecks}-observation safety limit.`,
|
|
498
882
|
checks,
|
|
499
883
|
};
|
|
500
884
|
}
|
|
501
|
-
if (
|
|
502
|
-
|
|
885
|
+
if (observation.route === "wait")
|
|
886
|
+
return { route: "wait", reason: observation.reason, checks };
|
|
887
|
+
const request = actionFrom(context.outputs);
|
|
888
|
+
return { route: request.kind, reason: observation.reason, checks };
|
|
889
|
+
},
|
|
890
|
+
}),
|
|
891
|
+
act: agent({
|
|
892
|
+
statusDetail: "performing authorized monitor action",
|
|
893
|
+
timeoutMs: DEFAULT_CHECK_TIMEOUT_MINUTES * 60_000,
|
|
894
|
+
prompt: ({ outputs }) => {
|
|
895
|
+
const request = actionFrom(outputs);
|
|
896
|
+
if (request.kind === "repair") throw new Error("repair must use the composed repair path");
|
|
897
|
+
return [
|
|
898
|
+
`Perform this one ${request.kind} action with normal tools: ${request.nextAction}`,
|
|
899
|
+
`Incomplete work: ${request.incomplete}`,
|
|
900
|
+
`Evidence: ${JSON.stringify(request.evidence)}`,
|
|
901
|
+
`Authorization: ${JSON.stringify(request.authority)}`,
|
|
902
|
+
`Cost safety: ${JSON.stringify(request.cost)}`,
|
|
903
|
+
`Defect safety: ${JSON.stringify(request.defect)}`,
|
|
904
|
+
`Verification: ${request.verification}`,
|
|
905
|
+
"Perform only the stated action and only on the allowed files, systems, and resources. Do not plan, document, redesign, broaden scope, change a protected contract, or perform another action. Verify the direct result before submitting. Preserve the supplied failure and target-state IDs exactly.",
|
|
906
|
+
].join("\n\n");
|
|
503
907
|
},
|
|
908
|
+
expectedOutput:
|
|
909
|
+
'{ "status": "succeeded" | "failed" | "blocked", "summary": "action performed and real result", "evidence": {}, "verification": "verification performed", "failureId": "unchanged failure ID", "targetStateId": "unchanged target-state ID" }',
|
|
910
|
+
validate: (output, context) =>
|
|
911
|
+
validateMonitorActionResult(output, actionFrom(context.outputs)),
|
|
504
912
|
}),
|
|
505
|
-
|
|
506
|
-
run: (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
:
|
|
913
|
+
repairComplete: compute({
|
|
914
|
+
run: ({ outputs }): RecordedAction => {
|
|
915
|
+
const request = actionFrom(outputs);
|
|
916
|
+
if (request.kind !== "repair") throw new Error("monitor repair action is missing");
|
|
917
|
+
return {
|
|
918
|
+
status: "succeeded",
|
|
919
|
+
summary: request.nextAction,
|
|
920
|
+
evidence: outputs.implementation,
|
|
921
|
+
verification: request.verification,
|
|
922
|
+
failureId: request.failureId,
|
|
923
|
+
targetStateId: request.targetStateId,
|
|
924
|
+
};
|
|
925
|
+
},
|
|
514
926
|
}),
|
|
515
927
|
repairBlocked: compute({
|
|
516
928
|
run: (context) => ({
|
|
517
929
|
reason: repairBlockedReason(context.outputs),
|
|
518
|
-
observation: (context.outputs
|
|
519
|
-
checks:
|
|
930
|
+
observation: currentObservation(context.outputs).observation,
|
|
931
|
+
checks: completedObservations(context),
|
|
520
932
|
reported: true,
|
|
521
933
|
}),
|
|
522
934
|
}),
|
|
@@ -525,11 +937,11 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
525
937
|
kind: "final",
|
|
526
938
|
message: ({ outputs }) => {
|
|
527
939
|
const result = outputs.repairBlocked as { reason: string };
|
|
528
|
-
return `
|
|
940
|
+
return `Monitor repair stopped: ${result.reason}`;
|
|
529
941
|
},
|
|
530
942
|
}),
|
|
531
943
|
schedule: action({
|
|
532
|
-
statusDetail: "scheduling next monitor
|
|
944
|
+
statusDetail: "scheduling next monitor observation",
|
|
533
945
|
run: async ({ outputs, publishUpdate }) => {
|
|
534
946
|
const config = configFrom(outputs);
|
|
535
947
|
const lastCheckAt = new Date().toISOString();
|
|
@@ -550,7 +962,7 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
550
962
|
},
|
|
551
963
|
}),
|
|
552
964
|
sleep: shell({
|
|
553
|
-
statusDetail: "waiting for next monitor
|
|
965
|
+
statusDetail: "waiting for next monitor observation",
|
|
554
966
|
timeoutMs: MAX_INTERVAL_MINUTES * 60_000 + NODE_TIMEOUT_MARGIN_MS,
|
|
555
967
|
exec: ({ outputs }) => {
|
|
556
968
|
const sleepMs = configFrom(outputs).everyMinutes * 60_000;
|
|
@@ -565,26 +977,24 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
565
977
|
}),
|
|
566
978
|
finish: compute({
|
|
567
979
|
run: ({ outputs }) => {
|
|
568
|
-
const
|
|
980
|
+
const observation = currentObservation(outputs);
|
|
569
981
|
const decision = outputs.decide as { reason?: string; checks?: number } | undefined;
|
|
570
982
|
const repair = outputs.repairBlocked as { reason?: string } | undefined;
|
|
571
983
|
return {
|
|
572
|
-
reason: repair?.reason ?? decision?.reason ??
|
|
573
|
-
observation:
|
|
984
|
+
reason: repair?.reason ?? decision?.reason ?? observation.reason,
|
|
985
|
+
observation: observation.observation,
|
|
986
|
+
goalState: repair === undefined ? observation.goalState : "blocked",
|
|
987
|
+
workState: observation.workState,
|
|
574
988
|
checks: decision?.checks ?? 1,
|
|
575
989
|
reported: true,
|
|
576
|
-
...(outputs.implementation !== undefined
|
|
577
|
-
? { repair: outputs.implementation }
|
|
578
|
-
: repair !== undefined
|
|
579
|
-
? { repair }
|
|
580
|
-
: {}),
|
|
581
990
|
};
|
|
582
991
|
},
|
|
583
992
|
}),
|
|
584
993
|
},
|
|
585
994
|
edges: [
|
|
586
|
-
{ from: "prepare", to: "
|
|
587
|
-
{ from: "
|
|
995
|
+
{ from: "prepare", to: "observe" },
|
|
996
|
+
{ from: "observe", to: "guard" },
|
|
997
|
+
{ from: "guard", to: "estimate" },
|
|
588
998
|
{ from: "estimate", to: "publish_progress" },
|
|
589
999
|
{ from: "publish_progress", to: "report" },
|
|
590
1000
|
{ from: "report", to: "decide" },
|
|
@@ -592,21 +1002,25 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
592
1002
|
from: "decide",
|
|
593
1003
|
switch: {
|
|
594
1004
|
on: "$.route",
|
|
595
|
-
cases: {
|
|
1005
|
+
cases: {
|
|
1006
|
+
stop: "finish",
|
|
1007
|
+
wait: "schedule",
|
|
1008
|
+
advance: "act",
|
|
1009
|
+
recover: "act",
|
|
1010
|
+
repair: "planChange",
|
|
1011
|
+
},
|
|
596
1012
|
},
|
|
597
1013
|
},
|
|
598
|
-
{
|
|
599
|
-
from: "repairGuard",
|
|
600
|
-
switch: { on: "$.route", cases: { repair: "planChange", blocked: "repairBlocked" } },
|
|
601
|
-
},
|
|
1014
|
+
{ from: "act", to: "observe" },
|
|
602
1015
|
{ from: "planChange.ready", to: "implementation" },
|
|
603
1016
|
{ from: "planChange.blocked", to: "repairBlocked" },
|
|
604
|
-
{ from: "implementation.completed", to: "
|
|
1017
|
+
{ from: "implementation.completed", to: "repairComplete" },
|
|
605
1018
|
{ from: "implementation.blocked", to: "repairBlocked" },
|
|
1019
|
+
{ from: "repairComplete", to: "observe" },
|
|
606
1020
|
{ from: "repairBlocked", to: "repairReport" },
|
|
607
1021
|
{ from: "repairReport", to: "finish" },
|
|
608
1022
|
{ from: "schedule", to: "sleep" },
|
|
609
|
-
{ from: "sleep", to: "
|
|
1023
|
+
{ from: "sleep", to: "observe" },
|
|
610
1024
|
],
|
|
611
1025
|
});
|
|
612
1026
|
|