@nightowlsdev/core 0.4.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -109,6 +109,31 @@ function bindSecrets(resolver, ctx) {
109
109
  };
110
110
  }
111
111
 
112
+ // src/run-state.ts
113
+ var RUN_STATE_KEY = "__nightowls_run_state__";
114
+ function snapshotCopy(store) {
115
+ const plain = Object.fromEntries(store);
116
+ try {
117
+ return (globalThis.structuredClone ?? ((v) => JSON.parse(JSON.stringify(v))))(plain);
118
+ } catch {
119
+ try {
120
+ return JSON.parse(JSON.stringify(plain));
121
+ } catch {
122
+ return plain;
123
+ }
124
+ }
125
+ }
126
+ function createRunState(seed) {
127
+ const m = new Map(seed ? Object.entries(seed) : void 0);
128
+ return {
129
+ get: (key) => m.get(key),
130
+ set: (key, value) => void m.set(key, value),
131
+ has: (key) => m.has(key),
132
+ delete: (key) => m.delete(key),
133
+ entries: () => snapshotCopy(m)
134
+ };
135
+ }
136
+
112
137
  // src/step-driver.ts
113
138
  function initialWorkflowState(wf) {
114
139
  return { workflow: wf.name, cursor: wf.start, outputs: {}, generationIndex: 0 };
@@ -1152,13 +1177,14 @@ var SwarmEngine = class {
1152
1177
  agent() {
1153
1178
  return this.mastra.getAgent(AGENT_KEY);
1154
1179
  }
1155
- requestContext(ctx) {
1180
+ requestContext(ctx, state) {
1156
1181
  const rc = new RequestContext();
1157
1182
  for (const [k, v] of Object.entries(ctx)) {
1158
1183
  if (v !== void 0) rc.set(k, v);
1159
1184
  }
1160
1185
  rc.set(TOOL_GATE_KEY, this.toolGate);
1161
1186
  if (this.opts.secrets) rc.set(SECRET_RESOLVER_KEY, this.opts.secrets);
1187
+ if (state) rc.set(RUN_STATE_KEY, state);
1162
1188
  return rc;
1163
1189
  }
1164
1190
  /**
@@ -1428,6 +1454,9 @@ var SwarmEngine = class {
1428
1454
  const modelId = this.priceModelId((await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown", ctx.tenantId, ctx.agentSlug);
1429
1455
  const workflowDef = input.workflow ? this.opts.workflows?.find((w) => w.name === input.workflow) : this.opts.agentWorkflows?.[ctx.agentSlug];
1430
1456
  if (input.workflow && !workflowDef) throw new Error(`unknown workflow: ${input.workflow}`);
1457
+ if (this.opts.storage.threads) {
1458
+ await this.opts.storage.threads.ensure({ id: ctx.threadId, orgId: ctx.tenantId, userId: ctx.userId });
1459
+ }
1431
1460
  await this.opts.storage.runs.create({
1432
1461
  runId: ctx.runId,
1433
1462
  tenantId: ctx.tenantId,
@@ -1445,8 +1474,17 @@ var SwarmEngine = class {
1445
1474
  const streamed = /* @__PURE__ */ new Set();
1446
1475
  const activity = /* @__PURE__ */ new Map();
1447
1476
  const turnUsage = [];
1448
- const rc = this.requestContext(ctx);
1477
+ const runState = createRunState();
1478
+ const rc = this.requestContext(ctx, runState);
1449
1479
  if (this.opts.pageContext) attachPageContext(rc, input.context);
1480
+ let outcome = "failed";
1481
+ if (this.opts.onRunStart) {
1482
+ try {
1483
+ await this.opts.onRunStart(ctx, { input, state: runState });
1484
+ } catch (err) {
1485
+ console.error(`[@nightowlsdev/core] onRunStart threw for run ${ctx.runId}:`, err);
1486
+ }
1487
+ }
1450
1488
  const collector = this.opts.telemetry ? new SpanCollector(ctx.runId, () => Date.now(), "run", { agentSlug: ctx.agentSlug }) : null;
1451
1489
  let ts = 0;
1452
1490
  const emit = async (e) => {
@@ -1476,7 +1514,7 @@ var SwarmEngine = class {
1476
1514
  if (workflowDef) {
1477
1515
  const wfMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
1478
1516
  await emit(ev("swarm.message", base(ctx, ts++), { role: "user", text: wfMessage }));
1479
- yield* this.driveWorkflow(workflowDef, initialWorkflowState(workflowDef), ctx, { message: wfMessage, context: input.context }, {
1517
+ outcome = yield* this.driveWorkflow(workflowDef, initialWorkflowState(workflowDef), ctx, { message: wfMessage, context: input.context }, {
1480
1518
  gov,
1481
1519
  modelIdFor,
1482
1520
  streamed,
@@ -1486,7 +1524,11 @@ var SwarmEngine = class {
1486
1524
  turnUsage,
1487
1525
  nextTs: () => ts++,
1488
1526
  emit,
1489
- emitTurn
1527
+ emitTurn,
1528
+ segmentIndex: 0,
1529
+ // FR-004: a run segment starts at generation 0
1530
+ state: runState
1531
+ // FR-003: workflow steps' tools see the run's ctx.state
1490
1532
  });
1491
1533
  return;
1492
1534
  }
@@ -1514,23 +1556,14 @@ var SwarmEngine = class {
1514
1556
  const sp = payload.suspendPayload ?? {};
1515
1557
  await recordSuspend(this.opts.storage, ctx, followupId, toolCallId);
1516
1558
  await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
1517
- await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId }, genIndex: generationIndex + 1 });
1559
+ await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId }, genIndex: generationIndex + 1, state: runState.entries() });
1518
1560
  {
1519
1561
  const t = await emitTurn();
1520
1562
  if (t) yield t;
1521
1563
  }
1522
1564
  yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
1523
- yield await emit(
1524
- ev("swarm.question", base(ctx, ts++), {
1525
- followupId,
1526
- toolCallId,
1527
- to: sp.to ?? "user",
1528
- from: sp.asker || ctx.agentSlug,
1529
- // the agent that actually asked (a delegate), for UI attribution
1530
- prompt: sp.prompt ?? "",
1531
- field: sp.field
1532
- })
1533
- );
1565
+ yield await emit(clientActionOrQuestion(ctx, ts++, followupId, toolCallId, sp));
1566
+ outcome = "suspended";
1534
1567
  return;
1535
1568
  }
1536
1569
  if (part?.type === "error") {
@@ -1548,7 +1581,7 @@ var SwarmEngine = class {
1548
1581
  );
1549
1582
  return;
1550
1583
  }
1551
- for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage)) {
1584
+ for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage, 0)) {
1552
1585
  if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
1553
1586
  lastOutputSlug = e.agentSlug;
1554
1587
  if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
@@ -1565,7 +1598,9 @@ var SwarmEngine = class {
1565
1598
  await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
1566
1599
  await this.opts.storage.runs.saveSnapshot(ctx.runId, {
1567
1600
  capHit: { message: userMessage, spentUsd: gov.costUsd() },
1568
- genIndex: generationIndex + 1
1601
+ genIndex: generationIndex + 1,
1602
+ state: runState.entries()
1603
+ // FR-003: persist per-run state across the cap-ask boundary
1569
1604
  });
1570
1605
  {
1571
1606
  const t = await emitTurn();
@@ -1573,6 +1608,7 @@ var SwarmEngine = class {
1573
1608
  }
1574
1609
  yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
1575
1610
  yield await emit(ev("swarm.question", base(ctx, ts++), capQuestion(ctx, followupId, gov)));
1611
+ outcome = "suspended";
1576
1612
  return;
1577
1613
  }
1578
1614
  await this.opts.storage.runs.setStatus(ctx.runId, "failed");
@@ -1621,6 +1657,7 @@ var SwarmEngine = class {
1621
1657
  if (t) yield t;
1622
1658
  }
1623
1659
  yield await emit(ev("swarm.status", base(ctx, ts++), { state: "done" }));
1660
+ outcome = "done";
1624
1661
  }
1625
1662
  } catch (err) {
1626
1663
  const stage = err instanceof ReserveDenied ? "reserve" : "exception";
@@ -1635,6 +1672,13 @@ var SwarmEngine = class {
1635
1672
  }
1636
1673
  yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage, message: errMessage(err), retryable: false }));
