@llblab/pi-kit 0.13.0 → 0.14.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 (95) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -7
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +8 -9
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +26 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +1 -3
  7. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +21 -0
  8. package/node_modules/@llblab/pi-state-flow/dist/index.js +20 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +39 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +78 -0
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +110 -0
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +334 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +49 -0
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +67 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +11 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +53 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +23 -0
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +109 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +111 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +189 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +21 -0
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +125 -0
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +102 -0
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +507 -0
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +8 -0
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +27 -0
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +22 -0
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1263 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +72 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +565 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +22 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +79 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +12 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +109 -0
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +25 -0
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +24 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +36 -0
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +98 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +15 -0
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +42 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +13 -0
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +133 -0
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +59 -0
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +69 -0
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +335 -0
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -0
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +35 -0
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +8 -0
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +27 -0
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +36 -0
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +38 -0
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +142 -0
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +529 -0
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +21 -0
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +44 -0
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +25 -0
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +131 -0
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +88 -0
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +255 -0
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +55 -0
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +31 -0
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +38 -0
  64. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +79 -0
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +46 -0
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +217 -0
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +98 -0
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +231 -0
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +39 -0
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +203 -0
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +25 -0
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +204 -0
  73. package/node_modules/@llblab/pi-state-flow/dist/package.json +79 -0
  74. package/node_modules/@llblab/pi-state-flow/dist/pi-state-flow/index.js +1 -0
  75. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +138 -0
  76. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -5
  77. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +5 -1
  78. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -3
  79. package/node_modules/@llblab/pi-state-flow/docs/usage.md +6 -4
  80. package/node_modules/@llblab/pi-state-flow/index.ts +1 -0
  81. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +17 -1
  82. package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -1
  83. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +88 -96
  84. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +64 -12
  85. package/node_modules/@llblab/pi-state-flow/lib/git.ts +32 -188
  86. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +84 -48
  87. package/node_modules/@llblab/pi-state-flow/lib/query.ts +40 -0
  88. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +7 -30
  89. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +48 -50
  90. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +41 -97
  91. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +17 -12
  92. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +4 -4
  93. package/node_modules/@llblab/pi-state-flow/package.json +23 -6
  94. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +3 -1
  95. package/package.json +4 -4
@@ -0,0 +1,334 @@
1
+ import { createHash } from "node:crypto";
2
+ import { containsNull, isJsonValue, isObject } from "./json.js";
3
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
4
+ const MODEL_FORBIDDEN_PROVENANCE_FIELDS = ["hash", "compiler", "sourceHash", "compilerRevision", "compiledAt", "source_hash_verified"];
5
+ /** Current compiler protocol for ordinary source artifacts such as Knowledge Markdown. */
6
+ export const ORDINARY_ARTIFACT_COMPILER = "artifact-v1";
7
+ export function hashArtifactSource(source) {
8
+ return `sha256:${createHash("sha256").update(source).digest("hex")}`;
9
+ }
10
+ export function isArtifactHash(value) {
11
+ return typeof value === "string" && SHA256_PATTERN.test(value);
12
+ }
13
+ export function validateArtifactMetadata(value, path = "<unknown>") {
14
+ if (!isObject(value) || !isJsonValue(value)) {
15
+ throw new Error(`Artifact metadata at ${path} must be finite, acyclic JSON data`);
16
+ }
17
+ if (containsNull(value))
18
+ throw new Error(`Artifact metadata at ${path} cannot contain null`);
19
+ if (typeof value.description !== "string" || value.description.trim().length === 0) {
20
+ throw new Error(`Artifact metadata at ${path} must have a non-empty description`);
21
+ }
22
+ if (Object.hasOwn(value, "compilation") && !isObject(value.compilation)) {
23
+ throw new Error(`Artifact metadata at ${path} compilation must be an object`);
24
+ }
25
+ if (Object.hasOwn(value, "kind")
26
+ && (typeof value.kind !== "string" || value.kind.trim().length === 0)) {
27
+ throw new Error(`Artifact metadata at ${path} kind must be a non-empty string`);
28
+ }
29
+ if (Object.hasOwn(value, "tags") && (!Array.isArray(value.tags)
30
+ || value.tags.some((tag) => typeof tag !== "string" || tag.trim().length === 0 || tag !== tag.trim())
31
+ || new Set(value.tags).size !== value.tags.length)) {
32
+ throw new Error(`Artifact metadata at ${path} tags must be unique non-empty trimmed strings`);
33
+ }
34
+ if (Object.hasOwn(value, "hash") && !isArtifactHash(value.hash)) {
35
+ throw new Error(`Artifact metadata at ${path} must have a sha256:<64 lowercase hex characters> hash`);
36
+ }
37
+ if (Object.hasOwn(value, "compiler") && (typeof value.compiler !== "string" || value.compiler.trim().length === 0)) {
38
+ throw new Error(`Artifact metadata at ${path} must have a non-empty compiler revision`);
39
+ }
40
+ if (Object.hasOwn(value, "compiled_at") && typeof value.compiled_at !== "string") {
41
+ throw new Error(`Artifact metadata at ${path} compiled_at must be a string`);
42
+ }
43
+ }
44
+ export function isArtifactMetadata(value) {
45
+ try {
46
+ validateArtifactMetadata(value);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ export function validateArtifactRegistry(value) {
54
+ if (!isObject(value))
55
+ throw new Error("Artifacts must be a path-keyed JSON object");
56
+ for (const [path, metadata] of Object.entries(value)) {
57
+ if (path.trim().length === 0)
58
+ throw new Error("Artifact path keys must be non-empty");
59
+ validateArtifactMetadata(metadata, path);
60
+ }
61
+ }
62
+ export function selectArtifactsByTags(registry, tags, match = "all") {
63
+ validateArtifactRegistry(registry);
64
+ if (tags.length === 0 || tags.some((tag) => typeof tag !== "string" || tag.trim().length === 0 || tag !== tag.trim())) {
65
+ throw new Error("Artifact tag query requires one or more non-empty trimmed strings");
66
+ }
67
+ const requested = new Set(tags);
68
+ return Object.entries(registry)
69
+ .filter(([, metadata]) => {
70
+ const available = new Set(metadata.tags ?? []);
71
+ return match === "all"
72
+ ? [...requested].every((tag) => available.has(tag))
73
+ : [...requested].some((tag) => available.has(tag));
74
+ })
75
+ .map(([path]) => path)
76
+ .sort();
77
+ }
78
+ export function isArtifactRegistry(value) {
79
+ try {
80
+ validateArtifactRegistry(value);
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ function validateSourceIdentity(source) {
88
+ if (typeof source.path !== "string" || source.path.trim().length === 0) {
89
+ throw new Error("Artifact source path must be non-empty");
90
+ }
91
+ if (!isArtifactHash(source.hash)) {
92
+ throw new Error(`Artifact source at ${source.path} must have a sha256:<64 lowercase hex characters> hash`);
93
+ }
94
+ }
95
+ function validateCompilerRevision(compiler, path) {
96
+ if (typeof compiler !== "string" || compiler.trim().length === 0) {
97
+ throw new Error(path === undefined
98
+ ? "Artifact compiler revision must be non-empty"
99
+ : `Artifact compiler revision at ${path} must be non-empty`);
100
+ }
101
+ }
102
+ function refreshRequested(path, explicitRefresh) {
103
+ if (explicitRefresh === true)
104
+ return true;
105
+ if (!explicitRefresh || typeof explicitRefresh !== "object" || typeof explicitRefresh.has !== "function")
106
+ return false;
107
+ return explicitRefresh.has(path);
108
+ }
109
+ function isProvenanceEntry(value) {
110
+ return isObject(value) && (value.malformed === undefined || value.malformed === true);
111
+ }
112
+ /** Parse one scope `meta.json` provenance registry; missing input means no recorded evidence. */
113
+ export function parseArtifactProvenanceRegistry(value, context = "Artifact provenance") {
114
+ if (value === undefined)
115
+ return {};
116
+ if (!isObject(value))
117
+ throw new Error(`${context} must be a path-keyed JSON object`);
118
+ const registry = {};
119
+ for (const [path, entry] of Object.entries(value)) {
120
+ if (path.trim().length === 0)
121
+ throw new Error(`${context} path keys must be non-empty`);
122
+ if (!isObject(entry) || !isJsonValue(entry)) {
123
+ registry[path] = { malformed: true };
124
+ continue;
125
+ }
126
+ const known = new Set(["sourceHash", "compilerRevision", "compiledAt"]);
127
+ if (Object.keys(entry).some((key) => !known.has(key))) {
128
+ registry[path] = { malformed: true };
129
+ continue;
130
+ }
131
+ registry[path] = {
132
+ ...(Object.hasOwn(entry, "sourceHash") ? { sourceHash: entry.sourceHash } : {}),
133
+ ...(Object.hasOwn(entry, "compilerRevision") ? { compilerRevision: entry.compilerRevision } : {}),
134
+ ...(Object.hasOwn(entry, "compiledAt") ? { compiledAt: entry.compiledAt } : {}),
135
+ };
136
+ }
137
+ return registry;
138
+ }
139
+ /** Canonical retained form; uninterpretable entries cannot round-trip and are omitted. */
140
+ export function serializeArtifactProvenanceRegistry(registry) {
141
+ const artifacts = {};
142
+ for (const [path, entry] of Object.entries(registry)) {
143
+ if (entry.malformed === true)
144
+ continue;
145
+ const fields = {};
146
+ if (entry.sourceHash !== undefined)
147
+ fields.sourceHash = entry.sourceHash;
148
+ if (entry.compilerRevision !== undefined)
149
+ fields.compilerRevision = entry.compilerRevision;
150
+ if (entry.compiledAt !== undefined)
151
+ fields.compiledAt = entry.compiledAt;
152
+ if (Object.keys(fields).length === 0)
153
+ continue;
154
+ artifacts[path] = fields;
155
+ }
156
+ return artifacts;
157
+ }
158
+ function invalidField(value, validate) {
159
+ return value !== undefined && !validate(value);
160
+ }
161
+ const INVALID_EVIDENCE = Symbol("invalid-evidence");
162
+ /** Later authority wins per field; absent runtime evidence falls back to retired embedded values. */
163
+ function fieldEvidence(entry, field, legacy) {
164
+ if (entry?.malformed === true)
165
+ return INVALID_EVIDENCE;
166
+ if (entry !== undefined && Object.hasOwn(entry, field))
167
+ return entry[field];
168
+ return legacy;
169
+ }
170
+ /** Classify freshness from semantic state and runtime provenance without acquiring the source body. */
171
+ export function classifyArtifactFreshness(source, metadata, compiler, explicitRefresh = false, provenance) {
172
+ validateSourceIdentity(source);
173
+ validateCompilerRevision(compiler);
174
+ if (metadata === undefined)
175
+ return { kind: "requires-compilation", reason: "new" };
176
+ if (!isArtifactMetadata(metadata))
177
+ return { kind: "requires-compilation", reason: "invalid-metadata" };
178
+ const entry = isProvenanceEntry(provenance) ? provenance : undefined;
179
+ const runtime = entry?.malformed === true;
180
+ const sourceHash = runtime ? INVALID_EVIDENCE : fieldEvidence(entry, "sourceHash", metadata.hash);
181
+ if (sourceHash === INVALID_EVIDENCE || invalidField(sourceHash, (value) => isArtifactHash(value))) {
182
+ return { kind: "requires-compilation", reason: "invalid-metadata" };
183
+ }
184
+ if (typeof sourceHash === "string" && sourceHash !== source.hash) {
185
+ return { kind: "requires-compilation", reason: "source-changed" };
186
+ }
187
+ const compilerRevision = runtime ? INVALID_EVIDENCE : fieldEvidence(entry, "compilerRevision", metadata.compiler);
188
+ if (compilerRevision === INVALID_EVIDENCE || invalidField(compilerRevision, (value) => typeof value === "string" && value.trim().length > 0)) {
189
+ return { kind: "requires-compilation", reason: "invalid-metadata" };
190
+ }
191
+ if (typeof compilerRevision === "string" && compilerRevision !== compiler) {
192
+ return { kind: "requires-compilation", reason: "compiler-changed" };
193
+ }
194
+ if (explicitRefresh)
195
+ return { kind: "requires-compilation", reason: "explicit-refresh" };
196
+ return { kind: "fresh" };
197
+ }
198
+ /** Produce a deterministic acquisition plan from path/hash candidates and retained evidence. */
199
+ export function planArtifactInvalidation(sources, registry, compiler, options = {}, provenance = {}) {
200
+ if (!isObject(registry))
201
+ throw new Error("Artifacts must be a path-keyed JSON object");
202
+ validateCompilerRevision(compiler);
203
+ const ordered = [...sources].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
204
+ const seen = new Set();
205
+ const fresh = [];
206
+ const requiresCompilation = [];
207
+ for (const source of ordered) {
208
+ if (seen.has(source.path))
209
+ throw new Error(`Duplicate artifact source path: ${source.path}`);
210
+ seen.add(source.path);
211
+ const metadata = Object.hasOwn(registry, source.path) ? registry[source.path] : undefined;
212
+ const entry = Object.hasOwn(provenance, source.path) ? provenance[source.path] : undefined;
213
+ const freshness = classifyArtifactFreshness(source, metadata, compiler, refreshRequested(source.path, options.explicitRefresh), entry);
214
+ const identity = { path: source.path, hash: source.hash };
215
+ if (freshness.kind === "fresh")
216
+ fresh.push(identity);
217
+ else
218
+ requiresCompilation.push({ ...identity, reason: freshness.reason });
219
+ }
220
+ if (options.removed !== undefined && !Array.isArray(options.removed))
221
+ throw new Error("Removed artifact paths must be an array");
222
+ const removed = [...new Set(options.removed ?? [])];
223
+ for (const path of removed) {
224
+ if (typeof path !== "string" || path.trim().length === 0)
225
+ throw new Error("Removed artifact paths must be non-empty");
226
+ if (seen.has(path))
227
+ throw new Error(`Artifact source cannot be both present and removed: ${path}`);
228
+ }
229
+ return { fresh, requiresCompilation, removed: removed.filter((path) => Object.hasOwn(registry, path)).sort() };
230
+ }
231
+ /** Split one compiler output into model-visible semantics and runtime-owned provenance. */
232
+ export function compileArtifact(update) {
233
+ validateSourceIdentity(update.source);
234
+ validateCompilerRevision(update.compiler, update.source.path);
235
+ if (!isObject(update.output) || MODEL_FORBIDDEN_PROVENANCE_FIELDS.some((field) => Object.hasOwn(update.output, field))) {
236
+ throw new Error(`Artifact compiler output at ${update.source.path} cannot set runtime-owned provenance fields`);
237
+ }
238
+ // Timestamps are runtime evidence; the model-visible entry never retains them.
239
+ const semantic = structuredClone(update.output);
240
+ delete semantic.compiled_at;
241
+ validateArtifactMetadata(semantic, update.source.path);
242
+ return {
243
+ semantic,
244
+ provenance: {
245
+ sourceHash: update.source.hash,
246
+ compilerRevision: update.compiler,
247
+ ...(typeof update.output.compiled_at === "string" ? { compiledAt: update.output.compiled_at } : {}),
248
+ },
249
+ };
250
+ }
251
+ /** Merge one compilation/removal cohort into the runtime-owned provenance registry. */
252
+ export function updateArtifactProvenance(registry, updates, removed = []) {
253
+ const next = structuredClone(registry);
254
+ for (const update of updates)
255
+ next[update.source.path] = compileArtifact(update).provenance;
256
+ for (const path of removed)
257
+ delete next[path];
258
+ return next;
259
+ }
260
+ /** Keep only provenance whose artifact path still exists in the given semantic registry. */
261
+ export function pruneArtifactProvenance(registry, semantic) {
262
+ const next = {};
263
+ for (const [path, entry] of Object.entries(registry)) {
264
+ if (Object.hasOwn(semantic, path))
265
+ next[path] = structuredClone(entry);
266
+ }
267
+ return next;
268
+ }
269
+ /** Validate and apply a whole compilation/removal cohort of model-visible artifacts. */
270
+ export function updateArtifactRegistry(registry, updates, removed = []) {
271
+ validateArtifactRegistry(registry);
272
+ const compiled = new Map();
273
+ for (const update of updates) {
274
+ if (compiled.has(update.source.path))
275
+ throw new Error(`Duplicate artifact compilation: ${update.source.path}`);
276
+ compiled.set(update.source.path, compileArtifact(update).semantic);
277
+ }
278
+ const removals = new Set();
279
+ for (const path of removed) {
280
+ if (typeof path !== "string" || path.trim().length === 0)
281
+ throw new Error("Removed artifact paths must be non-empty");
282
+ if (removals.has(path))
283
+ throw new Error(`Duplicate artifact removal: ${path}`);
284
+ if (compiled.has(path))
285
+ throw new Error(`Artifact cannot be compiled and removed atomically: ${path}`);
286
+ removals.add(path);
287
+ }
288
+ const next = structuredClone(registry);
289
+ for (const path of removals)
290
+ delete next[path];
291
+ for (const [path, metadata] of compiled) {
292
+ Object.defineProperty(next, path, {
293
+ value: metadata,
294
+ enumerable: true,
295
+ configurable: true,
296
+ writable: true,
297
+ });
298
+ }
299
+ return next;
300
+ }
301
+ /** Runtime-owned artifact fields that never belong in ordinary model context. */
302
+ const RUNTIME_ARTIFACT_FIELDS = [...MODEL_FORBIDDEN_PROVENANCE_FIELDS, "compiled_at"];
303
+ /** Validate authored fields only: legacy retained evidence stays readable but cannot be model-edited. */
304
+ export function validateModelArtifactPatch(patch) {
305
+ for (const [path, entry] of Object.entries(patch)) {
306
+ if (!isObject(entry))
307
+ continue; // Whole-artifact deletion and materialized shape belong to the transition owner.
308
+ const field = RUNTIME_ARTIFACT_FIELDS.find((field) => Object.hasOwn(entry, field));
309
+ if (field !== undefined)
310
+ throw new Error(`Artifact patch at ${path} cannot set runtime-owned provenance field ${field}`);
311
+ }
312
+ }
313
+ /** Strip retained runtime bookkeeping from one model-visible artifact entry. */
314
+ export function projectArtifactForModel(entry) {
315
+ if (!isObject(entry))
316
+ return entry;
317
+ const projected = structuredClone(entry);
318
+ for (const field of RUNTIME_ARTIFACT_FIELDS)
319
+ delete projected[field];
320
+ return projected;
321
+ }
322
+ /** Strip retained runtime bookkeeping from a model-visible artifact registry. */
323
+ export function projectArtifactsForModel(registry) {
324
+ const projected = {};
325
+ for (const [path, entry] of Object.entries(registry)) {
326
+ Object.defineProperty(projected, path, {
327
+ value: projectArtifactForModel(entry),
328
+ enumerable: true,
329
+ configurable: true,
330
+ writable: true,
331
+ });
332
+ }
333
+ return projected;
334
+ }
@@ -0,0 +1,49 @@
1
+ import type { CompactionResult } from "@earendil-works/pi-coding-agent";
2
+ export declare const STATE_FLOW_COMPACTION_SUMMARY = "State Flow accepted the completed work before this boundary. Current memory is restored from its durable revision and projected separately; use the retained native entries for subsequent work.";
3
+ /** A modest margin above Pi's default 20k retained suffix absorbs estimation drift. */
4
+ export declare const STATE_FLOW_COMPACTION_MIN_CONTEXT_TOKENS = 24000;
5
+ export interface StateFlowCompactionDetails {
6
+ version: 1;
7
+ owner: "state-flow";
8
+ revision: string;
9
+ step: number;
10
+ }
11
+ export interface StateFlowCompactionPlan {
12
+ leafId: string;
13
+ firstKeptEntryId: string;
14
+ details: StateFlowCompactionDetails;
15
+ }
16
+ type ActiveEntry = {
17
+ id?: unknown;
18
+ type?: unknown;
19
+ customType?: unknown;
20
+ message?: {
21
+ role?: unknown;
22
+ stopReason?: unknown;
23
+ content?: unknown;
24
+ };
25
+ };
26
+ export declare function shouldRequestStateFlowCompaction(usage: {
27
+ tokens: number | null;
28
+ } | undefined): boolean;
29
+ /**
30
+ * Pi's public usage includes the system prompt, while native compaction can only
31
+ * shorten persisted messages. Avoid requesting a visibly failing manual
32
+ * compaction when that transcript is still below the useful-history floor.
33
+ */
34
+ export declare function hasCompactionSizedTranscript(entries: readonly ActiveEntry[]): boolean;
35
+ /** Retain the complete latest accepted user iteration without hiding foreign extension context. */
36
+ export declare function planStateFlowCompaction(entries: readonly ActiveEntry[], revision: string, step: number): StateFlowCompactionPlan | undefined;
37
+ /** Customize only the extension-owned manual request and only while its planned leaf remains selected. */
38
+ export declare function stateFlowCompactionResult(plan: StateFlowCompactionPlan, marker: string, event: {
39
+ reason: "manual" | "threshold" | "overflow";
40
+ customInstructions?: string;
41
+ branchEntries: readonly ActiveEntry[];
42
+ preparation: {
43
+ tokensBefore: number;
44
+ };
45
+ signal: AbortSignal;
46
+ }): CompactionResult<StateFlowCompactionDetails> | {
47
+ cancel: true;
48
+ } | undefined;
49
+ export {};
@@ -0,0 +1,67 @@
1
+ import { estimateTokens } from "@earendil-works/pi-agent-core";
2
+ export const STATE_FLOW_COMPACTION_SUMMARY = "State Flow accepted the completed work before this boundary. Current memory is restored from its durable revision and projected separately; use the retained native entries for subsequent work.";
3
+ /** A modest margin above Pi's default 20k retained suffix absorbs estimation drift. */
4
+ export const STATE_FLOW_COMPACTION_MIN_CONTEXT_TOKENS = 24_000;
5
+ function stateFlowEntry(entry) {
6
+ return entry.type === "custom"
7
+ && typeof entry.customType === "string"
8
+ && entry.customType.startsWith("state-flow-");
9
+ }
10
+ export function shouldRequestStateFlowCompaction(usage) {
11
+ return typeof usage?.tokens === "number"
12
+ && Number.isFinite(usage.tokens)
13
+ && usage.tokens >= STATE_FLOW_COMPACTION_MIN_CONTEXT_TOKENS;
14
+ }
15
+ /**
16
+ * Pi's public usage includes the system prompt, while native compaction can only
17
+ * shorten persisted messages. Avoid requesting a visibly failing manual
18
+ * compaction when that transcript is still below the useful-history floor.
19
+ */
20
+ export function hasCompactionSizedTranscript(entries) {
21
+ let tokens = 0;
22
+ for (const entry of entries) {
23
+ if (entry.type !== "message" || typeof entry.message?.role !== "string")
24
+ continue;
25
+ tokens += estimateTokens(entry.message);
26
+ if (tokens >= STATE_FLOW_COMPACTION_MIN_CONTEXT_TOKENS)
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ /** Retain the complete latest accepted user iteration without hiding foreign extension context. */
32
+ export function planStateFlowCompaction(entries, revision, step) {
33
+ if (!/^[0-9a-f]{40,64}$/.test(revision) && !/^file:[0-9a-f]{64}$/.test(revision))
34
+ return undefined;
35
+ if (!Number.isSafeInteger(step) || step < 0 || entries.length === 0)
36
+ return undefined;
37
+ const terminal = entries.findLastIndex((entry) => entry.type === "message" && entry.message?.role === "assistant");
38
+ if (terminal < 0 || entries[terminal]?.message?.stopReason === "aborted"
39
+ || entries[terminal]?.message?.stopReason === "error"
40
+ || entries[terminal]?.message?.stopReason === "length")
41
+ return undefined;
42
+ let keep = terminal;
43
+ while (keep >= 0 && !(entries[keep]?.type === "message" && entries[keep]?.message?.role === "user"))
44
+ keep--;
45
+ if (keep < 0)
46
+ return undefined;
47
+ if (entries.slice(0, keep).some((entry) => entry.type === "custom" && !stateFlowEntry(entry)))
48
+ return undefined;
49
+ const firstKeptEntryId = entries[keep]?.id;
50
+ const leafId = entries.at(-1)?.id;
51
+ if (typeof firstKeptEntryId !== "string" || typeof leafId !== "string")
52
+ return undefined;
53
+ return { leafId, firstKeptEntryId, details: { version: 1, owner: "state-flow", revision, step } };
54
+ }
55
+ /** Customize only the extension-owned manual request and only while its planned leaf remains selected. */
56
+ export function stateFlowCompactionResult(plan, marker, event) {
57
+ if (event.reason !== "manual" || event.customInstructions !== marker)
58
+ return undefined;
59
+ if (event.signal.aborted || event.branchEntries.at(-1)?.id !== plan.leafId)
60
+ return { cancel: true };
61
+ return {
62
+ summary: STATE_FLOW_COMPACTION_SUMMARY,
63
+ firstKeptEntryId: plan.firstKeptEntryId,
64
+ tokensBefore: event.preparation.tokensBefore,
65
+ details: structuredClone(plan.details),
66
+ };
67
+ }
@@ -0,0 +1,11 @@
1
+ export interface StateFlowConfig {
2
+ directory: string;
3
+ autoStart: boolean;
4
+ /** Opt-in local capture of rejected patch attempts and unresolved terminal drafts. */
5
+ logging: boolean;
6
+ /** Show successful patch_state arguments in the interactive tool row. */
7
+ showSuccessfulPatches: boolean;
8
+ remotePublication?: "off" | "turn-end" | "transition";
9
+ }
10
+ /** Read once at extension load/reload. Missing config uses defaults; invalid config never falls back. */
11
+ export declare function loadStateFlowConfig(agentDir?: string): StateFlowConfig;
@@ -0,0 +1,53 @@
1
+ // Domain: agent-level State Flow configuration, independent of session runtime and semantic state.
2
+ import { lstatSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, resolve } from "node:path";
5
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
+ import { getDurableRepositoryRoot } from "./durable.js";
7
+ import { isObject } from "./json.js";
8
+ /** Read once at extension load/reload. Missing config uses defaults; invalid config never falls back. */
9
+ export function loadStateFlowConfig(agentDir = getAgentDir()) {
10
+ const path = resolve(agentDir, "state-flow.json");
11
+ const defaults = {
12
+ directory: getDurableRepositoryRoot(agentDir),
13
+ autoStart: false,
14
+ logging: false,
15
+ showSuccessfulPatches: true,
16
+ };
17
+ let value;
18
+ try {
19
+ if (!lstatSync(path, { throwIfNoEntry: false }))
20
+ return defaults;
21
+ value = JSON.parse(readFileSync(path, "utf8"));
22
+ }
23
+ catch (error) {
24
+ throw new Error(`Cannot read State Flow configuration: ${path}`, { cause: error });
25
+ }
26
+ const allowed = new Set(["directory", "autoStart", "logging", "showSuccessfulPatches", "remotePublication"]);
27
+ if (!isObject(value) || Object.keys(value).some((key) => !allowed.has(key))) {
28
+ throw new Error(`State Flow configuration contains unknown settings: ${path}`);
29
+ }
30
+ if (Object.hasOwn(value, "autoStart") && typeof value.autoStart !== "boolean")
31
+ throw new Error(`State Flow autoStart must be a boolean: ${path}`);
32
+ if (Object.hasOwn(value, "logging") && typeof value.logging !== "boolean")
33
+ throw new Error(`State Flow logging must be a boolean: ${path}`);
34
+ if (Object.hasOwn(value, "showSuccessfulPatches") && typeof value.showSuccessfulPatches !== "boolean")
35
+ throw new Error(`State Flow showSuccessfulPatches must be a boolean: ${path}`);
36
+ if (Object.hasOwn(value, "remotePublication") && value.remotePublication !== "off" && value.remotePublication !== "turn-end" && value.remotePublication !== "transition") {
37
+ throw new Error(`State Flow remotePublication must be off, turn-end, or transition: ${path}`);
38
+ }
39
+ if (Object.hasOwn(value, "directory") && (typeof value.directory !== "string" || !value.directory.trim() || value.directory.includes("\0"))) {
40
+ throw new Error(`State Flow directory must be a non-empty path: ${path}`);
41
+ }
42
+ const directory = value.directory;
43
+ if (directory?.startsWith("~") && directory !== "~" && !directory.startsWith("~/"))
44
+ throw new Error(`State Flow directory supports ~ or ~/ paths, not named-user expansion: ${path}`);
45
+ const expanded = directory === "~" ? homedir() : directory?.startsWith("~/") ? resolve(homedir(), directory.slice(2)) : directory;
46
+ return {
47
+ directory: expanded === undefined ? defaults.directory : resolve(dirname(path), expanded),
48
+ autoStart: value.autoStart === true,
49
+ logging: value.logging === true,
50
+ showSuccessfulPatches: value.showSuccessfulPatches !== false,
51
+ ...(value.remotePublication === undefined ? {} : { remotePublication: value.remotePublication }),
52
+ };
53
+ }
@@ -0,0 +1,23 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import { type ArtifactInvalidationNotice } from "./artifact.ts";
3
+ import type { RecentTransitionWindow } from "./history.ts";
4
+ import type { Snapshot } from "./snapshot.ts";
5
+ import type { RehydrationPhase } from "./rehydration.ts";
6
+ import { type MaterializedState } from "./state.ts";
7
+ export declare const VALIDATION_MESSAGE_TYPE = "state-flow-validation";
8
+ /** Bounded context retained after semantic State Flow is stopped in this physical session. */
9
+ export interface PassiveContinuation {
10
+ startedAt: number;
11
+ activeRunStartedAt?: number;
12
+ handoff: AgentMessage;
13
+ }
14
+ export declare function syntheticUser(text: string): AgentMessage;
15
+ export declare function withoutPrivateValidation(messages: AgentMessage[]): AgentMessage[];
16
+ export declare function createPassiveContinuation(state: MaterializedState, startedAt?: number, activeRunStartedAt?: number): PassiveContinuation;
17
+ /** Keep the interrupted run through later results; an idle stop retains only later conversation. */
18
+ export declare function passiveContinuationMessages(messages: AgentMessage[], continuation: PassiveContinuation): AgentMessage[];
19
+ export declare function runtimeContextMessage(snapshot: Snapshot, state: MaterializedState, recentTransitions?: RecentTransitionWindow, artifactInvalidations?: readonly ArtifactInvalidationNotice[], rehydrationPhase?: RehydrationPhase, resolutionPending?: boolean): AgentMessage;
20
+ export declare function currentRunTrajectory(messages: AgentMessage[], specification: string, anchorTimestamp: number | undefined): {
21
+ messages: AgentMessage[];
22
+ anchorTimestamp?: number;
23
+ };
@@ -0,0 +1,109 @@
1
+ import { projectArtifactForModel } from "./artifact.js";
2
+ import { canonicalJson } from "./json.js";
3
+ import { projectStateForModel } from "./state.js";
4
+ export const VALIDATION_MESSAGE_TYPE = "state-flow-validation";
5
+ export function syntheticUser(text) {
6
+ return { role: "user", content: [{ type: "text", text }], timestamp: Date.now() };
7
+ }
8
+ function contentText(content) {
9
+ if (typeof content === "string")
10
+ return content;
11
+ if (!Array.isArray(content))
12
+ return "";
13
+ return content.map((part) => {
14
+ if (typeof part !== "object" || part === null)
15
+ return "";
16
+ const block = part;
17
+ return block.type === "text" && typeof block.text === "string" ? block.text : "";
18
+ }).filter(Boolean).join("\n");
19
+ }
20
+ function messageText(message) {
21
+ return contentText(message.content);
22
+ }
23
+ export function withoutPrivateValidation(messages) {
24
+ return messages.filter((message) => {
25
+ return !(message.role === "custom" && message.customType === VALIDATION_MESSAGE_TYPE);
26
+ });
27
+ }
28
+ export function createPassiveContinuation(state, startedAt = Date.now(), activeRunStartedAt) {
29
+ return {
30
+ startedAt,
31
+ ...(activeRunStartedAt === undefined ? {} : { activeRunStartedAt }),
32
+ handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${canonicalJson({ state, continuation: "State Flow semantics are disabled; this handoff replaces completed history while retaining the active and post-stop trajectory." })}`),
33
+ };
34
+ }
35
+ /** Keep the interrupted run through later results; an idle stop retains only later conversation. */
36
+ export function passiveContinuationMessages(messages, continuation) {
37
+ let start = continuation.activeRunStartedAt === undefined ? -1
38
+ : messages.findIndex((message) => message.role === "user" && message.timestamp === continuation.activeRunStartedAt);
39
+ if (start < 0)
40
+ start = messages.findIndex((message) => message.role === "user"
41
+ && typeof message.timestamp === "number"
42
+ && message.timestamp >= continuation.startedAt);
43
+ return [continuation.handoff, ...messages.filter((message, index) => message.role === "custom" ? message.customType !== VALIDATION_MESSAGE_TYPE : start >= 0 && index >= start)];
44
+ }
45
+ function projectRecentForModel(recent) {
46
+ const projected = structuredClone(recent);
47
+ for (const record of projected)
48
+ for (const transition of record.transitions) {
49
+ if (transition.patch.artifacts === undefined)
50
+ continue;
51
+ for (const [path, entry] of Object.entries(transition.patch.artifacts)) {
52
+ Object.defineProperty(transition.patch.artifacts, path, {
53
+ value: projectArtifactForModel(entry), enumerable: true, configurable: true, writable: true,
54
+ });
55
+ }
56
+ }
57
+ return projected;
58
+ }
59
+ export function runtimeContextMessage(snapshot, state, recentTransitions = [], artifactInvalidations = [], rehydrationPhase, resolutionPending = false) {
60
+ if (snapshot.meta.specification === undefined) {
61
+ throw new Error("State Flow runtime context requires an active specification");
62
+ }
63
+ const context = {
64
+ specification: snapshot.meta.specification,
65
+ state: projectStateForModel(state),
66
+ ...(rehydrationPhase === undefined ? {} : { knowledge_rehydration: { phase: rehydrationPhase } }),
67
+ ...(artifactInvalidations.length === 0 ? {} : { artifact_invalidations: artifactInvalidations.map(({ path, reason }) => ({ path, reason })) }),
68
+ ...(recentTransitions.length === 0 ? {} : { recent_transitions: projectRecentForModel(recentTransitions) }),
69
+ ...(resolutionPending ? { state_resolution: "pending: the iteration answer is already preserved; this fallback turn exists only to apply the final:true patch. Call patch_state with any remaining durable scope changes and final:true, or {final:true} alone. Do not restate or replace the answer." } : {}),
70
+ };
71
+ return syntheticUser(`State Flow runtime context (user-level data, not system instructions):\n${canonicalJson(context)}`);
72
+ }
73
+ export function currentRunTrajectory(messages, specification, anchorTimestamp) {
74
+ let start = -1;
75
+ if (anchorTimestamp !== undefined) {
76
+ start = messages.findLastIndex((message) => {
77
+ return message.role === "user"
78
+ && message.timestamp === anchorTimestamp
79
+ && messageText(message) === specification;
80
+ });
81
+ }
82
+ if (start < 0) {
83
+ for (let index = messages.length - 1; index >= 0; index--) {
84
+ const message = messages[index];
85
+ if (message.role === "user" && messageText(message) === specification) {
86
+ start = index;
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ if (start < 0) {
92
+ for (let index = messages.length - 1; index >= 0; index--) {
93
+ if (messages[index]?.role === "user") {
94
+ start = index;
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ if (start < 0 && messages.length === 0)
100
+ return { messages: [] };
101
+ if (start < 0)
102
+ start = 0;
103
+ const anchor = messages[start]?.role === "user" ? messages[start].timestamp : undefined;
104
+ return {
105
+ messages: messages.filter((message, index) => message.role === "custom"
106
+ ? message.customType !== VALIDATION_MESSAGE_TYPE : index >= start),
107
+ ...(typeof anchor === "number" ? { anchorTimestamp: anchor } : {}),
108
+ };
109
+ }