@signalridge/pi-workflows 1.7.4 → 1.8.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/CHANGELOG.md +26 -0
- package/README.md +145 -10
- package/package.json +7 -6
- package/skills/workflow-authoring/SKILL.md +1 -1
- package/skills/workflow-authoring/references/capabilities.md +1 -1
- package/skills/workflow-authoring/references/capability-details.md +1 -1
- package/skills/workflow-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +1 -1
- package/src/arming.ts +264 -3
- package/src/engine.ts +156 -14
- package/src/errors.ts +26 -0
- package/src/index.ts +381 -156
- package/src/journal.ts +5 -0
- package/src/runtime.ts +40 -20
- package/src/safe-text.ts +4 -35
- package/src/ui.ts +38 -14
- package/src/vm-bootstrap.ts +218 -0
- package/src/vm-helper.ts +502 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.0
|
|
4
|
+
### Minor Changes
|
|
5
|
+
|
|
6
|
+
- c2fd867: Buffer and coalesce completed workflow notifications until an owned session is safely idle. Persist context-only results and confirm them on the active branch before acknowledging pending markers, then wake through the ordinary user-message preparation hooks. Fence delivery and admission synchronously at bus-first shutdown and cancel stale handoffs on committed navigation. Validate private, bounded one-use wake admissions at input consumption to reject retired notifications held by earlier async input hooks, with internal command/template expansion disabled.
|
|
7
|
+
|
|
8
|
+
Move keyword arming and standing effort guidance into hidden, turn-local model messages without appending raw reminders to native user text. Preserve queued follow-up and steering guidance through a bounded public context overlay, without changing native persistence, fork selection or editor history. Distinguish standing effort permission from an explicit keyword instead of inventing a user trigger. Preserve images and unrelated reminders, exclude synthetic input, and deny ambiguous or stale preparation admissions. Add real SDK/faux-provider and native user-component regressions for clean rendering, provider guidance, queued and consumed input, overlapping prompts, navigation and reload. The fix applies after updating/reloading the extension; previously persisted tagged user history is not rewritten. Idle and queued arming now use request-local overlays only, preserve repeated literal directive strings verbatim, and remove only legacy extension-owned hidden guidance from public summarization/compaction preparation.
|
|
9
|
+
|
|
10
|
+
Move workflow-authored JavaScript and VM-created callbacks into an owned, plain-JavaScript Node helper while retaining runtime policy, dispatch, state, scheduling, journals, and managed cleanup in the parent. Bound initial evaluation and asynchronous/callback CPU stalls with a local heartbeat, startup deadline, memory/IPC/log limits, and actual-handle TERM/KILL/close ownership. Legitimate agent awaits continue heartbeats; Bun/compiled hosts require Node on PATH and debugger flags remain inherited. Preserve callback error classification, intermediate JS values, synchronous phase/log, shared budgets, replay and one-level nested execution; final results return their validated JSON projection. Drain all accepted native RPC operations and nested frames before helper/frame success, separating callback response settlement from reference-counted execution-context lifetime through accepted RPC descendant chains and cancelling/confirming owned work on failure. Track actual unresolved local promise lifetimes while the root is running, including descendants whose first native call follows a locally suspended promise and a later root-native await. Release completed contexts event-by-event after promise settlement and native response publication, retaining the original parent async-local budget/control attribution without reopening root admission or accepting retired/foreign identities. At root result retire local-only continuations before publication; only accepted native work and its unsettled callbacks retain descendant authority, so ignored pure-JavaScript awaits do not hang successful teardown. Bound live contexts and tracked promises separately from pending callbacks, recycle sequential contexts, and clean them at frame teardown. Add exact delayed-first-RPC, nested private-budget/control, local-only post-root rejection, retained-context capacity, ignored-promise success, and cancellation/native-cleanup/helper-close regressions. Observe ignored native RPC rejections without replacing author promises. Reconstruct intermediate snapshots with workflow-realm built-ins and standard error subclasses, retaining original callback closures. Grant parent-lag watchdog grace only after actual heartbeat progress, with one bounded I/O turn. Keep transient journal retries in accepted-fact order and drain them before terminal success so restored calls cannot regress to running. Add disposable strict-Node regressions for forgotten nested/control helpers, infinite-await cancellation, realm identity, ignored rejections and repeated parent stalls, plus terminal checkpoint/timeout and smoke-equivalent journal recovery. This is not an OS sandbox or a protocol change.
|
|
11
|
+
|
|
12
|
+
Apply journaled terminal recovery before accepted failed-run resume, give checkpoints local attempt identity, record timeout terminal failure after exact-owner quiescence, and union run/per-call tool denials. Rearm owned delivery for every background resume, record per-run batch membership across partial ACK failures, and clear retired provider errors on restart. Sanitize display copies through pi-ui and bound detail viewports while keeping controls reachable. Add disposable-process external-deadline regressions for CPU loops, replay/schema/nesting/cancel/long-await, Node/Bun startup and debugger pause, alongside real SDK summary/compaction and viewport tests.
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [c2fd867]
|
|
17
|
+
- @signalridge/pi-ui@1.4.0
|
|
18
|
+
|
|
19
|
+
## 1.7.5
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 3364cc0: Declare `@sinclair/typebox` as a host-provided peer dependency with a `*` range instead of a runtime dependency, avoiding duplicate runtime modules and Pi extension-loader warnings.
|
|
23
|
+
- 3364cc0: Adopt Pi 1.0.4's host-provided dependency contract: declare imported Pi and TypeBox modules as `*` peers rather than installing duplicate runtime modules, and align Pi development pins with the repository's tested 1.0.4 baseline. The wildcard identifies host module ownership; it does not promise compatibility with every Pi version.
|
|
24
|
+
- 3364cc0: Bind code-review discovery and workflow executions (including resumes and nested scripts) to the session working directory. Preserve structured user content when deduplicating the workflow directive.
|
|
25
|
+
- Updated dependencies [3364cc0]
|
|
26
|
+
- Updated dependencies [3364cc0]
|
|
27
|
+
- @signalridge/pi-subagents-protocol@1.5.1
|
|
28
|
+
|
|
3
29
|
## 1.7.4
|
|
4
30
|
### Patch Changes
|
|
5
31
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @signalridge/pi-workflows
|
|
2
2
|
|
|
3
3
|
JavaScript orchestration for Pi. A workflow is a raw JavaScript module executed
|
|
4
|
-
in a determinism-guarded `node:vm` realm; the runtime supplies `agent()`,
|
|
4
|
+
in a determinism-guarded `node:vm` realm inside an owned Node helper; the runtime supplies `agent()`,
|
|
5
5
|
`parallel()`, `pipeline()`, `orchestrate()`, `workflow()`, quality helpers (`verify`,
|
|
6
6
|
`judgePanel`, `loopUntilDry`, `completenessCheck`, `retry`, `gate`), human
|
|
7
7
|
`checkpoint()`, `phase()`, `log()`, `args`, `cwd`, restricted `process`, and
|
|
@@ -40,6 +40,52 @@ registration.
|
|
|
40
40
|
|
|
41
41
|
Pi activates each package from its own `pi.extensions` manifest.
|
|
42
42
|
|
|
43
|
+
### JavaScript execution boundary
|
|
44
|
+
|
|
45
|
+
Only workflow-authored JavaScript and its callbacks run in the helper. Agent
|
|
46
|
+
policy/dispatch, scheduling, limits, shared state, journals, and managed-child
|
|
47
|
+
cleanup stay in the parent. Callback arguments cross bounded, validated IPC as
|
|
48
|
+
opaque callback identities; the parent never executes workflow function objects.
|
|
49
|
+
Nested saved workflows get their own helper frame, share the same parent-owned
|
|
50
|
+
limits, and remain limited to one nested level. There are no new public globals,
|
|
51
|
+
protocol changes, TypeScript helper loaders, or extra runtime dependencies.
|
|
52
|
+
|
|
53
|
+
A Node host uses its Node executable. Bun or a compiled host requires `node` on
|
|
54
|
+
PATH; missing Node fails with an actionable diagnostic. `NODE_OPTIONS` is inherited
|
|
55
|
+
unchanged, including debugger flags. A helper that does not announce readiness
|
|
56
|
+
within five seconds fails (including `--inspect-brk` awaiting a debugger).
|
|
57
|
+
|
|
58
|
+
Initial synchronous evaluation retains its one-second VM guard. A local-process
|
|
59
|
+
heartbeat detects JavaScript CPU stalls after awaits and in callbacks (including
|
|
60
|
+
DAG, gate, retry, and loop callbacks); a stalled heartbeat fails after 1.5 seconds.
|
|
61
|
+
This is not a wall-clock deadline for legitimate agent awaits, and never polls a
|
|
62
|
+
model. Cancellation/failure closes the actual owned helper through TERM, KILL
|
|
63
|
+
escalation after 250ms, and a bounded 1.5-second close wait. Managed children must
|
|
64
|
+
still settle exact-owner cleanup before terminal publication or resumability; a
|
|
65
|
+
cleanup failure is fenced, not claimed as success. A returned value closes new
|
|
66
|
+
root-call admission but drains every accepted native operation and nested frame
|
|
67
|
+
before success; callbacks needed by those operations remain available. Infinite
|
|
68
|
+
awaits in forgotten helpers still require cancellation. Ignored native RPC promises
|
|
69
|
+
are observed without changing their awaited/caught values or helper error policy.
|
|
70
|
+
Repeated parent event-loop lag earns only one I/O turn per actual heartbeat advance,
|
|
71
|
+
never a synthetic heartbeat or an indefinitely renewed stall exemption.
|
|
72
|
+
|
|
73
|
+
Helpers have a 128MiB V8 heap limit, 4MiB IPC message limit, bounded 32MiB queued
|
|
74
|
+
and retained request payloads, 8,192 callbacks/pending requests, 100,000 non-heartbeat
|
|
75
|
+
messages, and intermediate value limits of 100,000 nodes/64 levels. Log entries
|
|
76
|
+
are bounded to 64KiB characters and 1MiB total bytes per frame. Intermediate arrays,
|
|
77
|
+
plain objects, maps, sets, dates, regexes, bigint values, registered callback
|
|
78
|
+
references, and error names/classification cross IPC. Input and callback snapshots
|
|
79
|
+
use the workflow realm's built-in constructors (including standard error subclasses),
|
|
80
|
+
preserve cycles, array holes and undefined, and return registered callbacks to their
|
|
81
|
+
original closures. Data transfers do not preserve arbitrary custom prototypes or
|
|
82
|
+
cross-transfer object identity.
|
|
83
|
+
VM-origin exceptions retain their provenance rather than acquiring native provider
|
|
84
|
+
classification from words like "quota"; exception coercion stays in the helper.
|
|
85
|
+
Final results must serialize to JSON and return their JSON projection. `phase()`
|
|
86
|
+
and `log()` remain synchronous; budget reads mirror parent accounting at RPC replies.
|
|
87
|
+
These bounds are guardrails for trusted scripts, **not an OS/security sandbox**.
|
|
88
|
+
|
|
43
89
|
## Writing a workflow
|
|
44
90
|
|
|
45
91
|
A workflow script starts with the only legal export — a literal-only meta
|
|
@@ -84,9 +130,9 @@ user's own LLM and run on the user's own machine.
|
|
|
84
130
|
plain JSON Schema; the reply is parsed and validated client-side with bounded
|
|
85
131
|
repair across attempts, and exhaustion throws `SCHEMA_NONCOMPLIANCE`.
|
|
86
132
|
`strength` is the provider-neutral workflow route; `thread` chains sequential turns
|
|
87
|
-
within a named conversation thread.
|
|
88
|
-
|
|
89
|
-
|
|
133
|
+
within a named conversation thread. Per-call `model`, `thinking`, and `tier`
|
|
134
|
+
options are rejected; workflow meta/phase model or thinking fields are also
|
|
135
|
+
rejected and are never a workflow policy source. Pre-schema-v4
|
|
90
136
|
journal runs are quarantined and cannot be resumed.
|
|
91
137
|
- `parallel(thunks)` — run thunks concurrently, preserve input order. Recoverable
|
|
92
138
|
thunk failures become `null`; non-recoverable failures (token budget, agent
|
|
@@ -125,7 +171,13 @@ original invocation `args` and execution limits are frozen and reused on manual
|
|
|
125
171
|
or automatic resume. Pass `resumeFromRunId` with the edited script; unchanged
|
|
126
172
|
calls report zero tokens,
|
|
127
173
|
and edited calls rotate their managed spawn key generation so pi-subagents
|
|
128
|
-
never raises a fingerprint conflict.
|
|
174
|
+
never raises a fingerprint conflict. A failed run accepted through
|
|
175
|
+
`resumeFromRunId` first appends/applies a legal interrupted recovery after owned
|
|
176
|
+
cleanup and journal settlement; terminal status is never mutated directly to
|
|
177
|
+
running. Intentional stops and unconfirmed cleanup remain non-resumable. Local
|
|
178
|
+
checkpoints journal attempt identity without inventing a managed child, and a
|
|
179
|
+
timed-out call journals its terminal failure after quiescence before returning
|
|
180
|
+
recoverable `null`.
|
|
129
181
|
|
|
130
182
|
## Skills
|
|
131
183
|
|
|
@@ -189,13 +241,63 @@ workflow, and the review skill before accepting a topology or recovery change.
|
|
|
189
241
|
`session_start` (Pi cannot unregister commands, so registration happens there
|
|
190
242
|
rather than at factory time, and existing commands are never overwritten).
|
|
191
243
|
|
|
244
|
+
### Background result notifications
|
|
245
|
+
|
|
246
|
+
Completed background runs are buffered in branch-local pending-delivery markers
|
|
247
|
+
(up to 256, expiring after seven days). Notifications wait for an owned, idle
|
|
248
|
+
session with no queued input, input preparation, compaction, or navigation in
|
|
249
|
+
progress. A cancellable one-shot timer defers handoff out of the notification-only
|
|
250
|
+
`agent_settled` handler and rechecks ownership and readiness; later handlers can navigate
|
|
251
|
+
or compact without receiving an old result. Up to eight results are coalesced
|
|
252
|
+
per notification, each preview bounded to 8,000 characters. There is no model
|
|
253
|
+
polling or repeating delivery timer.
|
|
254
|
+
|
|
255
|
+
Results first enter context as a `pi-workflows:delivery` custom message with
|
|
256
|
+
`triggerTurn: false`. The active branch must actually contain that message before
|
|
257
|
+
pending markers are acknowledged. Delivery details record individual run IDs,
|
|
258
|
+
so a partial acknowledgement failure cannot resend an already persisted member
|
|
259
|
+
of a batch. Every accepted background resume (tool, navigator, or provider timer)
|
|
260
|
+
rearms its generation-owned observer; suspension is not final delivery.
|
|
261
|
+
Pi's void `sendMessage` return is not an
|
|
262
|
+
acceptance promise. A short `sendUserMessage` notification then uses the normal
|
|
263
|
+
input and `before_agent_start` path, so prompt preparation hooks still run.
|
|
264
|
+
Internal wakes disable command/template expansion and carry private, unique,
|
|
265
|
+
one-use admission tokens (at most 256 in memory). Our input hook checks the
|
|
266
|
+
original generation, engine, context, and branch ownership before accepting a
|
|
267
|
+
wake and removes its token from model-facing text. If an earlier input hook
|
|
268
|
+
awaits while navigation or shutdown retires the owner, the obsolete
|
|
269
|
+
extension-sourced wake is handled without starting a turn. Interactive/RPC
|
|
270
|
+
input and other extensions' messages are not consumed by this guard.
|
|
271
|
+
|
|
272
|
+
This is a handoff check, not a navigation lock: public hooks cannot fence an
|
|
273
|
+
arbitrary later async input or preparation handler that navigates after our
|
|
274
|
+
input hook has accepted the wake. Earlier transforms must preserve the private
|
|
275
|
+
token until this check; a hook that removes it also removes its ownership evidence.
|
|
276
|
+
|
|
277
|
+
Unacknowledged markers recover from the active branch on session start or tree
|
|
278
|
+
commit; siblings never inherit abandoned-branch results. A blocked or failed
|
|
279
|
+
handoff waits for a later lifecycle boundary, not a retry loop. If input or
|
|
280
|
+
navigation is abandoned without a completion event, delivery stays conservative
|
|
281
|
+
until a fresh prompt (and its eventual settlement) or session/tree rebind.
|
|
282
|
+
|
|
283
|
+
Shutdown immediately retires both admission and delivery before aborting the
|
|
284
|
+
engine, including a peer's bus-first shutdown handshake. Cleanup retains the
|
|
285
|
+
outgoing journal until native shutdown finishes; late results cannot wake the
|
|
286
|
+
retiring session.
|
|
287
|
+
|
|
192
288
|
### Keyword arming
|
|
193
289
|
|
|
194
290
|
Typing the bounded word `workflow` or `workflows` in an ordinary message counts
|
|
195
|
-
as an explicit opt-in to multi-agent orchestration
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
291
|
+
as an explicit opt-in to multi-agent orchestration. A request-local hidden
|
|
292
|
+
`context` overlay tells the model the `workflow` tool is authorized for this turn;
|
|
293
|
+
`before_agent_start` admits idle guidance but persists nothing. Keyword and standing
|
|
294
|
+
`/effort high|ultra` guidance never append reminders or effort text to native user
|
|
295
|
+
messages. Original text/images remain exact, even repeated literal copies of either
|
|
296
|
+
directive; only extension-owned custom guidance is filtered/rebuilt. Legacy owned
|
|
297
|
+
hidden arming entries are excluded from public branch-summary/compaction preparation,
|
|
298
|
+
without rewriting historical user text or stored entries.
|
|
299
|
+
|
|
300
|
+
Arming **authorizes; it does not compel**. The hidden guidance says the model may run
|
|
199
301
|
a workflow and may still decline, so "how do workflows work?" stays an ordinary
|
|
200
302
|
question with an ordinary answer. Nothing is swallowed and no UI opens.
|
|
201
303
|
This is **model guidance, not an execution permission gate**: the extension does
|
|
@@ -211,6 +313,39 @@ sentence-ending dot is not a filename dot: "please run a workflow." arms, and
|
|
|
211
313
|
<word>` (matched exactly — only the default word also matches its plural) or
|
|
212
314
|
turn it off entirely with `/workflows-trigger off`.
|
|
213
315
|
|
|
316
|
+
Only observed interactive/RPC input can arm a turn; extension-sourced
|
|
317
|
+
notifications cannot. Idle admission is one-use and requires unchanged prompt
|
|
318
|
+
text, images, session and leaf. Queued follow-up and steering input can also arm:
|
|
319
|
+
Pi continues them without a fresh `before_agent_start`, so one bounded queued
|
|
320
|
+
admission binds to its public `message_start` and a unique persisted user entry
|
|
321
|
+
on the owning branch. The public `context` hook adds a model-only overlay; it
|
|
322
|
+
never enters native history. Both paths retire guidance after settlement,
|
|
323
|
+
navigation or reload. Standing `/effort high|ultra` permission identifies the
|
|
324
|
+
standing setting, not a keyword the user did not type.
|
|
325
|
+
|
|
326
|
+
Pi's public preparation event has no input source/request ID, and chained input
|
|
327
|
+
handlers have no public post-`handled` notification. Overlapping or unmatched
|
|
328
|
+
input is therefore denied rather than transferring authorization to another
|
|
329
|
+
prompt, even if the text is identical. Multiple unresolved queued observations
|
|
330
|
+
are ambiguous and suppress queued guidance until settlement. A consumed idle
|
|
331
|
+
prompt may similarly suppress arming on the next ordinary turn. Template
|
|
332
|
+
expansion, later text/image transforms, or non-unique native-message evidence
|
|
333
|
+
also fail closed. These limits affect advisory guidance only, never direct tool
|
|
334
|
+
authorization.
|
|
335
|
+
|
|
336
|
+
The fix applies to **new messages after updating and reloading the extension**;
|
|
337
|
+
source changes do not update an already installed/running package. Previously
|
|
338
|
+
persisted user messages containing raw reminders remain unchanged. Pi's public
|
|
339
|
+
UI API does not provide a history rewrite: those messages can still appear in
|
|
340
|
+
the native TUI, fork selector or editor history. No private renderer patch or
|
|
341
|
+
generic reminder stripping is used. Regression tests exercise the real SDK,
|
|
342
|
+
faux provider, and native user-message component, not a physical terminal or a
|
|
343
|
+
live model service.
|
|
344
|
+
|
|
345
|
+
Navigator/result metadata is sanitized only in display copies through `pi-ui`;
|
|
346
|
+
stored results and journals stay original. Detail screens keep actions reachable
|
|
347
|
+
in a row-bounded viewport; PgUp/PgDn scroll the detail independently of ↑↓ actions.
|
|
348
|
+
|
|
214
349
|
Saved workflows use project scope first and a user scope fallback; writes are atomic and names are path-safe.
|
|
215
350
|
|
|
216
351
|
## Model routing
|
|
@@ -396,4 +531,4 @@ used it.
|
|
|
396
531
|
- **Agent registry and tier catalogue ownership.** `agentType` and the tier catalogue resolve in pi-subagents; the workflow package supplies only a tier key its own `strengths` table chose, avoiding a second model/thinking policy.
|
|
397
532
|
- **Schema validation is client-side.** The managed request does not assume a structured-output tool; the runtime asks for JSON, parses, validates (including `additionalProperties`), and repairs across bounded attempts.
|
|
398
533
|
|
|
399
|
-
State is persisted as `pi.appendEntry("pi-workflows:journal", ...)` custom entries (schema v4), including frozen invocation args, resolved tier identity, terminal result previews, script revisions, named nested-workflow result boundaries, and durable removals. Interrupted/provider-limited runs replay from the journal; pre-schema-v4 journals are quarantined rather than replayed. Foreground `AbortSignal`s stop owned children through explicit owner-scoped stop/quiescence, and dispose/branch replacement reject stale waiters.
|
|
534
|
+
State is persisted as `pi.appendEntry("pi-workflows:journal", ...)` custom entries (schema v4), including frozen invocation args, resolved tier identity, terminal result previews, script revisions, named nested-workflow result boundaries, and durable removals. Interrupted/provider-limited runs replay from the journal; pre-schema-v4 journals are quarantined rather than replayed. Foreground `AbortSignal`s stop owned children through explicit owner-scoped stop/quiescence, and dispose/branch replacement reject stale waiters. Navigation preflight vetoes while agents or workflows are active or cleaning up, without interrupting them. An accepted tree commit fences old callbacks in either extension load order and restores only the committed branch; preflight itself is not a commit point.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalridge/pi-workflows",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Deterministic JavaScript orchestration workflows backed by pi-subagents managed spawning.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
"lifecycle": "stable"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@earendil-works/pi-coding-agent": "
|
|
19
|
-
"@earendil-works/pi-tui": "
|
|
20
|
-
"@signalridge/pi-subagents": ">=1.9.0"
|
|
18
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
19
|
+
"@earendil-works/pi-tui": "*",
|
|
20
|
+
"@signalridge/pi-subagents": ">=1.9.0",
|
|
21
|
+
"@sinclair/typebox": "*"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@signalridge/pi-subagents-protocol": "^1.5.
|
|
24
|
-
"@
|
|
24
|
+
"@signalridge/pi-subagents-protocol": "^1.5.1",
|
|
25
|
+
"@signalridge/pi-ui": "^1.4.0",
|
|
25
26
|
"acorn": "^8.16.0"
|
|
26
27
|
},
|
|
27
28
|
"pi": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-authoring
|
|
3
3
|
description: Design, write, review, and debug Pi JavaScript workflows that coordinate multiple subagents. Use when a request needs named dependencies, bounded fan-out, per-item pipelines, quality gates, saved or nested workflows, pause/resume, or workflow progress; do not use it merely to run an existing named workflow.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.8.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow authoring
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Workflow capability index
|
|
3
3
|
|
|
4
4
|
Contract format: `2.0.0`<br>
|
|
5
|
-
Contract content / skill / extension: `1.
|
|
5
|
+
Contract content / skill / extension: `1.8.0`
|
|
6
6
|
|
|
7
7
|
This compact generated index covers supported runtime globals and workflow-tool inputs. For constraints, compatibility behavior, internal boundaries, and dynamic-reference ownership, follow the [exhaustive generated facts](capability-details.md).
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Exhaustive workflow capability facts
|
|
3
3
|
|
|
4
4
|
Contract format: `2.0.0`<br>
|
|
5
|
-
Contract content / skill / extension: `1.
|
|
5
|
+
Contract content / skill / extension: `1.8.0`
|
|
6
6
|
|
|
7
7
|
Every exact fact below is projected from the installed extension's capability contract. Explanatory judgment belongs in the hand-written references next to this file.
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-patterns
|
|
3
3
|
description: Choose and run one of Pi's built-in multi-agent workflows for research, adversarial review, code review, multi-perspective analysis, or codebase auditing. Use when the request matches a named pattern; use workflow-authoring instead for a new script or custom topology.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.8.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Built-in workflow patterns
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-review
|
|
3
3
|
description: Audit an existing Pi JavaScript workflow for topology, prompt/data flow, limits, failure handling, cancellation, resume safety, and publication compatibility. Use when reviewing or debugging a workflow script or saved run; do not use to run a standard named workflow.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.8.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow review
|
package/src/arming.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
BeforeAgentStartEvent,
|
|
4
|
+
ContextEvent,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
InputEvent,
|
|
7
|
+
MessageStartEvent,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* arming.ts — a keyword that tells the model a workflow is appropriate this turn.
|
|
3
12
|
* It is advisory prompt guidance, not an execution-time permission gate.
|
|
4
13
|
*
|
|
5
14
|
* Typing the bounded word `workflow` (or `workflows`, or a configured synonym)
|
|
6
15
|
* in an ordinary message is an explicit opt-in to multi-agent orchestration, so
|
|
7
|
-
*
|
|
16
|
+
* hidden model context tells the model the tool is available for this
|
|
8
17
|
* turn. Two properties matter, and both are easy to get wrong:
|
|
9
18
|
*
|
|
10
19
|
* 1. **Arming authorizes; it does not compel.** The directive says the model
|
|
@@ -23,14 +32,20 @@
|
|
|
23
32
|
export const DEFAULT_TRIGGER_WORD = "workflow";
|
|
24
33
|
|
|
25
34
|
/**
|
|
26
|
-
*
|
|
35
|
+
* Hidden guidance for an armed turn. Phrased as permission with an explicit escape
|
|
27
36
|
* hatch: the model is told the user opted in, and told it may still answer
|
|
28
37
|
* directly when a workflow would be overkill.
|
|
29
38
|
*/
|
|
30
39
|
export const WORKFLOW_ARMED_DIRECTIVE =
|
|
31
40
|
"<system-reminder>You typed the workflow trigger word, which counts as an explicit opt-in to multi-agent orchestration: the `workflow` tool is authorized for this turn. It runs in the background by default — the turn ends and the result is delivered back into the conversation when it finishes, which is expected rather than a stall, so you need not stay and block. Pass background:false only when the user is waiting for the result inline. This is permission, not an instruction: if the request is conversational, trivial, or better answered directly, answer it directly and do not run a workflow.</system-reminder>";
|
|
32
41
|
|
|
33
|
-
/**
|
|
42
|
+
/** Standing effort is permission, but does not imply the user typed a keyword. */
|
|
43
|
+
export const WORKFLOW_EFFORT_ARMED_DIRECTIVE = WORKFLOW_ARMED_DIRECTIVE.replace(
|
|
44
|
+
"You typed the workflow trigger word, which counts as an explicit opt-in to multi-agent orchestration:",
|
|
45
|
+
"The standing /effort setting permits multi-agent orchestration:",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** Legacy explicit-call helper. Never apply it to native user text or context. */
|
|
34
49
|
export function deduplicateWorkflowDirective(text: string): string {
|
|
35
50
|
let seen = false;
|
|
36
51
|
return text.replaceAll(WORKFLOW_ARMED_DIRECTIVE, (directive) => {
|
|
@@ -78,3 +93,249 @@ function triggerRegex(word: string): RegExp {
|
|
|
78
93
|
export function hasTriggerWord(text: string, configuredWord?: string): boolean {
|
|
79
94
|
return triggerRegex(configuredWord ?? DEFAULT_TRIGGER_WORD).test(text);
|
|
80
95
|
}
|
|
96
|
+
|
|
97
|
+
export const WORKFLOW_ARMING_MESSAGE_TYPE = "pi-workflows:turn-arming";
|
|
98
|
+
|
|
99
|
+
function effortDirective(level: "off" | "high" | "ultra"): string {
|
|
100
|
+
return level === "ultra"
|
|
101
|
+
? 'Effort: ULTRA. Be exhaustive: use broad parallel review, verification, and completeness checks. Width is where the exhaustiveness goes: keep steps at strength: "medium" and reserve "high" for a step whose error is expensive to reverse, if the run has one at all.'
|
|
102
|
+
: level === "high"
|
|
103
|
+
? "Effort: HIGH. Be thorough: use several independent reviewers and an adversarial verification pass."
|
|
104
|
+
: "";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type NativeUserMessage = Extract<ContextEvent["messages"][number], { role: "user" }>;
|
|
108
|
+
|
|
109
|
+
function userText(message: NativeUserMessage): string {
|
|
110
|
+
return typeof message.content === "string"
|
|
111
|
+
? message.content
|
|
112
|
+
: message.content
|
|
113
|
+
.filter((part) => part.type === "text")
|
|
114
|
+
.map((part) => part.text)
|
|
115
|
+
.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function inputKey(text: string, images: InputEvent["images"]): string {
|
|
119
|
+
return createHash("sha256")
|
|
120
|
+
.update(JSON.stringify([text, (images ?? []).map((image) => [image.mimeType, image.data])]))
|
|
121
|
+
.digest("hex");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function nativeKey(message: NativeUserMessage): string {
|
|
125
|
+
return inputKey(
|
|
126
|
+
userText(message),
|
|
127
|
+
typeof message.content === "string" ? [] : message.content.filter((part) => part.type === "image"),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type NativeUserIdentity = { key: string; timestamp: number };
|
|
132
|
+
|
|
133
|
+
function nativeIdentity(message: NativeUserMessage): NativeUserIdentity {
|
|
134
|
+
return { key: nativeKey(message), timestamp: message.timestamp };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sameNativeUser(left: NativeUserIdentity, right: ContextEvent["messages"][number]): boolean {
|
|
138
|
+
return right.role === "user" && right.timestamp === left.timestamp && nativeKey(right) === left.key;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Public before_agent_start has no source or input ID. Admit only one observed,
|
|
143
|
+
* idle ordinary input, with unchanged text/images/session/leaf. Chained handled
|
|
144
|
+
* input has no public completion event: an unmatched observation makes a later
|
|
145
|
+
* overlapping ordinary input ambiguous rather than transferring its permission.
|
|
146
|
+
* Extension input replaces permission with a denied observation; settlement
|
|
147
|
+
* and navigation retire all observations. Queued input instead binds one bounded
|
|
148
|
+
* admission to a public user message_start, then adds only model context. Multiple
|
|
149
|
+
* unresolved queue admissions are ambiguous. Expanded/transformed input fails
|
|
150
|
+
* closed, and no path changes native history.
|
|
151
|
+
*/
|
|
152
|
+
export class WorkflowTurnArming {
|
|
153
|
+
private pending:
|
|
154
|
+
| {
|
|
155
|
+
text: string;
|
|
156
|
+
images: string;
|
|
157
|
+
sessionId: string;
|
|
158
|
+
leaf: string | null;
|
|
159
|
+
content?: string;
|
|
160
|
+
}
|
|
161
|
+
| undefined;
|
|
162
|
+
private ambiguous = false;
|
|
163
|
+
private current:
|
|
164
|
+
| {
|
|
165
|
+
id: string;
|
|
166
|
+
key: string;
|
|
167
|
+
message?: NativeUserIdentity;
|
|
168
|
+
sessionId: string;
|
|
169
|
+
anchor: string | null;
|
|
170
|
+
content: string;
|
|
171
|
+
}
|
|
172
|
+
| undefined;
|
|
173
|
+
// One unresolved queued observation is enough. Do not accumulate large input
|
|
174
|
+
// bodies or infer ownership when later handlers consume/reorder/transform them.
|
|
175
|
+
private queued: { key: string; sessionId: string; anchor: string | null; content?: string } | undefined;
|
|
176
|
+
private queuedAmbiguous = false;
|
|
177
|
+
private queuedCurrent:
|
|
178
|
+
| { id: string; message: NativeUserIdentity; sessionId: string; anchor: string | null; content: string }
|
|
179
|
+
| undefined;
|
|
180
|
+
|
|
181
|
+
retire(): void {
|
|
182
|
+
this.pending = undefined;
|
|
183
|
+
this.current = undefined;
|
|
184
|
+
this.ambiguous = false;
|
|
185
|
+
this.queued = undefined;
|
|
186
|
+
this.queuedCurrent = undefined;
|
|
187
|
+
this.queuedAmbiguous = false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
observe(
|
|
191
|
+
event: InputEvent,
|
|
192
|
+
ctx: ExtensionContext,
|
|
193
|
+
armed: boolean,
|
|
194
|
+
effort: "off" | "high" | "ultra",
|
|
195
|
+
explicitTrigger = true,
|
|
196
|
+
): void {
|
|
197
|
+
const ordinary = event.source === "interactive" || event.source === "rpc";
|
|
198
|
+
const directive = explicitTrigger ? WORKFLOW_ARMED_DIRECTIVE : WORKFLOW_EFFORT_ARMED_DIRECTIVE;
|
|
199
|
+
const content = ordinary && armed ? [directive, effortDirective(effort)].filter(Boolean).join("\n") : undefined;
|
|
200
|
+
if (event.streamingBehavior === "steer" || event.streamingBehavior === "followUp") {
|
|
201
|
+
this.queuedAmbiguous ||= this.queued !== undefined || this.pending !== undefined || this.ambiguous;
|
|
202
|
+
this.queued = undefined;
|
|
203
|
+
try {
|
|
204
|
+
if (this.queuedAmbiguous || ctx.isIdle() || event.text.trim().startsWith("/")) return;
|
|
205
|
+
this.queued = {
|
|
206
|
+
key: inputKey(event.text, event.images ?? []),
|
|
207
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
208
|
+
anchor: ctx.sessionManager.getLeafId(),
|
|
209
|
+
content,
|
|
210
|
+
};
|
|
211
|
+
} catch {
|
|
212
|
+
this.queuedAmbiguous = true;
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.ambiguous ||= this.queued !== undefined || this.queuedAmbiguous;
|
|
217
|
+
this.queued = undefined;
|
|
218
|
+
this.queuedCurrent = undefined;
|
|
219
|
+
this.ambiguous ||= this.pending !== undefined || this.current !== undefined;
|
|
220
|
+
this.current = undefined;
|
|
221
|
+
this.pending = undefined;
|
|
222
|
+
try {
|
|
223
|
+
if (!ctx.isIdle() || event.text.trim().startsWith("/")) return;
|
|
224
|
+
this.pending = {
|
|
225
|
+
text: event.text,
|
|
226
|
+
images: inputKey("", event.images),
|
|
227
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
228
|
+
leaf: ctx.sessionManager.getLeafId(),
|
|
229
|
+
content,
|
|
230
|
+
};
|
|
231
|
+
} catch {
|
|
232
|
+
this.pending = undefined;
|
|
233
|
+
this.ambiguous = true; // No public ownership evidence, no arming until settlement.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
consume(event: BeforeAgentStartEvent, ctx: ExtensionContext): void {
|
|
238
|
+
const pending = this.pending;
|
|
239
|
+
const ambiguous = this.ambiguous;
|
|
240
|
+
this.pending = undefined; // one-use, also for mismatched/consumed prompts
|
|
241
|
+
this.current = undefined;
|
|
242
|
+
try {
|
|
243
|
+
if (
|
|
244
|
+
ambiguous ||
|
|
245
|
+
!pending?.content ||
|
|
246
|
+
!ctx.isIdle() ||
|
|
247
|
+
event.prompt !== pending.text ||
|
|
248
|
+
inputKey("", event.images) !== pending.images ||
|
|
249
|
+
ctx.sessionManager.getSessionId() !== pending.sessionId ||
|
|
250
|
+
ctx.sessionManager.getLeafId() !== pending.leaf
|
|
251
|
+
)
|
|
252
|
+
return;
|
|
253
|
+
const id = randomUUID();
|
|
254
|
+
this.current = {
|
|
255
|
+
id,
|
|
256
|
+
key: inputKey(event.prompt, event.images ?? []),
|
|
257
|
+
sessionId: pending.sessionId,
|
|
258
|
+
anchor: pending.leaf,
|
|
259
|
+
content: pending.content,
|
|
260
|
+
};
|
|
261
|
+
} catch {
|
|
262
|
+
this.retire();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
messageStarted(event: MessageStartEvent, ctx: ExtensionContext): void {
|
|
268
|
+
if (event.message.role !== "user") return;
|
|
269
|
+
const message = event.message;
|
|
270
|
+
// The ordinary first message follows its own before_agent_start admission.
|
|
271
|
+
if (this.current && !this.current.message && nativeKey(message) === this.current.key) {
|
|
272
|
+
this.current.message = nativeIdentity(message);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.current = undefined;
|
|
276
|
+
this.queuedCurrent = undefined;
|
|
277
|
+
const queued = this.queued;
|
|
278
|
+
this.queued = undefined; // one-use, even for an unmatched/synthetic message
|
|
279
|
+
try {
|
|
280
|
+
if (
|
|
281
|
+
this.queuedAmbiguous ||
|
|
282
|
+
!queued?.content ||
|
|
283
|
+
queued.key !== nativeKey(message) ||
|
|
284
|
+
queued.sessionId !== ctx.sessionManager.getSessionId() ||
|
|
285
|
+
(queued.anchor !== null && !ctx.sessionManager.getBranch().some((entry) => entry.id === queued.anchor))
|
|
286
|
+
)
|
|
287
|
+
return;
|
|
288
|
+
this.queuedCurrent = {
|
|
289
|
+
id: randomUUID(),
|
|
290
|
+
message: nativeIdentity(message),
|
|
291
|
+
sessionId: queued.sessionId,
|
|
292
|
+
anchor: queued.anchor,
|
|
293
|
+
content: queued.content,
|
|
294
|
+
};
|
|
295
|
+
} catch {
|
|
296
|
+
this.queuedAmbiguous = true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
filterContext(event: ContextEvent, ctx: ExtensionContext): { messages: ContextEvent["messages"] } | undefined {
|
|
301
|
+
const lastUser = [...event.messages].reverse().find((message) => message.role === "user");
|
|
302
|
+
// Only extension-owned messages are removed. User content is opaque, even
|
|
303
|
+
// when it quotes our directive verbatim. Rebuild a single request-local copy.
|
|
304
|
+
const messages = event.messages.filter(
|
|
305
|
+
(message) => message.role !== "custom" || message.customType !== WORKFLOW_ARMING_MESSAGE_TYPE,
|
|
306
|
+
);
|
|
307
|
+
const queued = this.queuedCurrent ?? this.current;
|
|
308
|
+
const identity = queued?.message;
|
|
309
|
+
let injected = false;
|
|
310
|
+
try {
|
|
311
|
+
// Public context dispatch clones messages. Match the emitted timestamp and
|
|
312
|
+
// content against a UNIQUE native entry on this branch and in model context;
|
|
313
|
+
// equal-text collisions or transformations must not transfer permission.
|
|
314
|
+
if (
|
|
315
|
+
queued &&
|
|
316
|
+
identity &&
|
|
317
|
+
lastUser &&
|
|
318
|
+
sameNativeUser(identity, lastUser) &&
|
|
319
|
+
event.messages.filter((message) => sameNativeUser(identity, message)).length === 1 &&
|
|
320
|
+
queued.sessionId === ctx.sessionManager.getSessionId() &&
|
|
321
|
+
ctx.sessionManager
|
|
322
|
+
.getBranch()
|
|
323
|
+
.filter((entry) => entry.type === "message" && sameNativeUser(identity, entry.message)).length === 1 &&
|
|
324
|
+
(queued.anchor === null || ctx.sessionManager.getBranch().some((entry) => entry.id === queued.anchor))
|
|
325
|
+
) {
|
|
326
|
+
injected = true;
|
|
327
|
+
messages.push({
|
|
328
|
+
role: "custom",
|
|
329
|
+
customType: WORKFLOW_ARMING_MESSAGE_TYPE,
|
|
330
|
+
content: queued.content,
|
|
331
|
+
display: false,
|
|
332
|
+
details: { id: queued.id },
|
|
333
|
+
timestamp: identity.timestamp,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
this.queuedCurrent = undefined;
|
|
338
|
+
}
|
|
339
|
+
return injected || messages.length !== event.messages.length ? { messages } : undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|