1637
1674
  } finally {
1675
+ if (this.opts.onRunEnd) {
1676
+ try {
1677
+ await this.opts.onRunEnd(ctx, { state: runState, outcome });
1678
+ } catch (err) {
1679
+ console.error(`[@nightowlsdev/core] onRunEnd threw for run ${ctx.runId}:`, err);
1680
+ }
1681
+ }
1638
1682
  floorAbort.abort();
1639
1683
  await releaseFloor?.();
1640
1684
  await exportSpans(this.opts.telemetry, collector);
@@ -1651,7 +1695,7 @@ var SwarmEngine = class {
1651
1695
  const driver = new StepDriver(wf, {
1652
1696
  nextTs: m.nextTs,
1653
1697
  runAgentStep: (slug, message, genIndex) => this.streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m),
1654
- runToolStep: (toolName, args) => this.runWorkflowToolStep(toolName, args, ctx),
1698
+ runToolStep: (toolName, args) => this.runWorkflowToolStep(toolName, args, ctx, m.state),
1655
1699
  saveState: (rid, st) => this.opts.storage.runs.saveSnapshot(rid, { workflow: st })
1656
1700
  });
1657
1701
  const it = driver.drive(state, ctx, input);
@@ -1670,7 +1714,7 @@ var SwarmEngine = class {
1670
1714
  const t = await m.emitTurn();
1671
1715
  if (t) yield t;
1672
1716
  }
1673
- return;
1717
+ return "suspended";
1674
1718
  }
