@godspeedai/cognate-runtime-bun 0.2.0 → 0.4.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/README.md +7 -5
- package/package.json +13 -13
- package/src/execution-slots.ts +37 -0
- package/src/index.ts +9 -1
- package/src/runtime.ts +51 -8
package/README.md
CHANGED
|
@@ -124,12 +124,14 @@ kept current automatically), `cognition` (index completed runs and recall them v
|
|
|
124
124
|
|
|
125
125
|
## Limitations
|
|
126
126
|
|
|
127
|
-
- **One
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
- **One runtime execution owner per store.** PostgreSQL supports multiple durable-store writers;
|
|
128
|
+
that does not establish distributed run ownership. Run execution defaults to serial. Opt-in
|
|
129
|
+
`execution.maxConcurrentRuns` admits bounded independent runs in one process and requires a
|
|
130
|
+
trusted `execution.resourceKeys` eligibility policy above one. Shared resource keys serialize;
|
|
131
|
+
callers must canonicalize aliases. Do not start competing runtimes against the same runnable work.
|
|
130
132
|
- **Steps are at-least-once.** A step that was in flight during a crash re-executes after restart.
|
|
131
|
-
Keep steps idempotent, and
|
|
132
|
-
|
|
133
|
+
Keep steps idempotent, and use native commitment receipts and explicit reconciliation for external
|
|
134
|
+
actions. A continuation or permission gate alone does not establish exactly-once physical effects.
|
|
133
135
|
- **Polling, not pushing, inside the process.** Background work (run execution, resume delivery,
|
|
134
136
|
continuation expiry) runs on a loop every `tickMs`.
|
|
135
137
|
- **Erasure has copies.** `runtime.erase` removes an event's payload from the log, derived views,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godspeedai/cognate-runtime-bun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0",
|
|
26
|
+
"@godspeedai/cognate-capabilities": "^0.4.0",
|
|
27
|
+
"@godspeedai/cognate-cognitive-api": "^0.4.0",
|
|
28
|
+
"@godspeedai/cognate-continuations": "^0.4.0",
|
|
29
|
+
"@godspeedai/cognate-events": "^0.4.0",
|
|
30
|
+
"@godspeedai/cognate-kernel-api": "^0.4.0",
|
|
31
|
+
"@godspeedai/cognate-kernel-cordis": "^0.4.0",
|
|
32
|
+
"@godspeedai/cognate-projections": "^0.4.0",
|
|
33
|
+
"@godspeedai/cognate-runtime-api": "^0.4.0",
|
|
34
|
+
"@godspeedai/cognate-store-memory": "^0.4.0",
|
|
35
|
+
"@godspeedai/cognate-store-postgres": "^0.4.0",
|
|
36
|
+
"@godspeedai/cognate-store-sqlite": "^0.4.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@godspeedai/cognate-cognitive-memory": "workspace:*"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Admission inside the native executor, shared by start/recovery/resume paths. This is a
|
|
2
|
+
* process-local limit and resource exclusion, not durable ownership of an execution. */
|
|
3
|
+
export class ExecutionSlots {
|
|
4
|
+
private active = 0;
|
|
5
|
+
private readonly held = new Set<string>();
|
|
6
|
+
private readonly waiting: {keys: readonly string[]; resolve: (release: () => void) => void}[] = [];
|
|
7
|
+
constructor(readonly capacity: number) {}
|
|
8
|
+
|
|
9
|
+
acquire(raw: readonly string[]): Promise<() => void> {
|
|
10
|
+
if (!Array.isArray(raw) || raw.length > 100 || raw.some(key=>typeof key!=="string" || !key || key.length>500)) {
|
|
11
|
+
return Promise.reject(new Error("execution.resourceKeys must return at most 100 nonempty resource identities"));
|
|
12
|
+
}
|
|
13
|
+
return new Promise(resolve=>{
|
|
14
|
+
this.waiting.push({keys:[...new Set(raw)].sort(),resolve});
|
|
15
|
+
this.pump();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
private pump(): void {
|
|
19
|
+
const earlier=new Set<string>();
|
|
20
|
+
for(let i=0;i<this.waiting.length && this.active<this.capacity;){
|
|
21
|
+
const item=this.waiting[i]!;
|
|
22
|
+
if(item.keys.some(key=>this.held.has(key) || earlier.has(key))){
|
|
23
|
+
for(const key of item.keys)earlier.add(key);
|
|
24
|
+
i++;continue;
|
|
25
|
+
}
|
|
26
|
+
this.waiting.splice(i,1);this.active++;
|
|
27
|
+
for(const key of item.keys)this.held.add(key);
|
|
28
|
+
let released=false;
|
|
29
|
+
item.resolve(()=>{
|
|
30
|
+
if(released)return;
|
|
31
|
+
released=true;this.active--;
|
|
32
|
+
for(const key of item.keys)this.held.delete(key);
|
|
33
|
+
this.pump();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { CognitiveIndex, Embedder, IndexDocument } from "@godspeedai/cognat
|
|
|
2
2
|
import type { DurableStore, Json, NewEvent, OutboxIntent, StoredEvent } from "@godspeedai/cognate-events";
|
|
3
3
|
import type { Component, Kernel, Policy, Principal } from "@godspeedai/cognate-kernel-api";
|
|
4
4
|
import type { Projection } from "@godspeedai/cognate-projections";
|
|
5
|
-
import type { AgentDefinition, Caller, RuntimeService } from "@godspeedai/cognate-runtime-api";
|
|
5
|
+
import type { AgentDefinition, Caller, RuntimeService, RunView } from "@godspeedai/cognate-runtime-api";
|
|
6
6
|
import type { GovernanceOptions } from "./governed-operation.ts";
|
|
7
7
|
import { startRuntime } from "./runtime.ts";
|
|
8
8
|
|
|
@@ -66,6 +66,14 @@ export interface RuntimeOptions {
|
|
|
66
66
|
readonly outboxFor?: (event: PendingEvent) => readonly OutboxIntent[];
|
|
67
67
|
/** Background work loop interval (run execution, resume delivery, expiry). Default 10 ms. */
|
|
68
68
|
readonly tickMs?: number;
|
|
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.
|
|
73
|
+
* Trusted resource identities derived from frozen run inputs, never model-granted authority.
|
|
74
|
+
* Runs sharing a key serialize in this process; this is not a distributed lock or OS isolation. */
|
|
75
|
+
readonly resourceKeys?: (run: RunView) => readonly string[];
|
|
76
|
+
};
|
|
69
77
|
readonly system?: Principal;
|
|
70
78
|
}
|
|
71
79
|
|
package/src/runtime.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { openSqliteStore } from "@godspeedai/cognate-store-sqlite";
|
|
|
10
10
|
import { COGNITION_PROJECTION, cognitionProjection, recallFor } from "./cognition.ts";
|
|
11
11
|
import type { Core } from "./core.ts";
|
|
12
12
|
import { executeOnce } from "./executor.ts";
|
|
13
|
+
import { ExecutionSlots } from "./execution-slots.ts";
|
|
13
14
|
import { foldRun, isTerminal, RESUME_HANDLER, RUN_EXECUTE_TOPIC, RUNS, runStream } from "./fold.ts";
|
|
14
15
|
import type { Runtime, RuntimeOptions } from "./index.ts";
|
|
15
16
|
import { inspectCorrelation } from "./inspect.ts";
|
|
@@ -22,6 +23,25 @@ const DELIVERY = { baseMs: 50, maxMs: 5_000, maxAttempts: 10 };
|
|
|
22
23
|
const TOPICS = [RUN_EXECUTE_TOPIC, "continuation.resume"];
|
|
23
24
|
|
|
24
25
|
export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
26
|
+
const capacity=options.execution?.maxConcurrentRuns ?? 1;
|
|
27
|
+
if(!Number.isInteger(capacity) || capacity<1 || capacity>64)throw new Error("execution.maxConcurrentRuns must be an integer between 1 and 64");
|
|
28
|
+
if(capacity>1 && typeof options.execution?.resourceKeys!=="function")throw new Error("Concurrent execution requires an explicit execution.resourceKeys eligibility policy");
|
|
29
|
+
if(capacity>1 && typeof options.store!=="string" && "store" in options.store){
|
|
30
|
+
const injected=options.store.store;
|
|
31
|
+
if(typeof injected.claimOutboxLeased!=="function" || typeof injected.renewOutboxLease!=="function" || typeof injected.settleOutboxLease!=="function"){
|
|
32
|
+
throw new Error("Concurrent execution requires a store with complete fenced outbox lease support");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const slots=new ExecutionSlots(capacity);
|
|
36
|
+
// Run and resume topics share one pre-claim budget. ExecutionSlots separately enforces
|
|
37
|
+
// run-local/resource exclusion for every execution path, including direct recovery.
|
|
38
|
+
let admittedDeliveries = 0;
|
|
39
|
+
const deliveryAdmission = { tryAcquire(): (() => void) | undefined {
|
|
40
|
+
if (admittedDeliveries >= capacity) return undefined;
|
|
41
|
+
admittedDeliveries++;
|
|
42
|
+
let released = false;
|
|
43
|
+
return () => { if (!released) { released = true; admittedDeliveries--; } };
|
|
44
|
+
} };
|
|
25
45
|
// The runtime closes only stores it opened itself; an injected store is owned by its creator.
|
|
26
46
|
// (The PostgreSQL store's close() ends its pool and LISTEN connection, so one call covers both.)
|
|
27
47
|
const config = options.store;
|
|
@@ -47,14 +67,24 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
47
67
|
await invoker.attach(kernel, options.components ?? []);
|
|
48
68
|
|
|
49
69
|
// Authorization happens in the service against `actions`; the continuation layer trusts its caller.
|
|
50
|
-
const continuations = new ContinuationService(store, { policy: { authorize: () => ({ allow: true, reason: "authorized by runtime service" }) }, delivery: DELIVERY });
|
|
70
|
+
const continuations = new ContinuationService(store, { policy: { authorize: () => ({ allow: true, reason: "authorized by runtime service" }) }, delivery: DELIVERY, deliveryConcurrency: capacity, deliveryAdmission });
|
|
51
71
|
|
|
52
72
|
let closed = false;
|
|
53
73
|
const chains = new Map<string, Promise<void>>();
|
|
54
74
|
const pending = new Set<string>();
|
|
75
|
+
const deliveries = new Set<Promise<void>>();
|
|
76
|
+
const trackDelivery=(job:Promise<unknown>):void=>{
|
|
77
|
+
const tracked=job.then(()=>{},()=>{}).finally(()=>deliveries.delete(tracked));
|
|
78
|
+
deliveries.add(tracked);
|
|
79
|
+
};
|
|
55
80
|
const execute = (runId: string): Promise<void> => {
|
|
56
81
|
const next = (chains.get(runId) ?? Promise.resolve()).then(async () => {
|
|
57
|
-
|
|
82
|
+
const view=foldRun(runId,await store.readStream(runStream(runId)));
|
|
83
|
+
if(closed || !view || isTerminal(view))return;
|
|
84
|
+
const release=await slots.acquire(options.execution?.resourceKeys?.(view) ?? []);
|
|
85
|
+
try{
|
|
86
|
+
for (let attempt = 0; attempt < 5 && !closed; attempt++) if ((await executeOnce(core, runId)) === "done") return;
|
|
87
|
+
}finally{release();}
|
|
58
88
|
});
|
|
59
89
|
const tracked = next.finally(() => {
|
|
60
90
|
if (chains.get(runId) === tracked) chains.delete(runId);
|
|
@@ -88,6 +118,8 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
88
118
|
|
|
89
119
|
const runDispatcher = new OutboxDispatcher(store, {
|
|
90
120
|
topic: RUN_EXECUTE_TOPIC,
|
|
121
|
+
concurrency: capacity,
|
|
122
|
+
admission: deliveryAdmission,
|
|
91
123
|
backoff: DELIVERY,
|
|
92
124
|
deliver: async (item) => {
|
|
93
125
|
await execute((item.payload as { runId: string }).runId);
|
|
@@ -95,14 +127,24 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
95
127
|
},
|
|
96
128
|
});
|
|
97
129
|
|
|
130
|
+
let resumeFirst = false;
|
|
98
131
|
async function tick(): Promise<void> {
|
|
99
132
|
for (const record of await continuations.expireDue()) pending.add(record.runId);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
133
|
+
// Dispatchers reserve a bounded set of slots across overlapping drains. A new runnable
|
|
134
|
+
// arrival can fill a free slot while an earlier long-running delivery is still executing.
|
|
135
|
+
// Rotate priority: even an empty topic briefly reserves capacity while its claim is
|
|
136
|
+
// in flight. A fixed first topic could otherwise starve the other at capacity one.
|
|
137
|
+
if (resumeFirst) {
|
|
138
|
+
trackDelivery(continuations.dispatchResumes());
|
|
139
|
+
trackDelivery(runDispatcher.drain());
|
|
140
|
+
} else {
|
|
141
|
+
trackDelivery(runDispatcher.drain());
|
|
142
|
+
trackDelivery(continuations.dispatchResumes());
|
|
105
143
|
}
|
|
144
|
+
resumeFirst = !resumeFirst;
|
|
145
|
+
const recover=[...pending];
|
|
146
|
+
for(const runId of recover)pending.delete(runId);
|
|
147
|
+
for(const runId of recover)trackDelivery(execute(runId));
|
|
106
148
|
}
|
|
107
149
|
|
|
108
150
|
let ticking: Promise<void> | null = null;
|
|
@@ -290,7 +332,7 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
290
332
|
}
|
|
291
333
|
|
|
292
334
|
async function quiescent(): Promise<boolean> {
|
|
293
|
-
if (ticking || pending.size > 0 || chains.size > 0) return false;
|
|
335
|
+
if (ticking || pending.size > 0 || chains.size > 0 || deliveries.size>0) return false;
|
|
294
336
|
const now = Date.now();
|
|
295
337
|
for (const topic of TOPICS) {
|
|
296
338
|
const items = await store.listOutbox({ topic, status: "pending" });
|
|
@@ -342,6 +384,7 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
342
384
|
clearInterval(timer);
|
|
343
385
|
if (approvalTimer) clearInterval(approvalTimer);
|
|
344
386
|
await ticking;
|
|
387
|
+
await Promise.allSettled([...deliveries]);
|
|
345
388
|
await Promise.allSettled([...chains.values()]);
|
|
346
389
|
for (const runner of runners) await runner.stop();
|
|
347
390
|
unregister();
|