@tangle-network/agent-runtime 0.15.0 → 0.15.1

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.js CHANGED
@@ -1040,6 +1040,29 @@ var D1DurableRunStore = class {
1040
1040
  const row = await this.db.prepare("SELECT * FROM durable_events WHERE run_id = ? AND key = ?").bind(runId, key).first();
1041
1041
  return row ? rowToEventRecord(row) : void 0;
1042
1042
  }
1043
+ async appendStreamEvent(input) {
1044
+ const nowIso2 = new Date(this.now()).toISOString();
1045
+ const res = await this.db.prepare(
1046
+ `INSERT OR IGNORE INTO durable_stream_events (run_id, seq, event_id, payload_json, appended_at)
1047
+ VALUES (
1048
+ ?,
1049
+ (SELECT COALESCE(MAX(seq), -1) + 1 FROM durable_stream_events WHERE run_id = ?),
1050
+ ?, ?, ?
1051
+ )`
1052
+ ).bind(input.runId, input.runId, input.eventId, JSON.stringify(input.payload ?? null), nowIso2).run();
1053
+ const accepted = (res.meta?.changes ?? 0) > 0;
1054
+ const row = await this.db.prepare("SELECT * FROM durable_stream_events WHERE run_id = ? AND event_id = ?").bind(input.runId, input.eventId).first();
1055
+ if (!row) throw new Error("durable-runs: appendStreamEvent failed to persist or read back");
1056
+ return { accepted, record: rowToStreamEventRecord(row) };
1057
+ }
1058
+ async readStreamEvents(runId, afterSeq) {
1059
+ const { results } = await this.db.prepare("SELECT * FROM durable_stream_events WHERE run_id = ? AND seq > ? ORDER BY seq").bind(runId, afterSeq ?? -1).all();
1060
+ return results.map(rowToStreamEventRecord);
1061
+ }
1062
+ async setRunHandle(input) {
1063
+ const nowIso2 = new Date(this.now()).toISOString();
1064
+ await this.db.prepare("UPDATE durable_runs SET handle_json = ?, updated_at = ? WHERE run_id = ?").bind(JSON.stringify(input.handle), nowIso2, input.runId).run();
1065
+ }
1043
1066
  async close() {
1044
1067
  }
1045
1068
  /** Inspect the currently-applied schema version. */
@@ -1069,7 +1092,17 @@ function rowToRunRecord(row) {
1069
1092
  leaseHolderId: row.lease_holder_id ?? void 0,
1070
1093
  leaseExpiresAt: row.lease_expires_at ?? void 0,
1071
1094
  outcome: row.outcome_json ? JSON.parse(row.outcome_json) : void 0,
1072
- stepCount: row.step_count
1095
+ stepCount: row.step_count,
1096
+ handle: row.handle_json ? JSON.parse(row.handle_json) : void 0
1097
+ };
1098
+ }
1099
+ function rowToStreamEventRecord(row) {
1100
+ return {
1101
+ runId: row.run_id,
1102
+ seq: row.seq,
1103
+ eventId: row.event_id,
1104
+ payload: row.payload_json ? JSON.parse(row.payload_json) : null,
1105
+ appendedAt: row.appended_at
1073
1106
  };
1074
1107
  }
1075
1108
  function rowToStepRecord(row) {
@@ -1134,6 +1167,7 @@ var FileSystemDurableRunStore = class {
1134
1167
  await this.writeLease(input.runId, { workerId: input.workerId, leaseExpiresAt });
1135
1168
  await appendFile(join(dir, "steps.jsonl"), "", "utf8");
1136
1169
  await appendFile(join(dir, "events.jsonl"), "", "utf8");
1170
+ await appendFile(join(dir, "stream-events.jsonl"), "", "utf8");
1137
1171
  return { run: record2, completedSteps: [], leaseExpiresAt };
1138
1172
  }
1139
1173
  const record = await this.readRun(input.runId);
@@ -1296,8 +1330,48 @@ var FileSystemDurableRunStore = class {
1296
1330
  }
1297
1331
  return void 0;
1298
1332
  }