1675
1719
  if (outcome.kind === "failed") {
1676
1720
  await this.opts.storage.runs.setStatus(ctx.runId, "failed");
@@ -1679,7 +1723,7 @@ var SwarmEngine = class {
1679
1723
  if (t) yield t;
1680
1724
  }
1681
1725
  yield await m.emit(ev("swarm.run_failed", base(ctx, m.nextTs()), { stage: outcome.stage, message: outcome.message, retryable: true }));
1682
- return;
1726
+ return "failed";
1683
1727
  }
1684
1728
  await this.mirrorDelegations(ctx);
1685
1729
  await this.attributeRun(ctx);
@@ -1689,19 +1733,20 @@ var SwarmEngine = class {
1689
1733
  if (t) yield t;
1690
1734
  }
1691
1735
  yield await m.emit(ev("swarm.status", base(ctx, m.nextTs()), { state: "done" }));
1736
+ return "done";
1692
1737
  }
1693
1738
  /** A workflow `agent` step: stream `slug` with `message` (a per-step requestContext so it inherits the agent's
1694
1739
  * persona/tools/gate/model), reserving + metering through the caller's machinery, returning the final text. */
1695
1740
  async *streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m) {
1696
1741
  await this.guardGeneration({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: slug, modelId: m.modelIdFor(slug), generationIndex: genIndex, kind: "run" });
1697
1742
  const sctx = { ...ctx, agentSlug: slug };
1698
- const stepRc = this.requestContext(sctx);
1743
+ const stepRc = this.requestContext(sctx, m.state);
1699
1744
  if (this.opts.pageContext) attachPageContext(stepRc, input.context);
1700
1745
  const result = await this.agent().stream(message, { runId: ctx.runId, requestContext: stepRc, ...this.memoryOpts(sctx) });
1701
1746
  let text = "";
1702
1747
  for await (const part of result.fullStream) {
1703
1748
  if (part?.type === "step-finish") m.gov.step();
1704
- for (const e of mapChunk(part, sctx, m.gov, m.modelIdFor, m.nextTs, m.streamed, m.delegateBudgets, m.activity, m.gatesApproval, m.turnUsage)) {
1749
+ for (const e of mapChunk(part, sctx, m.gov, m.modelIdFor, m.nextTs, m.streamed, m.delegateBudgets, m.activity, m.gatesApproval, m.turnUsage, m.segmentIndex)) {
1705
1750
  if (e.type === "swarm.message") text += e.data.delta ?? e.data.text ?? "";
1706
1751
  yield e;
1707
1752
  }
@@ -1709,11 +1754,11 @@ var SwarmEngine = class {
1709
1754
  return { text };
1710
1755
  }
1711
1756
  /** A workflow `tool` step: run the gate-free tool body through `executeToolWithGate` (the engine-owned gate). */
1712
- async runWorkflowToolStep(toolName, args, ctx) {
1757
+ async runWorkflowToolStep(toolName, args, ctx, state) {
1713
1758
  const skill = this.opts.resolveSkill?.(toolName);
1714
1759
  const exec = skill ? getToolExecutor(skill) : void 0;
1715
1760
  if (!skill || !exec) return { ok: false, error: `unknown tool "${toolName}"` };
1716
- const toolCtx = { tenantId: ctx.tenantId, userId: ctx.userId, runId: ctx.runId, secrets: bindSecrets(this.opts.secrets, ctx) };
1761
+ const toolCtx = { tenantId: ctx.tenantId, userId: ctx.userId, runId: ctx.runId, secrets: bindSecrets(this.opts.secrets, ctx), state };
1717
1762
  return executeToolWithGate({
1718
1763
  ev: toolPreCallEvent({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, toolName, origin: skill.origin ?? "first-party", needsApproval: this.gatesApproval(toolName), args }),
1719
1764
  gate: this.toolGate,
@@ -1723,6 +1768,7 @@ var SwarmEngine = class {
1723
1768
  async *resume(args, ctx) {
1724
1769
  const snap = await this.opts.storage.runs.loadSnapshot(ctx.tenantId, args.runId);
1725
1770
  if (!snap) throw new Error(`no suspended run: ${args.runId}`);
1771
+ const resumedState = createRunState(snap.state ?? void 0);
1726
1772
  const generationIndex = typeof snap.genIndex === "number" ? snap.genIndex : 1;
1727
1773
  const capHit = snap.capHit;
1728
1774
  await this.opts.storage.markFollowupAnswered?.(args.followupId, ctx.tenantId);
@@ -1756,6 +1802,7 @@ var SwarmEngine = class {
1756
1802
  turnEmitted = true;
1757
1803
  return emit(turnUsageEvent(rctx, ts++, turnUsage, generationIndex));
1758
1804
  };
1805
+ let resumeOutcome = "failed";
1759
1806
  try {
1760
1807
  if (!releaseFloor) {
1761
1808
  const held = await this.floor.holder(floorKey);
@@ -1787,7 +1834,7 @@ var SwarmEngine = class {
1787
1834
  wfState.outputs[wfState.pending.stepId] = args.answer;
1788
1835
  wfState.pending = void 0;
1789
1836
  }
1790
- yield* this.driveWorkflow(wf, wfState, rctx, { message: "", context: args.context }, {
1837
+ resumeOutcome = yield* this.driveWorkflow(wf, wfState, rctx, { message: "", context: args.context }, {
1791
1838
  gov,
1792
1839
  modelIdFor,
1793
1840
  streamed,
@@ -1797,7 +1844,11 @@ var SwarmEngine = class {
1797
1844
  turnUsage,
1798
1845
  nextTs: () => ts++,
1799
1846
  emit,
1800
- emitTurn
1847
+ emitTurn,
1848
+ segmentIndex: generationIndex,
1849
+ // FR-004: a resume segment starts at the snapshot's genIndex
1850
+ state: resumedState
1851
+ // FR-003: workflow steps' tools see the restored ctx.state
1801
1852
  });
1802
1853
  return;
1803
1854
  }
@@ -1816,7 +1867,7 @@ var SwarmEngine = class {
1816
1867
  );
1817
1868
  return;
1818
1869
  }
1819
- const rc = this.requestContext({ ...ctx, runId: args.runId });
1870
+ const rc = this.requestContext({ ...ctx, runId: args.runId }, resumedState);
1820
1871
  if (this.opts.pageContext) attachPageContext(rc, args.context);
1821
1872
  await this.guardGeneration({ runId: args.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, modelId, generationIndex, kind: "resume" });
1822
1873
  let resumeNudges = 0;
@@ -1848,22 +1899,14 @@ var SwarmEngine = class {
1848
1899
  const sp = payload.suspendPayload ?? {};
1849
1900
  await recordSuspend(this.opts.storage, rctx, followupId, toolCallId);
1850
1901
  await this.opts.storage.runs.setStatus(args.runId, "suspended");
1851
- await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId }, genIndex: generationIndex + 1 });
1902
+ await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId }, genIndex: generationIndex + 1, state: resumedState.entries() });
1852
1903
  {
1853
1904
  const t = await emitTurn();
1854
1905
  if (t) yield t;
1855
1906
  }
1856
1907
  yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
1857
- yield await emit(
1858
- ev("swarm.question", base(rctx, ts++), {
1859
- followupId,
1860
- toolCallId,
1861
- to: sp.to ?? "user",
1862
- from: sp.asker || rctx.agentSlug,
1863
- prompt: sp.prompt ?? "",
1864
- field: sp.field
1865
- })
1866
- );
1908
+ yield await emit(clientActionOrQuestion(rctx, ts++, followupId, toolCallId, sp));
1909
+ resumeOutcome = "suspended";
1867
1910
  return;
1868
1911
  }
1869
1912
  if (part?.type === "error") {
@@ -1882,7 +1925,7 @@ var SwarmEngine = class {
1882
1925
  return;
1883
1926
  }
1884
1927
  collectSpans(collector, part, modelId, gov);
1885
- for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage)) {
1928
+ for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage, generationIndex)) {
1886
1929
  if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
1887
1930
  lastOutputSlug = e.agentSlug;
1888
1931
  if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
@@ -1898,7 +1941,9 @@ var SwarmEngine = class {
1898
1941
  await this.opts.storage.runs.setStatus(args.runId, "suspended");
1899
1942
  await this.opts.storage.runs.saveSnapshot(args.runId, {
1900
1943
  capHit: { message: capHit.message, spentUsd: gov.costUsd() },
1901
- genIndex: generationIndex + 1
1944
+ genIndex: generationIndex + 1,
1945
+ state: resumedState.entries()
1946
+ // FR-003: persist per-run state across the cap-ask boundary
1902
1947
  });
1903
1948
  {
1904
1949
  const t = await emitTurn();
@@ -1906,6 +1951,7 @@ var SwarmEngine = class {
1906
1951
  }
1907
1952
  yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
1908
1953
  yield await emit(ev("swarm.question", base(rctx, ts++), capQuestion(rctx, followupId, gov)));
1954
+ resumeOutcome = "suspended";
1909
1955
  return;
1910
1956
  }
1911
1957
  await this.opts.storage.runs.setStatus(args.runId, "failed");
@@ -1952,6 +1998,7 @@ var SwarmEngine = class {
1952
1998
  if (t) yield t;
1953
1999
  }
1954
2000
  yield await emit(ev("swarm.status", base(rctx, ts++), { state: "done" }));
2001
+ resumeOutcome = "done";
1955
2002
  }
1956
2003
  } catch (err) {
1957
2004
  const stage = err instanceof ReserveDenied ? "reserve" : "exception";
@@ -1966,6 +2013,13 @@ var SwarmEngine = class {
1966
2013
  }
1967
2014
  yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage, message: errMessage(err), retryable: false }));
1968
2015
  } finally {
2016
+ if (this.opts.onRunEnd) {
2017
+ try {
2018
+ await this.opts.onRunEnd(ctx, { state: resumedState, outcome: resumeOutcome });
2019
+ } catch (err) {
2020
+ console.error(`[@nightowlsdev/core] onRunEnd threw for resume ${args.runId}:`, err);
2021
+ }
2022
+ }
1969
2023
  floorAbort.abort();
1970
2024
  await releaseFloor?.();
1971
2025
  await exportSpans(this.opts.telemetry, collector);
@@ -1985,6 +2039,26 @@ function errMessage(err) {
1985
2039
  function base(ctx, ts) {
1986
2040
  return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts };
1987
2041
  }
2042
+ function clientActionOrQuestion(ctx, ts, followupId, toolCallId, sp) {
2043
+ if (sp.clientAction) {
2044
+ return ev("swarm.client_action", base(ctx, ts), {
2045
+ followupId,
2046
+ toolCallId,
2047
+ tool: sp.clientAction.tool,
2048
+ input: sp.clientAction.input,
2049
+ needsApproval: sp.clientAction.needsApproval ?? false,
2050
+ from: sp.asker || ctx.agentSlug
2051
+ });
2052
+ }
2053
+ return ev("swarm.question", base(ctx, ts), {
2054
+ followupId,
2055
+ toolCallId,
2056
+ to: sp.to ?? "user",
2057
+ from: sp.asker || ctx.agentSlug,
2058
+ prompt: sp.prompt ?? "",
2059
+ field: sp.field
2060
+ });
2061
+ }
1988
2062
  var CAP_FOLLOWUP_SUFFIX = "cap";
1989
2063
  function capQuestion(ctx, followupId, gov) {
1990
2064
  const spent = gov.costUsd();
@@ -2080,7 +2154,7 @@ async function recordSuspend(storage, ctx, followupId, toolCallId) {
2080
2154
  }
2081
2155
  await storage.recordSuspend?.(ctx.runId, ctx.tenantId, followupId, toolCallId);
2082
2156
  }
2083
- function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage) {
2157
+ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage, segmentIndex) {
2084
2158
  const p = part.payload ?? {};
2085
2159
  const modelId = modelIdFor(ctx.agentSlug);
2086
2160
  const act = (slug) => {
@@ -2150,8 +2224,9 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets,
2150
2224
  gov.addUsage(modelId, u);
2151
2225
  delegateBudgets?.addUsage(ctx.agentSlug, modelId, u);
2152
2226
  const cost = gov.costOf(modelId, u);
2227
+ const generationId = `${ctx.runId}:${segmentIndex}:${turnUsage.length}`;
2153
2228
  turnUsage.push({ slug: ctx.agentSlug, breakdown: u, cost });
2154
- return [ev("swarm.usage", base(ctx, nextTs()), { slug: ctx.agentSlug, modelId, breakdown: u, cost })];
2229
+ return [ev("swarm.usage", base(ctx, nextTs()), { slug: ctx.agentSlug, modelId, breakdown: u, cost, generationId })];
2155
2230
  }
2156
2231
  return [];
2157
2232
  }
@@ -2162,7 +2237,7 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets,
2162
2237
  const inner = p.output;
2163
2238
  if (!inner || typeof inner.type !== "string") return [];
2164
2239
  if (inner.type === "text-delta") streamed.add(p.toolCallId);
2165
- return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage);
2240
+ return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage, segmentIndex);
2166
2241
  }
2167
2242
  case "tool-error": {
2168
2243
  const name = p.toolName ?? "";
@@ -2359,7 +2434,11 @@ function defineTool(spec) {
2359
2434
  tenantId,
2360
2435
  userId,
2361
2436
  runId,
2362
- secrets: bindSecrets(resolver, scopedCtx)
2437
+ secrets: bindSecrets(resolver, scopedCtx),
2438
+ // FR-003: the per-run state handle the engine put on the rc (same object across the run's tool calls +
2439
+ // delegated sub-agents). Absent on a raw test stream built without the engine — then `ctx.state` is
2440
+ // undefined, unchanged from prior behaviour.
2441
+ state: rc?.get?.(RUN_STATE_KEY)
2363
2442
  };
2364
2443
  const run = () => spec.execute(inputData, ctx);
2365
2444
  const agentCtx = context?.agent;
@@ -2401,6 +2480,48 @@ function deriveAsker(agentCtx, rc) {
2401
2480
  if (agentId.startsWith("swarm-sub-")) return agentId.slice("swarm-sub-".length);
2402
2481
  return rc?.get?.("agentSlug") ?? "";
2403
2482
  }
2483
+ var CLIENT_ACTION_SUSPEND_SCHEMA = z4.object({
2484
+ clientAction: z4.object({ tool: z4.string(), input: z4.any(), needsApproval: z4.boolean().optional() }),
2485
+ asker: z4.string().optional()
2486
+ });
2487
+ var CLIENT_ACTION_RESUME_SCHEMA = z4.object({ answer: z4.any() });
2488
+ var ClientToolError = class extends Error {
2489
+ constructor(toolName, reason) {
2490
+ super(reason ? `client tool ${toolName} failed: ${reason}` : `client tool ${toolName} failed`);
2491
+ this.name = "ClientToolError";
2492
+ }
2493
+ };
2494
+ function defineClientTool(spec) {
2495
+ const needsApproval = spec.needsApproval ?? false;
2496
+ const mastraTool = createTool4({
2497
+ id: spec.name,
2498
+ description: spec.description ?? spec.name,
2499
+ inputSchema: spec.inputSchema,
2500
+ outputSchema: spec.outputSchema,
2501
+ suspendSchema: CLIENT_ACTION_SUSPEND_SCHEMA,
2502
+ resumeSchema: CLIENT_ACTION_RESUME_SCHEMA,
2503
+ execute: async (inputData, context) => {
2504
+ const rc = context?.requestContext;
2505
+ const agentCtx = context?.agent;
2506
+ if (agentCtx?.resumeData !== void 0 && agentCtx.resumeData !== null) {
2507
+ const answer = agentCtx.resumeData.answer;
2508
+ if (answer && typeof answer === "object" && "error" in answer && answer.error) {
2509
+ throw new ClientToolError(spec.name, String(answer.error));
2510
+ }
2511
+ return answer && typeof answer === "object" && "output" in answer ? answer.output : answer;
2512
+ }
2513
+ if (typeof agentCtx?.suspend !== "function") {
2514
+ throw new ClientToolError(spec.name, "client tools require an agent-driven run (no server execute)");
2515
+ }
2516
+ const asker = deriveAsker(agentCtx, rc);
2517
+ await agentCtx.suspend({ clientAction: { tool: spec.name, input: inputData, needsApproval }, asker });
2518
+ throw new ClientToolError(spec.name, "awaiting client action");
2519
+ }
2520
+ });
2521
+ const handle = { name: spec.name, needsApproval, origin: "first-party" };
2522
+ MASTRA.set(handle, mastraTool);
2523
+ return handle;
2524
+ }
2404
2525
  function __getMastraTool(t) {
2405
2526
  return MASTRA.get(t);
2406
2527
  }
@@ -2727,7 +2848,10 @@ function defineSwarm(cfg) {
2727
2848
  secrets: cfg.secrets,
2728
2849
  // SP3: best-effort per-event observer (metering debit/settle) — transport-agnostic, fired in run + resume.
2729
2850
  onEvent: cfg.onEvent,
2730
- verifyCompletion: cfg.verifyCompletion
2851
+ verifyCompletion: cfg.verifyCompletion,
2852
+ // FR-003: per-run lifecycle hooks (seed `ctx.state` at run start, drain at run end).
2853
+ onRunStart: cfg.onRunStart,
2854
+ onRunEnd: cfg.onRunEnd
2731
2855
  });
