@toren-run/core 0.1.0 → 0.1.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 +9 -0
- package/dist/loop.js +12 -2
- package/dist/orchestrator.js +3 -2
- package/dist/providers/echo.d.ts +0 -4
- package/dist/providers/echo.js +5 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.js +14 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @toren-run/core
|
|
2
|
+
|
|
3
|
+
The engine of [Toren](https://toren.run), the open-source runtime for long-running, durable AI agents. This package is the durability itself: the append-only Postgres event log, the agent task loop, the orchestrator with parallel waves, workers, leases, guardians, approvals, schedules, and sessions. A resumed run never re-pays for a completed model call; a CI kill matrix crashes the stack after every database write and asserts exactly-once billing.
|
|
4
|
+
|
|
5
|
+
Most people want the CLI instead: `npm install toren-run` gives you `toren init`, `toren dev`, the HTTP API, and the web console, all built on this package. Reach for `@toren-run/core` directly when you embed the runtime in your own host process: `startRun`, `tick`, `LocalWorkerRuntime`, `defineTool`, and friends.
|
|
6
|
+
|
|
7
|
+
Zero model SDKs in here. The core depends on `pg` and `zod`, nothing environment-specific; providers, queues, and sandboxes plug in behind interfaces.
|
|
8
|
+
|
|
9
|
+
**Docs:** [toren.run/docs](https://toren.run/docs) (durability and architecture live under Concepts). Apache-2.0.
|
package/dist/loop.js
CHANGED
|
@@ -129,14 +129,24 @@ async function runTaskLoopImpl(args) {
|
|
|
129
129
|
else {
|
|
130
130
|
// Crash window: call was issued but the response never landed. Re-issue (at-least-once).
|
|
131
131
|
ptr += 1;
|
|
132
|
-
response = await withSpan("toren.llm", { "gen_ai.request.model": request.model }, () =>
|
|
132
|
+
response = await withSpan("toren.llm", { "gen_ai.request.model": request.model }, async (span) => {
|
|
133
|
+
const r = await provider.complete(request);
|
|
134
|
+
if (r.usage)
|
|
135
|
+
span.setAttributes({ "gen_ai.usage.input_tokens": r.usage.inputTokens, "gen_ai.usage.output_tokens": r.usage.outputTokens });
|
|
136
|
+
return r;
|
|
137
|
+
});
|
|
133
138
|
await append([ev("LlmCallCompleted", { stepId: next.payload.stepId, response, usage: response.usage })]);
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
else {
|
|
137
142
|
const stepId = `s${head + 1}`;
|
|
138
143
|
await append([ev("LlmCallStarted", { stepId, requestDigest: digest, model: request.model })]);
|
|
139
|
-
response = await withSpan("toren.llm", { "gen_ai.request.model": request.model }, () =>
|
|
144
|
+
response = await withSpan("toren.llm", { "gen_ai.request.model": request.model }, async (span) => {
|
|
145
|
+
const r = await provider.complete(request);
|
|
146
|
+
if (r.usage)
|
|
147
|
+
span.setAttributes({ "gen_ai.usage.input_tokens": r.usage.inputTokens, "gen_ai.usage.output_tokens": r.usage.outputTokens });
|
|
148
|
+
return r;
|
|
149
|
+
});
|
|
140
150
|
await append([ev("LlmCallCompleted", { stepId, response, usage: response.usage })]);
|
|
141
151
|
}
|
|
142
152
|
if (response.usage) {
|
package/dist/orchestrator.js
CHANGED
|
@@ -107,14 +107,15 @@ async function tickImpl(deps, runId) {
|
|
|
107
107
|
const r = await deps.store.append(runId, "run", session.head, [ev("RunCompleted", { output })]);
|
|
108
108
|
if (!r.ok)
|
|
109
109
|
throw new RunLeaseLostError("run stream advanced concurrently");
|
|
110
|
-
await deps.store.updateRun(runId, { status: "completed", output });
|
|
110
|
+
await deps.store.updateRun(runId, { status: "completed", output, error: null });
|
|
111
111
|
await flush();
|
|
112
112
|
return "completed";
|
|
113
113
|
}
|
|
114
114
|
catch (e) {
|
|
115
115
|
if (e instanceof WorkflowBlocked) {
|
|
116
116
|
await flush();
|
|
117
|
-
|
|
117
|
+
// Parking cleanly is progress: a transient error recorded by a worker retry is stale now.
|
|
118
|
+
await deps.store.updateRun(runId, { status: "running", error: null });
|
|
118
119
|
return "blocked";
|
|
119
120
|
}
|
|
120
121
|
if (e instanceof RunLeaseLostError)
|
package/dist/providers/echo.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { ModelProvider, ModelRequest, ModelResponse } from "../model.js";
|
|
2
|
-
/**
|
|
3
|
-
* Deterministic offline provider for demos, scaffolds, and tests: replies
|
|
4
|
-
* `echo(<first user text>)`. Lets `toren init` output run with zero API keys.
|
|
5
|
-
*/
|
|
6
2
|
export declare class EchoProvider implements ModelProvider {
|
|
7
3
|
calls: Map<string, number>;
|
|
8
4
|
complete(req: ModelRequest): Promise<ModelResponse>;
|
package/dist/providers/echo.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deterministic offline provider for demos, scaffolds, and tests: replies
|
|
3
3
|
* `echo(<first user text>)`. Lets `toren init` output run with zero API keys.
|
|
4
|
+
* `mock/slow` is the same echo at three seconds per call, so the quickstart's
|
|
5
|
+
* kill test has something to murder without an API key.
|
|
4
6
|
*/
|
|
7
|
+
const SLOW_CALL_MS = 3_000;
|
|
5
8
|
export class EchoProvider {
|
|
6
9
|
calls = new Map();
|
|
7
10
|
async complete(req) {
|
|
11
|
+
if (req.model.endsWith("/slow"))
|
|
12
|
+
await new Promise((r) => setTimeout(r, SLOW_CALL_MS));
|
|
8
13
|
const first = req.messages[0]?.content.find((b) => b.type === "text");
|
|
9
14
|
const input = first && first.type === "text" ? first.text : "";
|
|
10
15
|
this.calls.set(input, (this.calls.get(input) ?? 0) + 1);
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type TickDeps } from "./orchestrator.js";
|
|
2
|
+
/** Exponential backoff for task retries: 0.2s doubling per attempt, capped at 60s. */
|
|
3
|
+
export declare const retryDelaySeconds: (attempt: number) => number;
|
|
2
4
|
export interface WorkerOpts {
|
|
3
5
|
concurrency?: number;
|
|
4
6
|
visibilitySeconds?: number;
|
package/dist/worker.js
CHANGED
|
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { InvalidationStormError, runTaskLoop, TaskLeaseLostError } from "./loop.js";
|
|
3
3
|
import { findTaskSpec, tick } from "./orchestrator.js";
|
|
4
4
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
/** Exponential backoff for task retries: 0.2s doubling per attempt, capped at 60s. */
|
|
6
|
+
export const retryDelaySeconds = (attempt) => Math.min(60, 0.2 * 2 ** Math.max(0, attempt - 1));
|
|
5
7
|
/**
|
|
6
8
|
* Local worker runtime: in-process pollers over the orchestrator and task
|
|
7
9
|
* queues (local binding). One instance serves one agent or a whole
|
|
@@ -165,7 +167,18 @@ export class LocalWorkerRuntime {
|
|
|
165
167
|
await this.shared.queue.nack(d, { delaySeconds: 20 });
|
|
166
168
|
return;
|
|
167
169
|
}
|
|
168
|
-
|
|
170
|
+
// An outside dependency failed (a provider, a tool, the network). The
|
|
171
|
+
// retry is durability doing its job; the reason must not vanish into
|
|
172
|
+
// it. Record it on the run so jobs/console answer "why is it stuck"
|
|
173
|
+
// while the retries continue; a later success clears it.
|
|
174
|
+
try {
|
|
175
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
176
|
+
await deps.store.updateRun(msg.runId, {
|
|
177
|
+
error: `${reason}${msg.taskId ? ` (task ${msg.taskId}, attempt ${d.attempt})` : ""}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch { /* the store may be the thing that failed */ }
|
|
181
|
+
await this.shared.queue.nack(d, { delaySeconds: retryDelaySeconds(d.attempt) });
|
|
169
182
|
}
|
|
170
183
|
catch {
|
|
171
184
|
// Queue unreachable too: do nothing. The visibility timeout redelivers
|