pi-taskflow 0.0.23 → 0.0.25
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 +37 -0
- package/README.md +5 -4
- package/README.zh-CN.md +1 -1
- package/extensions/cache.ts +6 -1
- package/extensions/compile.ts +371 -0
- package/extensions/flowir/hash.ts +97 -0
- package/extensions/index.ts +257 -6
- package/extensions/interpolate.ts +17 -0
- package/extensions/runtime.ts +326 -27
- package/extensions/stale.ts +137 -0
- package/extensions/store.ts +14 -0
- package/package.json +1 -1
- package/skills/taskflow/SKILL.md +2 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stale-marking (M4) — conservative transitive invalidation over the observed
|
|
3
|
+
* readSet captured in M3.
|
|
4
|
+
*
|
|
5
|
+
* This is the "mark stale, don't rerun" half of overstory's cost-asymmetric
|
|
6
|
+
* reactivity (VISION §2.3): the cheap effects (figuring out what WOULD be
|
|
7
|
+
* invalidated) run for free; the expensive effects (actually re-running an LLM
|
|
8
|
+
* phase) are gated for M5. Given a run's observed readSets and a set of phases
|
|
9
|
+
* assumed to have changed, `computeStaleFrontier` returns the transitive
|
|
10
|
+
* closure of phases whose recorded dependencies are no longer trustworthy.
|
|
11
|
+
*
|
|
12
|
+
* Pure module: no IO, no Date, no randomness. Deterministic.
|
|
13
|
+
*
|
|
14
|
+
* Scope (honest): this is TOPOLOGICAL propagation only — a changed seed
|
|
15
|
+
* invalidates everything that (transitively) read it. The overstory
|
|
16
|
+
* "early cutoff" refinement (a re-run whose output HASH is unchanged does NOT
|
|
17
|
+
* invalidate, even if the version advanced) needs before/after content hashes,
|
|
18
|
+
* which only exist when a phase is actually re-run — that is the M5
|
|
19
|
+
* recomputation concern, deliberately out of scope here. Marking is the safe,
|
|
20
|
+
* conservative prerequisite that lets M5 rerun with confidence.
|
|
21
|
+
*
|
|
22
|
+
* @see docs/internal/overstory-convergence-roadmap.md §3 (M4)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { PhaseState } from "./store.ts";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Read graph
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** phaseId → the upstream stepIds it observed-reading (M3 PhaseState.reads). */
|
|
32
|
+
export type ReadMap = Map<string, readonly string[]>;
|
|
33
|
+
|
|
34
|
+
/** Fold a run's PhaseStates into a read map (drops phases with no reads). */
|
|
35
|
+
export function readMapOf(phases: Record<string, PhaseState>): ReadMap {
|
|
36
|
+
const m: ReadMap = new Map();
|
|
37
|
+
for (const [id, ps] of Object.entries(phases)) {
|
|
38
|
+
const deps = (ps.reads ?? []).map((r) => r.stepId);
|
|
39
|
+
if (deps.length) m.set(id, deps);
|
|
40
|
+
}
|
|
41
|
+
return m;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Phases that directly read `phaseId` (its immediate dependents). */
|
|
45
|
+
export function dependentsOf(reads: ReadMap, phaseId: string): string[] {
|
|
46
|
+
const out: string[] = [];
|
|
47
|
+
for (const [reader, deps] of reads) {
|
|
48
|
+
if (deps.includes(phaseId)) out.push(reader);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Stale frontier (transitive closure, union semantics)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The set of phases that are stale if `seeds` change, transitively. A reader
|
|
59
|
+
* is stale if ANY phase it observed-reading is stale (union/I5: when in doubt,
|
|
60
|
+
* assume dependency). Includes the seeds themselves.
|
|
61
|
+
*
|
|
62
|
+
* Deterministic. O(phases + read-edges). Cycles in the read graph (which a
|
|
63
|
+
* correct DAG can't produce, but a pathological one could) terminate because a
|
|
64
|
+
* phase is enqueued at most once.
|
|
65
|
+
*/
|
|
66
|
+
export function computeStaleFrontier(reads: ReadMap, seeds: Iterable<string>): Set<string> {
|
|
67
|
+
const stale = new Set<string>();
|
|
68
|
+
const queue: string[] = [...seeds];
|
|
69
|
+
while (queue.length) {
|
|
70
|
+
const s = queue.shift() as string;
|
|
71
|
+
if (stale.has(s)) continue;
|
|
72
|
+
stale.add(s);
|
|
73
|
+
for (const dep of dependentsOf(reads, s)) {
|
|
74
|
+
if (!stale.has(dep)) queue.push(dep);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return stale;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Rendering
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Render either the full observed dependency graph (no seeds) or the stale
|
|
86
|
+
* frontier given assumed-changed seeds. Each stale phase lists the stale
|
|
87
|
+
* upstreams that caused it (its "why").
|
|
88
|
+
*/
|
|
89
|
+
export function formatWhyStale(
|
|
90
|
+
runId: string,
|
|
91
|
+
flowName: string,
|
|
92
|
+
reads: ReadMap,
|
|
93
|
+
seeds: readonly string[],
|
|
94
|
+
): string {
|
|
95
|
+
const lines: string[] = [];
|
|
96
|
+
lines.push(`why-stale — run ${runId} · flow "${flowName}"`);
|
|
97
|
+
lines.push("");
|
|
98
|
+
|
|
99
|
+
if (seeds.length === 0) {
|
|
100
|
+
// No seeds → show the full observed dependency graph (who reads what).
|
|
101
|
+
if (reads.size === 0) {
|
|
102
|
+
lines.push("(No observed readSets in this run — provenance is empty.)");
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
lines.push("Observed dependency graph (who reads what):");
|
|
106
|
+
lines.push("");
|
|
107
|
+
for (const [reader, deps] of reads) {
|
|
108
|
+
lines.push(`■ ${reader} reads: ${deps.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push("Pass a phase id to compute its stale frontier: /tf why-stale <runId> <phaseId>");
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const frontier = computeStaleFrontier(reads, seeds);
|
|
116
|
+
const seedSet = new Set(seeds);
|
|
117
|
+
lines.push(`Assuming changed: ${[...seedSet].join(", ")}`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
if (frontier.size <= seedSet.size) {
|
|
120
|
+
lines.push(`Stale frontier: only the seed(s) themselves — nothing else observed-reading them.`);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
lines.push(`Stale frontier (transitive, ${frontier.size} phases):`);
|
|
124
|
+
// Order: seeds first, then the rest, for readability.
|
|
125
|
+
const ordered = [...seeds.filter((s) => frontier.has(s)), ...[...frontier].filter((s) => !seedSet.has(s))];
|
|
126
|
+
for (const id of ordered) {
|
|
127
|
+
if (seedSet.has(id)) {
|
|
128
|
+
lines.push(` ■ ${id} (changed — seed)`);
|
|
129
|
+
} else {
|
|
130
|
+
// Why is it stale? The stale upstreams it read.
|
|
131
|
+
const deps = reads.get(id) ?? [];
|
|
132
|
+
const causes = deps.filter((d) => frontier.has(d));
|
|
133
|
+
lines.push(` ■ ${id} ← reads ${causes.length ? causes.join(", ") : "(nothing stale?)"}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
package/extensions/store.ts
CHANGED
|
@@ -70,6 +70,14 @@ export interface PhaseState {
|
|
|
70
70
|
/** Non-fatal diagnostic warnings accumulated during this phase (e.g.
|
|
71
71
|
* unresolved interpolation placeholders, suspicious templates). */
|
|
72
72
|
warnings?: string[];
|
|
73
|
+
/** Observed readSet (M3): the upstream phase outputs this phase actually
|
|
74
|
+
* consumed at interpolation time — not what it *declared* to depend on
|
|
75
|
+
* (dependsOn), but what it truly *read* (`{steps.X...}`). Each entry
|
|
76
|
+
* carries the version (= the read phase's inputHash) it consumed, so a
|
|
77
|
+
* later staleness check (M4/M5) can tell whether the upstream has moved.
|
|
78
|
+
* This is the overstory "observed readSet@version" moat: no other
|
|
79
|
+
* orchestrator records what a result actually depended on. */
|
|
80
|
+
reads?: Array<{ stepId: string; version?: string }>;
|
|
73
81
|
/** Truncated previews of interpolated strings used to execute this phase,
|
|
74
82
|
* useful when diagnosing why a model saw a literal placeholder. */
|
|
75
83
|
interpolation?: Array<{ source: string; text: string; missing?: string[] }>;
|
|
@@ -89,6 +97,12 @@ export interface RunState {
|
|
|
89
97
|
pid?: number;
|
|
90
98
|
/** True for runs spawned via `detach: true` (background execution). */
|
|
91
99
|
detached?: boolean;
|
|
100
|
+
/** Content fingerprint of the desugared flow definition (overstory hash
|
|
101
|
+
* algorithm). Folded into every phase's cache key so a structural change
|
|
102
|
+
* to the flow always invalidates cross-run cache hits — and an identical
|
|
103
|
+
* re-run always reuses them. Filled once at run start; persisted for
|
|
104
|
+
* audit/resume consistency. */
|
|
105
|
+
flowDefHash?: string | "failed";
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-taskflow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "A declarative, verifiable graph of task nodes for the Pi coding agent — not a workflow you script, but a DAG you declare: statically verified before it runs, with dynamic fan-out, gates, isolated subagent context, resumable runs, and saveable commands.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/skills/taskflow/SKILL.md
CHANGED
|
@@ -558,7 +558,7 @@ Quick reference:
|
|
|
558
558
|
- `action: "run"` — run an inline `define` (a one-off DAG) **or** a saved `name` (with optional `args`). Use `define` for an ad-hoc flow; use `name` to invoke something previously saved. Add `detach: true` to run in the background (returns immediately with the runId; poll the store for status).
|
|
559
559
|
- `action: "save"` — persist `define` (scope `project` — default, committed/shared — or `user`); it becomes `/tf:<name>`. On a name collision, project overrides user.
|
|
560
560
|
- `action: "resume"` — continue a paused/failed run by `runId`.
|
|
561
|
-
- `action: "list"` — list saved flows. `action: "verify"` — static-check a `define` (zero tokens). `action: "agents"` — list available agents.
|
|
561
|
+
- `action: "list"` — list saved flows. `action: "verify"` — static-check a `define` (zero tokens). `action: "compile"` — render a saved or inline flow as a Mermaid diagram + verification report (zero tokens, no LLM). `action: "agents"` — list available agents.
|
|
562
562
|
|
|
563
563
|
## Background (detached) runs
|
|
564
564
|
|
|
@@ -580,5 +580,5 @@ A run moves through: **running →** `completed` (a `final` phase produced outpu
|
|
|
580
580
|
|
|
581
581
|
## User commands
|
|
582
582
|
|
|
583
|
-
- `/tf list` · `/tf run <name> [args]` · `/tf show <name>` · `/tf runs` · `/tf resume <runId>`
|
|
583
|
+
- `/tf list` · `/tf run <name> [args]` · `/tf show <name>` · `/tf compile <name> [lr|td]` · `/tf runs` · `/tf resume <runId>`
|
|
584
584
|
- `/tf:<name> [args]` — shortcut for each saved flow
|