@osolmaz/pi-workflows 0.9.1 → 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 +64 -19
- 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 +353 -0
- package/dist/builtins/autoimplement.workflow.js +961 -0
- package/dist/builtins/autoimplement.workflow.js.map +1 -0
- package/dist/builtins/autoplan.workflow.d.ts +58 -0
- package/dist/builtins/autoplan.workflow.js +190 -0
- package/dist/builtins/autoplan.workflow.js.map +1 -0
- package/dist/builtins/catalog.js +9 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +6 -0
- package/dist/builtins/index.js +7 -0
- package/dist/builtins/index.js.map +1 -0
- package/dist/builtins/monitor.workflow.d.ts +29 -3
- package/dist/builtins/monitor.workflow.js +325 -13
- 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 +46 -10
- 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/catalog.d.ts +1 -0
- package/dist/workflows/catalog.js +6 -0
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.d.ts +45 -0
- package/dist/workflows/composition.js +485 -0
- package/dist/workflows/composition.js.map +1 -0
- 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/decision.d.ts +11 -5
- package/dist/workflows/decision.js.map +1 -1
- package/dist/workflows/definition.d.ts +22 -3
- package/dist/workflows/definition.js +46 -3
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +2 -1
- package/dist/workflows/engine.js +199 -18
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/graph.js +8 -6
- package/dist/workflows/graph.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 +5 -2
- package/dist/workflows/index.js +4 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +5 -4
- package/dist/workflows/loader.js +118 -18
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.d.ts +3 -1
- package/dist/workflows/schema.js +64 -2
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +39 -2
- 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 +300 -2
- package/docs/CONTROLLERS.md +1 -1
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +371 -0
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
- package/docs/MONITOR.md +46 -18
- package/docs/WORKFLOW_COMPOSITION.md +333 -0
- 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 +308 -0
- package/docs/run-bundles.md +45 -10
- package/docs/workflows.md +106 -15
- package/examples/workflows/approved-plan.workflow.ts +58 -0
- package/examples/workflows/autoimplement.workflow.ts +1 -92
- 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 +6 -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 +8 -1
- package/skills/pi-workflows/SKILL.md +5 -3
- package/src/builtins/autodoc.workflow.ts +325 -0
- package/src/builtins/autoimplement.workflow.ts +1123 -0
- package/src/builtins/autoplan.workflow.ts +231 -0
- package/src/builtins/catalog.ts +9 -1
- package/src/builtins/index.ts +32 -0
- package/src/builtins/monitor.workflow.ts +396 -15
- 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 +54 -5
- package/src/viewer/render.ts +56 -3
- package/src/workflows/catalog.ts +7 -0
- package/src/workflows/composition.ts +642 -0
- package/src/workflows/decision-presentation.ts +501 -0
- package/src/workflows/decision.ts +12 -5
- package/src/workflows/definition.ts +118 -8
- package/src/workflows/engine.ts +257 -20
- package/src/workflows/graph.ts +8 -6
- package/src/workflows/human-decision.ts +819 -0
- package/src/workflows/index.ts +89 -0
- package/src/workflows/loader.ts +186 -18
- package/src/workflows/schema.ts +79 -2
- package/src/workflows/store.ts +45 -2
- package/src/workflows/tool-input.ts +202 -0
- package/src/workflows/types.ts +374 -2
- package/examples/workflows/elegant-solution.workflow.ts +0 -95
|
@@ -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.
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Human decision presentations
|
|
2
|
+
|
|
3
|
+
This contract is implemented. New human decision requests separate their canonical
|
|
4
|
+
subject from the complete readable message shown to an operator. Historical requests
|
|
5
|
+
that use `body` remain compatible through a deterministic readable formatter.
|
|
6
|
+
The implementation plan is in
|
|
7
|
+
[the human decision presentations plan](plans/2026-08-19-human-decision-presentations-plan.md).
|
|
8
|
+
|
|
9
|
+
A human decision contains machine data and a separate message for the operator.
|
|
10
|
+
Pi Workflows stores and validates the machine data. Pi and Telegram render only
|
|
11
|
+
the operator message, and future channels follow the same rule.
|
|
12
|
+
|
|
13
|
+
## Minimal example
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
approve: humanDecision({
|
|
17
|
+
audience: "operator",
|
|
18
|
+
choices,
|
|
19
|
+
request: () => ({
|
|
20
|
+
title: "Approve the implementation plan",
|
|
21
|
+
subject: {
|
|
22
|
+
task: "Add readable human decisions",
|
|
23
|
+
plan: selectedPlan,
|
|
24
|
+
planDigest,
|
|
25
|
+
revision: 1,
|
|
26
|
+
},
|
|
27
|
+
presentation: {
|
|
28
|
+
schema: "pi-workflows.decision-presentation.v1",
|
|
29
|
+
summary: "Replace raw JSON decisions with readable messages.",
|
|
30
|
+
blocks: [
|
|
31
|
+
{ kind: "section", title: "Changes" },
|
|
32
|
+
{
|
|
33
|
+
kind: "bullets",
|
|
34
|
+
items: [
|
|
35
|
+
"Keep structured data as the durable source.",
|
|
36
|
+
"Show the same readable message in Pi and Telegram.",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
kind: "fields",
|
|
41
|
+
items: [{ label: "Plan revision", value: "1" }],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The operator sees the summary and sections along with lists and fields. The operator does
|
|
50
|
+
not see `subject` unless the workflow explicitly copies selected text into
|
|
51
|
+
`presentation`.
|
|
52
|
+
|
|
53
|
+
## Request contract
|
|
54
|
+
|
|
55
|
+
A new request uses `pi-workflows.human-decision-request.v2`. It contains:
|
|
56
|
+
|
|
57
|
+
| Field | Required | Meaning |
|
|
58
|
+
| -------------------- | -------- | ----------------------------------------------- |
|
|
59
|
+
| `title` | Yes | Short decision title shown by every channel. |
|
|
60
|
+
| `subject` | Yes | Canonical JSON data used by the workflow. |
|
|
61
|
+
| `presentation` | Yes | Human-readable content defined below. |
|
|
62
|
+
| `audience` | Yes | Named audience resolved by the host. |
|
|
63
|
+
| `choices` | Yes | Typed choices and optional input contracts. |
|
|
64
|
+
| `revision` | Yes | Positive decision revision. |
|
|
65
|
+
| `subjectDigest` | Yes | SHA-256 digest of the canonical subject. |
|
|
66
|
+
| `presentationDigest` | Yes | SHA-256 digest of the normalized presentation. |
|
|
67
|
+
| `requestDigest` | Yes | SHA-256 digest that binds the complete request. |
|
|
68
|
+
|
|
69
|
+
The runtime adds the usual run binding and lifecycle fields as it does for
|
|
70
|
+
current decisions.
|
|
71
|
+
|
|
72
|
+
A request that supplies `subject` without `presentation` is invalid. A channel
|
|
73
|
+
must not derive a presentation from `subject`.
|
|
74
|
+
|
|
75
|
+
## Presentation contract
|
|
76
|
+
|
|
77
|
+
`pi-workflows.decision-presentation.v1` is a small ordered document:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
type DecisionPresentation = {
|
|
81
|
+
schema: "pi-workflows.decision-presentation.v1";
|
|
82
|
+
summary: string;
|
|
83
|
+
blocks: DecisionPresentationBlock[];
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`summary` is required and non-empty. Every channel shows it first. `blocks` can be empty. Block
|
|
88
|
+
order is meaningful and every channel preserves it.
|
|
89
|
+
|
|
90
|
+
### Blocks
|
|
91
|
+
|
|
92
|
+
The first version supports five flat block types.
|
|
93
|
+
|
|
94
|
+
#### Paragraph
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "kind": "paragraph", "text": "This change affects active workflows." }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Section
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{ "kind": "section", "title": "Verification" }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A section labels the blocks that follow it until the next section. Sections do
|
|
107
|
+
not contain nested blocks.
|
|
108
|
+
|
|
109
|
+
#### Bullets
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"kind": "bullets",
|
|
114
|
+
"items": ["Run the unit tests.", "Run the real-Pi test."]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Fields
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"kind": "fields",
|
|
123
|
+
"items": [
|
|
124
|
+
{ "label": "Repository", "value": "pi-workflows" },
|
|
125
|
+
{ "label": "Action", "value": "Update the decision contract" }
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Preformatted text
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"kind": "preformatted",
|
|
135
|
+
"text": "npm run check\nnpm run test:e2e"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Preformatted text preserves spacing and line breaks. It does not permit HTML,
|
|
140
|
+
Markdown, terminal escape sequences, or executable channel instructions.
|
|
141
|
+
|
|
142
|
+
## Validation and normalization
|
|
143
|
+
|
|
144
|
+
The authoring API validates the presentation before it creates a pending
|
|
145
|
+
request.
|
|
146
|
+
|
|
147
|
+
- Unknown fields and unknown block kinds are invalid.
|
|
148
|
+
- Display values are strings. Arbitrary nested objects are invalid.
|
|
149
|
+
- The summary, section titles, labels, bullet items, and field values must not
|
|
150
|
+
be empty after whitespace checks.
|
|
151
|
+
- The normalizer converts CRLF and CR line endings to LF.
|
|
152
|
+
- The normalizer preserves block order, item order, Unicode text, and other
|
|
153
|
+
spacing.
|
|
154
|
+
- Terminal control characters are invalid. Paragraph and preformatted text can
|
|
155
|
+
contain LF and tab. Titles and labels plus bullet items cannot contain control
|
|
156
|
+
characters.
|
|
157
|
+
- The complete normalized presentation is limited to 64,000 UTF-16 code units.
|
|
158
|
+
- A presentation can contain at most 256 blocks.
|
|
159
|
+
- One bullets or fields block can contain at most 256 items.
|
|
160
|
+
- One string can contain at most 16,000 UTF-16 code units.
|
|
161
|
+
- A renderer can produce at most 20 transport parts for one recipient.
|
|
162
|
+
|
|
163
|
+
If a presentation exceeds a limit, request creation fails with the exact limit
|
|
164
|
+
and observed value. The runtime does not create a pending request and does not
|
|
165
|
+
send a partial message.
|
|
166
|
+
|
|
167
|
+
## Digests
|
|
168
|
+
|
|
169
|
+
The runtime uses canonical JSON with a trailing newline, as defined by the run
|
|
170
|
+
bundle contract.
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
subjectDigest = sha256(canonicalJson(subject))
|
|
174
|
+
presentationDigest = sha256(canonicalJson(normalizedPresentation))
|
|
175
|
+
requestDigest = sha256(canonicalJson(requestBasis))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`requestBasis` contains the request schema and run binding. It also contains the
|
|
179
|
+
audience, title, subject, normalized presentation, choices, typed input
|
|
180
|
+
contracts, revision and optional expiry. It does not contain the three digest
|
|
181
|
+
fields or the derived decision ID.
|
|
182
|
+
|
|
183
|
+
An answer must match the exact request revision and request digest. A change to
|
|
184
|
+
only the subject, visible text, choice label, or input prompt makes an older
|
|
185
|
+
answer stale.
|
|
186
|
+
|
|
187
|
+
Channels can show a short prefix of `presentationDigest` so an operator can
|
|
188
|
+
compare the decision in Pi and Telegram. The viewer shows the same fingerprint.
|
|
189
|
+
The complete digest remains in the durable request and accepted answer evidence.
|
|
190
|
+
|
|
191
|
+
## Channel rendering
|
|
192
|
+
|
|
193
|
+
The workflow creates one presentation. It does not contain channel names,
|
|
194
|
+
Telegram markup, terminal colors, message limits, buttons, or callback data.
|
|
195
|
+
|
|
196
|
+
Each channel can change spacing and wrapping as well as styling, navigation, or
|
|
197
|
+
controls. It must preserve the title and summary together with block order and
|
|
198
|
+
contents. It must also preserve every choice and input prompt. A channel must
|
|
199
|
+
not omit or reinterpret approval-relevant content.
|
|
200
|
+
|
|
201
|
+
### Telegram
|
|
202
|
+
|
|
203
|
+
Telegram uses plain text without a parse mode. Text that resembles HTML,
|
|
204
|
+
Markdown, a command, or a mention remains inert text.
|
|
205
|
+
|
|
206
|
+
The renderer:
|
|
207
|
+
|
|
208
|
+
1. Renders the complete normalized presentation.
|
|
209
|
+
2. Splits at block and paragraph boundaries before splitting a line.
|
|
210
|
+
3. Splits a long line at Unicode-safe boundaries only when required.
|
|
211
|
+
4. Reserves space for `Part n/N` and a short presentation fingerprint.
|
|
212
|
+
5. Keeps every Telegram message below the Telegram limit.
|
|
213
|
+
6. Adds choice buttons only to the final part.
|
|
214
|
+
7. Stores durable part indexes and content digests.
|
|
215
|
+
8. Keeps chat and message IDs only in the private channel projection.
|
|
216
|
+
|
|
217
|
+
The renderer never adds an ellipsis in place of omitted decision content.
|
|
218
|
+
|
|
219
|
+
The channel records intent before sending. It records each part after an
|
|
220
|
+
unambiguous response. If a send result is ambiguous, it marks delivery unknown
|
|
221
|
+
and does not retry that part or later parts automatically. Another configured
|
|
222
|
+
channel can still answer the decision.
|
|
223
|
+
|
|
224
|
+
### Pi
|
|
225
|
+
|
|
226
|
+
Pi uses the documented extension TUI API. A custom decision component shows the
|
|
227
|
+
complete presentation and fingerprint together with the choices and optional
|
|
228
|
+
text prompt. It wraps and scrolls while responding to resize, theme, cancellation,
|
|
229
|
+
and `AbortSignal` events.
|
|
230
|
+
|
|
231
|
+
When Telegram or another channel accepts the decision, the signal closes the Pi
|
|
232
|
+
dialog. Pi Workflows does not modify Pi core or use undocumented TUI state.
|
|
233
|
+
|
|
234
|
+
### Other channels
|
|
235
|
+
|
|
236
|
+
A new channel consumes the normalized presentation and typed interaction
|
|
237
|
+
contract. It must pass the same conformance tests. It cannot access the subject
|
|
238
|
+
for display.
|
|
239
|
+
|
|
240
|
+
## Plan presentation
|
|
241
|
+
|
|
242
|
+
Pi Workflows provides a reusable plan presenter for built-in workflows. It
|
|
243
|
+
derives a presentation from the same typed plan stored as the subject.
|
|
244
|
+
|
|
245
|
+
The presenter uses these sections when data exists:
|
|
246
|
+
|
|
247
|
+
1. Goal
|
|
248
|
+
2. Proposed changes
|
|
249
|
+
3. Verification
|
|
250
|
+
4. Boundaries
|
|
251
|
+
5. Risks and mitigations
|
|
252
|
+
6. Evidence
|
|
253
|
+
|
|
254
|
+
It omits absent sections. It never serializes the plan object. `plan-approval`,
|
|
255
|
+
`monitor`, `autoplan`, `autodoc`, and `autoimplement` reuse this presenter where
|
|
256
|
+
they ask a person to approve a plan.
|
|
257
|
+
|
|
258
|
+
## Compatibility
|
|
259
|
+
|
|
260
|
+
Current v1 requests remain immutable.
|
|
261
|
+
|
|
262
|
+
- A v1 string body becomes one readable paragraph at delivery time.
|
|
263
|
+
- A v1 object body uses a deterministic compatibility formatter. It converts
|
|
264
|
+
stable key order into readable labels and fields with sections and lists.
|
|
265
|
+
- An oversized v1 body remains answerable. The formatter keeps a bounded readable
|
|
266
|
+
prefix and adds an explicit omission notice with the full body digest and size.
|
|
267
|
+
It never hides omitted content behind an ellipsis.
|
|
268
|
+
- The compatibility formatter reads only the historical `body`, which was
|
|
269
|
+
already the display field. It never reads a new structured subject.
|
|
270
|
+
- V1 request bytes and digests do not change.
|
|
271
|
+
- Pending and accepted v1 decisions continue to use v1 validation and digest
|
|
272
|
+
rules.
|
|
273
|
+
- New preferred authoring emits v2. The existing body form remains a deprecated
|
|
274
|
+
compatibility overload until a separate removal is approved.
|
|
275
|
+
|
|
276
|
+
No migration rewrites run bundles or decision records.
|
|
277
|
+
|
|
278
|
+
## Privacy and security
|
|
279
|
+
|
|
280
|
+
The presentation is an explicit display allowlist. Data does not become visible
|
|
281
|
+
because it exists in the subject.
|
|
282
|
+
|
|
283
|
+
Channel adapters do not receive the subject. Durable public records do not
|
|
284
|
+
contain Telegram chat IDs, message IDs, bot tokens, credential profile values,
|
|
285
|
+
or private audience configuration. The private channel projection retains only
|
|
286
|
+
the transport identifiers required for recovery and settlement.
|
|
287
|
+
|
|
288
|
+
Workflow authors remain responsible for not putting a secret in the explicit
|
|
289
|
+
presentation. Renderers treat all text as untrusted and inert.
|
|
290
|
+
|
|
291
|
+
## Failure behavior
|
|
292
|
+
|
|
293
|
+
- Invalid or excessive content fails before a request becomes pending.
|
|
294
|
+
- A renderer that cannot represent every block fails closed.
|
|
295
|
+
- Confirmed multipart delivery requires a receipt for every part.
|
|
296
|
+
- Ambiguous Telegram delivery remains unknown and is not retried blindly.
|
|
297
|
+
- A channel failure does not choose a default answer.
|
|
298
|
+
- Rules for the first valid answer and stale answers remain unchanged, as do
|
|
299
|
+
cancellation and settlement rules together with exactly-once continuation.
|
|
300
|
+
|
|
301
|
+
## Verification
|
|
302
|
+
|
|
303
|
+
Implementation must pass:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm run check
|
|
307
|
+
npm run test:e2e
|
|
308
|
+
cargo test --manifest-path tui/Cargo.toml
|
|
309
|
+
npx slophammer-ts@latest dry .
|
|
310
|
+
npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
|
|
311
|
+
npx -y @simpledoc/simpledoc check
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Tests must cover schema validation, canonical digests, readable legacy bodies,
|
|
315
|
+
plan rendering, Pi and Telegram content parity, unsafe text, Unicode, multipart
|
|
316
|
+
delivery, ambiguous sends, recovery, stale answers, viewer output, and proof
|
|
317
|
+
that no channel serializes a subject as JSON.
|
|
318
|
+
|
|
319
|
+
## Boundaries
|
|
320
|
+
|
|
321
|
+
This contract does not add a new workflow node, template language, rich text
|
|
322
|
+
framework, translation service, delivery relay, Telegram resource, credential,
|
|
323
|
+
or Pi core change.
|