@osolmaz/pi-workflows 0.12.0 → 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 +56 -31
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +797 -142
- package/dist/builtins/autoimplement.workflow.js +17 -105
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- 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 +7 -5
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +4 -2
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -23
- package/dist/builtins/monitor.workflow.js +508 -329
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- 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-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +449 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +50 -6
- package/dist/builtins/sanity-check.workflow.js +150 -28
- 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 +4 -2
- package/dist/extension/decision-channels.js +333 -146
- 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 +209 -127
- 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/session-events.d.ts +2 -2
- 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 +29 -3
- 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 +15 -10
- 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 +9 -23
- 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/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.js +20 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.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 +7 -7
- package/dist/workflows/engine.js +196 -115
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +16 -3
- package/dist/workflows/errors.js +18 -3
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +45 -8
- package/dist/workflows/human-decision.js +641 -274
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +5 -6
- package/dist/workflows/index.js +4 -5
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +27 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +907 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +88 -126
- 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 +36 -47
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
- package/docs/MONITOR.md +179 -237
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +27 -26
- 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 +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
- 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 +127 -53
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +7 -7
- package/plugins/herdr/viewer.mjs +1 -11
- package/schemas/human-decision-accepted-v1.schema.json +15 -3
- package/schemas/human-decision-continuation-v1.schema.json +10 -1
- package/schemas/human-decision-delivery-v1.schema.json +8 -0
- package/schemas/human-decision-receipt-v1.schema.json +8 -0
- package/schemas/human-decision-request-v1.schema.json +24 -4
- package/skills/autoimplement/SKILL.md +27 -0
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -135
- package/skills/pi-workflows/SKILL.md +9 -8
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement.workflow.ts +19 -118
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +7 -5
- package/src/builtins/index.ts +18 -1
- package/src/builtins/monitor.workflow.ts +673 -382
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +248 -48
- 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 +456 -203
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +251 -151
- package/src/extension/recorder.ts +13 -22
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +32 -5
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +12 -6
- 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 +19 -30
- 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/catalog.ts +7 -2
- package/src/workflows/composition.ts +21 -3
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +228 -130
- package/src/workflows/errors.ts +27 -3
- package/src/workflows/human-decision.ts +921 -349
- package/src/workflows/index.ts +20 -30
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +36 -1
- package/src/workflows/store.ts +1300 -1163
- package/src/workflows/types.ts +100 -137
- package/dist/builtins/sanity-check-session.d.ts +0 -17
- package/dist/builtins/sanity-check-session.js +0 -168
- package/dist/builtins/sanity-check-session.js.map +0 -1
- 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/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
- package/src/builtins/sanity-check-session.ts +0 -205
- package/src/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
|
@@ -20,46 +20,33 @@ import type {
|
|
|
20
20
|
WorkflowProgressData,
|
|
21
21
|
} from "../workflows/types.js";
|
|
22
22
|
import { validateProgressData } from "../workflows/updates.js";
|
|
23
|
-
import autodocWorkflow, { type AutodocInput } from "./autodoc.workflow.js";
|
|
24
23
|
import autoimplementWorkflow, { type AutoimplementInput } from "./autoimplement.workflow.js";
|
|
25
|
-
import
|
|
26
|
-
|
|
24
|
+
import {
|
|
25
|
+
parsePlanApprovalPolicy,
|
|
26
|
+
type ResolvedPlanApprovalPolicy,
|
|
27
|
+
} from "./plan-approval.workflow.js";
|
|
28
|
+
import planChangeWorkflow, { type NormalizedPlanChangeInput } from "./plan-change.workflow.js";
|
|
27
29
|
|
|
28
30
|
const MIN_INTERVAL_MINUTES = 1;
|
|
29
31
|
const MAX_INTERVAL_MINUTES = 24 * 60;
|
|
30
32
|
const DEFAULT_INTERVAL_MINUTES = 30;
|
|
31
|
-
const
|
|
32
|
-
const MAX_CHECK_TIMEOUT_MINUTES = 24 * 60;
|
|
33
|
-
const DEFAULT_MIN_CHECK_TIMEOUT_MINUTES = 60;
|
|
33
|
+
const DEFAULT_CHECK_TIMEOUT_MINUTES = 60;
|
|
34
34
|
const DEFAULT_MAX_CHECKS = 1_000;
|
|
35
35
|
const MAX_CHECKS = 1_000;
|
|
36
36
|
const MAX_TRACKS = 256;
|
|
37
37
|
const MAX_OBSERVATION_CHARS = 8_000;
|
|
38
38
|
const MAX_REPORT_CHARS = 4_000;
|
|
39
39
|
const MAX_REASON_CHARS = 2_000;
|
|
40
|
+
const MAX_ACTION_TEXT_CHARS = 8_000;
|
|
41
|
+
const MAX_ID_CHARS = 256;
|
|
40
42
|
const SLEEP_TIMEOUT_MARGIN_MS = 60_000;
|
|
41
43
|
const NODE_TIMEOUT_MARGIN_MS = 2 * 60_000;
|
|
42
44
|
|
|
43
|
-
export type MonitorRepairPolicy = {
|
|
44
|
-
authorized: true;
|
|
45
|
-
scope?: string;
|
|
46
|
-
constraints?: string[];
|
|
47
|
-
repository?: string;
|
|
48
|
-
baseBranch?: string;
|
|
49
|
-
merge?: boolean;
|
|
50
|
-
approval?: {
|
|
51
|
-
audience: string;
|
|
52
|
-
maxReplans: number;
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
|
|
56
45
|
export type MonitorInput = {
|
|
57
46
|
task: string;
|
|
58
47
|
everyMinutes?: number;
|
|
59
48
|
stopWhen?: string;
|
|
60
49
|
maxChecks?: number;
|
|
61
|
-
checkTimeoutMinutes?: number;
|
|
62
|
-
repair?: MonitorRepairPolicy;
|
|
63
50
|
};
|
|
64
51
|
|
|
65
52
|
type MonitorConfig = {
|
|
@@ -67,25 +54,78 @@ type MonitorConfig = {
|
|
|
67
54
|
everyMinutes: number;
|
|
68
55
|
stopWhen: string;
|
|
69
56
|
maxChecks: number;
|
|
70
|
-
checkTimeoutMinutes: number;
|
|
71
|
-
repair?: MonitorRepairPolicy;
|
|
72
57
|
};
|
|
73
|
-
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";
|
|
74
62
|
type MonitorTrack = { key: string; data: WorkflowProgressData };
|
|
75
|
-
type
|
|
76
|
-
|
|
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;
|
|
77
91
|
evidence: unknown;
|
|
78
|
-
|
|
92
|
+
nextAction: string;
|
|
93
|
+
authority: MonitorAuthority;
|
|
94
|
+
cost: MonitorCostSafety;
|
|
95
|
+
defect: MonitorDefectSafety;
|
|
96
|
+
verification: string;
|
|
97
|
+
failureId: string;
|
|
98
|
+
targetStateId: string;
|
|
79
99
|
};
|
|
80
|
-
type
|
|
100
|
+
export type MonitorObservation = {
|
|
81
101
|
route: MonitorRoute;
|
|
102
|
+
goalState: MonitorGoalState;
|
|
103
|
+
workState: MonitorWorkState;
|
|
82
104
|
observation: string;
|
|
83
105
|
report: string;
|
|
106
|
+
targetStateId: string;
|
|
107
|
+
authorizedActions: string[];
|
|
84
108
|
progress?: { tracks: MonitorTrack[] };
|
|
85
|
-
|
|
109
|
+
action?: MonitorActionRequest;
|
|
86
110
|
reason: string;
|
|
87
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
|
+
};
|
|
88
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
|
+
};
|
|
89
129
|
|
|
90
130
|
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
91
131
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -94,6 +134,15 @@ function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
|
94
134
|
return value as Record<string, unknown>;
|
|
95
135
|
}
|
|
96
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
|
+
|
|
97
146
|
function requireBoundedString(value: unknown, label: string, maxChars: number): string {
|
|
98
147
|
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
99
148
|
const trimmed = value.trim();
|
|
@@ -102,6 +151,15 @@ function requireBoundedString(value: unknown, label: string, maxChars: number):
|
|
|
102
151
|
return trimmed;
|
|
103
152
|
}
|
|
104
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
|
+
|
|
105
163
|
async function waitForUpdateSlot(signal: AbortSignal): Promise<void> {
|
|
106
164
|
await new Promise<void>((resolve, reject) => {
|
|
107
165
|
const onAbort = () => {
|
|
@@ -119,17 +177,8 @@ async function waitForUpdateSlot(signal: AbortSignal): Promise<void> {
|
|
|
119
177
|
|
|
120
178
|
export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
121
179
|
const value = requireRecord(input, "monitor input") as Partial<MonitorInput>;
|
|
122
|
-
const allowed =
|
|
123
|
-
|
|
124
|
-
"everyMinutes",
|
|
125
|
-
"stopWhen",
|
|
126
|
-
"maxChecks",
|
|
127
|
-
"checkTimeoutMinutes",
|
|
128
|
-
"repair",
|
|
129
|
-
]);
|
|
130
|
-
for (const field of Object.keys(value)) {
|
|
131
|
-
if (!allowed.has(field)) throw new Error(`monitor input field ${field} is not supported`);
|
|
132
|
-
}
|
|
180
|
+
const allowed = ["task", "everyMinutes", "stopWhen", "maxChecks"] as const;
|
|
181
|
+
requireExactKeys(value, allowed, "monitor input");
|
|
133
182
|
const task = requireBoundedString(value.task, "task", 8_000);
|
|
134
183
|
const everyMinutes = value.everyMinutes ?? DEFAULT_INTERVAL_MINUTES;
|
|
135
184
|
if (
|
|
@@ -145,62 +194,6 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
|
145
194
|
if (!Number.isInteger(maxChecks) || maxChecks <= 0 || maxChecks > MAX_CHECKS) {
|
|
146
195
|
throw new Error(`maxChecks must be an integer from 1 through ${MAX_CHECKS}`);
|
|
147
196
|
}
|
|
148
|
-
const checkTimeoutMinutes =
|
|
149
|
-
value.checkTimeoutMinutes ?? Math.max(DEFAULT_MIN_CHECK_TIMEOUT_MINUTES, everyMinutes);
|
|
150
|
-
if (
|
|
151
|
-
!Number.isInteger(checkTimeoutMinutes) ||
|
|
152
|
-
checkTimeoutMinutes < MIN_CHECK_TIMEOUT_MINUTES ||
|
|
153
|
-
checkTimeoutMinutes > MAX_CHECK_TIMEOUT_MINUTES
|
|
154
|
-
) {
|
|
155
|
-
throw new Error(
|
|
156
|
-
`checkTimeoutMinutes must be an integer from ${MIN_CHECK_TIMEOUT_MINUTES} through ${MAX_CHECK_TIMEOUT_MINUTES}`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
let repair: MonitorRepairPolicy | undefined;
|
|
160
|
-
if (value.repair !== undefined) {
|
|
161
|
-
const raw = requireRecord(value.repair, "repair policy");
|
|
162
|
-
if (raw.authorized !== true) throw new Error("repair policy must set authorized to true");
|
|
163
|
-
if (
|
|
164
|
-
raw.constraints !== undefined &&
|
|
165
|
-
(!Array.isArray(raw.constraints) || raw.constraints.some((item) => typeof item !== "string"))
|
|
166
|
-
) {
|
|
167
|
-
throw new Error("repair constraints must be an array of strings");
|
|
168
|
-
}
|
|
169
|
-
if (raw.merge !== undefined && typeof raw.merge !== "boolean") {
|
|
170
|
-
throw new Error("repair merge must be a boolean");
|
|
171
|
-
}
|
|
172
|
-
let approval: MonitorRepairPolicy["approval"];
|
|
173
|
-
if (raw.approval !== undefined) {
|
|
174
|
-
const value = requireRecord(raw.approval, "repair approval");
|
|
175
|
-
const maxReplans = value.maxReplans ?? 3;
|
|
176
|
-
if (
|
|
177
|
-
!Number.isInteger(maxReplans) ||
|
|
178
|
-
(maxReplans as number) < 1 ||
|
|
179
|
-
(maxReplans as number) > 20
|
|
180
|
-
) {
|
|
181
|
-
throw new Error("repair approval maxReplans must be from 1 through 20");
|
|
182
|
-
}
|
|
183
|
-
approval = {
|
|
184
|
-
audience: requireBoundedString(value.audience, "repair approval audience", 200),
|
|
185
|
-
maxReplans: maxReplans as number,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
repair = {
|
|
189
|
-
authorized: true,
|
|
190
|
-
...(raw.scope !== undefined
|
|
191
|
-
? { scope: requireBoundedString(raw.scope, "repair scope", 4_000) }
|
|
192
|
-
: {}),
|
|
193
|
-
...(raw.constraints !== undefined ? { constraints: [...raw.constraints] as string[] } : {}),
|
|
194
|
-
...(raw.repository !== undefined
|
|
195
|
-
? { repository: requireBoundedString(raw.repository, "repair repository", 4_000) }
|
|
196
|
-
: {}),
|
|
197
|
-
...(raw.baseBranch !== undefined
|
|
198
|
-
? { baseBranch: requireBoundedString(raw.baseBranch, "repair base branch", 256) }
|
|
199
|
-
: {}),
|
|
200
|
-
...(raw.merge !== undefined ? { merge: raw.merge !== false } : {}),
|
|
201
|
-
...(approval !== undefined ? { approval } : {}),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
197
|
return {
|
|
205
198
|
task,
|
|
206
199
|
everyMinutes,
|
|
@@ -209,8 +202,6 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
|
209
202
|
? "Stop only when the user explicitly asks to stop."
|
|
210
203
|
: requireBoundedString(value.stopWhen, "stopWhen", 4_000),
|
|
211
204
|
maxChecks,
|
|
212
|
-
checkTimeoutMinutes,
|
|
213
|
-
...(repair !== undefined ? { repair } : {}),
|
|
214
205
|
};
|
|
215
206
|
}
|
|
216
207
|
|
|
@@ -218,63 +209,305 @@ function configFrom(outputs: Record<string, unknown>): MonitorConfig {
|
|
|
218
209
|
return outputs.prepare as MonitorConfig;
|
|
219
210
|
}
|
|
220
211
|
|
|
221
|
-
function
|
|
222
|
-
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")
|
|
223
214
|
.length;
|
|
224
215
|
}
|
|
225
216
|
|
|
226
|
-
|
|
227
|
-
const value = requireRecord(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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");
|
|
233
425
|
}
|
|
234
|
-
if (
|
|
235
|
-
|
|
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");
|
|
432
|
+
}
|
|
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");
|
|
236
442
|
}
|
|
237
|
-
const
|
|
443
|
+
const observation: MonitorObservation = {
|
|
238
444
|
route: value.route,
|
|
445
|
+
goalState: value.goalState,
|
|
446
|
+
workState: value.workState,
|
|
239
447
|
observation: requireBoundedString(value.observation, "observation", MAX_OBSERVATION_CHARS),
|
|
240
448
|
report: requireBoundedString(value.report, "report", MAX_REPORT_CHARS),
|
|
449
|
+
targetStateId: requireBoundedString(value.targetStateId, "targetStateId", MAX_ID_CHARS),
|
|
450
|
+
authorizedActions: requireStringArray(value.authorizedActions, "authorizedActions"),
|
|
241
451
|
reason: requireBoundedString(value.reason, "reason", MAX_REASON_CHARS),
|
|
242
452
|
};
|
|
243
|
-
if (value.progress !== undefined)
|
|
244
|
-
if (value.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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");
|
|
255
466
|
}
|
|
256
|
-
if (
|
|
257
|
-
throw new Error("route
|
|
467
|
+
if (observation.route === "act" && observation.goalState !== "incomplete") {
|
|
468
|
+
throw new Error("route act requires goalState incomplete");
|
|
258
469
|
}
|
|
259
|
-
|
|
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;
|
|
260
495
|
}
|
|
261
496
|
|
|
262
497
|
function validateMonitorProgress(input: unknown): { tracks: MonitorTrack[] } {
|
|
263
498
|
const value = requireRecord(input, "progress");
|
|
264
|
-
|
|
265
|
-
throw new Error("progress only supports tracks");
|
|
499
|
+
requireExactKeys(value, ["tracks"], "progress");
|
|
266
500
|
if (!Array.isArray(value.tracks) || value.tracks.length < 1 || value.tracks.length > MAX_TRACKS) {
|
|
267
501
|
throw new Error(`progress.tracks must contain 1 through ${MAX_TRACKS} entries`);
|
|
268
502
|
}
|
|
269
503
|
const keys = new Set<string>();
|
|
270
504
|
const tracks = value.tracks.map((raw, index) => {
|
|
271
505
|
const track = requireRecord(raw, `progress.tracks[${index}]`);
|
|
272
|
-
|
|
273
|
-
throw new Error(`progress.tracks[${index}] has an unsupported field`);
|
|
274
|
-
}
|
|
506
|
+
requireExactKeys(track, ["key", "data"], `progress.tracks[${index}]`);
|
|
275
507
|
const key = requireBoundedString(track.key, `progress.tracks[${index}].key`, 128);
|
|
276
|
-
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)) {
|
|
277
509
|
throw new Error(`progress.tracks[${index}].key is invalid`);
|
|
510
|
+
}
|
|
278
511
|
if (keys.has(key)) throw new Error(`progress track key ${key} is duplicated`);
|
|
279
512
|
keys.add(key);
|
|
280
513
|
return {
|
|
@@ -285,14 +518,18 @@ function validateMonitorProgress(input: unknown): { tracks: MonitorTrack[] } {
|
|
|
285
518
|
return { tracks };
|
|
286
519
|
}
|
|
287
520
|
|
|
521
|
+
function currentObservation(outputs: Record<string, unknown>): MonitorObservation {
|
|
522
|
+
return (outputs.guard ?? outputs.observe) as MonitorObservation;
|
|
523
|
+
}
|
|
524
|
+
|
|
288
525
|
function estimateTracks(outputs: Record<string, unknown>): MonitorEstimate {
|
|
289
|
-
const
|
|
290
|
-
if (
|
|
526
|
+
const observation = currentObservation(outputs);
|
|
527
|
+
if (observation.progress === undefined) return { tracks: [] };
|
|
291
528
|
const previous = outputs.estimate as MonitorEstimate | undefined;
|
|
292
529
|
const previousByKey = new Map((previous?.tracks ?? []).map((track) => [track.key, track]));
|
|
293
530
|
const at = new Date().toISOString();
|
|
294
531
|
return {
|
|
295
|
-
tracks:
|
|
532
|
+
tracks: observation.progress.tracks.map((track) => {
|
|
296
533
|
const samples: ProgressSample[] = [
|
|
297
534
|
...(previousByKey.get(track.key)?.samples ?? []),
|
|
298
535
|
{ at, data: track.data },
|
|
@@ -302,114 +539,216 @@ function estimateTracks(outputs: Record<string, unknown>): MonitorEstimate {
|
|
|
302
539
|
};
|
|
303
540
|
}
|
|
304
541
|
|
|
305
|
-
function
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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");
|
|
319
566
|
}
|
|
320
567
|
return false;
|
|
321
568
|
}
|
|
322
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
|
+
|
|
323
629
|
function currentRepairPlan(outputs: Record<string, unknown>): {
|
|
324
630
|
plan: unknown;
|
|
325
631
|
planDigest: string;
|
|
326
632
|
documents: string[];
|
|
327
633
|
} {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
| undefined;
|
|
338
|
-
if (
|
|
339
|
-
documentation?.exit === "ready" &&
|
|
340
|
-
documentation.output?.plan !== undefined &&
|
|
341
|
-
typeof documentation.output.planDigest === "string"
|
|
342
|
-
) {
|
|
343
|
-
return {
|
|
344
|
-
plan: documentation.output.plan,
|
|
345
|
-
planDigest: documentation.output.planDigest,
|
|
346
|
-
documents: documentation.output.documentation?.files ?? [],
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
const design = includedResult(autoplanWorkflow, outputs.initialDesign);
|
|
350
|
-
if (design.exit !== "ready") throw new Error("monitor design did not return a ready plan");
|
|
351
|
-
return { plan: design.output.plan, planDigest: design.output.planDigest, documents: [] };
|
|
634
|
+
const result = includedResult(planChangeWorkflow, outputs.planChange);
|
|
635
|
+
if (result.exit !== "ready") throw new Error("monitor plan change did not return a ready plan");
|
|
636
|
+
return {
|
|
637
|
+
plan: result.output.plan,
|
|
638
|
+
planDigest: result.output.planDigest,
|
|
639
|
+
documents: result.output.documents,
|
|
640
|
+
};
|
|
352
641
|
}
|
|
353
642
|
|
|
354
|
-
function
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
:
|
|
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
|
+
];
|
|
361
665
|
}
|
|
362
666
|
|
|
363
667
|
function repairBlockedReason(outputs: Record<string, unknown>): string {
|
|
364
|
-
const
|
|
365
|
-
if (guard?.route === "blocked" && guard.reason !== undefined) return guard.reason;
|
|
366
|
-
const replan = outputs.replanGuard as { reason?: string; route?: string } | undefined;
|
|
367
|
-
if (replan?.route === "blocked" && replan.reason !== undefined) return replan.reason;
|
|
368
|
-
const approval = outputs.approval as { exit?: string; output?: { status?: string } } | undefined;
|
|
369
|
-
if (approval?.exit === "stop") return "The operator stopped the proposed repair.";
|
|
370
|
-
const documentation = outputs.documentation as
|
|
668
|
+
const planChange = outputs.planChange as
|
|
371
669
|
| { exit?: string; output?: { reason?: string } }
|
|
372
670
|
| undefined;
|
|
373
671
|
const implementation = outputs.implementation as
|
|
374
672
|
| { exit?: string; output?: { reason?: string } }
|
|
375
673
|
| undefined;
|
|
376
|
-
const design = outputs.initialDesign as
|
|
377
|
-
| { exit?: string; output?: { reason?: string } }
|
|
378
|
-
| undefined;
|
|
379
674
|
return (
|
|
380
675
|
implementation?.output?.reason ??
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
"The repair did not produce new verified progress."
|
|
676
|
+
planChange?.output?.reason ??
|
|
677
|
+
"The repair did not produce a verified result."
|
|
384
678
|
);
|
|
385
679
|
}
|
|
386
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
|
+
|
|
387
710
|
function reportMessage(context: WorkflowNodeContext): string {
|
|
388
|
-
const
|
|
711
|
+
const observation = currentObservation(context.outputs);
|
|
389
712
|
const estimate = context.outputs.estimate as MonitorEstimate;
|
|
390
713
|
const config = configFrom(context.outputs);
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
formatProgressReport(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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}`);
|
|
733
|
+
}
|
|
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`);
|
|
401
739
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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)}`;
|
|
413
752
|
}
|
|
414
753
|
|
|
415
754
|
const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
@@ -427,67 +766,25 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
427
766
|
startAt: "prepare",
|
|
428
767
|
maxSteps: 200_000,
|
|
429
768
|
includes: {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const config = configFrom(outputs);
|
|
435
|
-
const repair = (outputs.check as MonitorCheck).repair;
|
|
436
|
-
if (repair === undefined) throw new Error("monitor repair details are missing");
|
|
437
|
-
const prior = outputs.initialDesign as
|
|
438
|
-
| { exit?: string; output?: { plan?: unknown } }
|
|
439
|
-
| undefined;
|
|
440
|
-
const instructions = latestReplanInstructions(outputs);
|
|
441
|
-
return {
|
|
442
|
-
problem: repair.problem,
|
|
443
|
-
...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
|
|
444
|
-
...(config.repair?.constraints !== undefined
|
|
445
|
-
? { constraints: config.repair.constraints }
|
|
446
|
-
: {}),
|
|
447
|
-
...(prior?.exit === "ready" && prior.output?.plan !== undefined
|
|
448
|
-
? { previousPlan: prior.output.plan }
|
|
449
|
-
: {}),
|
|
450
|
-
newEvidence:
|
|
451
|
-
instructions === undefined
|
|
452
|
-
? repair.evidence
|
|
453
|
-
: { observedEvidence: repair.evidence, operatorInstructions: instructions },
|
|
454
|
-
};
|
|
455
|
-
},
|
|
456
|
-
}),
|
|
457
|
-
documentation: includeWorkflow(autodocWorkflow, {
|
|
458
|
-
input: ({ outputs }): AutodocInput => {
|
|
459
|
-
const config = configFrom(outputs);
|
|
460
|
-
const repair = (outputs.check as MonitorCheck).repair;
|
|
461
|
-
const design = includedResult(autoplanWorkflow, outputs.initialDesign);
|
|
462
|
-
if (design.exit !== "ready") throw new Error("monitor design did not return a ready plan");
|
|
463
|
-
if (repair === undefined) throw new Error("monitor repair details are missing");
|
|
769
|
+
planChange: includeWorkflow(planChangeWorkflow, {
|
|
770
|
+
input: ({ outputs }): NormalizedPlanChangeInput => {
|
|
771
|
+
const request = actionFrom(outputs);
|
|
772
|
+
if (request.kind !== "repair") throw new Error("monitor repair action is missing");
|
|
464
773
|
return {
|
|
465
|
-
task:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
774
|
+
task: request.nextAction,
|
|
775
|
+
scope: authorityScope(request),
|
|
776
|
+
constraints: authorityConstraints(request),
|
|
777
|
+
...(request.authority.repository !== undefined
|
|
778
|
+
? { repository: request.authority.repository }
|
|
469
779
|
: {}),
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const approval = config.repair?.approval;
|
|
479
|
-
if (repair === undefined || approval === undefined) {
|
|
480
|
-
throw new Error("monitor approval was entered without an approval policy");
|
|
481
|
-
}
|
|
482
|
-
const current = currentRepairPlan(outputs);
|
|
483
|
-
const revision =
|
|
484
|
-
state.steps.filter((step) => step.nodeId === "approval/approve").length + 1;
|
|
485
|
-
return {
|
|
486
|
-
task: repair.problem,
|
|
487
|
-
plan: current.plan,
|
|
488
|
-
planDigest: current.planDigest,
|
|
489
|
-
audience: approval.audience,
|
|
490
|
-
revision,
|
|
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,
|
|
491
788
|
};
|
|
492
789
|
},
|
|
493
790
|
}),
|
|
@@ -495,138 +792,143 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
495
792
|
workflow: "autoimplement",
|
|
496
793
|
contract: autoimplementWorkflow,
|
|
497
794
|
input: ({ outputs }) => {
|
|
498
|
-
const
|
|
499
|
-
const repair = (outputs.check as MonitorCheck).repair;
|
|
795
|
+
const request = actionFrom(outputs);
|
|
500
796
|
const documented = currentRepairPlan(outputs);
|
|
501
|
-
if (
|
|
502
|
-
const
|
|
503
|
-
task:
|
|
797
|
+
if (request.kind !== "repair") throw new Error("monitor repair action is missing");
|
|
798
|
+
const implementationInput: AutoimplementInput = {
|
|
799
|
+
task: request.nextAction,
|
|
504
800
|
plan: documented.plan,
|
|
505
801
|
documentation: {
|
|
506
802
|
status: "current",
|
|
507
803
|
planDigest: documented.planDigest,
|
|
508
804
|
documents: documented.documents,
|
|
509
805
|
},
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
806
|
+
scope: authorityScope(request),
|
|
807
|
+
constraints: authorityConstraints(request),
|
|
808
|
+
...(request.authority.repository !== undefined
|
|
809
|
+
? { repository: request.authority.repository }
|
|
513
810
|
: {}),
|
|
514
|
-
...(
|
|
515
|
-
? {
|
|
811
|
+
...(request.authority.baseBranch !== undefined
|
|
812
|
+
? { baseBranch: request.authority.baseBranch }
|
|
516
813
|
: {}),
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
: {}),
|
|
520
|
-
merge: config.repair?.merge === true,
|
|
814
|
+
approval: request.authority.repairApproval,
|
|
815
|
+
merge: request.authority.merge,
|
|
521
816
|
};
|
|
522
|
-
return
|
|
817
|
+
return implementationInput;
|
|
523
818
|
},
|
|
524
819
|
}),
|
|
525
820
|
},
|
|
526
821
|
nodes: {
|
|
527
822
|
prepare: compute({ run: ({ input }) => prepareMonitorInput(input) }),
|
|
528
|
-
|
|
529
|
-
statusDetail: "
|
|
530
|
-
timeoutMs:
|
|
823
|
+
observe: agent({
|
|
824
|
+
statusDetail: "observing monitored goal",
|
|
825
|
+
timeoutMs: DEFAULT_CHECK_TIMEOUT_MINUTES * 60_000,
|
|
531
826
|
prompt: (context) => {
|
|
532
827
|
const config = configFrom(context.outputs);
|
|
533
|
-
const previous = context.outputs.
|
|
828
|
+
const previous = context.outputs.observe as MonitorObservation | undefined;
|
|
534
829
|
const priorEstimate = context.outputs.estimate as MonitorEstimate | undefined;
|
|
535
830
|
return [
|
|
536
|
-
`Perform
|
|
537
|
-
`
|
|
831
|
+
`Perform read-only observation ${completedObservations(context) + 1} of at most ${config.maxChecks}.`,
|
|
832
|
+
`Goal and authority: ${config.task}`,
|
|
538
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.",
|
|
539
839
|
previous === undefined
|
|
540
|
-
? "There is no previous observation."
|
|
541
|
-
: `Previous accepted observation: ${previous
|
|
840
|
+
? "There is no previous accepted observation."
|
|
841
|
+
: `Previous accepted observation: ${JSON.stringify(previous)}`,
|
|
842
|
+
previousActionPrompt(context),
|
|
542
843
|
priorEstimate?.tracks.length
|
|
543
844
|
? `Previous progress: ${formatProgressReport(priorEstimate.tracks.map((track) => track.estimate))}`
|
|
544
845
|
: "There is no previous measured progress.",
|
|
545
|
-
config.repair === undefined
|
|
546
|
-
? "Observe only. This monitor has no mutation authorization."
|
|
547
|
-
: "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.",
|
|
548
|
-
"Use available tools to inspect the current source of truth.",
|
|
549
|
-
"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.",
|
|
550
|
-
"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.",
|
|
551
|
-
config.repair === undefined
|
|
552
|
-
? "Choose route continue or stop."
|
|
553
|
-
: "Choose route continue, repair, or stop.",
|
|
554
846
|
].join("\n\n");
|
|
555
847
|
},
|
|
556
848
|
expectedOutput:
|
|
557
|
-
'{ "route": "
|
|
558
|
-
validate:
|
|
559
|
-
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,
|
|
560
851
|
}),
|
|
852
|
+
guard: compute({ run: guardObservation }),
|
|
561
853
|
estimate: compute({ run: ({ outputs }) => estimateTracks(outputs) }),
|
|
562
854
|
publish_progress: action({
|
|
563
855
|
statusDetail: "publishing monitor progress",
|
|
564
856
|
run: async ({ outputs, publishUpdate, signal }) => {
|
|
565
|
-
const
|
|
566
|
-
if (
|
|
567
|
-
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) {
|
|
568
860
|
await waitForUpdateSlot(signal);
|
|
569
861
|
await publishUpdate({ type: "progress", key: track.key, data: track.data });
|
|
570
862
|
}
|
|
571
|
-
return { published:
|
|
863
|
+
return { published: observation.progress.tracks.length };
|
|
572
864
|
},
|
|
573
865
|
}),
|
|
574
866
|
report: notify({
|
|
575
867
|
statusDetail: "queueing monitor update",
|
|
576
|
-
message:
|
|
868
|
+
message: reportMessage,
|
|
577
869
|
kind: "progress",
|
|
578
870
|
}),
|
|
579
871
|
decide: compute({
|
|
580
872
|
run: (context) => {
|
|
581
|
-
const
|
|
873
|
+
const observation = currentObservation(context.outputs);
|
|
582
874
|
const config = configFrom(context.outputs);
|
|
583
|
-
const checks =
|
|
584
|
-
if (
|
|
875
|
+
const checks = completedObservations(context);
|
|
876
|
+
if (observation.route === "stop")
|
|
877
|
+
return { route: "stop", reason: observation.reason, checks };
|
|
585
878
|
if (checks >= config.maxChecks) {
|
|
586
879
|
return {
|
|
587
880
|
route: "stop",
|
|
588
|
-
reason: `Reached the ${config.maxChecks}-
|
|
881
|
+
reason: `Reached the ${config.maxChecks}-observation safety limit.`,
|
|
589
882
|
checks,
|
|
590
883
|
};
|
|
591
884
|
}
|
|
592
|
-
if (
|
|
593
|
-
|
|
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 };
|
|
594
889
|
},
|
|
595
890
|
}),
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
: {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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");
|
|
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)),
|
|
610
912
|
}),
|
|
611
|
-
|
|
612
|
-
run: (
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
};
|
|
623
925
|
},
|
|
624
926
|
}),
|
|
625
927
|
repairBlocked: compute({
|
|
626
928
|
run: (context) => ({
|
|
627
929
|
reason: repairBlockedReason(context.outputs),
|
|
628
|
-
observation: (context.outputs
|
|
629
|
-
checks:
|
|
930
|
+
observation: currentObservation(context.outputs).observation,
|
|
931
|
+
checks: completedObservations(context),
|
|
630
932
|
reported: true,
|
|
631
933
|
}),
|
|
632
934
|
}),
|
|
@@ -635,11 +937,11 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
635
937
|
kind: "final",
|
|
636
938
|
message: ({ outputs }) => {
|
|
637
939
|
const result = outputs.repairBlocked as { reason: string };
|
|
638
|
-
return `
|
|
940
|
+
return `Monitor repair stopped: ${result.reason}`;
|
|
639
941
|
},
|
|
640
942
|
}),
|
|
641
943
|
schedule: action({
|
|
642
|
-
statusDetail: "scheduling next monitor
|
|
944
|
+
statusDetail: "scheduling next monitor observation",
|
|
643
945
|
run: async ({ outputs, publishUpdate }) => {
|
|
644
946
|
const config = configFrom(outputs);
|
|
645
947
|
const lastCheckAt = new Date().toISOString();
|
|
@@ -660,7 +962,7 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
660
962
|
},
|
|
661
963
|
}),
|
|
662
964
|
sleep: shell({
|
|
663
|
-
statusDetail: "waiting for next monitor
|
|
965
|
+
statusDetail: "waiting for next monitor observation",
|
|
664
966
|
timeoutMs: MAX_INTERVAL_MINUTES * 60_000 + NODE_TIMEOUT_MARGIN_MS,
|
|
665
967
|
exec: ({ outputs }) => {
|
|
666
968
|
const sleepMs = configFrom(outputs).everyMinutes * 60_000;
|
|
@@ -675,26 +977,24 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
675
977
|
}),
|
|
676
978
|
finish: compute({
|
|
677
979
|
run: ({ outputs }) => {
|
|
678
|
-
const
|
|
980
|
+
const observation = currentObservation(outputs);
|
|
679
981
|
const decision = outputs.decide as { reason?: string; checks?: number } | undefined;
|
|
680
982
|
const repair = outputs.repairBlocked as { reason?: string } | undefined;
|
|
681
983
|
return {
|
|
682
|
-
reason: repair?.reason ?? decision?.reason ??
|
|
683
|
-
observation:
|
|
984
|
+
reason: repair?.reason ?? decision?.reason ?? observation.reason,
|
|
985
|
+
observation: observation.observation,
|
|
986
|
+
goalState: repair === undefined ? observation.goalState : "blocked",
|
|
987
|
+
workState: observation.workState,
|
|
684
988
|
checks: decision?.checks ?? 1,
|
|
685
989
|
reported: true,
|
|
686
|
-
...(outputs.implementation !== undefined
|
|
687
|
-
? { repair: outputs.implementation }
|
|
688
|
-
: repair !== undefined
|
|
689
|
-
? { repair }
|
|
690
|
-
: {}),
|
|
691
990
|
};
|
|
692
991
|
},
|
|
693
992
|
}),
|
|
694
993
|
},
|
|
695
994
|
edges: [
|
|
696
|
-
{ from: "prepare", to: "
|
|
697
|
-
{ from: "
|
|
995
|
+
{ from: "prepare", to: "observe" },
|
|
996
|
+
{ from: "observe", to: "guard" },
|
|
997
|
+
{ from: "guard", to: "estimate" },
|
|
698
998
|
{ from: "estimate", to: "publish_progress" },
|
|
699
999
|
{ from: "publish_progress", to: "report" },
|
|
700
1000
|
{ from: "report", to: "decide" },
|
|
@@ -702,34 +1002,25 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
702
1002
|
from: "decide",
|
|
703
1003
|
switch: {
|
|
704
1004
|
on: "$.route",
|
|
705
|
-
cases: {
|
|
1005
|
+
cases: {
|
|
1006
|
+
stop: "finish",
|
|
1007
|
+
wait: "schedule",
|
|
1008
|
+
advance: "act",
|
|
1009
|
+
recover: "act",
|
|
1010
|
+
repair: "planChange",
|
|
1011
|
+
},
|
|
706
1012
|
},
|
|
707
1013
|
},
|
|
708
|
-
{
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
},
|
|
712
|
-
{ from: "initialDesign.ready", to: "documentation" },
|
|
713
|
-
{ from: "initialDesign.blocked", to: "repairBlocked" },
|
|
714
|
-
{ from: "documentation.ready", to: "approvalRoute" },
|
|
715
|
-
{ from: "documentation.blocked", to: "repairBlocked" },
|
|
716
|
-
{
|
|
717
|
-
from: "approvalRoute",
|
|
718
|
-
switch: { on: "$.route", cases: { approve: "approval", implement: "implementation" } },
|
|
719
|
-
},
|
|
720
|
-
{ from: "approval.continue", to: "implementation" },
|
|
721
|
-
{ from: "approval.stop", to: "repairBlocked" },
|
|
722
|
-
{ from: "approval.replan", to: "replanGuard" },
|
|
723
|
-
{
|
|
724
|
-
from: "replanGuard",
|
|
725
|
-
switch: { on: "$.route", cases: { design: "initialDesign", blocked: "repairBlocked" } },
|
|
726
|
-
},
|
|
727
|
-
{ from: "implementation.completed", to: "check" },
|
|
1014
|
+
{ from: "act", to: "observe" },
|
|
1015
|
+
{ from: "planChange.ready", to: "implementation" },
|
|
1016
|
+
{ from: "planChange.blocked", to: "repairBlocked" },
|
|
1017
|
+
{ from: "implementation.completed", to: "repairComplete" },
|
|
728
1018
|
{ from: "implementation.blocked", to: "repairBlocked" },
|
|
1019
|
+
{ from: "repairComplete", to: "observe" },
|
|
729
1020
|
{ from: "repairBlocked", to: "repairReport" },
|
|
730
1021
|
{ from: "repairReport", to: "finish" },
|
|
731
1022
|
{ from: "schedule", to: "sleep" },
|
|
732
|
-
{ from: "sleep", to: "
|
|
1023
|
+
{ from: "sleep", to: "observe" },
|
|
733
1024
|
],
|
|
734
1025
|
});
|
|
735
1026
|
|