@godspeedai/cognate-runtime-bun 0.5.0 → 0.6.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.
- package/package.json +13 -13
- package/src/executor.ts +63 -9
- package/src/index.ts +16 -0
- package/src/runtime.ts +30 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godspeedai/cognate-runtime-bun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Bun reference runtime: composes kernel, durable store, runs, continuations, commitments, artifacts, and cognition behind RuntimeService.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
".": "./src/index.ts"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@godspeedai/cognate-artifacts": "^0.
|
|
26
|
-
"@godspeedai/cognate-capabilities": "^0.
|
|
27
|
-
"@godspeedai/cognate-cognitive-api": "^0.
|
|
28
|
-
"@godspeedai/cognate-continuations": "^0.
|
|
29
|
-
"@godspeedai/cognate-events": "^0.
|
|
30
|
-
"@godspeedai/cognate-kernel-api": "^0.
|
|
31
|
-
"@godspeedai/cognate-kernel-cordis": "^0.
|
|
32
|
-
"@godspeedai/cognate-projections": "^0.
|
|
33
|
-
"@godspeedai/cognate-runtime-api": "^0.
|
|
34
|
-
"@godspeedai/cognate-store-memory": "^0.
|
|
35
|
-
"@godspeedai/cognate-store-postgres": "^0.
|
|
36
|
-
"@godspeedai/cognate-store-sqlite": "^0.
|
|
25
|
+
"@godspeedai/cognate-artifacts": "^0.6.0",
|
|
26
|
+
"@godspeedai/cognate-capabilities": "^0.6.0",
|
|
27
|
+
"@godspeedai/cognate-cognitive-api": "^0.6.0",
|
|
28
|
+
"@godspeedai/cognate-continuations": "^0.6.0",
|
|
29
|
+
"@godspeedai/cognate-events": "^0.6.0",
|
|
30
|
+
"@godspeedai/cognate-kernel-api": "^0.6.0",
|
|
31
|
+
"@godspeedai/cognate-kernel-cordis": "^0.6.0",
|
|
32
|
+
"@godspeedai/cognate-projections": "^0.6.0",
|
|
33
|
+
"@godspeedai/cognate-runtime-api": "^0.6.0",
|
|
34
|
+
"@godspeedai/cognate-store-memory": "^0.6.0",
|
|
35
|
+
"@godspeedai/cognate-store-postgres": "^0.6.0",
|
|
36
|
+
"@godspeedai/cognate-store-sqlite": "^0.6.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@godspeedai/cognate-cognitive-memory": "workspace:*"
|
package/src/executor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Durable run execution by deterministic replay (ADR-0011, spec §11, §12).
|
|
2
2
|
// Each execution re-reads the run stream, re-enters the agent from the top, and answers every
|
|
3
3
|
// RunContext call that already has a recorded result from the stream instead of repeating it.
|
|
4
|
-
import { ConcurrencyError, fingerprint, IdempotencyMismatchError, type Json, type NewEvent, type StoredEvent } from "@godspeedai/cognate-events";
|
|
4
|
+
import { ConcurrencyError, fingerprint, IdempotencyMismatchError, type Json, type NewEvent, StaleRunOwnerError, type StoredEvent } from "@godspeedai/cognate-events";
|
|
5
5
|
import type { Principal, SemanticRef } from "@godspeedai/cognate-kernel-api";
|
|
6
6
|
import { applyStateUpdate, type ArtifactInput, ContinuationClosedError, FrontendCapabilityError, foldState, InvocationFailedError, type RemoteCapabilityOffer, type RunContext, StatePatchError, type WaitSpec } from "@godspeedai/cognate-runtime-api";
|
|
7
7
|
import type { Core, RecallResultJson } from "./core.ts";
|
|
@@ -14,7 +14,17 @@ class Halt {
|
|
|
14
14
|
|
|
15
15
|
export type ExecutionResult = "done" | "retry";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/** The fenced run-write ownership a worker holds for one execution pass
|
|
18
|
+
* (docs/contracts/run-write-ownership.md). The credential fences every run-scoped append; renew
|
|
19
|
+
* refreshes the lease and reports whether ownership was lost. */
|
|
20
|
+
export interface RunExecutionOwnership {
|
|
21
|
+
readonly ownerId: string;
|
|
22
|
+
readonly generation: number;
|
|
23
|
+
readonly ambiguousSteps: "reexecute" | "wait";
|
|
24
|
+
readonly renew: () => Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function executeOnce(core: Core, runId: string, ownership?: RunExecutionOwnership): Promise<ExecutionResult> {
|
|
18
28
|
// Register before the first store await so cancellation and shutdown can reach this attempt
|
|
19
29
|
// while its initial snapshot is being read.
|
|
20
30
|
const controller = new AbortController();
|
|
@@ -23,14 +33,14 @@ export async function executeOnce(core: Core, runId: string): Promise<ExecutionR
|
|
|
23
33
|
if (core.shutdownSignal.aborted) stopForShutdown();
|
|
24
34
|
else core.shutdownSignal.addEventListener("abort", stopForShutdown, { once: true });
|
|
25
35
|
try {
|
|
26
|
-
return await executeRegistered(core, runId, controller);
|
|
36
|
+
return await executeRegistered(core, runId, controller, ownership);
|
|
27
37
|
} finally {
|
|
28
38
|
core.shutdownSignal.removeEventListener("abort", stopForShutdown);
|
|
29
39
|
if (core.controllers.get(runId) === controller) core.controllers.delete(runId);
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
async function executeRegistered(core: Core, runId: string, controller: AbortController): Promise<ExecutionResult> {
|
|
43
|
+
async function executeRegistered(core: Core, runId: string, controller: AbortController, ownership?: RunExecutionOwnership): Promise<ExecutionResult> {
|
|
34
44
|
const streamId = runStream(runId);
|
|
35
45
|
const events = await core.store.readStream(streamId);
|
|
36
46
|
if (controller.signal.aborted) return "done";
|
|
@@ -81,13 +91,23 @@ async function executeRegistered(core: Core, runId: string, controller: AbortCon
|
|
|
81
91
|
try {
|
|
82
92
|
// Pre-assign the id so hooks can derive intents that commit atomically with the event (spec §13.1).
|
|
83
93
|
const pending = { ...envelope, eventId: crypto.randomUUID(), streamId, recordedAt: envelope.occurredAt };
|
|
84
|
-
const outcome = await core.store.commit({
|
|
94
|
+
const outcome = await core.store.commit({
|
|
95
|
+
appends: [{
|
|
96
|
+
streamId,
|
|
97
|
+
expectedVersion: version,
|
|
98
|
+
events: [{ ...envelope, eventId: pending.eventId }],
|
|
99
|
+
...(ownership ? { runOwnership: { runId, ownerId: ownership.ownerId, generation: ownership.generation } } : {}),
|
|
100
|
+
}],
|
|
101
|
+
outbox: core.outboxFor(pending),
|
|
102
|
+
});
|
|
85
103
|
const stored = outcome.events[0]!;
|
|
86
104
|
version += 1;
|
|
87
105
|
lastEventId = stored.eventId;
|
|
88
106
|
return stored;
|
|
89
107
|
} catch (error) {
|
|
90
108
|
if (error instanceof ConcurrencyError) halt(true);
|
|
109
|
+
// Ownership was reclaimed or released: this worker is stale and must never retry the pass.
|
|
110
|
+
if (error instanceof StaleRunOwnerError) halt(false);
|
|
91
111
|
throw error;
|
|
92
112
|
}
|
|
93
113
|
});
|
|
@@ -149,17 +169,49 @@ async function executeRegistered(core: Core, runId: string, controller: AbortCon
|
|
|
149
169
|
// recorded event) but are a distinct event type, so the completion check must name it explicitly.
|
|
150
170
|
let done = find(stepId, "run.invocation");
|
|
151
171
|
if (!done) {
|
|
172
|
+
// Refresh the ownership lease before any external effect: a lost lease must be discovered
|
|
173
|
+
// before dispatch, not after (docs/contracts/run-write-ownership.md).
|
|
174
|
+
if (ownership && !(await ownership.renew())) halt(false);
|
|
175
|
+
const priorProgress = (recorded.get(stepId) ?? []).filter((e) => e.eventType === "run.invocation.progress");
|
|
176
|
+
// A previous owner's generation started this invocation and never recorded its outcome.
|
|
177
|
+
// Under `ambiguousSteps: "wait"` the recovering owner does not re-perform the effect: it
|
|
178
|
+
// records the ambiguity and waits for explicit reconciliation.
|
|
179
|
+
if (ownership && ownership.ambiguousSteps === "wait") {
|
|
180
|
+
const priorGenerations = priorProgress
|
|
181
|
+
.map((e) => field<number>(meta(e), "ownerGeneration"))
|
|
182
|
+
.filter((generation): generation is number => typeof generation === "number" && generation < ownership.generation);
|
|
183
|
+
if (priorGenerations.length > 0) {
|
|
184
|
+
const priorGeneration = Math.max(...priorGenerations);
|
|
185
|
+
const ambiguousStepId = `${stepId}:ambiguous`;
|
|
186
|
+
if (!find(ambiguousStepId, "run.invocation.ambiguous")) {
|
|
187
|
+
await append({ eventType: "run.invocation.ambiguous", payload: { capability, priorGeneration }, metadata: { stepId: ambiguousStepId, capability, priorGeneration } });
|
|
188
|
+
}
|
|
189
|
+
const resolution = await context.waitFor(`ambiguous:${stepId}`, {
|
|
190
|
+
type: "ambiguous-invocation",
|
|
191
|
+
condition: { kind: "ambiguous_invocation", capability, priorGeneration },
|
|
192
|
+
payload: { capability, stepId, priorGeneration },
|
|
193
|
+
});
|
|
194
|
+
const reconciled = (resolution ?? null) as { readonly result?: Json; readonly failure?: { readonly kind: string; readonly message?: string } } | null;
|
|
195
|
+
if (reconciled?.failure) throw new InvocationFailedError(capability, reconciled.failure);
|
|
196
|
+
return reconciled?.result ?? null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
152
199
|
// A crash between a progress call and the final run.invocation event leaves partial progress
|
|
153
200
|
// recorded for this stepId with no completion; re-executing calls the handler again under a
|
|
154
201
|
// fresh attempt number so consumers can discard the earlier partial attempt.
|
|
155
|
-
const priorAttempts =
|
|
156
|
-
.filter((e) => e.eventType === "run.invocation.progress")
|
|
157
|
-
.map((e) => field<number>(meta(e), "attempt") ?? 0);
|
|
202
|
+
const priorAttempts = priorProgress.map((e) => field<number>(meta(e), "attempt") ?? 0);
|
|
158
203
|
const attempt = priorAttempts.length ? Math.max(...priorAttempts) + 1 : 0;
|
|
159
204
|
let seq = 0;
|
|
160
205
|
const progress = async (payload: Json): Promise<void> => {
|
|
161
206
|
guard();
|
|
162
|
-
await append({
|
|
207
|
+
await append({
|
|
208
|
+
eventType: "run.invocation.progress",
|
|
209
|
+
payload: toJson(payload),
|
|
210
|
+
metadata: {
|
|
211
|
+
stepId, capability, attempt, seq: seq++,
|
|
212
|
+
...(ownership ? { ownerGeneration: ownership.generation } : {}),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
163
215
|
};
|
|
164
216
|
const call = (approval?: { readonly approvalId: string }) =>
|
|
165
217
|
core.invoker.invoke({
|
|
@@ -358,12 +410,14 @@ async function executeRegistered(core: Core, runId: string, controller: AbortCon
|
|
|
358
410
|
payload: toJson(update),
|
|
359
411
|
metadata: { scope: "thread", threadId: view.correlationId, source: "agent", actor: context.actor.id, stepId: `shared-state.update:${id}` },
|
|
360
412
|
}],
|
|
413
|
+
...(ownership ? { runOwnership: { runId, ownerId: ownership.ownerId, generation: ownership.generation } } : {}),
|
|
361
414
|
}],
|
|
362
415
|
result: { state, version: events.length + 1 },
|
|
363
416
|
});
|
|
364
417
|
return outcome.result as { readonly state: Json; readonly version: number };
|
|
365
418
|
} catch (error) {
|
|
366
419
|
if (error instanceof ConcurrencyError) continue;
|
|
420
|
+
if (error instanceof StaleRunOwnerError) halt(false);
|
|
367
421
|
if (error instanceof IdempotencyMismatchError) throw new Error(`thread state update ${id} was replayed with different input`);
|
|
368
422
|
throw error;
|
|
369
423
|
}
|
package/src/index.ts
CHANGED
|
@@ -67,6 +67,9 @@ export interface RuntimeOptions {
|
|
|
67
67
|
/** Background work loop interval (run execution, resume delivery, expiry). Default 10 ms. */
|
|
68
68
|
readonly tickMs?: number;
|
|
69
69
|
/** Explicit single-process execution admission; defaults to serial compatibility. */
|
|
70
|
+
readonly execution?: {
|
|
71
|
+
readonly maxConcurrentRuns?: number;
|
|
72
|
+
/** Required above capacity one. Returning [] explicitly declares independent eligibility.
|
|
70
73
|
readonly execution?: {
|
|
71
74
|
readonly maxConcurrentRuns?: number;
|
|
72
75
|
/** Required above capacity one. Returning [] explicitly declares independent eligibility.
|
|
@@ -74,6 +77,19 @@ export interface RuntimeOptions {
|
|
|
74
77
|
* from authoritative application metadata. Its signal is aborted when the run is cancelled or the runtime closes.
|
|
75
78
|
* Runs sharing a key serialize in this process; this is not distributed ownership or OS isolation. */
|
|
76
79
|
readonly resourceKeys?: (run: RunView, signal?: AbortSignal) => readonly string[] | Promise<readonly string[]>;
|
|
80
|
+
/** Fenced run-write ownership tuning (docs/contracts/run-write-ownership.md). Claims happen
|
|
81
|
+
* automatically whenever the store implements the extension; SQLite/memory stay legacy. */
|
|
82
|
+
readonly runOwnership?: {
|
|
83
|
+
/** Stable worker identity for claims; defaults to a per-runtime random id. */
|
|
84
|
+
readonly ownerId?: string;
|
|
85
|
+
/** Lease duration in milliseconds; default 30_000. Expiry is reclaim eligibility, not
|
|
86
|
+
* ownership loss — a matching generation stays writable until someone reclaims. */
|
|
87
|
+
readonly leaseMs?: number;
|
|
88
|
+
/** What a recovering owner does with a previous generation's started-but-unrecorded
|
|
89
|
+
* invocation: "reexecute" (historical behavior, default) or "wait" — record an ambiguity
|
|
90
|
+
* and require explicit reconciliation before the external effect may repeat. */
|
|
91
|
+
readonly ambiguousSteps?: "reexecute" | "wait";
|
|
92
|
+
};
|
|
77
93
|
};
|
|
78
94
|
readonly system?: Principal;
|
|
79
95
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { ArtifactService } from "@godspeedai/cognate-artifacts";
|
|
4
4
|
import { ContinuationService } from "@godspeedai/cognate-continuations";
|
|
5
|
-
import { type DurableStore, type Json, NotFoundError, OutboxDispatcher } from "@godspeedai/cognate-events";
|
|
5
|
+
import { type DurableStore, type Json, NotFoundError, OutboxDispatcher, RunOwnershipHeldError } from "@godspeedai/cognate-events";
|
|
6
6
|
import { createCordisKernel } from "@godspeedai/cognate-kernel-cordis";
|
|
7
7
|
import type { Principal } from "@godspeedai/cognate-kernel-api";
|
|
8
8
|
import type { RunView } from "@godspeedai/cognate-runtime-api";
|
|
@@ -11,7 +11,7 @@ import { openPostgresStore, resolvePg0Url } from "@godspeedai/cognate-store-post
|
|
|
11
11
|
import { openSqliteStore } from "@godspeedai/cognate-store-sqlite";
|
|
12
12
|
import { COGNITION_PROJECTION, cognitionProjection, recallFor } from "./cognition.ts";
|
|
13
13
|
import type { Core } from "./core.ts";
|
|
14
|
-
import { executeOnce } from "./executor.ts";
|
|
14
|
+
import { executeOnce, type RunExecutionOwnership } from "./executor.ts";
|
|
15
15
|
import { ExecutionSlots } from "./execution-slots.ts";
|
|
16
16
|
import { foldRun, isTerminal, RESUME_HANDLER, RUN_EXECUTE_TOPIC, RUNS, runStream } from "./fold.ts";
|
|
17
17
|
import type { Runtime, RuntimeOptions } from "./index.ts";
|
|
@@ -35,6 +35,10 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
const slots=new ExecutionSlots(capacity);
|
|
38
|
+
// Fenced run-write ownership worker identity (docs/contracts/run-write-ownership.md). Per
|
|
39
|
+
// runtime instance: a restarted process reclaims with a fresh generation.
|
|
40
|
+
const ownerId=options.execution?.runOwnership?.ownerId ?? `worker-${crypto.randomUUID()}`;
|
|
41
|
+
const leaseMs=options.execution?.runOwnership?.leaseMs ?? 30_000;
|
|
38
42
|
// Recovery can enqueue many runs at once. Bound authoritative eligibility lookups with the
|
|
39
43
|
// same primitive/capacity so metadata resolution cannot fan out without limit.
|
|
40
44
|
const eligibilitySlots = new ExecutionSlots(capacity);
|
|
@@ -163,7 +167,30 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
163
167
|
if (!release) return;
|
|
164
168
|
try{
|
|
165
169
|
if (closed) return;
|
|
166
|
-
|
|
170
|
+
// Fenced run-write ownership (docs/contracts/run-write-ownership.md): claim before
|
|
171
|
+
// executing. A held lease means another live worker is executing this run — back off;
|
|
172
|
+
// its completion, suspension or death releases the run for a later pass to reclaim.
|
|
173
|
+
let ownership: RunExecutionOwnership | undefined;
|
|
174
|
+
if (store.claimRunOwnership) {
|
|
175
|
+
try {
|
|
176
|
+
const lease = await store.claimRunOwnership({ runId, ownerId, now: Date.now(), leaseMs });
|
|
177
|
+
ownership = {
|
|
178
|
+
ownerId,
|
|
179
|
+
generation: lease.generation,
|
|
180
|
+
ambiguousSteps: options.execution?.runOwnership?.ambiguousSteps ?? "reexecute",
|
|
181
|
+
renew: () => store.renewRunOwnership!({ runId, ownerId, generation: lease.generation, now: Date.now(), leaseMs }).then((ok) => ok === true),
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof RunOwnershipHeldError) return;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
try{
|
|
189
|
+
for (let attempt = 0; attempt < 5 && !closed; attempt++) if ((await executeOnce(core, runId, ownership)) === "done") return;
|
|
190
|
+
}finally{
|
|
191
|
+
// Best-effort release: a reclaimed or already-released row simply refuses.
|
|
192
|
+
if (ownership) await store.releaseRunOwnership!({ runId, ownerId, generation: ownership.generation }).catch(() => {});
|
|
193
|
+
}
|
|
167
194
|
}finally{release();}
|
|
168
195
|
});
|
|
169
196
|
const tracked = next.finally(() => {
|