2732
2856
  return { engine };
2733
2857
  }
@@ -2751,6 +2875,8 @@ var InMemoryStorage = class {
2751
2875
  heads = /* @__PURE__ */ new Map();
2752
2876
  // key: tenantId:slug -> version
2753
2877
  pads = /* @__PURE__ */ new Map();
2878
+ threadRows = /* @__PURE__ */ new Map();
2879
+ // FR-009
2754
2880
  seedAgent(v, tenantId = "default") {
2755
2881
  this.agentRows.set(`${tenantId}:${v.slug}:${v.version}`, v);
2756
2882
  this.heads.set(`${tenantId}:${v.slug}`, v.version);
@@ -2834,6 +2960,18 @@ var InMemoryStorage = class {
2834
2960
  },
2835
2961
  getWaitpoint: async (followupId) => this.waitpoints.get(followupId) ?? null
2836
2962
  };
2963
+ // FR-009: idempotent thread-row creation. The dev store has no FK, so `messages.append` never threw `unknown
2964
+ // thread` here — this implements the contract so the engine's run-start ensure works against the in-memory store
2965
+ // too, and a host can read back the recorded thread (getThread) in tests.
2966
+ threads = {
2967
+ ensure: async ({ id, orgId, userId, projectId }) => {
2968
+ if (!this.threadRows.has(id)) this.threadRows.set(id, { id, orgId, userId, projectId });
2969
+ }
2970
+ };
2971
+ /** Test/host helper: read a recorded thread row. */
2972
+ getThread(id) {
2973
+ return this.threadRows.get(id);
2974
+ }
2837
2975
  messages = {
2838
2976
  append: async (m) => {
2839
2977
  this.msgs.push(m);
@@ -2873,6 +3011,69 @@ var InMemoryStorage = class {
2873
3011
  };
2874
3012
  };
2875
3013
 
3014
+ // src/run-agent.ts
3015
+ function uid() {
3016
+ return globalThis.crypto?.randomUUID?.() ?? `id-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
3017
+ }
3018
+ async function drainTrajectory(stream) {
3019
+ const events = [];
3020
+ let output = "";
3021
+ for await (const e of stream) {
3022
+ events.push(e);
3023
+ if (e.type === "swarm.message") {
3024
+ const d = e.data;
3025
+ if (d.role === "assistant") output += d.delta ?? d.text ?? "";
3026
+ }
3027
+ }
3028
+ return { events, output };
3029
+ }
3030
+ async function runToTrajectory(target, input, ctx) {
3031
+ const engine = "engine" in target ? target.engine : target;
3032
+ const runInput = typeof input === "string" ? { message: input } : input;
3033
+ const full = ephemeralCtx(ctx?.agentSlug ?? "agent", ctx);
3034
+ return drainTrajectory(engine.run(runInput, full));
3035
+ }
3036
+ function ephemeralCtx(agentSlug, over) {
3037
+ return {
3038
+ tenantId: over?.tenantId ?? "default",
3039
+ userId: over?.userId ?? "local",
3040
+ agentSlug: over?.agentSlug ?? agentSlug,
3041
+ runId: over?.runId ?? uid(),
3042
+ threadId: over?.threadId ?? uid(),
3043
+ ...over?.agentVersion !== void 0 ? { agentVersion: over.agentVersion } : {}
3044
+ };
3045
+ }
3046
+ function buildSingleAgentSwarm(def, opts) {
3047
+ const t = opts.models?.tier;
3048
+ const tierAllow = t ? [t.tiers.swift, ...t.tiers.genius ? [t.tiers.genius] : []] : [];
3049
+ const allow = opts.models?.allow ?? [def.head.modelId, ...tierAllow];
3050
+ const storage = opts.storage ?? new InMemoryStorage();
3051
+ const cfg = {
3052
+ storage,
3053
+ agents: [def],
3054
+ models: { allow, ...opts.models?.tier ? { tier: opts.models.tier } : {} },
3055
+ modelFactory: opts.modelFactory,
3056
+ cost: { maxSteps: 50, maxCostUsd: 10, ...opts.cost },
3057
+ ...opts.telemetry !== void 0 ? { telemetry: opts.telemetry } : {},
3058
+ ...opts.memory !== void 0 ? { memory: opts.memory } : {},
3059
+ ...opts.hooks !== void 0 ? { hooks: opts.hooks } : {},
3060
+ ...opts.toolApproval !== void 0 ? { toolApproval: opts.toolApproval } : {},
3061
+ ...opts.secrets !== void 0 ? { secrets: opts.secrets } : {},
3062
+ ...opts.onEvent !== void 0 ? { onEvent: opts.onEvent } : {},
3063
+ ...opts.onRunStart !== void 0 ? { onRunStart: opts.onRunStart } : {},
3064
+ ...opts.onRunEnd !== void 0 ? { onRunEnd: opts.onRunEnd } : {},
3065
+ ...opts.pageContext !== void 0 ? { pageContext: opts.pageContext } : {},
3066
+ ...opts.mastraStore !== void 0 ? { mastraStore: opts.mastraStore } : {}
3067
+ };
3068
+ return defineSwarm(cfg);
3069
+ }
3070
+ async function runAgent(def, input, opts) {
3071
+ const swarm = buildSingleAgentSwarm(def, opts);
3072
+ const runInput = typeof input === "string" ? { message: input } : input;
3073
+ const ctx = ephemeralCtx(def.head.slug, { ...opts.ctx, agentSlug: def.head.slug });
3074
+ return drainTrajectory(swarm.engine.run(runInput, ctx));
3075
+ }
3076
+
2876
3077
  // src/auth.ts
2877
3078
  var customAuth = (fn) => ({ authenticate: fn });
2878
3079
 
@@ -2912,6 +3113,7 @@ export {
2912
3113
  ASK_TOOL_NAME,
2913
3114
  AgentMutationForbidden,
2914
3115
  CapturingExporter,
3116
+ ClientToolError,
2915
3117
  CostGovernor,
2916
3118
  DEFAULT_READ_ONLY_TOOLS,
2917
3119
  DelegateBudgets,
@@ -2930,6 +3132,7 @@ export {
2930
3132
  allowListModelProvider,
2931
3133
  ask2 as ask,
2932
3134
  assertActorMayMutateDefinition,
3135
+ buildSingleAgentSwarm,
2933
3136
  buildSkillResolver,
2934
3137
  composePolicyPrompt,
2935
3138
  composeSystemPrompt,
@@ -2937,11 +3140,13 @@ export {
2937
3140
  containerFloor,
2938
3141
  createHookDispatcher2 as createHookDispatcher,
2939
3142
  createInMemoryRateLimitStore,
3143
+ createRunState,
2940
3144
  customAuth,
2941
3145
  customTelemetry,
2942
3146
  decideFixedWindow,
2943
3147
  defineAgent,
2944
3148
  defineBundle,
3149
+ defineClientTool,
2945
3150
  defineHook,
2946
3151
  defineRule,
2947
3152
  defineSkill,
@@ -2949,6 +3154,7 @@ export {
2949
3154
  defineTool,
2950
3155
  defineWorkflow,
2951
3156
  deny2 as deny,
3157
+ drainTrajectory,
2952
3158
  ev,
2953
3159
  isEvent,
2954
3160
  isTierSentinel,
@@ -2957,6 +3163,8 @@ export {
2957
3163
  rateConfig,
2958
3164
  resolveTelemetry,
2959
3165
  resolveTier,
3166
+ runAgent,
3167
+ runToTrajectory,
2960
3168
  sumBreakdowns,
2961
3169
  sumTurnUsage,
2962
3170
  tierModelId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightowlsdev/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -47,8 +47,8 @@
47
47
  "tsup": "8.5.1",
48
48
  "typescript": "6.0.3",
49
49
  "vitest": "^3.2.0",
50
- "@nightowlsdev/eslint-config": "0.0.0",
51
- "@nightowlsdev/tsconfig": "0.0.0"
50
+ "@nightowlsdev/tsconfig": "0.0.0",
51
+ "@nightowlsdev/eslint-config": "0.0.0"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsup",