@toren-run/core 0.1.2 → 0.1.4

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/dist/index.d.ts CHANGED
@@ -25,3 +25,5 @@ export * from "./worker.js";
25
25
  export * from "./guardians.js";
26
26
  export * from "./conversations.js";
27
27
  export * from "./spawn.js";
28
+ export * from "./usage.js";
29
+ export * from "./tail.js";
package/dist/index.js CHANGED
@@ -25,3 +25,5 @@ export * from "./worker.js";
25
25
  export * from "./guardians.js";
26
26
  export * from "./conversations.js";
27
27
  export * from "./spawn.js";
28
+ export * from "./usage.js";
29
+ export * from "./tail.js";
package/dist/loop.d.ts CHANGED
@@ -22,6 +22,10 @@ export interface AgentSpec {
22
22
  tools: ToolDefAny[];
23
23
  maxTokens: number;
24
24
  maxSteps: number;
25
+ /** Passed to reasoning models (agent.yaml `reasoning_effort:`); absent leaves request digests untouched. */
26
+ reasoningEffort?: string;
27
+ /** Opt-in poison-pill (agent.yaml `limits.maxAttemptsPerTask`): terminally fail a task after this many attempts instead of retrying forever. */
28
+ maxTaskAttempts?: number;
25
29
  outputSchema?: z.ZodTypeAny;
26
30
  /** Declared env values (from agent.yaml `env:`) passed to tool handlers as ctx.env. */
27
31
  env?: Record<string, string>;
package/dist/loop.js CHANGED
@@ -93,6 +93,11 @@ async function runTaskLoopImpl(args) {
93
93
  invalidated = true;
94
94
  }
95
95
  const attempt = raw.filter((e) => e.type === "TaskStarted").length + 1;
96
+ if (agent.maxTaskAttempts && attempt > agent.maxTaskAttempts) {
97
+ const error = `gave up after ${agent.maxTaskAttempts} attempts (limits.maxAttemptsPerTask); the run's recorded error holds the last failure reason`;
98
+ await append([ev("TaskFailed", { error, willRetry: false })]);
99
+ return { status: "failed", error };
100
+ }
96
101
  await append([ev("TaskStarted", { attempt })]);
97
102
  const messages = [{ role: "user", content: [{ type: "text", text: args.input }] }];
98
103
  const specs = toolSpecs(agent.tools);
@@ -347,7 +352,7 @@ async function runTaskLoopImpl(args) {
347
352
  return { status: "failed", error };
348
353
  }
349
354
  await maybeCompact();
350
- const request = { model: agent.model, system, messages: [...messages], tools: specs, maxTokens: agent.maxTokens };
355
+ const request = { model: agent.model, system, messages: [...messages], tools: specs, maxTokens: agent.maxTokens, ...(agent.reasoningEffort ? { reasoningEffort: agent.reasoningEffort } : {}) };
351
356
  const response = await recordedLlmCall(request);
352
357
  messages.push({ role: "assistant", content: response.content });
353
358
  if (response.stopReason === "toolUse") {
package/dist/model.d.ts CHANGED
@@ -27,6 +27,8 @@ export interface ModelRequest {
27
27
  messages: ChatMessage[];
28
28
  tools: ToolSpec[];
29
29
  maxTokens: number;
30
+ /** OpenAI reasoning models: "none" | "low" | "medium" | "high". Absent keeps old request digests byte-identical. */
31
+ reasoningEffort?: string;
30
32
  }
31
33
  export type StopReason = "endTurn" | "toolUse" | "maxTokens" | "refusal";
32
34
  export interface ModelResponse {
@@ -27,6 +27,13 @@ export declare function startRun(deps: TickDeps, req: {
27
27
  runId?: string;
28
28
  mode?: "task" | "session";
29
29
  }): Promise<string>;
30
+ /**
31
+ * Retire a run from outside: appends RunCancelled and marks the row, so every
32
+ * queued hint for it becomes a no-op and retries stop. The escape hatch for a
33
+ * run stuck retrying a permanently broken dependency. Returns false for an
34
+ * unknown run; true if the run is (now) cancelled.
35
+ */
36
+ export declare function cancelRun(deps: TickDeps, runId: string, reason?: string): Promise<boolean>;
30
37
  export declare function tick(deps: TickDeps, runId: string): Promise<TickResult>;
31
38
  /** Locates a planned task's spec by scanning the run stream (workers use this). */
32
39
  export declare function findTaskSpec(store: PgStateStore, runId: string, taskId: string): Promise<{
@@ -20,6 +20,31 @@ export async function startRun(deps, req) {
20
20
  await deps.queue.send("orchestrator", { kind: "tick", runId, agent: req.agent, dedupeKey: `start-${runId}` });
21
21
  return runId;
22
22
  }
23
+ /**
24
+ * Retire a run from outside: appends RunCancelled and marks the row, so every
25
+ * queued hint for it becomes a no-op and retries stop. The escape hatch for a
26
+ * run stuck retrying a permanently broken dependency. Returns false for an
27
+ * unknown run; true if the run is (now) cancelled.
28
+ */
29
+ export async function cancelRun(deps, runId, reason = "cancelled by operator") {
30
+ for (let i = 0; i < 5; i++) {
31
+ const run = await deps.store.getRun(runId);
32
+ if (!run)
33
+ return false;
34
+ if (run.status === "cancelled")
35
+ return true;
36
+ if (run.status === "completed" || run.status === "failed")
37
+ return false;
38
+ const head = (await deps.store.read(runId, "run")).at(-1)?.seq ?? 0;
39
+ const r = await deps.store.append(runId, "run", head, [ev("RunCancelled", { reason })]);
40
+ if (r.ok) {
41
+ await deps.store.updateRun(runId, { status: "cancelled", error: reason });
42
+ return true;
43
+ }
44
+ // A worker advanced the stream between read and append; look again.
45
+ }
46
+ throw new Error(`could not cancel ${runId}: the run stream kept advancing`);
47
+ }
23
48
  export async function tick(deps, runId) {
24
49
  return withSpan("toren.run.tick", { "toren.run_id": runId }, () => tickImpl(deps, runId));
25
50
  }
package/dist/tail.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { PgStateStore } from "./store.js";
2
+ import type { RecordedEvent, StreamId } from "./events.js";
3
+ /**
4
+ * Incremental follow of a run's event streams, for `toren jobs tail` and the
5
+ * SSE endpoint. The cursor is per-stream last-seen seq; callers poll and get
6
+ * only what landed since. Streams are append-only, so this is exact, not
7
+ * best-effort.
8
+ */
9
+ export type TailCursor = Partial<Record<string, number>>;
10
+ export interface TailedEvent extends RecordedEvent {
11
+ streamId: StreamId;
12
+ }
13
+ export declare function followRun(store: PgStateStore, runId: string, cursor: TailCursor): Promise<{
14
+ events: TailedEvent[];
15
+ done: boolean;
16
+ }>;
package/dist/tail.js ADDED
@@ -0,0 +1,27 @@
1
+ const TERMINAL = new Set(["completed", "failed", "cancelled"]);
2
+ export async function followRun(store, runId, cursor) {
3
+ const events = [];
4
+ const pull = async (streamId) => {
5
+ const after = cursor[streamId] ?? 0;
6
+ for (const e of await store.read(runId, streamId)) {
7
+ if (e.seq <= after)
8
+ continue;
9
+ events.push({ ...e, streamId });
10
+ cursor[streamId] = e.seq;
11
+ }
12
+ };
13
+ await pull("run");
14
+ // Task streams come from every wave ever planned; the run stream is truth.
15
+ const taskIds = new Set();
16
+ for (const e of await store.read(runId, "run")) {
17
+ if (e.type !== "WavePlanned")
18
+ continue;
19
+ for (const t of e.payload.tasks)
20
+ taskIds.add(t.taskId);
21
+ }
22
+ for (const taskId of taskIds)
23
+ await pull(`task:${taskId}`);
24
+ events.sort((a, b) => a.recordedAt.getTime() - b.recordedAt.getTime());
25
+ const run = await store.getRun(runId);
26
+ return { events, done: !run || TERMINAL.has(run.status) };
27
+ }
@@ -0,0 +1,29 @@
1
+ import type { PgStateStore } from "./store.js";
2
+ /**
3
+ * Cost roll-up for one run, straight from the event log. Every model call's
4
+ * usage rides in LlmCallCompleted; the raw streams (not the effective view)
5
+ * are the tally, because invalidated calls were still paid for. The number
6
+ * the product exists for: completed calls recorded before a later attempt
7
+ * replayed from the log instead of the provider, so their cost was paid once.
8
+ */
9
+ export interface RunUsage {
10
+ models: Record<string, {
11
+ calls: number;
12
+ inputTokens: number;
13
+ outputTokens: number;
14
+ }>;
15
+ totalCalls: number;
16
+ /** Sum of TaskStarted across all task streams; > tasks means resumes happened. */
17
+ taskAttempts: number;
18
+ /** Completed calls that later attempts replayed from the log instead of re-buying. */
19
+ replayedCalls: number;
20
+ /** Present only when every used model has a known price. USD. */
21
+ estCostUsd?: number;
22
+ replaySavingsUsd?: number;
23
+ }
24
+ /** USD per million tokens. Approximate list prices; override or extend via TOREN_MODEL_PRICES (JSON, same shape). */
25
+ export declare const MODEL_PRICES: Record<string, {
26
+ in: number;
27
+ out: number;
28
+ }>;
29
+ export declare function runUsage(store: PgStateStore, runId: string): Promise<RunUsage>;
package/dist/usage.js ADDED
@@ -0,0 +1,73 @@
1
+ /** USD per million tokens. Approximate list prices; override or extend via TOREN_MODEL_PRICES (JSON, same shape). */
2
+ export const MODEL_PRICES = {
3
+ "openai/gpt-4o": { in: 2.5, out: 10 },
4
+ "openai/gpt-4o-mini": { in: 0.15, out: 0.6 },
5
+ "mock/echo": { in: 0, out: 0 },
6
+ "mock/slow": { in: 0, out: 0 },
7
+ "mock/m": { in: 0, out: 0 },
8
+ };
9
+ function prices() {
10
+ const env = process.env.TOREN_MODEL_PRICES;
11
+ if (!env)
12
+ return MODEL_PRICES;
13
+ try {
14
+ return { ...MODEL_PRICES, ...JSON.parse(env) };
15
+ }
16
+ catch {
17
+ return MODEL_PRICES;
18
+ }
19
+ }
20
+ export async function runUsage(store, runId) {
21
+ const runEvents = await store.read(runId, "run");
22
+ const taskIds = new Set();
23
+ for (const e of runEvents) {
24
+ if (e.type !== "WavePlanned")
25
+ continue;
26
+ for (const t of e.payload.tasks)
27
+ taskIds.add(t.taskId);
28
+ }
29
+ const table = prices();
30
+ const models = {};
31
+ let totalCalls = 0, taskAttempts = 0, replayedCalls = 0;
32
+ let cost = 0, savings = 0, priceable = true;
33
+ for (const taskId of taskIds) {
34
+ const events = await store.read(runId, `task:${taskId}`);
35
+ const stepModel = new Map();
36
+ let completedSoFar = 0, costSoFar = 0;
37
+ for (const e of events) {
38
+ if (e.type === "TaskStarted") {
39
+ taskAttempts += 1;
40
+ if (Number(e.payload.attempt ?? 1) > 1) {
41
+ replayedCalls += completedSoFar;
42
+ savings += costSoFar;
43
+ }
44
+ }
45
+ else if (e.type === "LlmCallStarted") {
46
+ stepModel.set(String(e.payload.stepId), String(e.payload.model ?? ""));
47
+ }
48
+ else if (e.type === "LlmCallCompleted") {
49
+ const model = stepModel.get(String(e.payload.stepId)) ?? "";
50
+ const usage = (e.payload.usage ?? {});
51
+ const m = (models[model] ??= { calls: 0, inputTokens: 0, outputTokens: 0 });
52
+ m.calls += 1;
53
+ m.inputTokens += usage.inputTokens ?? 0;
54
+ m.outputTokens += usage.outputTokens ?? 0;
55
+ totalCalls += 1;
56
+ completedSoFar += 1;
57
+ const p = table[model];
58
+ if (p) {
59
+ const c = ((usage.inputTokens ?? 0) * p.in + (usage.outputTokens ?? 0) * p.out) / 1_000_000;
60
+ cost += c;
61
+ costSoFar += c;
62
+ }
63
+ else {
64
+ priceable = false;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ return {
70
+ models, totalCalls, taskAttempts, replayedCalls,
71
+ ...(priceable && totalCalls > 0 ? { estCostUsd: cost, replaySavingsUsd: savings } : {}),
72
+ };
73
+ }
package/dist/worker.js CHANGED
@@ -140,6 +140,11 @@ export class LocalWorkerRuntime {
140
140
  if (!agent)
141
141
  throw new Error(`no agent registered for ref ${spec.agentRef}`);
142
142
  const run = await deps.store.getRun(msg.runId);
143
+ if (!run || run.status === "cancelled" || run.status === "completed" || run.status === "failed") {
144
+ // A hint for a retired run: messages are never truth.
145
+ await this.shared.queue.ack(d);
146
+ return;
147
+ }
143
148
  await runTaskLoop({
144
149
  store: deps.store, provider: deps.provider,
145
150
  runId: msg.runId, taskId, agent, input: spec.input, files: deps.files,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toren-run/core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {