@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 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 }, () => provider.complete(request));
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 }, () => provider.complete(request));
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) {
@@ -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
- await deps.store.updateRun(runId, { status: "running" });
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)
@@ -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>;
@@ -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
- await this.shared.queue.nack(d, { delaySeconds: 0.2 });
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toren-run/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {