@toren-run/core 0.1.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 (56) hide show
  1. package/LICENSE +202 -0
  2. package/dist/apiKeys.d.ts +28 -0
  3. package/dist/apiKeys.js +40 -0
  4. package/dist/approvals.d.ts +30 -0
  5. package/dist/approvals.js +54 -0
  6. package/dist/blobs.d.ts +12 -0
  7. package/dist/blobs.js +17 -0
  8. package/dist/builtins.d.ts +10 -0
  9. package/dist/builtins.js +244 -0
  10. package/dist/conversations.d.ts +38 -0
  11. package/dist/conversations.js +87 -0
  12. package/dist/db.d.ts +3 -0
  13. package/dist/db.js +20 -0
  14. package/dist/digest.d.ts +1 -0
  15. package/dist/digest.js +13 -0
  16. package/dist/events.d.ts +20 -0
  17. package/dist/events.js +4 -0
  18. package/dist/files.d.ts +28 -0
  19. package/dist/files.js +27 -0
  20. package/dist/fold.d.ts +3 -0
  21. package/dist/fold.js +6 -0
  22. package/dist/guardians.d.ts +9 -0
  23. package/dist/guardians.js +14 -0
  24. package/dist/index.d.ts +27 -0
  25. package/dist/index.js +27 -0
  26. package/dist/leases.d.ts +16 -0
  27. package/dist/leases.js +30 -0
  28. package/dist/loop.d.ts +69 -0
  29. package/dist/loop.js +413 -0
  30. package/dist/migrate.d.ts +4 -0
  31. package/dist/migrate.js +167 -0
  32. package/dist/model.d.ts +42 -0
  33. package/dist/model.js +1 -0
  34. package/dist/orchestrator.d.ts +35 -0
  35. package/dist/orchestrator.js +146 -0
  36. package/dist/providers/echo.d.ts +9 -0
  37. package/dist/providers/echo.js +17 -0
  38. package/dist/providers/mock.d.ts +12 -0
  39. package/dist/providers/mock.js +15 -0
  40. package/dist/queue.d.ts +68 -0
  41. package/dist/queue.js +54 -0
  42. package/dist/schedules.d.ts +61 -0
  43. package/dist/schedules.js +128 -0
  44. package/dist/spawn.d.ts +14 -0
  45. package/dist/spawn.js +113 -0
  46. package/dist/store.d.ts +50 -0
  47. package/dist/store.js +65 -0
  48. package/dist/tools.d.ts +98 -0
  49. package/dist/tools.js +14 -0
  50. package/dist/tracing.d.ts +6 -0
  51. package/dist/tracing.js +21 -0
  52. package/dist/worker.d.ts +34 -0
  53. package/dist/worker.js +176 -0
  54. package/dist/workflow.d.ts +91 -0
  55. package/dist/workflow.js +210 -0
  56. package/package.json +53 -0
@@ -0,0 +1,210 @@
1
+ import { canonicalDigest } from "./digest.js";
2
+ import { ev } from "./events.js";
3
+ export class WorkflowBlocked extends Error {
4
+ reason;
5
+ constructor(reason) {
6
+ super(`workflow blocked: ${reason}`);
7
+ this.reason = reason;
8
+ }
9
+ }
10
+ export class WaveFailedError extends Error {
11
+ }
12
+ export class RunLeaseLostError extends Error {
13
+ }
14
+ export function foldRunStream(eff) {
15
+ const state = { sideEffects: [], waves: [], timers: [], started: false };
16
+ for (const e of eff) {
17
+ switch (e.type) {
18
+ case "RunStarted":
19
+ state.started = true;
20
+ break;
21
+ case "SideEffectRecorded":
22
+ state.sideEffects.push(e);
23
+ break;
24
+ case "WavePlanned":
25
+ state.waves.push({
26
+ seq: e.seq,
27
+ waveId: String(e.payload.waveId),
28
+ index: Number(e.payload.index),
29
+ name: String(e.payload.name),
30
+ tasks: e.payload.tasks,
31
+ planDigest: String(e.payload.planDigest),
32
+ dispatched: false,
33
+ settledTasks: new Map(),
34
+ settled: false,
35
+ });
36
+ break;
37
+ case "WaveDispatched": {
38
+ const w = state.waves.find((x) => x.waveId === e.payload.waveId);
39
+ if (w)
40
+ w.dispatched = true;
41
+ break;
42
+ }
43
+ case "WaveTaskSettled": {
44
+ const w = state.waves.find((x) => x.waveId === e.payload.waveId);
45
+ if (w) {
46
+ w.settledTasks.set(String(e.payload.taskId), {
47
+ taskId: String(e.payload.taskId),
48
+ status: e.payload.status,
49
+ output: e.payload.output,
50
+ error: e.payload.error,
51
+ });
52
+ }
53
+ break;
54
+ }
55
+ case "WaveSettled": {
56
+ const w = state.waves.find((x) => x.waveId === e.payload.waveId);
57
+ if (w)
58
+ w.settled = true;
59
+ break;
60
+ }
61
+ case "TimerSet":
62
+ state.timers.push({ seq: e.seq, timerId: String(e.payload.timerId), fireAt: Number(e.payload.fireAt), fired: false });
63
+ break;
64
+ case "TimerFired": {
65
+ const t = state.timers.find((x) => x.timerId === e.payload.timerId);
66
+ if (t)
67
+ t.fired = true;
68
+ break;
69
+ }
70
+ case "RunCompleted":
71
+ state.terminal = { status: "completed", output: e.payload.output };
72
+ break;
73
+ case "RunFailed":
74
+ state.terminal = { status: "failed", error: e.payload.error };
75
+ break;
76
+ case "RunCancelled":
77
+ state.terminal = { status: "cancelled" };
78
+ break;
79
+ default: break;
80
+ }
81
+ }
82
+ return state;
83
+ }
84
+ export function makeSession(store, runId, input, raw, eff, parkedTasks = new Set()) {
85
+ return {
86
+ store, runId, input,
87
+ head: raw.at(-1)?.seq ?? 0,
88
+ folded: foldRunStream(eff),
89
+ seCursor: 0, waveCursor: 0, timerCursor: 0,
90
+ invalidated: false,
91
+ pendingDispatch: [],
92
+ parkedTasks,
93
+ };
94
+ }
95
+ export function createWorkflowCtx(s) {
96
+ async function append(events) {
97
+ const r = await s.store.append(s.runId, "run", s.head, events);
98
+ if (!r.ok)
99
+ throw new RunLeaseLostError(`run stream advanced concurrently (expected ${s.head}, actual ${r.actualSeq})`);
100
+ s.head = r.lastSeq;
101
+ }
102
+ async function invalidateFrom(fromSeq, reason) {
103
+ await append([ev("StreamInvalidated", { fromSeq, reason })]);
104
+ s.invalidated = true; // all remaining recorded entries are void (consumption is seq-ordered)
105
+ }
106
+ async function sideEffect(kind, generate) {
107
+ const rec = s.invalidated ? undefined : s.folded.sideEffects[s.seCursor];
108
+ if (rec) {
109
+ if (rec.payload.kind !== kind) {
110
+ await invalidateFrom(rec.seq, `side effect kind changed (recorded ${String(rec.payload.kind)}, expected ${kind})`);
111
+ }
112
+ else {
113
+ s.seCursor += 1;
114
+ return Number(rec.payload.value);
115
+ }
116
+ }
117
+ const value = generate();
118
+ await append([ev("SideEffectRecorded", { stepId: `s${s.head + 1}`, kind, value })]);
119
+ return value;
120
+ }
121
+ function enqueueTaskMessages(w) {
122
+ for (const t of w.tasks) {
123
+ if (!w.settledTasks.has(t.taskId) && !s.parkedTasks.has(t.taskId)) {
124
+ s.pendingDispatch.push({ queue: "tasks-short", msg: { kind: "task", runId: s.runId, taskId: t.taskId, dedupeKey: `${s.runId}:${t.taskId}` } });
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ input: s.input,
130
+ task: (agentRef, input) => ({ agentRef, input }),
131
+ now: () => sideEffect("now", () => Date.now()),
132
+ random: () => sideEffect("random", () => Math.random()),
133
+ async sleep(ms) {
134
+ const rec = s.invalidated ? undefined : s.folded.timers[s.timerCursor];
135
+ if (rec) {
136
+ s.timerCursor += 1;
137
+ if (rec.fired)
138
+ return;
139
+ if (Date.now() >= rec.fireAt) {
140
+ await append([ev("TimerFired", { timerId: rec.timerId })]);
141
+ return;
142
+ }
143
+ s.pendingDispatch.push({
144
+ queue: "orchestrator",
145
+ msg: { kind: "tick", runId: s.runId, dedupeKey: `timer-${rec.timerId}` },
146
+ delaySeconds: Math.max(0.05, (rec.fireAt - Date.now()) / 1000),
147
+ });
148
+ throw new WorkflowBlocked(`timer ${rec.timerId}`);
149
+ }
150
+ const base = await this.now();
151
+ const fireAt = base + ms;
152
+ const timerId = `tm${s.head + 1}`;
153
+ await append([ev("TimerSet", { timerId, fireAt })]);
154
+ if (Date.now() >= fireAt) {
155
+ await append([ev("TimerFired", { timerId })]);
156
+ return;
157
+ }
158
+ s.pendingDispatch.push({
159
+ queue: "orchestrator",
160
+ msg: { kind: "tick", runId: s.runId, dedupeKey: `timer-${timerId}` },
161
+ delaySeconds: Math.max(0.05, ms / 1000),
162
+ });
163
+ throw new WorkflowBlocked(`timer ${timerId}`);
164
+ },
165
+ async wave(name, tasks, opts) {
166
+ const planDigest = canonicalDigest({ name, tasks });
167
+ let w = s.invalidated ? undefined : s.folded.waves[s.waveCursor];
168
+ if (w && w.planDigest !== planDigest) {
169
+ await invalidateFrom(w.seq, `wave plan changed (${name})`);
170
+ w = undefined;
171
+ }
172
+ if (!w) {
173
+ const index = s.waveCursor;
174
+ const waveId = `w${index}`;
175
+ w = {
176
+ seq: s.head + 1, waveId, index, name,
177
+ tasks: tasks.map((t, i) => ({ taskId: `${waveId}t${i}`, agentRef: t.agentRef, input: t.input })),
178
+ planDigest, dispatched: false, settledTasks: new Map(), settled: false,
179
+ };
180
+ await append([ev("WavePlanned", { waveId, index, name, tasks: w.tasks, planDigest })]);
181
+ await append([ev("WaveDispatched", { waveId })]);
182
+ w.dispatched = true;
183
+ // task hints are enqueued once below, by the unsettled branch
184
+ }
185
+ s.waveCursor += 1;
186
+ const allSettled = w.tasks.every((t) => w.settledTasks.has(t.taskId));
187
+ if (!allSettled) {
188
+ // Messages are hints: re-nudge outstanding tasks so a lost
189
+ // dispatch self-heals on the next tick; duplicates no-op at the worker.
190
+ enqueueTaskMessages(w);
191
+ throw new WorkflowBlocked(`wave ${name} unsettled`);
192
+ }
193
+ const results = w.tasks.map((t) => w.settledTasks.get(t.taskId));
194
+ if (!w.settled) {
195
+ const coverage = {
196
+ dispatched: w.tasks.length,
197
+ completed: results.filter((r) => r.status === "completed").length,
198
+ failed: results.filter((r) => r.status === "failed").length,
199
+ };
200
+ await append([ev("WaveSettled", { waveId: w.waveId, coverage })]);
201
+ w.settled = true;
202
+ }
203
+ if ((opts?.onTaskFailure ?? "fail") === "fail" && results.some((r) => r.status === "failed")) {
204
+ const failed = results.filter((r) => r.status === "failed").map((r) => `${r.taskId}: ${r.error ?? "unknown"}`);
205
+ throw new WaveFailedError(`wave ${name} failed: ${failed.join("; ")}`);
206
+ }
207
+ return { name, results };
208
+ },
209
+ };
210
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@toren-run/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "dependencies": {
12
+ "pg": "^8.13.0",
13
+ "zod": "^3.23.0",
14
+ "zod-to-json-schema": "^3.23.0",
15
+ "@opentelemetry/api": "^1.9.0",
16
+ "croner": "^8.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/pg": "^8.11.0",
20
+ "@opentelemetry/sdk-trace-node": "^2.0.0",
21
+ "@opentelemetry/sdk-trace-base": "^2.0.0"
22
+ },
23
+ "description": "Durable agent runtime core: append-only Postgres event log, token replay, leases, workflows, approvals.",
24
+ "license": "Apache-2.0",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/toren-run/toren.git",
28
+ "directory": "packages/core"
29
+ },
30
+ "homepage": "https://toren.run",
31
+ "bugs": "https://github.com/toren-run/toren/issues",
32
+ "keywords": [
33
+ "agents",
34
+ "ai",
35
+ "durable-execution",
36
+ "workflow",
37
+ "postgres",
38
+ "anthropic",
39
+ "llm"
40
+ ],
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.build.json"
52
+ }
53
+ }