@osolmaz/pi-workflows 0.10.0 → 0.11.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 +23 -12
- package/dist/builtins/autodoc.workflow.d.ts +58 -0
- package/dist/builtins/autodoc.workflow.js +266 -0
- package/dist/builtins/autodoc.workflow.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +210 -11
- package/dist/builtins/autoimplement.workflow.js +245 -13
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/{autodevise.workflow.d.ts → autoplan.workflow.d.ts} +9 -9
- package/dist/builtins/{autodevise.workflow.js → autoplan.workflow.js} +20 -20
- package/dist/builtins/autoplan.workflow.js.map +1 -0
- package/dist/builtins/catalog.js +8 -4
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +6 -3
- package/dist/builtins/index.js +5 -2
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +4 -0
- package/dist/builtins/monitor.workflow.js +134 -9
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +83 -0
- package/dist/builtins/plan-approval.workflow.js +148 -0
- package/dist/builtins/plan-approval.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.d.ts +7 -0
- package/dist/builtins/plan-presentation.js +44 -0
- package/dist/builtins/plan-presentation.js.map +1 -0
- package/dist/extension/decision-channels.d.ts +132 -0
- package/dist/extension/decision-channels.js +1082 -0
- package/dist/extension/decision-channels.js.map +1 -0
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +426 -11
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/widget.js +26 -2
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +1 -37
- package/dist/extension/workflow-tool.js +1 -42
- package/dist/extension/workflow-tool.js.map +1 -1
- package/dist/host/rpc-bridge.js +4 -21
- package/dist/host/rpc-bridge.js.map +1 -1
- package/dist/render/graph-render.js +33 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +36 -2
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/composition.js +15 -1
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +18 -0
- package/dist/workflows/decision-presentation.js +417 -0
- package/dist/workflows/decision-presentation.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -1
- package/dist/workflows/engine.js +86 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +87 -0
- package/dist/workflows/human-decision.js +583 -0
- package/dist/workflows/human-decision.js.map +1 -0
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +15 -0
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +7 -0
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +72 -0
- package/dist/workflows/tool-input.js +141 -0
- package/dist/workflows/tool-input.js.map +1 -0
- package/dist/workflows/types.d.ts +223 -0
- package/docs/HUMAN_DECISIONS.md +371 -0
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
- package/docs/MONITOR.md +17 -6
- package/docs/WORKFLOW_COMPOSITION.md +13 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +296 -0
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +172 -0
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +45 -0
- package/docs/plans/2026-08-19-workflow-composition-plan.md +25 -17
- package/docs/run-bundles.md +23 -2
- package/docs/workflows.md +43 -5
- package/examples/workflows/approved-plan.workflow.ts +58 -0
- package/examples/workflows/autoplan.workflow.ts +1 -0
- package/examples/workflows/human-decision.workflow.ts +62 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +2 -1
- package/schemas/decision-presentation-v1.schema.json +83 -0
- package/schemas/human-decision-accepted-v1.schema.json +44 -0
- package/schemas/human-decision-accepted-v2.schema.json +50 -0
- package/schemas/human-decision-answer-attempt-v1.schema.json +37 -0
- package/schemas/human-decision-cancellation-v1.schema.json +15 -0
- package/schemas/human-decision-continuation-v1.schema.json +16 -0
- package/schemas/human-decision-delivery-v1.schema.json +28 -0
- package/schemas/human-decision-delivery-v2.schema.json +36 -0
- package/schemas/human-decision-receipt-v1.schema.json +33 -0
- package/schemas/human-decision-receipt-v2.schema.json +39 -0
- package/schemas/human-decision-request-v1.schema.json +59 -0
- package/schemas/human-decision-request-v2.schema.json +69 -0
- package/schemas/human-decision-resolution-v1.schema.json +27 -0
- package/schemas/human-decision-resolution-v2.schema.json +27 -0
- package/schemas/human-decision-settlement-v1.schema.json +28 -0
- package/skills/autodoc/SKILL.md +43 -0
- package/skills/autoimplement/SKILL.md +58 -0
- package/skills/autoimplement/agents/openai.yaml +4 -0
- package/skills/autoplan/SKILL.md +25 -0
- package/skills/monitor/SKILL.md +3 -1
- package/skills/pi-workflows/SKILL.md +3 -3
- package/src/builtins/autodoc.workflow.ts +325 -0
- package/src/builtins/autoimplement.workflow.ts +281 -14
- package/src/builtins/{autodevise.workflow.ts → autoplan.workflow.ts} +32 -32
- package/src/builtins/catalog.ts +8 -4
- package/src/builtins/index.ts +25 -6
- package/src/builtins/monitor.workflow.ts +162 -8
- package/src/builtins/plan-approval.workflow.ts +190 -0
- package/src/builtins/plan-presentation.ts +57 -0
- package/src/extension/decision-channels.ts +1533 -0
- package/src/extension/index.ts +499 -9
- package/src/extension/widget.ts +39 -2
- package/src/extension/workflow-tool.ts +5 -81
- package/src/host/rpc-bridge.ts +7 -30
- package/src/render/graph-render.ts +40 -3
- package/src/viewer/render.ts +56 -3
- package/src/workflows/composition.ts +16 -1
- package/src/workflows/decision-presentation.ts +501 -0
- package/src/workflows/engine.ts +108 -4
- package/src/workflows/human-decision.ts +819 -0
- package/src/workflows/index.ts +69 -0
- package/src/workflows/schema.ts +17 -0
- package/src/workflows/store.ts +8 -0
- package/src/workflows/tool-input.ts +202 -0
- package/src/workflows/types.ts +265 -0
- package/dist/builtins/autodevise.workflow.js.map +0 -1
- package/examples/workflows/autodevise.workflow.ts +0 -1
|
@@ -20,8 +20,10 @@ import type {
|
|
|
20
20
|
WorkflowProgressData,
|
|
21
21
|
} from "../workflows/types.js";
|
|
22
22
|
import { validateProgressData } from "../workflows/updates.js";
|
|
23
|
-
import
|
|
23
|
+
import autodocWorkflow, { type AutodocInput } from "./autodoc.workflow.js";
|
|
24
24
|
import autoimplementWorkflow, { type AutoimplementInput } from "./autoimplement.workflow.js";
|
|
25
|
+
import autoplanWorkflow from "./autoplan.workflow.js";
|
|
26
|
+
import planApprovalWorkflow, { type PlanApprovalInput } from "./plan-approval.workflow.js";
|
|
25
27
|
|
|
26
28
|
const MIN_INTERVAL_MINUTES = 1;
|
|
27
29
|
const MAX_INTERVAL_MINUTES = 24 * 60;
|
|
@@ -45,6 +47,10 @@ export type MonitorRepairPolicy = {
|
|
|
45
47
|
repository?: string;
|
|
46
48
|
baseBranch?: string;
|
|
47
49
|
merge?: boolean;
|
|
50
|
+
approval?: {
|
|
51
|
+
audience: string;
|
|
52
|
+
maxReplans: number;
|
|
53
|
+
};
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
export type MonitorInput = {
|
|
@@ -163,6 +169,22 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
|
163
169
|
if (raw.merge !== undefined && typeof raw.merge !== "boolean") {
|
|
164
170
|
throw new Error("repair merge must be a boolean");
|
|
165
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
|
+
}
|
|
166
188
|
repair = {
|
|
167
189
|
authorized: true,
|
|
168
190
|
...(raw.scope !== undefined
|
|
@@ -176,6 +198,7 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
|
|
|
176
198
|
? { baseBranch: requireBoundedString(raw.baseBranch, "repair base branch", 256) }
|
|
177
199
|
: {}),
|
|
178
200
|
...(raw.merge !== undefined ? { merge: raw.merge !== false } : {}),
|
|
201
|
+
...(approval !== undefined ? { approval } : {}),
|
|
179
202
|
};
|
|
180
203
|
}
|
|
181
204
|
return {
|
|
@@ -297,9 +320,56 @@ function repeatedRepairWithoutProgress(context: WorkflowNodeContext): boolean {
|
|
|
297
320
|
return false;
|
|
298
321
|
}
|
|
299
322
|
|
|
323
|
+
function currentRepairPlan(outputs: Record<string, unknown>): {
|
|
324
|
+
plan: unknown;
|
|
325
|
+
planDigest: string;
|
|
326
|
+
documents: string[];
|
|
327
|
+
} {
|
|
328
|
+
const documentation = outputs.documentation as
|
|
329
|
+
| {
|
|
330
|
+
exit?: string;
|
|
331
|
+
output?: {
|
|
332
|
+
plan?: unknown;
|
|
333
|
+
planDigest?: string;
|
|
334
|
+
documentation?: { files?: string[] };
|
|
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: [] };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function latestReplanInstructions(outputs: Record<string, unknown>): string | undefined {
|
|
355
|
+
const approval = outputs.approval as
|
|
356
|
+
| { exit?: string; output?: { instructions?: unknown } }
|
|
357
|
+
| undefined;
|
|
358
|
+
return approval?.exit === "replan" && typeof approval.output?.instructions === "string"
|
|
359
|
+
? approval.output.instructions
|
|
360
|
+
: undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
300
363
|
function repairBlockedReason(outputs: Record<string, unknown>): string {
|
|
301
364
|
const guard = outputs.repairGuard as { reason?: string; route?: string } | undefined;
|
|
302
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
|
|
371
|
+
| { exit?: string; output?: { reason?: string } }
|
|
372
|
+
| undefined;
|
|
303
373
|
const implementation = outputs.implementation as
|
|
304
374
|
| { exit?: string; output?: { reason?: string } }
|
|
305
375
|
| undefined;
|
|
@@ -308,6 +378,7 @@ function repairBlockedReason(outputs: Record<string, unknown>): string {
|
|
|
308
378
|
| undefined;
|
|
309
379
|
return (
|
|
310
380
|
implementation?.output?.reason ??
|
|
381
|
+
documentation?.output?.reason ??
|
|
311
382
|
design?.output?.reason ??
|
|
312
383
|
"The repair did not produce new verified progress."
|
|
313
384
|
);
|
|
@@ -357,19 +428,66 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
357
428
|
maxSteps: 200_000,
|
|
358
429
|
includes: {
|
|
359
430
|
initialDesign: includeWorkflow({
|
|
360
|
-
workflow: "
|
|
361
|
-
contract:
|
|
431
|
+
workflow: "autoplan",
|
|
432
|
+
contract: autoplanWorkflow,
|
|
362
433
|
input: ({ outputs }) => {
|
|
363
434
|
const config = configFrom(outputs);
|
|
364
435
|
const repair = (outputs.check as MonitorCheck).repair;
|
|
365
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);
|
|
366
441
|
return {
|
|
367
442
|
problem: repair.problem,
|
|
368
443
|
...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
|
|
369
444
|
...(config.repair?.constraints !== undefined
|
|
370
445
|
? { constraints: config.repair.constraints }
|
|
371
446
|
: {}),
|
|
372
|
-
|
|
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");
|
|
464
|
+
return {
|
|
465
|
+
task: repair.problem,
|
|
466
|
+
plan: design.output.plan,
|
|
467
|
+
...(config.repair?.repository !== undefined
|
|
468
|
+
? { repository: config.repair.repository }
|
|
469
|
+
: {}),
|
|
470
|
+
evidence: latestReplanInstructions(outputs) ?? repair.evidence,
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
}),
|
|
474
|
+
approval: includeWorkflow(planApprovalWorkflow, {
|
|
475
|
+
input: ({ outputs, state }): PlanApprovalInput => {
|
|
476
|
+
const config = configFrom(outputs);
|
|
477
|
+
const repair = (outputs.check as MonitorCheck).repair;
|
|
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,
|
|
373
491
|
};
|
|
374
492
|
},
|
|
375
493
|
}),
|
|
@@ -379,12 +497,16 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
379
497
|
input: ({ outputs }) => {
|
|
380
498
|
const config = configFrom(outputs);
|
|
381
499
|
const repair = (outputs.check as MonitorCheck).repair;
|
|
382
|
-
const
|
|
383
|
-
if (design.exit !== "ready") throw new Error("monitor design did not return a ready plan");
|
|
500
|
+
const documented = currentRepairPlan(outputs);
|
|
384
501
|
if (repair === undefined) throw new Error("monitor repair details are missing");
|
|
385
502
|
const request: AutoimplementInput = {
|
|
386
503
|
task: repair.problem,
|
|
387
|
-
plan:
|
|
504
|
+
plan: documented.plan,
|
|
505
|
+
documentation: {
|
|
506
|
+
status: "current",
|
|
507
|
+
planDigest: documented.planDigest,
|
|
508
|
+
documents: documented.documents,
|
|
509
|
+
},
|
|
388
510
|
...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
|
|
389
511
|
...(config.repair?.constraints !== undefined
|
|
390
512
|
? { constraints: config.repair.constraints }
|
|
@@ -481,6 +603,25 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
481
603
|
}
|
|
482
604
|
: { route: "repair", reason: "The issue is new or has changed evidence." },
|
|
483
605
|
}),
|
|
606
|
+
approvalRoute: compute({
|
|
607
|
+
run: ({ outputs }) => ({
|
|
608
|
+
route: configFrom(outputs).repair?.approval === undefined ? "implement" : "approve",
|
|
609
|
+
}),
|
|
610
|
+
}),
|
|
611
|
+
replanGuard: compute({
|
|
612
|
+
run: (context) => {
|
|
613
|
+
const limit = configFrom(context.outputs).repair?.approval?.maxReplans ?? 3;
|
|
614
|
+
const replans = context.state.steps.filter(
|
|
615
|
+
(step) => step.nodeId === "approval/replan",
|
|
616
|
+
).length;
|
|
617
|
+
return replans > limit
|
|
618
|
+
? {
|
|
619
|
+
route: "blocked",
|
|
620
|
+
reason: `Monitor repair reached the ${limit}-replan safety limit.`,
|
|
621
|
+
}
|
|
622
|
+
: { route: "design", replans, limit };
|
|
623
|
+
},
|
|
624
|
+
}),
|
|
484
625
|
repairBlocked: compute({
|
|
485
626
|
run: (context) => ({
|
|
486
627
|
reason: repairBlockedReason(context.outputs),
|
|
@@ -568,8 +709,21 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
|
|
|
568
709
|
from: "repairGuard",
|
|
569
710
|
switch: { on: "$.route", cases: { repair: "initialDesign", blocked: "repairBlocked" } },
|
|
570
711
|
},
|
|
571
|
-
{ from: "initialDesign.ready", to: "
|
|
712
|
+
{ from: "initialDesign.ready", to: "documentation" },
|
|
572
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
|
+
},
|
|
573
727
|
{ from: "implementation.completed", to: "check" },
|
|
574
728
|
{ from: "implementation.blocked", to: "repairBlocked" },
|
|
575
729
|
{ from: "repairBlocked", to: "repairReport" },
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { compute, defineWorkflow } from "../workflows/definition.js";
|
|
2
|
+
import {
|
|
3
|
+
choice,
|
|
4
|
+
defineHumanChoices,
|
|
5
|
+
humanDecision,
|
|
6
|
+
humanDecisionEdge,
|
|
7
|
+
textInput,
|
|
8
|
+
} from "../workflows/human-decision.js";
|
|
9
|
+
import type { HumanDecisionReceipt, HumanDecisionResponse } from "../workflows/types.js";
|
|
10
|
+
import { presentPlan } from "./plan-presentation.js";
|
|
11
|
+
|
|
12
|
+
export type PlanApprovalInput = {
|
|
13
|
+
task: string;
|
|
14
|
+
plan: unknown;
|
|
15
|
+
planDigest: string;
|
|
16
|
+
audience: string;
|
|
17
|
+
revision?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PlanApprovalContinue = {
|
|
21
|
+
status: "continue";
|
|
22
|
+
plan: unknown;
|
|
23
|
+
planDigest: string;
|
|
24
|
+
decision: HumanDecisionReceipt;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type PlanApprovalStop = {
|
|
28
|
+
status: "stop";
|
|
29
|
+
planDigest: string;
|
|
30
|
+
decision: HumanDecisionReceipt;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type PlanApprovalReplan = {
|
|
34
|
+
status: "replan";
|
|
35
|
+
plan: unknown;
|
|
36
|
+
planDigest: string;
|
|
37
|
+
instructions: string;
|
|
38
|
+
decision: HumanDecisionReceipt;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const planChoices = defineHumanChoices({
|
|
42
|
+
continue: choice({ label: "Yes, continue" }),
|
|
43
|
+
stop: choice({ label: "No, stop" }),
|
|
44
|
+
replan: choice({
|
|
45
|
+
label: "Replan",
|
|
46
|
+
input: textInput({
|
|
47
|
+
name: "instructions",
|
|
48
|
+
prompt: "What should change?",
|
|
49
|
+
minLength: 1,
|
|
50
|
+
maxLength: 4_000,
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
56
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
57
|
+
throw new Error(`${label} must be an object`);
|
|
58
|
+
}
|
|
59
|
+
return value as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function requireString(value: unknown, label: string): string {
|
|
63
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
64
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
65
|
+
}
|
|
66
|
+
return value.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseInput(value: unknown): PlanApprovalInput {
|
|
70
|
+
const input = requireRecord(value, "plan approval input");
|
|
71
|
+
const revision = input.revision;
|
|
72
|
+
if (revision !== undefined && (!Number.isInteger(revision) || (revision as number) < 1)) {
|
|
73
|
+
throw new Error("plan approval revision must be a positive integer");
|
|
74
|
+
}
|
|
75
|
+
if (input.plan === undefined) throw new Error("plan approval requires a plan");
|
|
76
|
+
return {
|
|
77
|
+
task: requireString(input.task, "plan approval task"),
|
|
78
|
+
plan: input.plan,
|
|
79
|
+
planDigest: requireString(input.planDigest, "plan approval digest"),
|
|
80
|
+
audience: requireString(input.audience, "plan approval audience"),
|
|
81
|
+
...(revision !== undefined ? { revision: revision as number } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function acceptedDecision(value: HumanDecisionReceipt | undefined): HumanDecisionReceipt {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
throw new Error("plan approval continuation is missing its accepted decision");
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function gateResponse(value: unknown): HumanDecisionResponse {
|
|
92
|
+
return requireRecord(value, "plan approval response") as HumanDecisionResponse;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const planApprovalWorkflow = defineWorkflow({
|
|
96
|
+
source: import.meta.url,
|
|
97
|
+
contractId: "pi-workflows.plan-approval.v1",
|
|
98
|
+
name: "plan-approval",
|
|
99
|
+
input: parseInput,
|
|
100
|
+
title: ({ input }) => `plan approval: ${input.task.slice(0, 60)}`,
|
|
101
|
+
startAt: "approve",
|
|
102
|
+
maxSteps: 8,
|
|
103
|
+
exits: {
|
|
104
|
+
continue: {
|
|
105
|
+
from: "continued",
|
|
106
|
+
validate: (value: unknown): PlanApprovalContinue => value as PlanApprovalContinue,
|
|
107
|
+
},
|
|
108
|
+
stop: {
|
|
109
|
+
from: "stopped",
|
|
110
|
+
validate: (value: unknown): PlanApprovalStop => value as PlanApprovalStop,
|
|
111
|
+
},
|
|
112
|
+
replan: {
|
|
113
|
+
from: "replan",
|
|
114
|
+
validate: (value: unknown): PlanApprovalReplan => value as PlanApprovalReplan,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
nodes: {
|
|
118
|
+
approve: humanDecision({
|
|
119
|
+
audience: ({ input }) => (input as PlanApprovalInput).audience,
|
|
120
|
+
choices: planChoices,
|
|
121
|
+
request: ({ input }) => {
|
|
122
|
+
const request = input as PlanApprovalInput;
|
|
123
|
+
const revision = request.revision ?? 1;
|
|
124
|
+
return {
|
|
125
|
+
title: "Approve the implementation plan",
|
|
126
|
+
subject: {
|
|
127
|
+
task: request.task,
|
|
128
|
+
plan: request.plan,
|
|
129
|
+
planDigest: request.planDigest,
|
|
130
|
+
revision,
|
|
131
|
+
},
|
|
132
|
+
presentation: presentPlan({
|
|
133
|
+
task: request.task,
|
|
134
|
+
plan: request.plan,
|
|
135
|
+
planDigest: request.planDigest,
|
|
136
|
+
revision,
|
|
137
|
+
}),
|
|
138
|
+
revision,
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
continued: compute({
|
|
143
|
+
run: ({ input, state }) => {
|
|
144
|
+
const request = input as PlanApprovalInput;
|
|
145
|
+
return {
|
|
146
|
+
status: "continue",
|
|
147
|
+
plan: request.plan,
|
|
148
|
+
planDigest: request.planDigest,
|
|
149
|
+
decision: acceptedDecision(state.humanDecision),
|
|
150
|
+
} satisfies PlanApprovalContinue;
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
stopped: compute({
|
|
154
|
+
run: ({ input, state }) => {
|
|
155
|
+
const request = input as PlanApprovalInput;
|
|
156
|
+
return {
|
|
157
|
+
status: "stop",
|
|
158
|
+
planDigest: request.planDigest,
|
|
159
|
+
decision: acceptedDecision(state.humanDecision),
|
|
160
|
+
} satisfies PlanApprovalStop;
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
replan: compute({
|
|
164
|
+
run: ({ input, outputs, state }) => {
|
|
165
|
+
const request = input as PlanApprovalInput;
|
|
166
|
+
const response = gateResponse(outputs.approve);
|
|
167
|
+
const instructions = response.input?.instructions;
|
|
168
|
+
if (typeof instructions !== "string" || instructions.length === 0) {
|
|
169
|
+
throw new Error("replan answer is missing exact instructions");
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
status: "replan",
|
|
173
|
+
plan: request.plan,
|
|
174
|
+
planDigest: request.planDigest,
|
|
175
|
+
instructions,
|
|
176
|
+
decision: acceptedDecision(state.humanDecision),
|
|
177
|
+
} satisfies PlanApprovalReplan;
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
edges: [
|
|
182
|
+
humanDecisionEdge({
|
|
183
|
+
from: "approve",
|
|
184
|
+
choices: planChoices,
|
|
185
|
+
cases: { continue: "continued", stop: "stopped", replan: "replan" },
|
|
186
|
+
}),
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export default planApprovalWorkflow;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
legacyDecisionPresentation,
|
|
3
|
+
normalizeDecisionPresentation,
|
|
4
|
+
} from "../workflows/decision-presentation.js";
|
|
5
|
+
import type { DecisionPresentation } from "../workflows/types.js";
|
|
6
|
+
|
|
7
|
+
export function presentPlan(input: {
|
|
8
|
+
task: string;
|
|
9
|
+
plan: unknown;
|
|
10
|
+
planDigest: string;
|
|
11
|
+
revision: number;
|
|
12
|
+
}): DecisionPresentation {
|
|
13
|
+
const plan = legacyDecisionPresentation(input.plan);
|
|
14
|
+
const summary =
|
|
15
|
+
readableSummary(planSummary(input.plan)) ??
|
|
16
|
+
readableSummary(`Review the implementation plan for ${input.task}.`)!;
|
|
17
|
+
return normalizeDecisionPresentation({
|
|
18
|
+
schema: "pi-workflows.decision-presentation.v1",
|
|
19
|
+
summary,
|
|
20
|
+
blocks: [
|
|
21
|
+
{ kind: "section", title: "Task" },
|
|
22
|
+
{ kind: "paragraph", text: input.task },
|
|
23
|
+
{ kind: "section", title: "Plan" },
|
|
24
|
+
...plan.blocks.filter(
|
|
25
|
+
(block, index) =>
|
|
26
|
+
!(index === 0 && block.kind === "section" && block.title === "Decision details"),
|
|
27
|
+
),
|
|
28
|
+
{ kind: "section", title: "Plan identity" },
|
|
29
|
+
{
|
|
30
|
+
kind: "fields",
|
|
31
|
+
items: [
|
|
32
|
+
{ label: "Revision", value: String(input.revision) },
|
|
33
|
+
{ label: "Digest", value: input.planDigest },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readableSummary(value: string | undefined): string | undefined {
|
|
41
|
+
if (value === undefined) return undefined;
|
|
42
|
+
const text = [...value]
|
|
43
|
+
.map((character) => {
|
|
44
|
+
const code = character.codePointAt(0) ?? 0;
|
|
45
|
+
return code <= 31 || code === 127 ? " " : character;
|
|
46
|
+
})
|
|
47
|
+
.join("")
|
|
48
|
+
.replaceAll(/\s+/gu, " ")
|
|
49
|
+
.trim();
|
|
50
|
+
return text.length === 0 ? undefined : text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function planSummary(plan: unknown): string | undefined {
|
|
54
|
+
if (plan === null || typeof plan !== "object" || Array.isArray(plan)) return undefined;
|
|
55
|
+
const summary = (plan as Record<string, unknown>).summary;
|
|
56
|
+
return typeof summary === "string" && summary.trim().length > 0 ? summary : undefined;
|
|
57
|
+
}
|