@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
|
@@ -141,6 +141,219 @@ export type ShellActionNodeDefinition = WorkflowNodeCommon & {
|
|
|
141
141
|
updates?: ShellActionUpdates;
|
|
142
142
|
};
|
|
143
143
|
export type ActionNodeDefinition = FunctionActionNodeDefinition | ShellActionNodeDefinition;
|
|
144
|
+
export type HumanDecisionTextInput = {
|
|
145
|
+
kind: "text";
|
|
146
|
+
name: string;
|
|
147
|
+
prompt: string;
|
|
148
|
+
minLength: number;
|
|
149
|
+
maxLength: number;
|
|
150
|
+
};
|
|
151
|
+
export type HumanDecisionChoice = {
|
|
152
|
+
label: string;
|
|
153
|
+
input?: HumanDecisionTextInput;
|
|
154
|
+
};
|
|
155
|
+
export type HumanDecisionChoiceMap = Record<string, HumanDecisionChoice>;
|
|
156
|
+
export type DecisionPresentationParagraph = {
|
|
157
|
+
kind: "paragraph";
|
|
158
|
+
text: string;
|
|
159
|
+
};
|
|
160
|
+
export type DecisionPresentationSection = {
|
|
161
|
+
kind: "section";
|
|
162
|
+
title: string;
|
|
163
|
+
};
|
|
164
|
+
export type DecisionPresentationBullets = {
|
|
165
|
+
kind: "bullets";
|
|
166
|
+
items: string[];
|
|
167
|
+
};
|
|
168
|
+
export type DecisionPresentationFields = {
|
|
169
|
+
kind: "fields";
|
|
170
|
+
items: Array<{
|
|
171
|
+
label: string;
|
|
172
|
+
value: string;
|
|
173
|
+
}>;
|
|
174
|
+
};
|
|
175
|
+
export type DecisionPresentationPreformatted = {
|
|
176
|
+
kind: "preformatted";
|
|
177
|
+
text: string;
|
|
178
|
+
};
|
|
179
|
+
export type DecisionPresentationBlock = DecisionPresentationParagraph | DecisionPresentationSection | DecisionPresentationBullets | DecisionPresentationFields | DecisionPresentationPreformatted;
|
|
180
|
+
export type DecisionPresentation = {
|
|
181
|
+
schema: "pi-workflows.decision-presentation.v1";
|
|
182
|
+
summary: string;
|
|
183
|
+
blocks: DecisionPresentationBlock[];
|
|
184
|
+
};
|
|
185
|
+
export type LegacyHumanDecisionPrompt = {
|
|
186
|
+
title: string;
|
|
187
|
+
body: unknown;
|
|
188
|
+
subject?: never;
|
|
189
|
+
presentation?: never;
|
|
190
|
+
revision?: never;
|
|
191
|
+
/** Optional absolute expiry. An expired request cannot accept an answer. */
|
|
192
|
+
expiresAt?: string;
|
|
193
|
+
};
|
|
194
|
+
export type PresentedHumanDecisionPrompt<TSubject = unknown> = {
|
|
195
|
+
title: string;
|
|
196
|
+
subject: TSubject;
|
|
197
|
+
presentation: DecisionPresentation;
|
|
198
|
+
body?: never;
|
|
199
|
+
/** Positive revision of the decision subject and presentation. Defaults to 1. */
|
|
200
|
+
revision?: number;
|
|
201
|
+
/** Optional absolute expiry. An expired request cannot accept an answer. */
|
|
202
|
+
expiresAt?: string;
|
|
203
|
+
};
|
|
204
|
+
export type HumanDecisionPrompt = LegacyHumanDecisionPrompt | PresentedHumanDecisionPrompt;
|
|
205
|
+
export type HumanDecisionAudience = string | ((context: WorkflowNodeContext) => MaybePromise<string>);
|
|
206
|
+
export type HumanDecisionNodeContract = {
|
|
207
|
+
audience: HumanDecisionAudience;
|
|
208
|
+
choices: HumanDecisionChoiceMap;
|
|
209
|
+
request: (context: WorkflowNodeContext) => MaybePromise<HumanDecisionPrompt>;
|
|
210
|
+
};
|
|
211
|
+
type HumanDecisionRequestCommon = {
|
|
212
|
+
decisionId: string;
|
|
213
|
+
requestDigest: string;
|
|
214
|
+
runId: string;
|
|
215
|
+
workflowName: string;
|
|
216
|
+
nodeId: string;
|
|
217
|
+
attemptId: string;
|
|
218
|
+
audience: string;
|
|
219
|
+
title: string;
|
|
220
|
+
choices: HumanDecisionChoiceMap;
|
|
221
|
+
createdAt: string;
|
|
222
|
+
expiresAt?: string;
|
|
223
|
+
};
|
|
224
|
+
export type HumanDecisionRequestV1 = HumanDecisionRequestCommon & {
|
|
225
|
+
schema: "pi-workflows.human-decision-request.v1";
|
|
226
|
+
body: unknown;
|
|
227
|
+
};
|
|
228
|
+
export type HumanDecisionRequestV2 = HumanDecisionRequestCommon & {
|
|
229
|
+
schema: "pi-workflows.human-decision-request.v2";
|
|
230
|
+
subject: unknown;
|
|
231
|
+
presentation: DecisionPresentation;
|
|
232
|
+
revision: number;
|
|
233
|
+
subjectDigest: string;
|
|
234
|
+
presentationDigest: string;
|
|
235
|
+
};
|
|
236
|
+
export type HumanDecisionRequest = HumanDecisionRequestV1 | HumanDecisionRequestV2;
|
|
237
|
+
/**
|
|
238
|
+
* Complete operator-facing request passed to a decision channel. It excludes
|
|
239
|
+
* the canonical subject and legacy body by design.
|
|
240
|
+
*/
|
|
241
|
+
export type HumanDecisionChannelRequest = HumanDecisionRequestCommon & {
|
|
242
|
+
schema: "pi-workflows.human-decision-channel-request.v1";
|
|
243
|
+
sourceSchema: HumanDecisionRequest["schema"];
|
|
244
|
+
presentation: DecisionPresentation;
|
|
245
|
+
presentationDigest: string;
|
|
246
|
+
revision: number;
|
|
247
|
+
};
|
|
248
|
+
export type HumanDecisionResponse = {
|
|
249
|
+
choice: string;
|
|
250
|
+
input?: Record<string, string>;
|
|
251
|
+
};
|
|
252
|
+
export type HumanDecisionAnswerSource = {
|
|
253
|
+
channel: string;
|
|
254
|
+
actorId: string;
|
|
255
|
+
eventId: string;
|
|
256
|
+
};
|
|
257
|
+
export type HumanDecisionSubmission = HumanDecisionResponse & {
|
|
258
|
+
decisionId: string;
|
|
259
|
+
requestDigest: string;
|
|
260
|
+
source: HumanDecisionAnswerSource;
|
|
261
|
+
idempotencyKey: string;
|
|
262
|
+
};
|
|
263
|
+
type AcceptedHumanDecisionCommon = {
|
|
264
|
+
decisionId: string;
|
|
265
|
+
requestDigest: string;
|
|
266
|
+
response: HumanDecisionResponse;
|
|
267
|
+
source: HumanDecisionAnswerSource;
|
|
268
|
+
idempotencyKey: string;
|
|
269
|
+
acceptedAt: string;
|
|
270
|
+
answerDigest: string;
|
|
271
|
+
};
|
|
272
|
+
export type AcceptedHumanDecisionV1 = AcceptedHumanDecisionCommon & {
|
|
273
|
+
schema: "pi-workflows.human-decision-accepted.v1";
|
|
274
|
+
};
|
|
275
|
+
export type AcceptedHumanDecisionV2 = AcceptedHumanDecisionCommon & {
|
|
276
|
+
schema: "pi-workflows.human-decision-accepted.v2";
|
|
277
|
+
subjectDigest: string;
|
|
278
|
+
presentationDigest: string;
|
|
279
|
+
revision: number;
|
|
280
|
+
};
|
|
281
|
+
export type AcceptedHumanDecision = AcceptedHumanDecisionV1 | AcceptedHumanDecisionV2;
|
|
282
|
+
type HumanDecisionReceiptCommon = {
|
|
283
|
+
decisionId: string;
|
|
284
|
+
requestDigest: string;
|
|
285
|
+
nodeId: string;
|
|
286
|
+
response: HumanDecisionResponse;
|
|
287
|
+
acceptedAt: string;
|
|
288
|
+
answerDigest: string;
|
|
289
|
+
};
|
|
290
|
+
export type HumanDecisionReceiptV1 = HumanDecisionReceiptCommon & {
|
|
291
|
+
schema: "pi-workflows.human-decision-receipt.v1";
|
|
292
|
+
};
|
|
293
|
+
export type HumanDecisionReceiptV2 = HumanDecisionReceiptCommon & {
|
|
294
|
+
schema: "pi-workflows.human-decision-receipt.v2";
|
|
295
|
+
subjectDigest: string;
|
|
296
|
+
presentationDigest: string;
|
|
297
|
+
revision: number;
|
|
298
|
+
};
|
|
299
|
+
export type HumanDecisionReceipt = HumanDecisionReceiptV1 | HumanDecisionReceiptV2;
|
|
300
|
+
export type HumanDecisionDeliveryRecordV1 = {
|
|
301
|
+
schema: "pi-workflows.human-decision-delivery.v1";
|
|
302
|
+
attemptId: string;
|
|
303
|
+
decisionId: string;
|
|
304
|
+
requestDigest: string;
|
|
305
|
+
channel: string;
|
|
306
|
+
state: "intent" | "confirmed" | "failed" | "unknown";
|
|
307
|
+
createdAt: string;
|
|
308
|
+
finishedAt?: string;
|
|
309
|
+
messageCount?: number;
|
|
310
|
+
errorCode?: string;
|
|
311
|
+
};
|
|
312
|
+
export type HumanDecisionDeliveryRecordV2 = {
|
|
313
|
+
schema: "pi-workflows.human-decision-delivery.v2";
|
|
314
|
+
attemptId: string;
|
|
315
|
+
decisionId: string;
|
|
316
|
+
requestDigest: string;
|
|
317
|
+
presentationDigest: string;
|
|
318
|
+
channel: string;
|
|
319
|
+
phase: "intent" | "part" | "complete";
|
|
320
|
+
state: "intent" | "confirmed" | "failed" | "unknown";
|
|
321
|
+
createdAt: string;
|
|
322
|
+
finishedAt?: string;
|
|
323
|
+
recipientIndex?: number;
|
|
324
|
+
partIndex?: number;
|
|
325
|
+
partCount?: number;
|
|
326
|
+
contentDigest?: string;
|
|
327
|
+
messageCount?: number;
|
|
328
|
+
errorCode?: string;
|
|
329
|
+
};
|
|
330
|
+
export type HumanDecisionDeliveryRecord = HumanDecisionDeliveryRecordV1 | HumanDecisionDeliveryRecordV2;
|
|
331
|
+
export type HumanDecisionSettlementRecord = {
|
|
332
|
+
schema: "pi-workflows.human-decision-settlement.v1";
|
|
333
|
+
attemptId: string;
|
|
334
|
+
decisionId: string;
|
|
335
|
+
requestDigest: string;
|
|
336
|
+
channel: string;
|
|
337
|
+
state: "confirmed" | "failed";
|
|
338
|
+
createdAt: string;
|
|
339
|
+
finishedAt: string;
|
|
340
|
+
errorCode?: string;
|
|
341
|
+
};
|
|
342
|
+
export type HumanDecisionCancellationRecord = {
|
|
343
|
+
schema: "pi-workflows.human-decision-cancellation.v1";
|
|
344
|
+
decisionId: string;
|
|
345
|
+
requestDigest: string;
|
|
346
|
+
cancelledAt: string;
|
|
347
|
+
reason: "cancelled" | "expired";
|
|
348
|
+
};
|
|
349
|
+
export type HumanDecisionContinuationRecord = {
|
|
350
|
+
schema: "pi-workflows.human-decision-continuation.v1";
|
|
351
|
+
decisionId: string;
|
|
352
|
+
requestDigest: string;
|
|
353
|
+
parentRunId: string;
|
|
354
|
+
runId: string;
|
|
355
|
+
createdAt: string;
|
|
356
|
+
};
|
|
144
357
|
/**
|
|
145
358
|
* A pause point. The run terminates with status `waiting` so a human (or an
|
|
146
359
|
* external trigger) can decide how to continue. The optional `run` callback
|
|
@@ -150,6 +363,8 @@ export type CheckpointNodeDefinition = WorkflowNodeCommon & {
|
|
|
150
363
|
nodeType: "checkpoint";
|
|
151
364
|
summary?: string;
|
|
152
365
|
run?: (context: WorkflowNodeContext) => MaybePromise<unknown>;
|
|
366
|
+
/** Typed verified-human request. Still executes as a checkpoint node. */
|
|
367
|
+
humanDecision?: HumanDecisionNodeContract;
|
|
153
368
|
};
|
|
154
369
|
export type WorkflowNodeDefinition = AgentNodeDefinition | ComputeNodeDefinition | NotifyNodeDefinition | ActionNodeDefinition | CheckpointNodeDefinition;
|
|
155
370
|
export type WorkflowPresentationContext = {
|
|
@@ -361,6 +576,8 @@ export type WorkflowRunState = {
|
|
|
361
576
|
currentAttemptId?: string;
|
|
362
577
|
currentNodeStartedAt?: string;
|
|
363
578
|
statusDetail?: string;
|
|
579
|
+
/** Redacted verified-human receipt carried by a continuation run. */
|
|
580
|
+
humanDecision?: HumanDecisionReceipt;
|
|
364
581
|
/** True while the run is held at a step boundary by a pause request. */
|
|
365
582
|
paused?: boolean;
|
|
366
583
|
waitingOn?: string;
|
|
@@ -377,6 +594,11 @@ export type WorkflowNodeSnapshot = {
|
|
|
377
594
|
mountPath?: string[];
|
|
378
595
|
localNodeId?: string;
|
|
379
596
|
includeTransition?: "entry" | "exit";
|
|
597
|
+
humanDecision?: {
|
|
598
|
+
audience: string;
|
|
599
|
+
dynamicAudience?: boolean;
|
|
600
|
+
choices: HumanDecisionChoiceMap;
|
|
601
|
+
};
|
|
380
602
|
};
|
|
381
603
|
export type WorkflowDefinitionSnapshot = {
|
|
382
604
|
schema: "pi-workflows.definition-snapshot.v1";
|
|
@@ -576,3 +798,4 @@ export type WorkflowEngineOptions = {
|
|
|
576
798
|
/** Observer invoked after every persisted trace event. */
|
|
577
799
|
onEvent?: (event: WorkflowTraceEvent, state: WorkflowRunState) => void;
|
|
578
800
|
};
|
|
801
|
+
export {};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Human decisions
|
|
2
|
+
|
|
3
|
+
Pi Workflows needs a reusable way to stop at a proposal and wait for a person. The same decision must appear in Pi and Telegram, and either channel must be able to continue the run. Workflows must be able to offer plain choices, choices that collect text, and choices that route back to planning.
|
|
4
|
+
|
|
5
|
+
This document defines that behavior. The implementation is tracked in the [human decision gates plan](plans/2026-08-19-human-decision-gates-plan.md).
|
|
6
|
+
|
|
7
|
+
The [human decision presentation contract](HUMAN_DECISION_PRESENTATIONS.md)
|
|
8
|
+
separates canonical subject data from the complete readable message shown in Pi
|
|
9
|
+
and Telegram. Its [implementation plan](plans/2026-08-19-human-decision-presentations-plan.md)
|
|
10
|
+
preserves v1 records without rewriting them.
|
|
11
|
+
|
|
12
|
+
## Goals
|
|
13
|
+
|
|
14
|
+
A workflow author can:
|
|
15
|
+
|
|
16
|
+
- add a typed human decision without writing channel code;
|
|
17
|
+
- route every choice exhaustively;
|
|
18
|
+
- collect exact text with a choice such as `replan`;
|
|
19
|
+
- address a logical audience instead of a Telegram chat or Pi session;
|
|
20
|
+
- reuse a standard plan approval workflow; and
|
|
21
|
+
- rely on one accepted answer and one continuation after a crash or concurrent reply.
|
|
22
|
+
|
|
23
|
+
The feature uses the existing checkpoint execution primitive. It does not add another engine node type.
|
|
24
|
+
|
|
25
|
+
## Authoring API
|
|
26
|
+
|
|
27
|
+
`humanDecision()` is the human counterpart to the existing model-driven `decision()` helper. It returns a checkpoint definition with a typed request and response contract.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import {
|
|
31
|
+
choice,
|
|
32
|
+
defineHumanChoices,
|
|
33
|
+
humanDecision,
|
|
34
|
+
humanDecisionEdge,
|
|
35
|
+
textInput,
|
|
36
|
+
} from "@osolmaz/pi-workflows";
|
|
37
|
+
|
|
38
|
+
const planChoices = defineHumanChoices({
|
|
39
|
+
continue: choice({ label: "Yes, continue" }),
|
|
40
|
+
stop: choice({ label: "No, stop" }),
|
|
41
|
+
replan: choice({
|
|
42
|
+
label: "Replan",
|
|
43
|
+
input: textInput({
|
|
44
|
+
name: "instructions",
|
|
45
|
+
prompt: "What should change?",
|
|
46
|
+
minLength: 1,
|
|
47
|
+
maxLength: 4_000,
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export default defineWorkflow({
|
|
53
|
+
name: "approve-example",
|
|
54
|
+
startAt: "propose",
|
|
55
|
+
nodes: {
|
|
56
|
+
propose: agent({
|
|
57
|
+
prompt: "Prepare a proposal.",
|
|
58
|
+
expectedOutput: '{ "summary": "text", "plan": {} }',
|
|
59
|
+
}),
|
|
60
|
+
approve: humanDecision({
|
|
61
|
+
audience: "operator",
|
|
62
|
+
choices: planChoices,
|
|
63
|
+
request: ({ outputs }) => ({
|
|
64
|
+
title: "Approve the implementation plan",
|
|
65
|
+
subject: outputs.propose,
|
|
66
|
+
presentation: {
|
|
67
|
+
schema: "pi-workflows.decision-presentation.v1",
|
|
68
|
+
summary: "Review the implementation plan.",
|
|
69
|
+
blocks: [
|
|
70
|
+
{ kind: "section", title: "Changes" },
|
|
71
|
+
{ kind: "paragraph", text: "Implement the proposed changes." },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
implement: agent({ prompt: "Implement the approved proposal." }),
|
|
77
|
+
revise: agent({
|
|
78
|
+
prompt: ({ outputs }) => `Revise the plan: ${JSON.stringify(outputs.approve)}`,
|
|
79
|
+
}),
|
|
80
|
+
stopped: compute({ run: () => ({ status: "stopped" }) }),
|
|
81
|
+
},
|
|
82
|
+
edges: [
|
|
83
|
+
{ from: "propose", to: "approve" },
|
|
84
|
+
humanDecisionEdge({
|
|
85
|
+
from: "approve",
|
|
86
|
+
choices: planChoices,
|
|
87
|
+
cases: {
|
|
88
|
+
continue: "implement",
|
|
89
|
+
stop: "stopped",
|
|
90
|
+
replan: "revise",
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
{ from: "revise", to: "approve" },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The accepted result is a discriminated union:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
type PlanDecision =
|
|
102
|
+
| { choice: "continue" }
|
|
103
|
+
| { choice: "stop" }
|
|
104
|
+
| { choice: "replan"; input: { instructions: string } };
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`humanDecisionEdge()` requires one destination for every choice. A missing or extra case is a TypeScript error. Runtime validation applies the same choice and input contract before an answer can win.
|
|
108
|
+
|
|
109
|
+
## Checkpoint behavior
|
|
110
|
+
|
|
111
|
+
A human decision still has `nodeType: "checkpoint"`. The helper adds a human decision contract to that checkpoint.
|
|
112
|
+
|
|
113
|
+
When the engine reaches the node, it:
|
|
114
|
+
|
|
115
|
+
1. evaluates and validates the decision request;
|
|
116
|
+
2. computes a canonical request digest from the subject, choices, audience, run, node, and attempt;
|
|
117
|
+
3. writes an immutable decision request linked to the waiting run;
|
|
118
|
+
4. asks the configured channels to deliver it; and
|
|
119
|
+
5. parks the run in `waiting` state.
|
|
120
|
+
|
|
121
|
+
When a valid answer is accepted, the continuation keeps the workflow's original input. The human answer becomes the checkpoint node's output in the continuation. Existing checkpoints keep their current behavior, where `/workflow answer` supplies the continuation input.
|
|
122
|
+
|
|
123
|
+
This distinction preserves old workflow definitions and run bundles.
|
|
124
|
+
|
|
125
|
+
## Request and response contracts
|
|
126
|
+
|
|
127
|
+
A new decision request contains:
|
|
128
|
+
|
|
129
|
+
- a decision ID;
|
|
130
|
+
- the waiting run, node, and attempt IDs;
|
|
131
|
+
- a short title;
|
|
132
|
+
- a canonical structured subject;
|
|
133
|
+
- a normalized human-readable presentation;
|
|
134
|
+
- separate subject and presentation digests;
|
|
135
|
+
- a positive decision revision;
|
|
136
|
+
- the logical audience;
|
|
137
|
+
- the complete choice contract;
|
|
138
|
+
- the canonical request digest;
|
|
139
|
+
- an optional expiry rule; and
|
|
140
|
+
- the creation time.
|
|
141
|
+
|
|
142
|
+
The presentation is an explicit display allowlist. A channel does not receive the subject and cannot infer operator text from it. The request digest binds the subject, visible presentation, title, revision, choices, and input prompts. Each choice has a stable ID and may have no input or one validated text input contract.
|
|
143
|
+
|
|
144
|
+
The former `body` form remains available for existing workflow definitions. It creates a v1 request and uses a deterministic readable compatibility formatter. Oversized historical bodies receive a bounded readable prefix and an explicit omission notice with the full body digest and size. V1 request bytes and digests do not change.
|
|
145
|
+
|
|
146
|
+
A submitted response contains:
|
|
147
|
+
|
|
148
|
+
- the decision ID and request digest;
|
|
149
|
+
- the selected choice;
|
|
150
|
+
- the validated input, when required;
|
|
151
|
+
- the channel and verified actor;
|
|
152
|
+
- a channel event ID;
|
|
153
|
+
- an idempotency key; and
|
|
154
|
+
- the receipt time.
|
|
155
|
+
|
|
156
|
+
A stale request digest, unknown choice, malformed input, unapproved actor, or model-originated answer is rejected before acceptance.
|
|
157
|
+
|
|
158
|
+
## Human answer boundary
|
|
159
|
+
|
|
160
|
+
The model-facing `workflow` tool cannot satisfy a human decision. Its `answer` action remains available for ordinary checkpoints, but returns an error when the waiting node requires a verified human channel.
|
|
161
|
+
|
|
162
|
+
The accepted human sources are:
|
|
163
|
+
|
|
164
|
+
- the Pi interactive decision view; and
|
|
165
|
+
- a configured external decision channel such as Telegram.
|
|
166
|
+
|
|
167
|
+
The host assigns the source. A workflow or model cannot claim that an answer came from a person. Pi non-interactive modes can wait for Telegram, but they cannot manufacture a Pi UI answer.
|
|
168
|
+
|
|
169
|
+
## Audiences and channel profiles
|
|
170
|
+
|
|
171
|
+
Workflows address a logical audience:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
humanDecision({ audience: "operator", ... });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Private configuration maps the audience to channels:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"schema": "pi-workflows.channels.v1",
|
|
182
|
+
"audiences": {
|
|
183
|
+
"operator": {
|
|
184
|
+
"channels": ["pi", "telegram:default"],
|
|
185
|
+
"accept": "first-valid-answer"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"telegramProfiles": {
|
|
189
|
+
"default": {
|
|
190
|
+
"credential": "telegram-default",
|
|
191
|
+
"allowedUserIds": ["<telegram-user-id>"],
|
|
192
|
+
"allowedChatIds": ["<telegram-chat-id>"]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The workflow never receives a bot token, user ID, chat ID, Telegram message ID, or Pi session detail. Channel profiles are private host configuration and are excluded from run presentation.
|
|
199
|
+
|
|
200
|
+
Pi Workflows keeps credential references in a separate private file. A Telegram credential points to an existing absolute mode-`0600` token file:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"schema": "pi-workflows.credentials.v1",
|
|
205
|
+
"telegram": {
|
|
206
|
+
"telegram-default": {
|
|
207
|
+
"tokenFile": "<absolute-mode-0600-token-file>"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Run `/workflow-channel setup` in Pi TUI to verify and install a profile, `/workflow-channel status` to inspect whether profiles are active, and `/workflow-channel reload` after a private configuration change. Setup asks for the token file path, not the token. It updates mode-`0600` private files and does not copy the token. Token values never enter source files, workflow inputs, run bundles, logs, child environments, or model-visible tool results.
|
|
214
|
+
|
|
215
|
+
The same Unix account can read a local credential file. This design prevents accidental propagation, not a hostile same-account process. A separately owned connector can implement the same channel interface later if stronger isolation becomes necessary.
|
|
216
|
+
|
|
217
|
+
## Channel interface
|
|
218
|
+
|
|
219
|
+
All decision channels implement one interface:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
interface HumanDecisionChannel {
|
|
223
|
+
readonly id: string;
|
|
224
|
+
start(): Promise<void>;
|
|
225
|
+
deliver(request: HumanDecisionRequest): Promise<DecisionDeliveryResult>;
|
|
226
|
+
settle(decision: AcceptedHumanDecision | HumanDecisionCancellation): Promise<void>;
|
|
227
|
+
stop(): Promise<void>;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The context exposes a narrow answer submission function. It does not expose the workflow engine, arbitrary run mutation, or another channel's credentials.
|
|
232
|
+
|
|
233
|
+
Channel delivery is independent from workflow routing. A failed Telegram delivery leaves the decision available in Pi. Audience policy decides whether one successful channel is enough or whether all configured channels are required before the request is considered delivered.
|
|
234
|
+
|
|
235
|
+
### Pi channel
|
|
236
|
+
|
|
237
|
+
The Pi channel uses documented extension UI APIs. It shows the request, lists the choices, and opens a text editor when the selected choice requires input. The host records the interactive session as the answer source.
|
|
238
|
+
|
|
239
|
+
The waiting workflow remains durable before the UI opens. Closing Pi or cancelling the view cannot lose the checkpoint. A later Pi session can reopen pending decisions.
|
|
240
|
+
|
|
241
|
+
### Telegram channel
|
|
242
|
+
|
|
243
|
+
The Telegram channel uses the Bot API and private profile configuration. It sends one decision message with inline buttons.
|
|
244
|
+
|
|
245
|
+
A choice without input submits from its button. A text choice such as `replan` works as follows:
|
|
246
|
+
|
|
247
|
+
1. the operator presses **Replan**;
|
|
248
|
+
2. the bot sends a `ForceReply` prompt tied to that decision and choice;
|
|
249
|
+
3. the operator replies to that exact prompt;
|
|
250
|
+
4. the channel verifies the numeric user ID, chat ID, reply message ID, decision ID, and request digest; and
|
|
251
|
+
5. the exact received text becomes `input.instructions`.
|
|
252
|
+
|
|
253
|
+
The adapter does not infer a choice from ordinary chat text. Callback payloads contain short opaque IDs because Telegram limits callback data. The private channel store maps each opaque ID to the full decision request.
|
|
254
|
+
|
|
255
|
+
Telegram permits one long-polling consumer for a bot profile. Active Pi processes use a shared lease so one process owns polling and the others use the same private channel state. The lease owner can accept a verified reply, but only the Pi session that owns the waiting run creates its continuation. Active sessions inspect the durable accepted-answer fence and recover their own continuation. If no Pi process is running, Telegram delivery and reply collection resume when Pi starts again. Running an always-on service is outside this design.
|
|
256
|
+
|
|
257
|
+
The Bot API does not provide an idempotency key for `sendMessage`. Pi Workflows therefore writes a delivery intent before sending and never blindly retries an ambiguous send. A timed-out send is recorded as `unknown`; Pi remains available and an operator can request another delivery. This avoids automatic duplicate messages while keeping decision acceptance exactly once.
|
|
258
|
+
|
|
259
|
+
## Durable decision records
|
|
260
|
+
|
|
261
|
+
Decision records live next to workflow run bundles under the Pi Workflows state root. They are additive and linked by run ID. A decision directory contains immutable records for:
|
|
262
|
+
|
|
263
|
+
- the request;
|
|
264
|
+
- channel delivery intents and results;
|
|
265
|
+
- answer attempts;
|
|
266
|
+
- the atomic accepted-or-cancelled resolution;
|
|
267
|
+
- the accepted answer or cancellation detail;
|
|
268
|
+
- channel settlement results; and
|
|
269
|
+
- the continuation request and result.
|
|
270
|
+
|
|
271
|
+
The resolution record uses a no-replace create. The first valid acceptance, cancellation, or expiry writer wins. Acceptance then materializes the matching accepted-answer detail. Cancellation or expiry materializes cancellation detail. A retry with the same response and idempotency key adopts the existing answer. A conflicting answer receives an `already decided` result.
|
|
272
|
+
|
|
273
|
+
The continuation run ID is derived from the accepted decision ID. Recovery adopts an existing matching continuation or creates it once. A crash after answer acceptance cannot run the next workflow step twice. The continuation run carries a redacted receipt with the decision ID, request digest, gate node ID, choice, acceptance time, and answer digest. Actor, channel, event, and idempotency provenance stays only in the private decision records and never enters the run bundle or model context.
|
|
274
|
+
|
|
275
|
+
SQLite may index pending decisions and channel leases, but immutable decision files remain the source of truth. The index is disposable and rebuildable.
|
|
276
|
+
|
|
277
|
+
## Planning workflow composition
|
|
278
|
+
|
|
279
|
+
Pi Workflows keeps solution choice, documentation, and implementation in separate built-ins:
|
|
280
|
+
|
|
281
|
+
- `autoplan` chooses a solution or revises one after new evidence.
|
|
282
|
+
- `autodoc` records an already selected solution in the canonical specification and implementation plan. It does not choose a solution or implement it.
|
|
283
|
+
- `autoimplement` executes a clear existing plan.
|
|
284
|
+
|
|
285
|
+
`autodoc` runs by itself and as an included workflow. It returns a documented-plan record with the plan, plan digest, document paths, document digests, and check results. If the canonical documents already describe the selected plan, it adopts them without rewriting files.
|
|
286
|
+
|
|
287
|
+
`autoimplement` first finds the clear existing plan in its input, the conversation, or referenced canonical documents. It blocks when no clear plan exists. A caller can bypass discovery and autodoc only by supplying both the explicit plan and a `documentation` receipt whose plan digest matches it. A plan without that current-document evidence enters autodoc so the canonical documents are inspected and adopted or updated. The absence of a structured `plan` input never authorizes `autoplan`.
|
|
288
|
+
|
|
289
|
+
If later implementation, verification, review, comments, or CI evidence invalidates the plan, autoimplement runs `autoplan`, sends the revised plan through `autodoc`, and then resumes implementation. An optional approval policy inserts `plan-approval` after the revised documentation.
|
|
290
|
+
|
|
291
|
+
## Reusable plan approval workflow
|
|
292
|
+
|
|
293
|
+
Pi Workflows ships a typed `plan-approval` workflow built on `humanDecision()`. Its input contains the documented plan, plan digest, audience, and display summary. It has three named exits:
|
|
294
|
+
|
|
295
|
+
- `continue`, with the approval receipt;
|
|
296
|
+
- `stop`, with the stop receipt; and
|
|
297
|
+
- `replan`, with the exact instructions and receipt.
|
|
298
|
+
|
|
299
|
+
A parent can include it without copying prompts or channel handling:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
includes: {
|
|
303
|
+
approval: includeWorkflow(planApproval, {
|
|
304
|
+
input: ({ outputs }) => ({
|
|
305
|
+
plan: outputs.devise.plan,
|
|
306
|
+
planDigest: outputs.devise.planDigest,
|
|
307
|
+
audience: "operator",
|
|
308
|
+
}),
|
|
309
|
+
}),
|
|
310
|
+
},
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
A `replan` exit returns to `autoplan` with the previous plan and the exact human instructions as new evidence. The revised plan passes through `autodoc`, receives a new digest, and enters a new approval decision. Step limits bound repeated replanning.
|
|
314
|
+
|
|
315
|
+
Monitor repair composes `autoplan`, `autodoc`, optional `plan-approval`, `autoimplement`, and a fresh target check. Autoimplement can mount the same approval workflow after evidence-driven redesign. Existing behavior stays unchanged when no approval policy is present.
|
|
316
|
+
|
|
317
|
+
## Recovery and cancellation
|
|
318
|
+
|
|
319
|
+
Recovery follows these rules:
|
|
320
|
+
|
|
321
|
+
- missing channel deliveries can be attempted;
|
|
322
|
+
- confirmed deliveries are adopted;
|
|
323
|
+
- Telegram records intent and confirmed evidence for every multipart message;
|
|
324
|
+
- Telegram resumes only when the next part is provably unsent;
|
|
325
|
+
- ambiguous Telegram sends are not retried automatically;
|
|
326
|
+
- duplicate channel updates are harmless;
|
|
327
|
+
- stale responses are rejected;
|
|
328
|
+
- one accepted response creates one continuation;
|
|
329
|
+
- the winning channel dismisses any pending Pi dialog;
|
|
330
|
+
- confirmed channel settlement is adopted without another remote call;
|
|
331
|
+
- failed channel settlement has a bounded retry count and cannot create an unbounded record loop; and
|
|
332
|
+
- cancellation resolves the owned waiting decision from durable state, even after restart, closes pending views, and prevents a later answer from continuing the run.
|
|
333
|
+
|
|
334
|
+
A decision with no available channel remains waiting and reports the configuration problem. It does not silently continue or choose a default.
|
|
335
|
+
|
|
336
|
+
## Compatibility
|
|
337
|
+
|
|
338
|
+
Ordinary checkpoints, their continuation input behavior, and existing run bundles remain unchanged. V2 requests, accepted records, receipts, resolutions, and multipart delivery records are additive. Older viewers ignore them. Updated viewers label a human decision as a checkpoint, show the readable presentation and its fingerprint, and keep the canonical subject separate. Private channel configuration and transport identifiers remain hidden.
|
|
339
|
+
|
|
340
|
+
The engine remains independent from Pi and Telegram. Core code owns decision contracts, validation, durable acceptance, and continuation. The Pi extension owns UI and channel lifecycle. The Telegram adapter owns Bot API translation. Workflow definitions own only the question, choices, audience, and routes.
|
|
341
|
+
|
|
342
|
+
## Contract impact
|
|
343
|
+
|
|
344
|
+
- **Session state:** Pi records normal workflow messages and interactive decision results.
|
|
345
|
+
- **Other persistent data:** additive decision records, a rebuildable private channel index, and private channel configuration.
|
|
346
|
+
- **Pi internals:** none.
|
|
347
|
+
- **Public Pi API:** documented extension lifecycle and UI methods only.
|
|
348
|
+
- **Public Pi Workflows API:** typed human choices, `humanDecision()`, `humanDecisionEdge()`, the channel interface, and the `plan-approval` workflow.
|
|
349
|
+
|
|
350
|
+
## Verification requirements
|
|
351
|
+
|
|
352
|
+
The implementation must test:
|
|
353
|
+
|
|
354
|
+
- compile-time exhaustive choice routing;
|
|
355
|
+
- choices with no input and choices with exact text input;
|
|
356
|
+
- runtime choice and input validation;
|
|
357
|
+
- legacy checkpoint continuation;
|
|
358
|
+
- model-tool answer rejection;
|
|
359
|
+
- Pi interactive answers;
|
|
360
|
+
- Telegram callbacks and reply binding with a fake Bot API;
|
|
361
|
+
- unauthorized users and chats;
|
|
362
|
+
- stale request digests;
|
|
363
|
+
- concurrent Pi and Telegram answers;
|
|
364
|
+
- identical and conflicting retries;
|
|
365
|
+
- crashes before and after answer acceptance and continuation creation;
|
|
366
|
+
- ambiguous Telegram sends;
|
|
367
|
+
- long-poll lease handoff between Pi processes;
|
|
368
|
+
- decision cancellation and expiry;
|
|
369
|
+
- included `plan-approval` routes and bounded replan loops;
|
|
370
|
+
- viewer redaction; and
|
|
371
|
+
- real Pi execution without real Telegram credentials or network calls.
|