@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,38 @@
1
+ import { type TickDeps } from "./orchestrator.js";
2
+ import type { PgStateStore } from "./store.js";
3
+ export type SessionState = "working" | "awaiting_input" | "completed" | "failed" | "cancelled";
4
+ export interface SessionTurn {
5
+ role: "user" | "assistant";
6
+ text: string;
7
+ channel?: string;
8
+ seq: number;
9
+ }
10
+ export declare class SessionBusyError extends Error {
11
+ constructor();
12
+ }
13
+ export declare function startSession(deps: TickDeps, req: {
14
+ agent: string;
15
+ message: string;
16
+ channel?: string;
17
+ }): Promise<string>;
18
+ export declare function getSession(store: PgStateStore, runId: string): Promise<{
19
+ runId: string;
20
+ agent: string;
21
+ state: SessionState;
22
+ transcript: SessionTurn[];
23
+ } | null>;
24
+ /**
25
+ * Appends the user's turn and wakes the worker. Rejects while the agent is
26
+ * mid-turn (strict turn-taking; the caller shows "agent is working").
27
+ */
28
+ export declare function sendSessionMessage(deps: TickDeps, runId: string, req: {
29
+ text: string;
30
+ channel?: string;
31
+ close?: boolean;
32
+ }): Promise<void>;
33
+ export declare function listSessions(store: PgStateStore, limit?: number): Promise<{
34
+ runId: string;
35
+ agent: string;
36
+ status: string;
37
+ createdAt: Date;
38
+ }[]>;
@@ -0,0 +1,87 @@
1
+ import { effectiveEvents } from "./fold.js";
2
+ import { ev } from "./events.js";
3
+ import { startRun } from "./orchestrator.js";
4
+ /**
5
+ * Sessions: turn-based conversations as durable runs. A session is a run in
6
+ * mode "session" whose root task alternates agent turns and user turns on one
7
+ * event stream — the transcript IS the log, so a resumed session replays every
8
+ * prior turn for free and survives anything mid-sentence.
9
+ *
10
+ * Turn-taking is strict: a message is accepted only while the session awaits
11
+ * input. That keeps the stream single-writer (the worker owns it mid-turn)
12
+ * and matches the product model — you talk, the agent works, it answers.
13
+ */
14
+ /** The default workflow plans exactly one task; sessions ride it. */
15
+ const SESSION_TASK = "w0t0";
16
+ export class SessionBusyError extends Error {
17
+ constructor() { super("the agent is mid-turn — wait for it to finish before sending"); }
18
+ }
19
+ export async function startSession(deps, req) {
20
+ return startRun(deps, { agent: req.agent, input: req.message, mode: "session" });
21
+ }
22
+ async function foldSession(store, runId) {
23
+ const run = await store.getRun(runId);
24
+ if (!run || run.mode !== "session")
25
+ return null;
26
+ const eff = effectiveEvents(await store.read(runId, `task:${SESSION_TASK}`));
27
+ let lastInputSeq = 0, lastUserSeq = 0;
28
+ for (const e of eff) {
29
+ if (e.type === "InputRequested")
30
+ lastInputSeq = e.seq;
31
+ else if (e.type === "UserMessage")
32
+ lastUserSeq = e.seq;
33
+ }
34
+ const awaiting = lastInputSeq > 0 && lastUserSeq < lastInputSeq;
35
+ const head = eff.at(-1)?.seq ?? 0;
36
+ return { run, eff, awaiting, head };
37
+ }
38
+ export async function getSession(store, runId) {
39
+ const s = await foldSession(store, runId);
40
+ if (!s)
41
+ return null;
42
+ const transcript = [];
43
+ const input = typeof s.run.input === "string" ? s.run.input : JSON.stringify(s.run.input);
44
+ transcript.push({ role: "user", text: input, seq: 0 });
45
+ for (const e of s.eff) {
46
+ if (e.type === "InputRequested") {
47
+ transcript.push({ role: "assistant", text: String(e.payload.text ?? ""), seq: e.seq });
48
+ }
49
+ else if (e.type === "UserMessage" && !e.payload.close) {
50
+ transcript.push({ role: "user", text: String(e.payload.text ?? ""), channel: e.payload.channel, seq: e.seq });
51
+ }
52
+ else if (e.type === "TaskCompleted") {
53
+ // The closing assistant text is already in the last InputRequested.
54
+ }
55
+ }
56
+ const state = s.run.status === "completed" || s.run.status === "failed" || s.run.status === "cancelled"
57
+ ? s.run.status
58
+ : s.awaiting ? "awaiting_input" : "working";
59
+ return { runId, agent: s.run.agent, state, transcript };
60
+ }
61
+ /**
62
+ * Appends the user's turn and wakes the worker. Rejects while the agent is
63
+ * mid-turn (strict turn-taking; the caller shows "agent is working").
64
+ */
65
+ export async function sendSessionMessage(deps, runId, req) {
66
+ const s = await foldSession(deps.store, runId);
67
+ if (!s)
68
+ throw new Error(`no session ${runId}`);
69
+ if (s.run.status === "completed" || s.run.status === "failed" || s.run.status === "cancelled") {
70
+ throw new Error(`session is ${s.run.status}`);
71
+ }
72
+ if (!s.awaiting)
73
+ throw new SessionBusyError();
74
+ const r = await deps.store.append(runId, `task:${SESSION_TASK}`, s.head, [
75
+ ev("UserMessage", { text: req.text, channel: req.channel, ...(req.close ? { close: true } : {}) }),
76
+ ]);
77
+ if (!r.ok)
78
+ throw new SessionBusyError(); // a worker touched the stream since we looked
79
+ await deps.queue.send("tasks-short", {
80
+ kind: "task", runId, taskId: SESSION_TASK, agent: s.run.agent,
81
+ dedupeKey: `msg-${runId}-${r.lastSeq}`,
82
+ });
83
+ }
84
+ export async function listSessions(store, limit = 50) {
85
+ const runs = await store.listRuns(limit * 2);
86
+ return runs.filter((r) => r.mode === "session").slice(0, limit);
87
+ }
package/dist/db.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import pg from "pg";
2
+ export declare function createPool(url?: string): pg.Pool;
3
+ export declare function tx<T>(pool: pg.Pool, fn: (c: pg.PoolClient) => Promise<T>): Promise<T>;
package/dist/db.js ADDED
@@ -0,0 +1,20 @@
1
+ import pg from "pg";
2
+ export function createPool(url = process.env.DATABASE_URL ?? "postgres://toren:toren@localhost:5433/toren") {
3
+ return new pg.Pool({ connectionString: url, max: 10 });
4
+ }
5
+ export async function tx(pool, fn) {
6
+ const c = await pool.connect();
7
+ try {
8
+ await c.query("BEGIN");
9
+ const r = await fn(c);
10
+ await c.query("COMMIT");
11
+ return r;
12
+ }
13
+ catch (e) {
14
+ await c.query("ROLLBACK");
15
+ throw e;
16
+ }
17
+ finally {
18
+ c.release();
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export declare function canonicalDigest(value: unknown): string;
package/dist/digest.js ADDED
@@ -0,0 +1,13 @@
1
+ import { createHash } from "node:crypto";
2
+ function canonical(v) {
3
+ if (Array.isArray(v))
4
+ return v.map(canonical);
5
+ if (v && typeof v === "object") {
6
+ return Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b))
7
+ .map(([k, val]) => [k, canonical(val)]));
8
+ }
9
+ return v;
10
+ }
11
+ export function canonicalDigest(value) {
12
+ return createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex");
13
+ }
@@ -0,0 +1,20 @@
1
+ export type StreamId = "run" | `task:${string}`;
2
+ export type EventType = "RunCreated" | "RunStarted" | "SideEffectRecorded" | "TimerSet" | "TimerFired" | "WavePlanned" | "WaveDispatched" | "WaveTaskSettled" | "WaveSettled" | "RunCompleted" | "RunFailed" | "RunCancelled" | "TaskStarted" | "LlmCallStarted" | "LlmCallCompleted" | "ToolCallStarted" | "ToolCallCompleted" | "ApprovalRequested" | "ApprovalResolved" | "StreamInvalidated" | "TaskCompleted" | "TaskFailed" | "InputRequested" | "UserMessage" | "ContextCompacted";
3
+ export interface NewEvent {
4
+ type: EventType;
5
+ payload: Record<string, unknown> & {
6
+ v: 1;
7
+ };
8
+ }
9
+ export interface RecordedEvent extends NewEvent {
10
+ seq: number;
11
+ recordedAt: Date;
12
+ }
13
+ export declare const ev: (type: EventType, payload: Record<string, unknown>) => NewEvent;
14
+ export declare function isInvalidation(e: RecordedEvent): e is RecordedEvent & {
15
+ payload: {
16
+ v: 1;
17
+ fromSeq: number;
18
+ reason: string;
19
+ };
20
+ };
package/dist/events.js ADDED
@@ -0,0 +1,4 @@
1
+ export const ev = (type, payload) => ({ type, payload: { v: 1, ...payload } });
2
+ export function isInvalidation(e) {
3
+ return e.type === "StreamInvalidated" && typeof e.payload.fromSeq === "number";
4
+ }
@@ -0,0 +1,28 @@
1
+ import type pg from "pg";
2
+ /**
3
+ * Deployment-wide file store: raw bytes plus parsed text, keyed by content
4
+ * hash. Parsing happens once at upload; agents read the parsed pages through
5
+ * the read_file builtin, and every read is recorded with keyed idempotency,
6
+ * so replay never re-reads and a 200-page document never floods a context
7
+ * window — the agent pages through it.
8
+ */
9
+ export interface StoredFile {
10
+ id: string;
11
+ name: string;
12
+ mediaType: string;
13
+ bytes: number;
14
+ pages: string[];
15
+ }
16
+ export declare class PgFiles {
17
+ private pool;
18
+ constructor(pool: pg.Pool);
19
+ put(file: {
20
+ name: string;
21
+ mediaType: string;
22
+ data: Buffer;
23
+ pages: string[];
24
+ }): Promise<StoredFile>;
25
+ get(id: string): Promise<StoredFile | null>;
26
+ }
27
+ /** The manifest line appended to a run input or session message for each attachment. */
28
+ export declare function fileManifest(files: StoredFile[]): string;
package/dist/files.js ADDED
@@ -0,0 +1,27 @@
1
+ import { createHash } from "node:crypto";
2
+ export class PgFiles {
3
+ pool;
4
+ constructor(pool) {
5
+ this.pool = pool;
6
+ }
7
+ async put(file) {
8
+ const id = createHash("sha256").update(file.data).digest("hex").slice(0, 16);
9
+ await this.pool.query(`INSERT INTO toren_control.files (id, name, media_type, bytes, pages, data)
10
+ VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO NOTHING`, [id, file.name, file.mediaType, file.data.length, JSON.stringify(file.pages), file.data]);
11
+ return { id, name: file.name, mediaType: file.mediaType, bytes: file.data.length, pages: file.pages };
12
+ }
13
+ async get(id) {
14
+ const r = await this.pool.query(`SELECT id, name, media_type, bytes, pages FROM toren_control.files WHERE id = $1`, [id]);
15
+ const row = r.rows[0];
16
+ if (!row)
17
+ return null;
18
+ return { id: row.id, name: row.name, mediaType: row.media_type, bytes: row.bytes, pages: row.pages };
19
+ }
20
+ }
21
+ /** The manifest line appended to a run input or session message for each attachment. */
22
+ export function fileManifest(files) {
23
+ if (files.length === 0)
24
+ return "";
25
+ const lines = files.map((f) => `- ${f.name} (file_id: ${f.id}, ${f.pages.length} page${f.pages.length === 1 ? "" : "s"})`);
26
+ return `\n\n[Attached files:\n${lines.join("\n")}\nRead them with the read_file tool.]`;
27
+ }
package/dist/fold.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type RecordedEvent } from "./events.js";
2
+ /** Events whose effects survive all StreamInvalidated markers, in order. */
3
+ export declare function effectiveEvents(events: RecordedEvent[]): RecordedEvent[];
package/dist/fold.js ADDED
@@ -0,0 +1,6 @@
1
+ import { isInvalidation } from "./events.js";
2
+ /** Events whose effects survive all StreamInvalidated markers, in order. */
3
+ export function effectiveEvents(events) {
4
+ const cuts = events.filter(isInvalidation).map((m) => ({ from: m.payload.fromSeq, at: m.seq }));
5
+ return events.filter((e) => !isInvalidation(e) && !cuts.some((c) => e.seq >= c.from && e.seq < c.at));
6
+ }
@@ -0,0 +1,9 @@
1
+ import type { TickDeps } from "./orchestrator.js";
2
+ /**
3
+ * The guardians: one scheduled pass that re-nudges every
4
+ * non-terminal run. Covers lost queue messages (reconciler role) and runs
5
+ * whose workers died mid-flight (watchdog role — expired leases simply stop
6
+ * blocking the next tick). Ticks are cheap no-ops when nothing changed, so
7
+ * the sweep needs no per-run state.
8
+ */
9
+ export declare function sweep(deps: Pick<TickDeps, "store" | "queue">, agent?: string): Promise<number>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The guardians: one scheduled pass that re-nudges every
3
+ * non-terminal run. Covers lost queue messages (reconciler role) and runs
4
+ * whose workers died mid-flight (watchdog role — expired leases simply stop
5
+ * blocking the next tick). Ticks are cheap no-ops when nothing changed, so
6
+ * the sweep needs no per-run state.
7
+ */
8
+ export async function sweep(deps, agent) {
9
+ const runs = await deps.store.listNonTerminalRuns();
10
+ for (const runId of runs) {
11
+ await deps.queue.send("orchestrator", { kind: "tick", runId, agent, dedupeKey: `sweep-${runId}` });
12
+ }
13
+ return runs.length;
14
+ }
@@ -0,0 +1,27 @@
1
+ export declare const TOREN_VERSION = "0.0.1";
2
+ export * from "./events.js";
3
+ export * from "./fold.js";
4
+ export * from "./db.js";
5
+ export * from "./migrate.js";
6
+ export * from "./store.js";
7
+ export * from "./leases.js";
8
+ export * from "./queue.js";
9
+ export * from "./blobs.js";
10
+ export * from "./files.js";
11
+ export * from "./model.js";
12
+ export * from "./digest.js";
13
+ export * from "./tools.js";
14
+ export * from "./builtins.js";
15
+ export * from "./providers/mock.js";
16
+ export * from "./providers/echo.js";
17
+ export * from "./approvals.js";
18
+ export * from "./apiKeys.js";
19
+ export * from "./schedules.js";
20
+ export * from "./tracing.js";
21
+ export * from "./loop.js";
22
+ export * from "./workflow.js";
23
+ export * from "./orchestrator.js";
24
+ export * from "./worker.js";
25
+ export * from "./guardians.js";
26
+ export * from "./conversations.js";
27
+ export * from "./spawn.js";
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ export const TOREN_VERSION = "0.0.1";
2
+ export * from "./events.js";
3
+ export * from "./fold.js";
4
+ export * from "./db.js";
5
+ export * from "./migrate.js";
6
+ export * from "./store.js";
7
+ export * from "./leases.js";
8
+ export * from "./queue.js";
9
+ export * from "./blobs.js";
10
+ export * from "./files.js";
11
+ export * from "./model.js";
12
+ export * from "./digest.js";
13
+ export * from "./tools.js";
14
+ export * from "./builtins.js";
15
+ export * from "./providers/mock.js";
16
+ export * from "./providers/echo.js";
17
+ export * from "./approvals.js";
18
+ export * from "./apiKeys.js";
19
+ export * from "./schedules.js";
20
+ export * from "./tracing.js";
21
+ export * from "./loop.js";
22
+ export * from "./workflow.js";
23
+ export * from "./orchestrator.js";
24
+ export * from "./worker.js";
25
+ export * from "./guardians.js";
26
+ export * from "./conversations.js";
27
+ export * from "./spawn.js";
@@ -0,0 +1,16 @@
1
+ import type pg from "pg";
2
+ import type { StreamId } from "./events.js";
3
+ export interface Lease {
4
+ runId: string;
5
+ streamId: StreamId;
6
+ owner: string;
7
+ epoch: number;
8
+ }
9
+ export declare class PgLeases {
10
+ private pool;
11
+ private schema;
12
+ constructor(pool: pg.Pool, schema: string);
13
+ acquire(runId: string, streamId: StreamId, owner: string, ttlSeconds: number): Promise<Lease | null>;
14
+ renew(lease: Lease, ttlSeconds?: number): Promise<boolean>;
15
+ release(lease: Lease): Promise<void>;
16
+ }
package/dist/leases.js ADDED
@@ -0,0 +1,30 @@
1
+ export class PgLeases {
2
+ pool;
3
+ schema;
4
+ constructor(pool, schema) {
5
+ this.pool = pool;
6
+ this.schema = schema;
7
+ }
8
+ async acquire(runId, streamId, owner, ttlSeconds) {
9
+ const r = await this.pool.query(`INSERT INTO ${this.schema}.leases (run_id, stream_id, owner, epoch, expires_at)
10
+ VALUES ($1, $2, $3, 1, now() + make_interval(secs => $4))
11
+ ON CONFLICT (run_id, stream_id) DO UPDATE
12
+ SET owner = EXCLUDED.owner,
13
+ epoch = ${this.schema}.leases.epoch + 1,
14
+ expires_at = EXCLUDED.expires_at
15
+ WHERE ${this.schema}.leases.expires_at < now()
16
+ RETURNING epoch`, [runId, streamId, owner, ttlSeconds]);
17
+ const row = r.rows[0];
18
+ return row ? { runId, streamId, owner, epoch: Number(row.epoch) } : null;
19
+ }
20
+ async renew(lease, ttlSeconds = 60) {
21
+ const r = await this.pool.query(`UPDATE ${this.schema}.leases SET expires_at = now() + make_interval(secs => $5)
22
+ WHERE run_id = $1 AND stream_id = $2 AND owner = $3 AND epoch = $4 AND expires_at >= now()`, [lease.runId, lease.streamId, lease.owner, lease.epoch, ttlSeconds]);
23
+ return (r.rowCount ?? 0) > 0;
24
+ }
25
+ async release(lease) {
26
+ // Expire rather than delete: the row keeps the epoch so the next acquire fences correctly.
27
+ await this.pool.query(`UPDATE ${this.schema}.leases SET expires_at = now() - interval '1 second'
28
+ WHERE run_id = $1 AND stream_id = $2 AND owner = $3 AND epoch = $4`, [lease.runId, lease.streamId, lease.owner, lease.epoch]);
29
+ }
30
+ }
package/dist/loop.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { z } from "zod";
2
+ import { type ToolDefAny } from "./tools.js";
3
+ import type { ModelProvider } from "./model.js";
4
+ import type { PgStateStore } from "./store.js";
5
+ export declare class TaskLeaseLostError extends Error {
6
+ }
7
+ /**
8
+ * Thrown instead of appending yet another StreamInvalidated when a stream has
9
+ * been invalidated repeatedly in a short window — the signature of two worker
10
+ * versions fighting over one stream during a rolling deploy, each re-paying
11
+ * the other's voided model calls. The worker defers the message instead; the
12
+ * war starves until one version drains, and a legitimate re-edit waits out
13
+ * the window at worst.
14
+ */
15
+ export declare class InvalidationStormError extends Error {
16
+ }
17
+ export declare const INVALIDATION_STORM_LIMIT = 3;
18
+ export declare const INVALIDATION_STORM_WINDOW_MS: number;
19
+ export interface AgentSpec {
20
+ model: string;
21
+ system: string;
22
+ tools: ToolDefAny[];
23
+ maxTokens: number;
24
+ maxSteps: number;
25
+ outputSchema?: z.ZodTypeAny;
26
+ /** Declared env values (from agent.yaml `env:`) passed to tool handlers as ctx.env. */
27
+ env?: Record<string, string>;
28
+ /** Model context window in tokens; drives compaction. Defaults per provider; unset for mock disables compaction. */
29
+ contextWindow?: number;
30
+ }
31
+ /** Provider defaults; agent.yaml `contextWindow:` overrides. mock/ gets none, so tests never compact by surprise. */
32
+ export declare function defaultContextWindow(model: string): number | undefined;
33
+ export declare const COMPACT_ELIDE_AT = 0.5;
34
+ export declare const COMPACT_SUMMARY_AT = 0.78;
35
+ export declare const COMPACT_KEEP_RESULTS = 3;
36
+ export declare const COMPACT_KEEP_TAIL = 6;
37
+ export declare const COMPACT_MIN_ELIDE_CHARS = 500;
38
+ export declare const COMPACT_SUMMARY_MAX_TOKENS = 2048;
39
+ export declare const SUMMARIZE_SYSTEM: string;
40
+ export interface TaskLoopArgs {
41
+ store: PgStateStore;
42
+ provider: ModelProvider;
43
+ runId: string;
44
+ taskId: string;
45
+ agent: AgentSpec;
46
+ input: string;
47
+ /** Attached-file access, passed through to tool handlers (read_file builtin). */
48
+ files?: import("./tools.js").ToolCtx["files"];
49
+ /** Per-run sandbox execution for the bash builtin; wired by the runtime. */
50
+ sandbox?: import("./tools.js").SandboxExec;
51
+ /** Background named-process runs for the run_process/check_run builtins; wired by the runtime. */
52
+ processes?: import("./tools.js").ProcessesCtx;
53
+ /** Conversational session: end-of-turn parks awaiting the next UserMessage instead of completing. */
54
+ sessionMode?: boolean;
55
+ }
56
+ export type TaskLoopResult = {
57
+ status: "completed";
58
+ output: string;
59
+ } | {
60
+ status: "waitingApproval";
61
+ } | {
62
+ status: "awaitingInput";
63
+ } | {
64
+ status: "failed";
65
+ error: string;
66
+ };
67
+ /** Layered onto the system prompt in session mode; constant, so replay digests stay stable. */
68
+ export declare const SESSION_PREAMBLE: string;
69
+ export declare function runTaskLoop(args: TaskLoopArgs): Promise<TaskLoopResult>;