@osolmaz/pi-workflows 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +4 -0
- package/dist/builtins/autoplan.workflow.d.ts +6 -0
- package/dist/builtins/autoplan.workflow.js +68 -32
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +3 -3
- package/dist/builtins/plain-summary.workflow.js +35 -24
- package/dist/builtins/plain-summary.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +2 -0
- package/dist/builtins/sanity-check.workflow.js +19 -22
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/controllers/sqlite.js +16 -51
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.js +45 -7
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/index.js +2 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +8 -8
- package/dist/extension/recorder.js.map +1 -1
- package/dist/state/schema.js +22 -3
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +0 -1
- package/dist/viewer/session-reducer.js +2 -24
- package/dist/viewer/session-reducer.js.map +1 -1
- package/dist/workflows/engine.js +4 -3
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/store.d.ts +6 -1
- package/dist/workflows/store.js +286 -33
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +1 -1
- package/docs/SQLITE_STATE.md +20 -14
- package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
- package/docs/session-event-journal.md +2 -3
- package/docs/workflows.md +36 -9
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoplan.workflow.ts +80 -32
- package/src/builtins/catalog.ts +3 -3
- package/src/builtins/plain-summary.workflow.ts +38 -40
- package/src/builtins/sanity-check.workflow.ts +19 -22
- package/src/controllers/sqlite.ts +19 -53
- package/src/extension/decision-channels.ts +47 -7
- package/src/extension/index.ts +2 -1
- package/src/extension/recorder.ts +10 -8
- package/src/state/schema.ts +22 -3
- package/src/viewer/session-reducer.ts +2 -29
- package/src/workflows/engine.ts +4 -3
- package/src/workflows/store.ts +397 -43
- package/src/workflows/types.ts +0 -1
|
@@ -28,6 +28,7 @@ const TELEGRAM_TEXT_LIMIT = 4_096;
|
|
|
28
28
|
const PI_PRESENTATION_WINDOW_LINES = 18;
|
|
29
29
|
const DEFAULT_API_BASE = "https://api.telegram.org";
|
|
30
30
|
const LEASE_TTL_MS = 60_000;
|
|
31
|
+
const LEASE_RENEW_WINDOW_MS = 20_000;
|
|
31
32
|
const LEASE_RETRY_MS = 5_000;
|
|
32
33
|
const POLL_BACKOFF_MS = 1_000;
|
|
33
34
|
const MAX_SETTLEMENT_ATTEMPTS = 3;
|
|
@@ -428,6 +429,21 @@ class TelegramProjection {
|
|
|
428
429
|
}
|
|
429
430
|
|
|
430
431
|
acquire(now = Date.now()): boolean {
|
|
432
|
+
const observed = this.lease();
|
|
433
|
+
const observedToken = tokenHash(this.token);
|
|
434
|
+
const ownedByThis =
|
|
435
|
+
observed.ownerId === this.owner &&
|
|
436
|
+
observed.tokenHash !== null &&
|
|
437
|
+
observed.tokenHash.equals(observedToken) &&
|
|
438
|
+
observed.expiresAt !== null &&
|
|
439
|
+
observed.expiresAt > now;
|
|
440
|
+
if (ownedByThis) {
|
|
441
|
+
this.generation = observed.generation;
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
if (observed.ownerId !== null && observed.expiresAt !== null && observed.expiresAt > now) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
431
447
|
return this.state.transaction(() => {
|
|
432
448
|
const lease = this.lease();
|
|
433
449
|
if (
|
|
@@ -438,7 +454,11 @@ class TelegramProjection {
|
|
|
438
454
|
) {
|
|
439
455
|
return false;
|
|
440
456
|
}
|
|
441
|
-
const
|
|
457
|
+
const sameToken =
|
|
458
|
+
lease.ownerId === this.owner &&
|
|
459
|
+
lease.tokenHash !== null &&
|
|
460
|
+
lease.tokenHash.equals(observedToken);
|
|
461
|
+
const generation = sameToken ? lease.generation : lease.generation + 1;
|
|
442
462
|
const result = this.state.connection
|
|
443
463
|
.prepare(
|
|
444
464
|
`UPDATE leases
|
|
@@ -449,7 +469,7 @@ class TelegramProjection {
|
|
|
449
469
|
.run(
|
|
450
470
|
generation,
|
|
451
471
|
this.owner,
|
|
452
|
-
|
|
472
|
+
observedToken,
|
|
453
473
|
now,
|
|
454
474
|
now,
|
|
455
475
|
now + LEASE_TTL_MS,
|
|
@@ -465,6 +485,18 @@ class TelegramProjection {
|
|
|
465
485
|
}
|
|
466
486
|
|
|
467
487
|
renew(now = Date.now()): boolean {
|
|
488
|
+
const lease = this.lease();
|
|
489
|
+
if (
|
|
490
|
+
lease.ownerId !== this.owner ||
|
|
491
|
+
lease.generation !== this.generation ||
|
|
492
|
+
lease.tokenHash === null ||
|
|
493
|
+
!lease.tokenHash.equals(tokenHash(this.token)) ||
|
|
494
|
+
lease.expiresAt === null ||
|
|
495
|
+
lease.expiresAt <= now
|
|
496
|
+
) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
if (lease.expiresAt - now > LEASE_RENEW_WINDOW_MS) return true;
|
|
468
500
|
return (
|
|
469
501
|
this.state.connection
|
|
470
502
|
.prepare(
|
|
@@ -1222,11 +1254,11 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
1222
1254
|
|
|
1223
1255
|
private async poll(signal: AbortSignal): Promise<void> {
|
|
1224
1256
|
while (this.running && !signal.aborted) {
|
|
1225
|
-
if (!this.projection.acquire()) {
|
|
1226
|
-
await wait(LEASE_RETRY_MS, signal);
|
|
1227
|
-
continue;
|
|
1228
|
-
}
|
|
1229
1257
|
try {
|
|
1258
|
+
if (!this.projection.acquire()) {
|
|
1259
|
+
await wait(leaseRetryDelay(), signal);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1230
1262
|
await this.deliverPendingRequests();
|
|
1231
1263
|
const result = await this.call(
|
|
1232
1264
|
"getUpdates",
|
|
@@ -1246,7 +1278,11 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
1246
1278
|
if (!this.projection.renew()) this.projection.release();
|
|
1247
1279
|
} catch {
|
|
1248
1280
|
if (signal.aborted) return;
|
|
1249
|
-
|
|
1281
|
+
try {
|
|
1282
|
+
await wait(POLL_BACKOFF_MS, signal);
|
|
1283
|
+
} catch {
|
|
1284
|
+
if (signal.aborted) return;
|
|
1285
|
+
}
|
|
1250
1286
|
}
|
|
1251
1287
|
}
|
|
1252
1288
|
}
|
|
@@ -1773,6 +1809,10 @@ async function writePrivateJson(filePath: string, value: unknown): Promise<void>
|
|
|
1773
1809
|
await fsp.chmod(filePath, 0o600);
|
|
1774
1810
|
}
|
|
1775
1811
|
|
|
1812
|
+
function leaseRetryDelay(): number {
|
|
1813
|
+
return LEASE_RETRY_MS + Math.floor(Math.random() * 1_000);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1776
1816
|
async function wait(milliseconds: number, signal: AbortSignal): Promise<void> {
|
|
1777
1817
|
await new Promise<void>((resolve) => {
|
|
1778
1818
|
const timer = setTimeout(resolve, milliseconds);
|
package/src/extension/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from "../controllers/sqlite.js";
|
|
10
10
|
import type { JsonObject } from "../controllers/types.js";
|
|
11
11
|
import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
|
|
12
|
+
import { canonicalJson } from "../state/json.js";
|
|
12
13
|
import { compositionMetadata } from "../workflows/composition.js";
|
|
13
14
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
14
15
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
@@ -219,7 +220,7 @@ type StartRunOptions = {
|
|
|
219
220
|
};
|
|
220
221
|
|
|
221
222
|
function definitionDigest(snapshot: WorkflowDefinitionSnapshot): string {
|
|
222
|
-
return `sha256:${createHash("sha256").update(
|
|
223
|
+
return `sha256:${createHash("sha256").update(canonicalJson(snapshot)).digest("hex")}`;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unknown {
|
|
@@ -246,6 +246,13 @@ export class SessionRecorder {
|
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
248
|
const normalized = normalizeAssistantEvent(event.assistantMessageEvent);
|
|
249
|
+
if (
|
|
250
|
+
normalized.type === "text_delta" ||
|
|
251
|
+
normalized.type === "thinking_delta" ||
|
|
252
|
+
normalized.type === "toolcall_delta"
|
|
253
|
+
) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
249
256
|
const toolCallId = toolCallIdFromAssistantEvent(normalized);
|
|
250
257
|
if (toolCallId) {
|
|
251
258
|
this.toolOwners.set(toolCallId, owner);
|
|
@@ -295,14 +302,9 @@ export class SessionRecorder {
|
|
|
295
302
|
});
|
|
296
303
|
}
|
|
297
304
|
|
|
298
|
-
handleToolUpdate(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
this.enqueue(owner, "tool_execution_updated", {}, undefined, {
|
|
304
|
-
toolCallId: event.toolCallId,
|
|
305
|
-
});
|
|
305
|
+
handleToolUpdate(_event: ToolExecutionUpdateEventLike): void {
|
|
306
|
+
// Incremental tool progress is transient. The recorder stores the settled
|
|
307
|
+
// tool result and the surrounding lifecycle facts.
|
|
306
308
|
}
|
|
307
309
|
|
|
308
310
|
handleToolEnd(event: ToolExecutionEndEventLike): void {
|
package/src/state/schema.ts
CHANGED
|
@@ -106,8 +106,15 @@ CREATE TABLE runs (
|
|
|
106
106
|
paused INTEGER NOT NULL DEFAULT 0 CHECK (paused IN (0, 1)),
|
|
107
107
|
status_detail TEXT,
|
|
108
108
|
input_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
|
|
109
|
-
|
|
109
|
+
workflow_sources_hash BLOB REFERENCES blobs(blob_hash),
|
|
110
|
+
human_decision_hash BLOB REFERENCES blobs(blob_hash),
|
|
111
|
+
final_output_hash BLOB REFERENCES blobs(blob_hash),
|
|
110
112
|
error_hash BLOB REFERENCES blobs(blob_hash),
|
|
113
|
+
carried_step_count INTEGER NOT NULL DEFAULT 0 CHECK (carried_step_count >= 0),
|
|
114
|
+
current_node TEXT,
|
|
115
|
+
current_attempt_id TEXT,
|
|
116
|
+
current_node_started_at INTEGER,
|
|
117
|
+
waiting_on TEXT,
|
|
111
118
|
created_at INTEGER NOT NULL,
|
|
112
119
|
updated_at INTEGER NOT NULL,
|
|
113
120
|
finished_at INTEGER,
|
|
@@ -159,9 +166,9 @@ CREATE TABLE node_attempts (
|
|
|
159
166
|
)),
|
|
160
167
|
input_hash BLOB REFERENCES blobs(blob_hash),
|
|
161
168
|
contract_hash BLOB REFERENCES blobs(blob_hash),
|
|
162
|
-
|
|
169
|
+
prompt_hash BLOB REFERENCES blobs(blob_hash),
|
|
163
170
|
output_hash BLOB REFERENCES blobs(blob_hash),
|
|
164
|
-
|
|
171
|
+
step_metadata_hash BLOB REFERENCES blobs(blob_hash),
|
|
165
172
|
error_hash BLOB REFERENCES blobs(blob_hash),
|
|
166
173
|
started_at INTEGER,
|
|
167
174
|
deadline_at INTEGER,
|
|
@@ -175,10 +182,22 @@ CREATE UNIQUE INDEX node_attempts_active_idx ON node_attempts(run_id)
|
|
|
175
182
|
WHERE status IN ('pending', 'running', 'waiting');
|
|
176
183
|
CREATE INDEX node_attempts_run_idx ON node_attempts(run_id, created_at);
|
|
177
184
|
|
|
185
|
+
CREATE TABLE run_steps (
|
|
186
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
187
|
+
step_index INTEGER NOT NULL CHECK (step_index >= 0),
|
|
188
|
+
attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id),
|
|
189
|
+
output_override_hash BLOB REFERENCES blobs(blob_hash),
|
|
190
|
+
PRIMARY KEY (run_id, step_index),
|
|
191
|
+
UNIQUE (run_id, attempt_id)
|
|
192
|
+
) STRICT;
|
|
193
|
+
|
|
194
|
+
CREATE INDEX run_steps_attempt_idx ON run_steps(attempt_id);
|
|
195
|
+
|
|
178
196
|
CREATE TABLE workflow_updates (
|
|
179
197
|
update_id TEXT PRIMARY KEY,
|
|
180
198
|
attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
|
|
181
199
|
update_seq INTEGER NOT NULL CHECK (update_seq > 0),
|
|
200
|
+
run_revision INTEGER NOT NULL CHECK (run_revision > 0),
|
|
182
201
|
update_type TEXT NOT NULL,
|
|
183
202
|
update_key TEXT NOT NULL,
|
|
184
203
|
data_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
|
|
@@ -20,7 +20,6 @@ export type TemporalTool = {
|
|
|
20
20
|
messageId: string;
|
|
21
21
|
toolName: string;
|
|
22
22
|
status: "running" | "finished" | "failed";
|
|
23
|
-
updates: number;
|
|
24
23
|
args?: unknown;
|
|
25
24
|
result?: unknown;
|
|
26
25
|
};
|
|
@@ -155,22 +154,6 @@ function foldSessionEvents(
|
|
|
155
154
|
? "thinking"
|
|
156
155
|
: "toolCall",
|
|
157
156
|
);
|
|
158
|
-
} else if (
|
|
159
|
-
contentIndex !== undefined &&
|
|
160
|
-
(assistantType === "text_delta" ||
|
|
161
|
-
assistantType === "thinking_delta" ||
|
|
162
|
-
assistantType === "toolcall_delta")
|
|
163
|
-
) {
|
|
164
|
-
const block = ensureBlock(
|
|
165
|
-
message,
|
|
166
|
-
contentIndex,
|
|
167
|
-
assistantType === "text_delta"
|
|
168
|
-
? "text"
|
|
169
|
-
: assistantType === "thinking_delta"
|
|
170
|
-
? "thinking"
|
|
171
|
-
: "toolCall",
|
|
172
|
-
);
|
|
173
|
-
block.text += payloadString(event.payload, "delta") ?? "";
|
|
174
157
|
} else if (
|
|
175
158
|
contentIndex !== undefined &&
|
|
176
159
|
(assistantType === "text_end" || assistantType === "thinking_end")
|
|
@@ -181,10 +164,10 @@ function foldSessionEvents(
|
|
|
181
164
|
assistantType === "text_end" ? "text" : "thinking",
|
|
182
165
|
);
|
|
183
166
|
const content = payloadString(event.payload, "content") ?? "";
|
|
184
|
-
if (block.text !== content) {
|
|
167
|
+
if (block.text.length > 0 && block.text !== content) {
|
|
185
168
|
diagnostics.push(`${assistantType} mismatch for ${event.messageId}:${contentIndex}`);
|
|
186
|
-
block.text = content;
|
|
187
169
|
}
|
|
170
|
+
block.text = content;
|
|
188
171
|
} else if (contentIndex !== undefined && assistantType === "toolcall_end") {
|
|
189
172
|
const block = ensureBlock(message, contentIndex, "toolCall");
|
|
190
173
|
block.value = event.payload.toolCall;
|
|
@@ -229,22 +212,12 @@ function foldSessionEvents(
|
|
|
229
212
|
messageId: event.messageId,
|
|
230
213
|
toolName: payloadString(event.payload, "toolName") ?? "tool",
|
|
231
214
|
status: "running",
|
|
232
|
-
updates: 0,
|
|
233
215
|
...(event.payload.args === undefined ? {} : { args: event.payload.args }),
|
|
234
216
|
};
|
|
235
217
|
tools.set(event.toolCallId, tool);
|
|
236
218
|
toolOrder.push(event.toolCallId);
|
|
237
219
|
break;
|
|
238
220
|
}
|
|
239
|
-
case "tool_execution_updated": {
|
|
240
|
-
const tool = event.toolCallId ? tools.get(event.toolCallId) : undefined;
|
|
241
|
-
if (tool) {
|
|
242
|
-
tool.updates += 1;
|
|
243
|
-
} else {
|
|
244
|
-
diagnostics.push(`tool_execution_updated ${event.seq} precedes start`);
|
|
245
|
-
}
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
221
|
case "tool_execution_finished": {
|
|
249
222
|
const tool = event.toolCallId ? tools.get(event.toolCallId) : undefined;
|
|
250
223
|
if (!tool) {
|
package/src/workflows/engine.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { canonicalJson } from "../state/json.js";
|
|
3
4
|
import {
|
|
4
5
|
compileWorkflowDefinition,
|
|
5
6
|
compositionMetadata,
|
|
@@ -723,8 +724,8 @@ export class WorkflowEngine {
|
|
|
723
724
|
throw new RunParkedError();
|
|
724
725
|
}
|
|
725
726
|
this.recordAttempt(workflow, state, attempt);
|
|
726
|
-
// The
|
|
727
|
-
//
|
|
727
|
+
// The durable step row owns the output. The trace keeps the terminal fact
|
|
728
|
+
// and compact execution metadata without copying the output value.
|
|
728
729
|
await this.persist(runId, state, {
|
|
729
730
|
scope: "node",
|
|
730
731
|
type: attempt.result.outcome === "ok" ? "node_finished" : "node_failed",
|
|
@@ -1561,7 +1562,7 @@ function workflowIdentityMismatch(
|
|
|
1561
1562
|
|
|
1562
1563
|
function definitionDigest(workflow: WorkflowDefinition): string {
|
|
1563
1564
|
return `sha256:${createHash("sha256")
|
|
1564
|
-
.update(
|
|
1565
|
+
.update(canonicalJson(createDefinitionSnapshot(workflow)))
|
|
1565
1566
|
.digest("hex")}`;
|
|
1566
1567
|
}
|
|
1567
1568
|
|