1333
+ async appendStreamEvent(input) {
1334
+ const existing = await this.readStreamEventsRaw(input.runId);
1335
+ const dup = existing.find((e) => e.eventId === input.eventId);
1336
+ if (dup) return { accepted: false, record: dup };
1337
+ const rec = {
1338
+ runId: input.runId,
1339
+ seq: existing.length,
1340
+ eventId: input.eventId,
1341
+ payload: input.payload,
1342
+ appendedAt: new Date(this.now()).toISOString()
1343
+ };
1344
+ await appendFile(
1345
+ join(this.runDir(input.runId), "stream-events.jsonl"),
1346
+ `${JSON.stringify(rec)}
1347
+ `,
1348
+ "utf8"
1349
+ );
1350
+ return { accepted: true, record: rec };
1351
+ }
1352
+ async readStreamEvents(runId, afterSeq) {
1353
+ const cutoff = afterSeq ?? -1;
1354
+ return (await this.readStreamEventsRaw(runId)).filter((e) => e.seq > cutoff);
1355
+ }
1356
+ async setRunHandle(input) {
1357
+ const record = await this.readRun(input.runId);
1358
+ record.handle = input.handle;
1359
+ record.updatedAt = new Date(this.now()).toISOString();
1360
+ await this.writeRun(record);
1361
+ }
1299
1362
  async close() {
1300
1363
  }
1364
+ async readStreamEventsRaw(runId) {
1365
+ const path = join(this.runDir(runId), "stream-events.jsonl");
1366
+ if (!existsSync(path)) return [];
1367
+ const content = await readFile(path, "utf8");
1368
+ const out = [];
1369
+ for (const line of content.split("\n")) {
1370
+ if (!line) continue;
1371
+ out.push(JSON.parse(line));
1372
+ }
1373
+ return out.sort((a, b) => a.seq - b.seq);
1374
+ }
1301
1375
  /** @internal — used by tests to list runs in the store. */
1302
1376
  async _listRunIds() {
1303
1377
  if (!existsSync(this.root)) return [];
@@ -1393,7 +1467,7 @@ var InMemoryDurableRunStore = class {
1393
1467
  leaseExpiresAt,
1394
1468
  stepCount: 0
1395
1469
  };
1396
- state = { record, steps: /* @__PURE__ */ new Map(), events: /* @__PURE__ */ new Map() };
1470
+ state = { record, steps: /* @__PURE__ */ new Map(), events: /* @__PURE__ */ new Map(), streamEvents: [] };
1397
1471
  this.runs.set(input.runId, state);
1398
1472
  return { run: { ...record }, completedSteps: [], leaseExpiresAt };
1399
1473
  }
@@ -1534,6 +1608,31 @@ var InMemoryDurableRunStore = class {
1534
1608
  const rec = state.events.get(key);
1535
1609
  return rec ? { ...rec } : void 0;
1536
1610
  }
1611
+ async appendStreamEvent(input) {
1612
+ const state = this.requireRun(input.runId);
1613
+ const existing = state.streamEvents.find((e) => e.eventId === input.eventId);
1614
+ if (existing) return { accepted: false, record: { ...existing } };
1615
+ const rec = {
1616
+ runId: input.runId,
1617
+ seq: state.streamEvents.length,
1618
+ eventId: input.eventId,
1619
+ payload: input.payload,
1620
+ appendedAt: new Date(this.now()).toISOString()
1621
+ };
1622
+ state.streamEvents.push(rec);
1623
+ return { accepted: true, record: { ...rec } };
1624
+ }
1625
+ async readStreamEvents(runId, afterSeq) {
1626
+ const state = this.runs.get(runId);
1627
+ if (!state) return [];
1628
+ const cutoff = afterSeq ?? -1;
1629
+ return state.streamEvents.filter((e) => e.seq > cutoff).map((e) => ({ ...e }));
1630
+ }
1631
+ async setRunHandle(input) {
1632
+ const state = this.requireRun(input.runId);
1633
+ state.record.handle = { ...input.handle };
1634
+ state.record.updatedAt = new Date(this.now()).toISOString();
1635
+ }
1537
1636
  async close() {
1538
1637
  this.runs.clear();
1539
1638
  }
@@ -1793,7 +1892,7 @@ function cryptoRandomUuid() {
1793
1892
  }
