@osolmaz/pi-workflows 0.1.0 → 0.3.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.
Files changed (171) hide show
  1. package/README.md +107 -26
  2. package/dist/builtins/monitor.workflow.d.ts +71 -0
  3. package/dist/builtins/monitor.workflow.js +234 -0
  4. package/dist/builtins/monitor.workflow.js.map +1 -0
  5. package/dist/controllers/conditions.d.ts +6 -0
  6. package/dist/controllers/conditions.js +68 -0
  7. package/dist/controllers/conditions.js.map +1 -0
  8. package/dist/controllers/definition.d.ts +6 -0
  9. package/dist/controllers/definition.js +45 -0
  10. package/dist/controllers/definition.js.map +1 -0
  11. package/dist/controllers/effects.d.ts +14 -0
  12. package/dist/controllers/effects.js +104 -0
  13. package/dist/controllers/effects.js.map +1 -0
  14. package/dist/controllers/errors.d.ts +12 -0
  15. package/dist/controllers/errors.js +25 -0
  16. package/dist/controllers/errors.js.map +1 -0
  17. package/dist/controllers/index.d.ts +13 -0
  18. package/dist/controllers/index.js +13 -0
  19. package/dist/controllers/index.js.map +1 -0
  20. package/dist/controllers/json.d.ts +5 -0
  21. package/dist/controllers/json.js +57 -0
  22. package/dist/controllers/json.js.map +1 -0
  23. package/dist/controllers/loader.d.ts +23 -0
  24. package/dist/controllers/loader.js +74 -0
  25. package/dist/controllers/loader.js.map +1 -0
  26. package/dist/controllers/manager.d.ts +58 -0
  27. package/dist/controllers/manager.js +399 -0
  28. package/dist/controllers/manager.js.map +1 -0
  29. package/dist/controllers/results.d.ts +5 -0
  30. package/dist/controllers/results.js +32 -0
  31. package/dist/controllers/results.js.map +1 -0
  32. package/dist/controllers/sqlite.d.ts +212 -0
  33. package/dist/controllers/sqlite.js +1009 -0
  34. package/dist/controllers/sqlite.js.map +1 -0
  35. package/dist/controllers/store.d.ts +112 -0
  36. package/dist/controllers/store.js +32 -0
  37. package/dist/controllers/store.js.map +1 -0
  38. package/dist/controllers/types.d.ts +159 -0
  39. package/dist/controllers/types.js +2 -0
  40. package/dist/controllers/types.js.map +1 -0
  41. package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
  42. package/dist/controllers/workflow-engine-scheduler.js +93 -0
  43. package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
  44. package/dist/controllers/workflows.d.ts +27 -0
  45. package/dist/controllers/workflows.js +109 -0
  46. package/dist/controllers/workflows.js.map +1 -0
  47. package/dist/extension/controller-host.d.ts +47 -0
  48. package/dist/extension/controller-host.js +110 -0
  49. package/dist/extension/controller-host.js.map +1 -0
  50. package/dist/extension/executor.d.ts +14 -1
  51. package/dist/extension/executor.js +12 -2
  52. package/dist/extension/executor.js.map +1 -1
  53. package/dist/extension/index.d.ts +7 -0
  54. package/dist/extension/index.js +1118 -89
  55. package/dist/extension/index.js.map +1 -1
  56. package/dist/extension/recorder.d.ts +91 -0
  57. package/dist/extension/recorder.js +537 -0
  58. package/dist/extension/recorder.js.map +1 -0
  59. package/dist/extension/session-events.d.ts +134 -0
  60. package/dist/extension/session-events.js +60 -0
  61. package/dist/extension/session-events.js.map +1 -0
  62. package/dist/extension/widget.js +25 -24
  63. package/dist/extension/widget.js.map +1 -1
  64. package/dist/extension/workflow-tool.d.ts +28 -0
  65. package/dist/extension/workflow-tool.js +33 -0
  66. package/dist/extension/workflow-tool.js.map +1 -0
  67. package/dist/host/processes.d.ts +24 -0
  68. package/dist/host/processes.js +114 -0
  69. package/dist/host/processes.js.map +1 -0
  70. package/dist/host/rpc-bridge.d.ts +9 -0
  71. package/dist/host/rpc-bridge.js +39 -0
  72. package/dist/host/rpc-bridge.js.map +1 -0
  73. package/dist/host/rpc-executor.d.ts +38 -0
  74. package/dist/host/rpc-executor.js +254 -0
  75. package/dist/host/rpc-executor.js.map +1 -0
  76. package/dist/host/runner.d.ts +49 -0
  77. package/dist/host/runner.js +350 -0
  78. package/dist/host/runner.js.map +1 -0
  79. package/dist/render/canvas.d.ts +1 -1
  80. package/dist/render/canvas.js +5 -0
  81. package/dist/render/canvas.js.map +1 -1
  82. package/dist/render/graph-render.d.ts +5 -0
  83. package/dist/render/graph-render.js +211 -48
  84. package/dist/render/graph-render.js.map +1 -1
  85. package/dist/viewer/cli.d.ts +7 -3
  86. package/dist/viewer/cli.js +150 -19
  87. package/dist/viewer/cli.js.map +1 -1
  88. package/dist/viewer/render.js +19 -3
  89. package/dist/viewer/render.js.map +1 -1
  90. package/dist/viewer/session-reducer.d.ts +45 -0
  91. package/dist/viewer/session-reducer.js +266 -0
  92. package/dist/viewer/session-reducer.js.map +1 -0
  93. package/dist/workflows/artifacts.d.ts +40 -0
  94. package/dist/workflows/artifacts.js +155 -0
  95. package/dist/workflows/artifacts.js.map +1 -0
  96. package/dist/workflows/engine.d.ts +38 -0
  97. package/dist/workflows/engine.js +281 -20
  98. package/dist/workflows/engine.js.map +1 -1
  99. package/dist/workflows/errors.d.ts +23 -0
  100. package/dist/workflows/errors.js +38 -0
  101. package/dist/workflows/errors.js.map +1 -1
  102. package/dist/workflows/graph.js +0 -5
  103. package/dist/workflows/graph.js.map +1 -1
  104. package/dist/workflows/index.d.ts +3 -2
  105. package/dist/workflows/index.js +2 -1
  106. package/dist/workflows/index.js.map +1 -1
  107. package/dist/workflows/loader.d.ts +5 -3
  108. package/dist/workflows/loader.js +10 -1
  109. package/dist/workflows/loader.js.map +1 -1
  110. package/dist/workflows/schema.js +1 -1
  111. package/dist/workflows/schema.js.map +1 -1
  112. package/dist/workflows/store.d.ts +98 -10
  113. package/dist/workflows/store.js +921 -46
  114. package/dist/workflows/store.js.map +1 -1
  115. package/dist/workflows/types.d.ts +136 -3
  116. package/docs/CONTROLLERS.md +215 -0
  117. package/docs/development.md +51 -24
  118. package/docs/live-replay-protocol.md +155 -0
  119. package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
  120. package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
  121. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
  122. package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
  123. package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
  124. package/docs/plans/session-event-replay-implementation-plan.md +494 -0
  125. package/docs/plans/tui-viewer-implementation-plan.md +64 -0
  126. package/docs/run-bundles.md +359 -55
  127. package/docs/session-event-journal.md +470 -0
  128. package/docs/tui-viewer.md +218 -0
  129. package/docs/workflows.md +131 -11
  130. package/examples/controllers/pull-request.controller.ts +215 -0
  131. package/package.json +11 -2
  132. package/src/builtins/monitor.workflow.ts +278 -0
  133. package/src/controllers/conditions.ts +110 -0
  134. package/src/controllers/definition.ts +65 -0
  135. package/src/controllers/effects.ts +123 -0
  136. package/src/controllers/errors.ts +27 -0
  137. package/src/controllers/index.ts +90 -0
  138. package/src/controllers/json.ts +62 -0
  139. package/src/controllers/loader.ts +104 -0
  140. package/src/controllers/manager.ts +533 -0
  141. package/src/controllers/results.ts +46 -0
  142. package/src/controllers/sqlite.ts +1427 -0
  143. package/src/controllers/store.ts +160 -0
  144. package/src/controllers/types.ts +183 -0
  145. package/src/controllers/workflow-engine-scheduler.ts +145 -0
  146. package/src/controllers/workflows.ts +152 -0
  147. package/src/extension/controller-host.ts +163 -0
  148. package/src/extension/executor.ts +29 -2
  149. package/src/extension/index.ts +1315 -109
  150. package/src/extension/recorder.ts +662 -0
  151. package/src/extension/session-events.ts +119 -0
  152. package/src/extension/widget.ts +26 -24
  153. package/src/extension/workflow-tool.ts +59 -0
  154. package/src/host/processes.ts +119 -0
  155. package/src/host/rpc-bridge.ts +44 -0
  156. package/src/host/rpc-executor.ts +299 -0
  157. package/src/host/runner.ts +406 -0
  158. package/src/render/canvas.ts +19 -1
  159. package/src/render/graph-render.ts +277 -44
  160. package/src/viewer/cli.ts +167 -21
  161. package/src/viewer/render.ts +21 -3
  162. package/src/viewer/session-reducer.ts +347 -0
  163. package/src/workflows/artifacts.ts +188 -0
  164. package/src/workflows/engine.ts +365 -19
  165. package/src/workflows/errors.ts +45 -0
  166. package/src/workflows/graph.ts +0 -5
  167. package/src/workflows/index.ts +15 -0
  168. package/src/workflows/loader.ts +13 -3
  169. package/src/workflows/schema.ts +1 -1
  170. package/src/workflows/store.ts +1157 -48
  171. package/src/workflows/types.ts +151 -3
@@ -0,0 +1,188 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import type { ArtifactRef, ArtifactValue } from "./types.js";
5
+
6
+ /**
7
+ * Value externalization for run bundles (see docs/run-bundles.md): string
8
+ * leaves larger than the threshold are written once, content-addressed, under
9
+ * `artifacts/` and replaced by `{ "$artifact": ref }`. Because artifacts are
10
+ * immutable and deduplicated by hash, the same output appearing in `outputs`,
11
+ * `results`, `steps`, and the trace costs one file plus small references.
12
+ */
13
+
14
+ export const ARTIFACT_THRESHOLD_BYTES = 4096;
15
+ export const ARTIFACTS_DIR = "artifacts";
16
+
17
+ const ARTIFACT_KEY = "$artifact";
18
+ const ESCAPED_KEY = "$escaped";
19
+
20
+ export function isArtifactValue(value: unknown): value is ArtifactValue {
21
+ return (
22
+ typeof value === "object" &&
23
+ value !== null &&
24
+ !Array.isArray(value) &&
25
+ Object.keys(value).length === 1 &&
26
+ ARTIFACT_KEY in value
27
+ );
28
+ }
29
+
30
+ function isEscapedValue(value: unknown): value is { $escaped: Record<string, unknown> } {
31
+ return (
32
+ typeof value === "object" &&
33
+ value !== null &&
34
+ !Array.isArray(value) &&
35
+ Object.keys(value).length === 1 &&
36
+ ESCAPED_KEY in value
37
+ );
38
+ }
39
+
40
+ /** True for a single-key object that would be misread as a sentinel. */
41
+ function needsEscape(value: Record<string, unknown>): boolean {
42
+ const keys = Object.keys(value);
43
+ return keys.length === 1 && (keys[0] === ARTIFACT_KEY || keys[0] === ESCAPED_KEY);
44
+ }
45
+
46
+ /**
47
+ * Writes externalized string leaves for one run bundle. Instances serialize
48
+ * their writes and deduplicate by content hash, so encoding the same large
49
+ * string in several value positions touches the filesystem once.
50
+ */
51
+ export class ArtifactWriter {
52
+ private readonly runDir: string;
53
+ private readonly written = new Set<string>();
54
+ private chain: Promise<unknown> = Promise.resolve();
55
+ private dirCreated = false;
56
+
57
+ constructor(runDir: string) {
58
+ this.runDir = runDir;
59
+ }
60
+
61
+ /** True once any artifact exists for this run. */
62
+ get hasArtifacts(): boolean {
63
+ return this.written.size > 0;
64
+ }
65
+
66
+ async externalize(text: string): Promise<ArtifactValue> {
67
+ const bytes = Buffer.from(text, "utf8");
68
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
69
+ const relativePath = `${ARTIFACTS_DIR}/sha256-${sha256}.txt`;
70
+ const task = this.chain.then(async () => {
71
+ if (this.written.has(sha256)) {
72
+ return;
73
+ }
74
+ if (!this.dirCreated) {
75
+ await fs.mkdir(path.join(this.runDir, ARTIFACTS_DIR), { recursive: true, mode: 0o700 });
76
+ this.dirCreated = true;
77
+ }
78
+ const filePath = path.join(this.runDir, relativePath);
79
+ // Content-addressed files are immutable; an existing file is complete
80
+ // unless a previous crash left a partial write, which the temp-rename
81
+ // pattern prevents.
82
+ const tempPath = `${filePath}.${process.pid}.tmp`;
83
+ await fs.writeFile(tempPath, bytes, { mode: 0o600 });
84
+ await fs.rename(tempPath, filePath);
85
+ this.written.add(sha256);
86
+ });
87
+ this.chain = task.catch(() => undefined);
88
+ await task;
89
+ return {
90
+ $artifact: {
91
+ path: relativePath,
92
+ mediaType: "text/plain",
93
+ bytes: bytes.byteLength,
94
+ sha256,
95
+ },
96
+ };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Encode a persisted value: externalize large string leaves and escape
102
+ * single-key `$artifact`/`$escaped` objects so the sentinel stays
103
+ * unambiguous. Returns the original value when nothing changed.
104
+ */
105
+ export async function encodeValue(
106
+ value: unknown,
107
+ writer: ArtifactWriter,
108
+ thresholdBytes: number = ARTIFACT_THRESHOLD_BYTES,
109
+ ): Promise<unknown> {
110
+ if (typeof value === "string") {
111
+ if (Buffer.byteLength(value, "utf8") <= thresholdBytes) {
112
+ return value;
113
+ }
114
+ return await writer.externalize(value);
115
+ }
116
+ if (Array.isArray(value)) {
117
+ const encoded = await Promise.all(
118
+ value.map((item) => encodeValue(item, writer, thresholdBytes)),
119
+ );
120
+ return encoded.some((item, index) => item !== value[index]) ? encoded : value;
121
+ }
122
+ if (typeof value === "object" && value !== null) {
123
+ const record = value as Record<string, unknown>;
124
+ const entries = await Promise.all(
125
+ Object.entries(record).map(
126
+ async ([key, item]) => [key, await encodeValue(item, writer, thresholdBytes)] as const,
127
+ ),
128
+ );
129
+ const changed = entries.some(([key, item]) => item !== record[key]);
130
+ const encoded = changed ? Object.fromEntries(entries) : record;
131
+ return needsEscape(record) ? { [ESCAPED_KEY]: encoded } : encoded;
132
+ }
133
+ return value;
134
+ }
135
+
136
+ /**
137
+ * Walk a persisted value, replacing every `$artifact` sentinel via `resolve`
138
+ * and unwrapping `$escaped` objects. `resolve` may return the artifact
139
+ * contents or a placeholder; it receives the reference untouched.
140
+ */
141
+ export function decodeValueWith(value: unknown, resolve: (ref: ArtifactRef) => unknown): unknown {
142
+ if (isArtifactValue(value)) {
143
+ return resolve(value.$artifact);
144
+ }
145
+ if (isEscapedValue(value)) {
146
+ // The unwrapped object is user data: process its children but do not
147
+ // re-test the object itself as a sentinel.
148
+ return decodeChildren(value.$escaped, resolve);
149
+ }
150
+ if (Array.isArray(value)) {
151
+ return value.map((item) => decodeValueWith(item, resolve));
152
+ }
153
+ if (typeof value === "object" && value !== null) {
154
+ return decodeChildren(value as Record<string, unknown>, resolve);
155
+ }
156
+ return value;
157
+ }
158
+
159
+ function decodeChildren(
160
+ record: Record<string, unknown>,
161
+ resolve: (ref: ArtifactRef) => unknown,
162
+ ): Record<string, unknown> {
163
+ return Object.fromEntries(
164
+ Object.entries(record).map(([key, item]) => [key, decodeValueWith(item, resolve)]),
165
+ );
166
+ }
167
+
168
+ /** Resolve every artifact reference in `value` by reading the bundle files. */
169
+ export async function resolveArtifacts(value: unknown, runDir: string): Promise<unknown> {
170
+ const refs: ArtifactRef[] = [];
171
+ decodeValueWith(value, (ref) => {
172
+ refs.push(ref);
173
+ return null;
174
+ });
175
+ const contents = new Map<string, string>();
176
+ for (const ref of refs) {
177
+ if (contents.has(ref.path)) {
178
+ continue;
179
+ }
180
+ const resolved = path.resolve(runDir, ref.path);
181
+ // A reference must never escape the bundle directory.
182
+ if (!resolved.startsWith(path.resolve(runDir) + path.sep)) {
183
+ throw new Error(`Artifact path escapes the bundle: ${ref.path}`);
184
+ }
185
+ contents.set(ref.path, await fs.readFile(resolved, "utf8"));
186
+ }
187
+ return decodeValueWith(value, (ref) => contents.get(ref.path) ?? null);
188
+ }