@osolmaz/pi-workflows 0.13.0 → 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 +11 -9
- package/dist/builtins/autodoc.workflow.d.ts +191 -4
- package/dist/builtins/autodoc.workflow.js +156 -30
- package/dist/builtins/autodoc.workflow.js.map +1 -1
- package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
- package/dist/builtins/autoimplement-command-batches.js +29 -31
- package/dist/builtins/autoimplement-command-batches.js.map +1 -1
- package/dist/builtins/autoimplement.workflow.d.ts +1259 -29
- package/dist/builtins/autoimplement.workflow.js +416 -153
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- 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 +5 -5
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/change-verification.workflow.d.ts +110 -0
- package/dist/builtins/change-verification.workflow.js +860 -0
- package/dist/builtins/change-verification.workflow.js.map +1 -0
- package/dist/builtins/monitor.workflow.js +4 -3
- package/dist/builtins/monitor.workflow.js.map +1 -1
- 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 +361 -6
- package/dist/builtins/plan-change.workflow.js +26 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.js +19 -22
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.d.ts +75 -0
- package/dist/builtins/workspace-preparation.workflow.js +498 -0
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
- 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/executor.js +1 -4
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.js +2 -15
- 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/WORKFLOW_COMPOSITION.md +8 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +1 -1
- package/docs/plans/2026-08-24-change-scoped-verification-plan.md +419 -0
- 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 +43 -21
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/skills/autodoc/SKILL.md +7 -0
- package/skills/autoimplement/SKILL.md +4 -0
- package/src/builtins/autodoc.workflow.ts +184 -33
- package/src/builtins/autoimplement-command-batches.ts +39 -33
- package/src/builtins/autoimplement.workflow.ts +483 -175
- package/src/builtins/autoplan.workflow.ts +80 -32
- package/src/builtins/catalog.ts +5 -5
- package/src/builtins/change-verification.workflow.ts +1143 -0
- package/src/builtins/monitor.workflow.ts +6 -3
- package/src/builtins/plain-summary.workflow.ts +38 -40
- package/src/builtins/plan-change.workflow.ts +35 -0
- package/src/builtins/sanity-check.workflow.ts +19 -22
- package/src/builtins/workspace-preparation.workflow.ts +668 -0
- package/src/controllers/sqlite.ts +19 -53
- package/src/extension/decision-channels.ts +47 -7
- package/src/extension/executor.ts +1 -4
- package/src/extension/index.ts +2 -15
- 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
|
@@ -156,9 +156,9 @@ type RunRow = {
|
|
|
156
156
|
workflowRef: string;
|
|
157
157
|
runStatus: string;
|
|
158
158
|
workflowSourceHash: Buffer;
|
|
159
|
+
workflowSourcesHash: Buffer | null;
|
|
159
160
|
definitionDigest: Buffer;
|
|
160
161
|
inputHash: Buffer;
|
|
161
|
-
outputHash: Buffer | null;
|
|
162
162
|
launchOptionsHash: Buffer;
|
|
163
163
|
status: WorkflowRunLaunchStatus;
|
|
164
164
|
availableAt: number;
|
|
@@ -1049,34 +1049,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1049
1049
|
const resourceId = resourceIdFor("run", options.runId);
|
|
1050
1050
|
const definitionHash = this.state.putJson(options.definitionSnapshot, now);
|
|
1051
1051
|
const queuedSource = queuedWorkflowSource(options.workflowSource);
|
|
1052
|
-
const sourceHash = this.state.putJson(
|
|
1052
|
+
const sourceHash = this.state.putJson(queuedSource.root, now);
|
|
1053
|
+
const mountedSourcesHash =
|
|
1054
|
+
queuedSource.mounted.length === 0 ? null : this.state.putJson(queuedSource.mounted, now);
|
|
1053
1055
|
const inputHash = this.state.putJson(options.input ?? null, now);
|
|
1054
1056
|
const launchHash = this.state.putJson(options.launchOptions ?? {}, now);
|
|
1055
|
-
const queuedStateHash = this.state.putJson(
|
|
1056
|
-
{
|
|
1057
|
-
schema: "pi-workflows.run-state.v1",
|
|
1058
|
-
traceSeq: 1,
|
|
1059
|
-
runId: options.runId,
|
|
1060
|
-
workflowName: options.workflowName,
|
|
1061
|
-
workflowSource: queuedSource.root,
|
|
1062
|
-
...(queuedSource.mounted.length === 0
|
|
1063
|
-
? {}
|
|
1064
|
-
: {
|
|
1065
|
-
workflowSources: queuedSource.mounted,
|
|
1066
|
-
definitionDigest: options.definitionDigest,
|
|
1067
|
-
}),
|
|
1068
|
-
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
1069
|
-
startedAt: new Date(now).toISOString(),
|
|
1070
|
-
updatedAt: new Date(now).toISOString(),
|
|
1071
|
-
status: "running",
|
|
1072
|
-
input: options.input ?? null,
|
|
1073
|
-
outputs: {},
|
|
1074
|
-
results: {},
|
|
1075
|
-
steps: [],
|
|
1076
|
-
updates: [],
|
|
1077
|
-
},
|
|
1078
|
-
now,
|
|
1079
|
-
);
|
|
1080
1057
|
this.state.connection
|
|
1081
1058
|
.prepare(
|
|
1082
1059
|
`INSERT INTO workflow_definitions(
|
|
@@ -1101,7 +1078,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1101
1078
|
run_id, resource_id, project_id, parent_run_id, definition_digest,
|
|
1102
1079
|
workflow_ref, workflow_source_hash, launch_options_hash,
|
|
1103
1080
|
source_type, source_ref, source_revision, status, paused,
|
|
1104
|
-
input_hash,
|
|
1081
|
+
input_hash, workflow_sources_hash, created_at, updated_at
|
|
1105
1082
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?, ?)`,
|
|
1106
1083
|
)
|
|
1107
1084
|
.run(
|
|
@@ -1117,7 +1094,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1117
1094
|
source.ref,
|
|
1118
1095
|
source.revision,
|
|
1119
1096
|
inputHash,
|
|
1120
|
-
|
|
1097
|
+
mountedSourcesHash,
|
|
1121
1098
|
now,
|
|
1122
1099
|
now,
|
|
1123
1100
|
);
|
|
@@ -2261,7 +2238,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2261
2238
|
runId: row.runId,
|
|
2262
2239
|
workflowName: row.workflowName,
|
|
2263
2240
|
workflowSourceRef: row.workflowRef,
|
|
2264
|
-
workflowSource:
|
|
2241
|
+
workflowSource: {
|
|
2242
|
+
root: this.state.readJson(row.workflowSourceHash),
|
|
2243
|
+
mounted:
|
|
2244
|
+
row.workflowSourcesHash === null ? [] : this.state.readJson(row.workflowSourcesHash),
|
|
2245
|
+
},
|
|
2265
2246
|
initialized: row.runStatus !== "queued",
|
|
2266
2247
|
definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
|
|
2267
2248
|
input: this.state.readJson(row.inputHash),
|
|
@@ -2461,27 +2442,8 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2461
2442
|
) {
|
|
2462
2443
|
return false;
|
|
2463
2444
|
}
|
|
2464
|
-
if (row.outputHash === null) throw new Error(`Workflow run ${runId} has no state projection`);
|
|
2465
|
-
const projected = this.state.readJson(row.outputHash);
|
|
2466
|
-
if (!isRecord(projected)) throw new Error(`Workflow run ${runId} state is invalid`);
|
|
2467
2445
|
const revision = this.resourceRevision(row.resourceId);
|
|
2468
|
-
const at = new Date(now).toISOString();
|
|
2469
|
-
const terminalState: Record<string, unknown> = {
|
|
2470
|
-
...projected,
|
|
2471
|
-
traceSeq: revision + 1,
|
|
2472
|
-
status,
|
|
2473
|
-
updatedAt: at,
|
|
2474
|
-
finishedAt: at,
|
|
2475
|
-
error,
|
|
2476
|
-
};
|
|
2477
|
-
delete terminalState.currentNode;
|
|
2478
|
-
delete terminalState.currentAttemptId;
|
|
2479
|
-
delete terminalState.currentNodeStartedAt;
|
|
2480
|
-
delete terminalState.statusDetail;
|
|
2481
|
-
delete terminalState.paused;
|
|
2482
|
-
delete terminalState.waitingOn;
|
|
2483
2446
|
const errorHash = this.state.putText(error, now);
|
|
2484
|
-
const outputHash = this.state.putJson(terminalState, now);
|
|
2485
2447
|
const queueUpdate = this.state.connection
|
|
2486
2448
|
.prepare(
|
|
2487
2449
|
`UPDATE run_queue
|
|
@@ -2506,10 +2468,13 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2506
2468
|
this.state.connection
|
|
2507
2469
|
.prepare(
|
|
2508
2470
|
`UPDATE runs
|
|
2509
|
-
SET status = ?,
|
|
2471
|
+
SET status = ?, paused = 0, status_detail = NULL, error_hash = ?,
|
|
2472
|
+
current_node = NULL, current_attempt_id = NULL,
|
|
2473
|
+
current_node_started_at = NULL, waiting_on = NULL,
|
|
2474
|
+
updated_at = ?, finished_at = ?
|
|
2510
2475
|
WHERE run_id = ?`,
|
|
2511
2476
|
)
|
|
2512
|
-
.run(status,
|
|
2477
|
+
.run(status, errorHash, now, now, runId);
|
|
2513
2478
|
this.bumpResource(row.resourceId, revision, now);
|
|
2514
2479
|
this.insertEvent(
|
|
2515
2480
|
row.resourceId,
|
|
@@ -2811,8 +2776,9 @@ function workflowSelect(clause: string): string {
|
|
|
2811
2776
|
function workflowRunSelect(clause: string): string {
|
|
2812
2777
|
return `SELECT r.run_id AS runId, r.resource_id AS resourceId,
|
|
2813
2778
|
d.workflow_name AS workflowName, r.workflow_ref AS workflowRef, r.status AS runStatus,
|
|
2814
|
-
r.workflow_source_hash AS workflowSourceHash,
|
|
2815
|
-
r.
|
|
2779
|
+
r.workflow_source_hash AS workflowSourceHash,
|
|
2780
|
+
r.workflow_sources_hash AS workflowSourcesHash,
|
|
2781
|
+
r.definition_digest AS definitionDigest, r.input_hash AS inputHash,
|
|
2816
2782
|
r.launch_options_hash AS launchOptionsHash,
|
|
2817
2783
|
q.status, q.available_at AS availableAt, q.affinity_runner_id AS affinityRunnerId,
|
|
2818
2784
|
q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
|
|
@@ -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);
|
|
@@ -253,10 +253,7 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
253
253
|
});
|
|
254
254
|
return {
|
|
255
255
|
accepted: true,
|
|
256
|
-
message:
|
|
257
|
-
`Output accepted for step ${JSON.stringify(stepId)}.`,
|
|
258
|
-
"If the workflow continues, the next step arrives as a new workflow message. End your turn now.",
|
|
259
|
-
].join(" "),
|
|
256
|
+
message: `Output accepted for step ${JSON.stringify(stepId)}.`,
|
|
260
257
|
};
|
|
261
258
|
}
|
|
262
259
|
|
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 {
|
|
@@ -560,7 +561,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
560
561
|
};
|
|
561
562
|
let activeRun: ActiveRun | null = null;
|
|
562
563
|
let systemTurnAbort: { contract: AgentStepContract; intentId?: string } | null = null;
|
|
563
|
-
let suppressWorkflowAssistantTail = false;
|
|
564
564
|
let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
|
|
565
565
|
// The interactive run currently parked at a checkpoint, if any.
|
|
566
566
|
let lastWaitingRunId: string | null = null;
|
|
@@ -999,7 +999,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
999
999
|
resolution: "presentation",
|
|
1000
1000
|
send: (turnIntentId) => {
|
|
1001
1001
|
presentationPending = run.generation;
|
|
1002
|
-
suppressWorkflowAssistantTail = false;
|
|
1003
1002
|
pi.sendMessage(
|
|
1004
1003
|
{
|
|
1005
1004
|
customType: PRESENTATION_MESSAGE_TYPE,
|
|
@@ -2916,7 +2915,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2916
2915
|
if (!result.accepted) {
|
|
2917
2916
|
throw new Error(result.message);
|
|
2918
2917
|
}
|
|
2919
|
-
suppressWorkflowAssistantTail = true;
|
|
2920
2918
|
return {
|
|
2921
2919
|
content: [{ type: "text", text: result.message }],
|
|
2922
2920
|
details: { action: "submit", step: params.step, accepted: true },
|
|
@@ -2994,7 +2992,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2994
2992
|
});
|
|
2995
2993
|
|
|
2996
2994
|
pi.on("agent_start", () => {
|
|
2997
|
-
suppressWorkflowAssistantTail = false;
|
|
2998
2995
|
if (!activeRun && presentationPending === null && presentationAbort) {
|
|
2999
2996
|
// A normal user turn started while an async presentation prompt was
|
|
3000
2997
|
// still resolving. The user's new request supersedes that old result.
|
|
@@ -3005,7 +3002,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3005
3002
|
});
|
|
3006
3003
|
|
|
3007
3004
|
pi.on("agent_end", (event, ctx) => {
|
|
3008
|
-
suppressWorkflowAssistantTail = false;
|
|
3009
3005
|
const aborted = event.messages.some(
|
|
3010
3006
|
(message) =>
|
|
3011
3007
|
typeof message === "object" &&
|
|
@@ -3057,14 +3053,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3057
3053
|
});
|
|
3058
3054
|
|
|
3059
3055
|
pi.on("message_end", (event) => {
|
|
3060
|
-
if (suppressWorkflowAssistantTail && event.message.role === "assistant") {
|
|
3061
|
-
const message = {
|
|
3062
|
-
...event.message,
|
|
3063
|
-
content: event.message.content.filter((part) => part.type !== "text"),
|
|
3064
|
-
};
|
|
3065
|
-
activeRun?.recorder?.handleMessageEnd({ ...event, message });
|
|
3066
|
-
return { message };
|
|
3067
|
-
}
|
|
3068
3056
|
activeRun?.recorder?.handleMessageEnd(event);
|
|
3069
3057
|
});
|
|
3070
3058
|
|
|
@@ -3127,7 +3115,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3127
3115
|
sessionClosed = true;
|
|
3128
3116
|
herdrProbeGeneration += 1;
|
|
3129
3117
|
systemTurnAbort = null;
|
|
3130
|
-
suppressWorkflowAssistantTail = false;
|
|
3131
3118
|
turnCoordinator.clearDeferred();
|
|
3132
3119
|
supersedePresentation();
|
|
3133
3120
|
const run = activeRun;
|
|
@@ -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
|
|