1794
1893
 
1795
1894
  // src/durable/schema.ts
1796
- var DURABLE_SCHEMA_VERSION = 1;
1895
+ var DURABLE_SCHEMA_VERSION = 2;
1797
1896
  var DURABLE_SCHEMA_SQL = `-- Durable-run substrate \u2014 versioned schema for D1 / SQLite.
1798
1897
  --
1799
1898
  -- Apply once per database. Subsequent migrations append; never rewrite a
@@ -1861,8 +1960,198 @@ CREATE TABLE IF NOT EXISTS durable_events (
1861
1960
 
1862
1961
  INSERT OR IGNORE INTO durable_schema_info (version, applied_at)
1863
1962
  VALUES (1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
1963
+
1964
+ -- \u2500\u2500 Migration v2 \u2014 durable event-stream log + run handle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1965
+ -- Run once on a database created at v1. \`ALTER TABLE\` is not idempotent; the
1966
+ -- version trail in \`durable_schema_info\` is how migrations are sequenced \u2014
1967
+ -- never by blind re-execution of this block.
1968
+ --
1969
+ -- - \`durable_stream_events\` is the ordered, replayable per-run event log.
1970
+ -- \`seq\` is the store-assigned monotonic cursor; the UNIQUE index on
1971
+ -- (run_id, event_id) makes appends idempotent \u2014 a reconnecting adapter
1972
+ -- that re-yields a boundary event cannot double-log it.
1973
+ -- - \`durable_runs.handle_json\` is the pointer (sandbox + substrate run id +
1974
+ -- cursor) a fresh supervisor re-attaches by.
1975
+
1976
+ ALTER TABLE durable_runs ADD COLUMN handle_json TEXT;
1977
+
1978
+ CREATE TABLE IF NOT EXISTS durable_stream_events (
1979
+ run_id TEXT NOT NULL,
1980
+ seq INTEGER NOT NULL,
1981
+ event_id TEXT NOT NULL,
1982
+ payload_json TEXT,
1983
+ appended_at TEXT NOT NULL,
1984
+ PRIMARY KEY (run_id, seq)
1985
+ );
1986
+
1987
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_durable_stream_events_event_id
1988
+ ON durable_stream_events(run_id, event_id);
1989
+
1990
+ INSERT OR IGNORE INTO durable_schema_info (version, applied_at)
1991
+ VALUES (2, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
1864
1992
  `;
1865
1993
 
1994
+ // src/durable/supervisor.ts
1995
+ var TURN_STEP = 0;
1996
+ function runSupervisedTurn(options) {
1997
+ const { store, runId, manifest, workerId, adapter } = options;
1998
+ const leaseMs = options.leaseMs ?? 6e4;
1999
+ const heartbeatMs = options.heartbeatMs ?? 3e4;
2000
+ const intent = options.intent ?? "turn";
2001
+ const now = options.now ?? (() => Date.now());
2002
+ const inputHash = canonicalHash(manifest.input);
2003
+ let mode = "fresh";
2004
+ let finalRecord;
2005
+ let currentHandle;
2006
+ let leaseLost = false;
2007
+ async function* drain(source) {
2008
+ let lastRenew = now();
2009
+ for await (const event of source) {
2010
+ if (event.handle) {
2011
+ currentHandle = event.handle;
2012
+ await store.setRunHandle({ runId, handle: event.handle });
2013
+ }
2014
+ const { accepted } = await store.appendStreamEvent({
2015
+ runId,
2016
+ eventId: event.eventId,
2017
+ payload: event.payload
2018
+ });
2019
+ if (now() - lastRenew >= heartbeatMs) {
2020
+ const renewed = await store.renewLease({ runId, workerId, leaseMs });
2021
+ if (!renewed.ok) {
2022
+ leaseLost = true;
2023
+ throw new Error(`durable-runs: lease lost on ${runId} \u2014 another supervisor took over`);
2024
+ }
2025
+ lastRenew = now();
2026
+ }
2027
+ if (accepted) yield event.payload;
2028
+ }
2029
+ }
2030
+ async function* stream() {
2031
+ const { run, completedSteps } = await store.startOrResume({
2032
+ runId,
2033
+ manifest,
2034
+ workerId,
2035
+ leaseMs
2036
+ });
2037
+ if (completedSteps.some((s) => s.stepIndex === TURN_STEP && s.status === "completed")) {
2038
+ mode = "replayed";
2039
+ for (const e of await store.readStreamEvents(runId)) yield e.payload;
2040
+ finalRecord = await store.endRun({ runId, workerId, status: "completed" });
2041
+ return;
2042
+ }
2043
+ const logged = await store.readStreamEvents(runId);
2044
+ const priorHandle = run.handle;
2045
+ const resumable = priorHandle !== void 0 && priorHandle.status === "running" && typeof priorHandle.runId === "string";
2046
+ let source;
2047
+ if (resumable && priorHandle) {
2048
+ mode = "resumed";
2049
+ currentHandle = priorHandle;
2050
+ for (const e of logged) yield e.payload;
2051
+ const cursor = logged.length > 0 ? logged[logged.length - 1].eventId : priorHandle.cursor;
2052
+ source = adapter.attach(priorHandle, cursor);
2053
+ } else {
2054
+ mode = "fresh";
2055
+ source = adapter.start();
2056
+ }
2057
+ await store.beginStep({ runId, stepIndex: TURN_STEP, intent, kind: "llm", inputHash });
2058
+ try {
2059
+ yield* drain(source);
2060
+ const eventCount = (await store.readStreamEvents(runId)).length;
2061
+ await store.completeStep({ runId, stepIndex: TURN_STEP, result: { eventCount } });
2062
+ if (currentHandle && currentHandle.status === "running") {
2063
+ await store.setRunHandle({ runId, handle: { ...currentHandle, status: "completed" } });
2064
+ }
2065
+ finalRecord = await store.endRun({
2066
+ runId,
2067
+ workerId,
2068
+ status: "completed",
2069
+ outcome: { notes: intent, metadata: { events: eventCount, mode } }
2070
+ });
2071
+ } catch (err) {
2072
+ if (!leaseLost) {
2073
+ await store.failStep({
2074
+ runId,
2075
+ stepIndex: TURN_STEP,
2076
+ error: { message: err instanceof Error ? err.message : String(err) }
2077
+ });
2078
+ finalRecord = await store.endRun({ runId, workerId, status: "failed" });
2079
+ }
2080
+ throw err;
2081
+ }
2082
+ }
2083
+ return {
2084
+ stream: stream(),
2085
+ mode: () => mode,
2086
+ record: () => finalRecord
2087
+ };
2088
+ }
2089
+
2090
+ // src/durable/session-supervisor-do.ts
2091
+ var ACTIVE_RUN_KEY = "agent-runtime:active-run-id";
2092
+ async function drainHeadless(stream) {
2093
+ let next = await stream.next();
2094
+ while (!next.done) next = await stream.next();
2095
+ }
2096
+ function createSessionSupervisorDO(config) {
2097
+ const orphanCheckMs = config.orphanCheckMs ?? 6e4;
2098
+ const now = config.now ?? (() => Date.now());
2099
+ return class {
2100
+ constructor(state, env) {
2101
+ this.state = state;
2102
+ this.env = env;
2103
+ }
2104
+ state;
2105
+ env;
2106
+ async fetch(request) {
2107
+ const opts = await config.resolveRun(request, this.env, this.state);
2108
+ if (!opts) return new Response("no run for this request", { status: 404 });
2109
+ await this.state.storage.put(ACTIVE_RUN_KEY, opts.runId);
2110
+ await this.state.storage.setAlarm(now() + orphanCheckMs);
2111
+ const supervised = runSupervisedTurn(opts);
2112
+ const storage = this.state.storage;
2113
+ const encoder2 = new TextEncoder();
2114
+ const body = new ReadableStream({
2115
+ async pull(controller) {
2116
+ try {
2117
+ const next = await supervised.stream.next();
2118
+ if (next.done) {
2119
+ await storage.delete(ACTIVE_RUN_KEY);
2120
+ controller.close();
2121
+ return;
2122
+ }
2123
+ controller.enqueue(encoder2.encode(config.encodeEvent(next.value)));
2124
+ } catch (err) {
2125
+ controller.error(err instanceof Error ? err : new Error(String(err)));
2126
+ }
2127
+ }
2128
+ });
2129
+ return new Response(body, {
2130
+ headers: { "content-type": "text/event-stream", "cache-control": "no-cache" }
2131
+ });
2132
+ }
2133
+ async alarm() {
2134
+ const runId = await this.state.storage.get(ACTIVE_RUN_KEY);
2135
+ if (!runId) return;
2136
+ const opts = await config.resolveOrphan(runId, this.env, this.state);
2137
+ if (!opts) {
2138
+ await this.state.storage.delete(ACTIVE_RUN_KEY);
2139
+ return;
2140
+ }
2141
+ try {
2142
+ await drainHeadless(runSupervisedTurn(opts).stream);
2143
+ await this.state.storage.delete(ACTIVE_RUN_KEY);
2144
+ } catch (err) {
2145
+ if (err instanceof DurableRunLeaseHeldError) {
2146
+ await this.state.storage.setAlarm(now() + orphanCheckMs);
2147
+ return;
2148
+ }
2149
+ await this.state.storage.delete(ACTIVE_RUN_KEY);
2150
+ }
2151
+ }
2152
+ };
2153
+ }
2154
+
1866
2155
  // src/durable/workflows.ts
1867
2156
  async function runOnWorkflowStep(workflowStep, input) {
1868
2157
  const stepCfg = input.stepConfig;
@@ -1983,6 +2272,85 @@ function classifyIntent(profile, message, opts = {}) {
1983
2272
  return { id: bestId, subagent: bestSubagent, score: bestScore, scores, evaluated };
1984
2273
  }
1985
2274
 
2275
+ // src/model-resolution.ts
2276
+ var DEFAULT_ROUTER_BASE_URL = "https://router.tangle.tools";
2277
+ function resolveRouterBaseUrl(env = {}) {
2278
+ return (env.TANGLE_ROUTER_URL ?? env.TANGLE_ROUTER_BASE_URL ?? DEFAULT_ROUTER_BASE_URL).replace(/\/v1\/?$/, "").replace(/\/$/, "");
2279
+ }
2280
+ async function getModels(routerBaseUrl = DEFAULT_ROUTER_BASE_URL) {
2281
+ const res = await fetch(`${routerBaseUrl}/v1/models`, {
2282
+ headers: { Accept: "application/json" }
2283
+ });
2284
+ if (!res.ok) throw new Error(`router /v1/models ${res.status}`);
2285
+ const body = await res.json();
2286
+ return Array.isArray(body.data) ? body.data : [];
2287
+ }
2288
+ function withConfiguredModels(models, extraIds) {
2289
+ const known = new Set(models.map((model) => model.id));
2290
+ const extra = extraIds.map((id) => cleanModelId(id)).filter((id) => id !== void 0 && !known.has(id)).map(
2291
+ (id) => ({
2292
+ id,
2293
+ name: id,
2294
+ description: "Configured chat model for this environment.",
2295
+ architecture: {
2296
+ modality: "text->text",
2297
+ input_modalities: ["text"],
2298
+ output_modalities: ["text"]
2299
+ }
2300
+ })
2301
+ );
2302
+ return extra.length > 0 ? [...extra, ...models] : models;
2303
+ }
2304
+ function cleanModelId(value) {
2305
+ if (typeof value !== "string") return void 0;
2306
+ const trimmed = value.trim();
2307
+ return trimmed.length > 0 ? trimmed : void 0;
2308
+ }
2309
+ function resolveChatModel(candidates, fallback) {
2310
+ for (const candidate of candidates) {
2311
+ const model = cleanModelId(candidate.model);
2312
+ if (model) return { source: candidate.source, model };
2313
+ }
2314
+ return fallback;
2315
+ }
2316
+ var WELL_FORMED_MODEL_ID = /^[A-Za-z0-9._/@:-]+$/;
2317
+ function isWellFormedModelId(modelId) {
2318
+ return modelId.length <= 200 && WELL_FORMED_MODEL_ID.test(modelId);
2319
+ }
2320
+ function catalogIdsForModel(model) {
2321
+ const ids = /* @__PURE__ */ new Set();
2322
+ const id = cleanModelId(model.id);
2323
+ if (id) ids.add(id);
2324
+ const provider = cleanModelId(model._provider) ?? cleanModelId(model.provider);
2325
+ if (provider && id && !id.includes("/")) ids.add(`${provider}/${id}`);
2326
+ return [...ids];
2327
+ }
2328
+ async function validateChatModelId(modelId, options = {}) {
2329
+ const {
2330
+ allowlist = [],
2331
+ routerBaseUrl = DEFAULT_ROUTER_BASE_URL,
2332
+ loadModels = getModels
2333
+ } = options;
2334
+ const cleaned = cleanModelId(modelId);
2335
+ if (!cleaned) return { succeeded: false, error: "Model id must be a non-empty string." };
2336
+ if (!isWellFormedModelId(cleaned)) {
2337
+ return { succeeded: false, error: `Model id is malformed: ${cleaned}` };
2338
+ }
2339
+ if (allowlist.some((id) => cleanModelId(id) === cleaned)) {
2340
+ return { succeeded: true, value: cleaned };
2341
+ }
2342
+ let catalog;
2343
+ try {
2344
+ catalog = await loadModels(routerBaseUrl);
2345
+ } catch (err) {
2346
+ const message = err instanceof Error ? err.message : String(err);
2347
+ return { succeeded: false, error: `Could not validate model catalog: ${message}` };
2348
+ }
2349
+ const ids = new Set(catalog.flatMap(catalogIdsForModel));
2350
+ if (!ids.has(cleaned)) return { succeeded: false, error: `Model is not available: ${cleaned}` };
2351
+ return { succeeded: true, value: cleaned };
2352
+ }
2353
+
1986
2354
  // src/profile-conformance.ts
1987
2355
  var DEFAULT_SHELL_CAPS = [
1988
2356
  "bash",
@@ -3187,6 +3555,7 @@ export {
3187
3555
  ChatTurnError,
3188
3556
  ConfigError,
3189
3557
  D1DurableRunStore,
3558
+ DEFAULT_ROUTER_BASE_URL,
3190
3559
  DURABLE_SCHEMA_SQL,
3191
3560
  DURABLE_SCHEMA_VERSION,
3192
3561
  DurableAwaitEventTimeoutError,
@@ -3209,25 +3578,31 @@ export {
3209
3578
  canonicalHash,
3210
3579
  canonicalJson,
3211
3580
  classifyIntent,
3581
+ cleanModelId,
3212
3582
  composeTurnProfile,
3213
3583
  createIterableBackend,
3214
3584
  createOpenAICompatibleBackend,
3215
3585
  createRuntimeEventCollector,
3216
3586
  createRuntimeStreamEventCollector,
3217
3587
  createSandboxPromptBackend,
3588
+ createSessionSupervisorDO,
3218
3589
  createTraceBridge,
3219
3590
  decideKnowledgeReadiness,
3220
3591
  deriveWorkerId,
3221
3592
  durableChatTurnEngine,
3222
3593
  encodeServerSentEvent,
3594
+ getModels,
3223
3595
  manifestHash,
3224
3596
  readinessServerSentEvent,
3597
+ resolveChatModel,
3598
+ resolveRouterBaseUrl,
3225
3599
  runAgentTask,
3226
3600
  runAgentTaskStream,
3227
3601
  runChatTurn,
3228
3602
  runDurable,
3229
3603
  runDurableTurn,
3230
3604
  runOnWorkflowStep,
3605
+ runSupervisedTurn,
3231
3606
  runtimeStreamServerSentEvent,
3232
3607
  sandboxAsChatTurnTarget,
3233
3608
  sanitizeAgentRuntimeEvent,
@@ -3236,6 +3611,8 @@ export {
3236
3611
  startRuntimeRun,
3237
3612
  stepId,
3238
3613
  summarizeAgentTaskRun,
3239
- toAgentEvalTrace
3614
+ toAgentEvalTrace,
3615
+ validateChatModelId,
3616
+ withConfiguredModels
3240
3617
  };
3241
3618
  //# sourceMappingURL=index.js.map