@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.cjs CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  ASK_TOOL_NAME: () => ASK_TOOL_NAME,
26
26
  AgentMutationForbidden: () => AgentMutationForbidden,
27
27
  CapturingExporter: () => CapturingExporter,
28
+ ClientToolError: () => ClientToolError,
28
29
  CostGovernor: () => CostGovernor,
29
30
  DEFAULT_READ_ONLY_TOOLS: () => import_hooks4.DEFAULT_READ_ONLY_TOOLS,
30
31
  DelegateBudgets: () => DelegateBudgets,
@@ -43,6 +44,7 @@ __export(index_exports, {
43
44
  allowListModelProvider: () => allowListModelProvider,
44
45
  ask: () => import_hooks4.ask,
45
46
  assertActorMayMutateDefinition: () => assertActorMayMutateDefinition,
47
+ buildSingleAgentSwarm: () => buildSingleAgentSwarm,
46
48
  buildSkillResolver: () => buildSkillResolver,
47
49
  composePolicyPrompt: () => composePolicyPrompt,
48
50
  composeSystemPrompt: () => composeSystemPrompt,
@@ -50,11 +52,13 @@ __export(index_exports, {
50
52
  containerFloor: () => containerFloor,
51
53
  createHookDispatcher: () => import_hooks4.createHookDispatcher,
52
54
  createInMemoryRateLimitStore: () => createInMemoryRateLimitStore,
55
+ createRunState: () => createRunState,
53
56
  customAuth: () => customAuth,
54
57
  customTelemetry: () => customTelemetry,
55
58
  decideFixedWindow: () => decideFixedWindow,
56
59
  defineAgent: () => defineAgent,
57
60
  defineBundle: () => defineBundle,
61
+ defineClientTool: () => defineClientTool,
58
62
  defineHook: () => import_hooks4.defineHook,
59
63
  defineRule: () => defineRule,
60
64
  defineSkill: () => defineSkill,
@@ -62,6 +66,7 @@ __export(index_exports, {
62
66
  defineTool: () => defineTool,
63
67
  defineWorkflow: () => defineWorkflow,
64
68
  deny: () => import_hooks4.deny,
69
+ drainTrajectory: () => drainTrajectory,
65
70
  ev: () => ev,
66
71
  isEvent: () => isEvent,
67
72
  isTierSentinel: () => isTierSentinel,
@@ -70,6 +75,8 @@ __export(index_exports, {
70
75
  rateConfig: () => rateConfig,
71
76
  resolveTelemetry: () => resolveTelemetry,
72
77
  resolveTier: () => resolveTier,
78
+ runAgent: () => runAgent,
79
+ runToTrajectory: () => runToTrajectory,
73
80
  sumBreakdowns: () => sumBreakdowns,
74
81
  sumTurnUsage: () => sumTurnUsage,
75
82
  tierModelId: () => tierModelId,
@@ -188,6 +195,31 @@ function bindSecrets(resolver, ctx) {
188
195
  };
189
196
  }
190
197
 
198
+ // src/run-state.ts
199
+ var RUN_STATE_KEY = "__nightowls_run_state__";
200
+ function snapshotCopy(store) {
201
+ const plain = Object.fromEntries(store);
202
+ try {
203
+ return (globalThis.structuredClone ?? ((v) => JSON.parse(JSON.stringify(v))))(plain);
204
+ } catch {
205
+ try {
206
+ return JSON.parse(JSON.stringify(plain));
207
+ } catch {
208
+ return plain;
209
+ }
210
+ }
211
+ }
212
+ function createRunState(seed) {
213
+ const m = new Map(seed ? Object.entries(seed) : void 0);
214
+ return {
215
+ get: (key) => m.get(key),
216
+ set: (key, value) => void m.set(key, value),
217
+ has: (key) => m.has(key),
218
+ delete: (key) => m.delete(key),
219
+ entries: () => snapshotCopy(m)
220
+ };
221
+ }
222
+
191
223
  // src/step-driver.ts
192
224
  function initialWorkflowState(wf) {
193
225
  return { workflow: wf.name, cursor: wf.start, outputs: {}, generationIndex: 0 };
@@ -1231,13 +1263,14 @@ var SwarmEngine = class {
1231
1263
  agent() {
1232
1264
  return this.mastra.getAgent(AGENT_KEY);
1233
1265
  }
1234
- requestContext(ctx) {
1266
+ requestContext(ctx, state) {
1235
1267
  const rc = new import_request_context.RequestContext();
1236
1268
  for (const [k, v] of Object.entries(ctx)) {
1237
1269
  if (v !== void 0) rc.set(k, v);
1238
1270
  }
1239
1271
  rc.set(TOOL_GATE_KEY, this.toolGate);
1240
1272
  if (this.opts.secrets) rc.set(SECRET_RESOLVER_KEY, this.opts.secrets);
1273
+ if (state) rc.set(RUN_STATE_KEY, state);
1241
1274
  return rc;
1242
1275
  }
1243
1276
  /**
@@ -1507,6 +1540,9 @@ var SwarmEngine = class {
1507
1540
  const modelId = this.priceModelId((await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown", ctx.tenantId, ctx.agentSlug);
1508
1541
  const workflowDef = input.workflow ? this.opts.workflows?.find((w) => w.name === input.workflow) : this.opts.agentWorkflows?.[ctx.agentSlug];
1509
1542
  if (input.workflow && !workflowDef) throw new Error(`unknown workflow: ${input.workflow}`);
1543
+ if (this.opts.storage.threads) {
1544
+ await this.opts.storage.threads.ensure({ id: ctx.threadId, orgId: ctx.tenantId, userId: ctx.userId });
1545
+ }
1510
1546
  await this.opts.storage.runs.create({
1511
1547
  runId: ctx.runId,
1512
1548
  tenantId: ctx.tenantId,
@@ -1524,8 +1560,17 @@ var SwarmEngine = class {
1524
1560
  const streamed = /* @__PURE__ */ new Set();
1525
1561
  const activity = /* @__PURE__ */ new Map();
1526
1562
  const turnUsage = [];
1527
- const rc = this.requestContext(ctx);
1563
+ const runState = createRunState();
1564
+ const rc = this.requestContext(ctx, runState);
1528
1565
  if (this.opts.pageContext) attachPageContext(rc, input.context);
1566
+ let outcome = "failed";
1567
+ if (this.opts.onRunStart) {
1568
+ try {
1569
+ await this.opts.onRunStart(ctx, { input, state: runState });
1570
+ } catch (err) {
1571
+ console.error(`[@nightowlsdev/core] onRunStart threw for run ${ctx.runId}:`, err);
1572
+ }
1573
+ }
1529
1574
  const collector = this.opts.telemetry ? new SpanCollector(ctx.runId, () => Date.now(), "run", { agentSlug: ctx.agentSlug }) : null;
1530
1575
  let ts = 0;
1531
1576
  const emit = async (e) => {
@@ -1555,7 +1600,7 @@ var SwarmEngine = class {
1555
1600
  if (workflowDef) {
1556
1601
  const wfMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
1557
1602
  await emit(ev("swarm.message", base(ctx, ts++), { role: "user", text: wfMessage }));
1558
- yield* this.driveWorkflow(workflowDef, initialWorkflowState(workflowDef), ctx, { message: wfMessage, context: input.context }, {
1603
+ outcome = yield* this.driveWorkflow(workflowDef, initialWorkflowState(workflowDef), ctx, { message: wfMessage, context: input.context }, {
1559
1604
  gov,
1560
1605
  modelIdFor,
1561
1606
  streamed,
@@ -1565,7 +1610,11 @@ var SwarmEngine = class {
1565
1610
  turnUsage,
1566
1611
  nextTs: () => ts++,
1567
1612
  emit,
1568
- emitTurn
1613
+ emitTurn,
1614
+ segmentIndex: 0,
1615
+ // FR-004: a run segment starts at generation 0
1616
+ state: runState
1617
+ // FR-003: workflow steps' tools see the run's ctx.state
1569
1618
  });
1570
1619
  return;
1571
1620
  }
@@ -1593,23 +1642,14 @@ var SwarmEngine = class {
1593
1642
  const sp = payload.suspendPayload ?? {};
1594
1643
  await recordSuspend(this.opts.storage, ctx, followupId, toolCallId);
1595
1644
  await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
1596
- await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId }, genIndex: generationIndex + 1 });
1645
+ await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId }, genIndex: generationIndex + 1, state: runState.entries() });
1597
1646
  {
1598
1647
  const t = await emitTurn();
1599
1648
  if (t) yield t;
1600
1649
  }
1601
1650
  yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
1602
- yield await emit(
1603
- ev("swarm.question", base(ctx, ts++), {
1604
- followupId,
1605
- toolCallId,
1606
- to: sp.to ?? "user",
1607
- from: sp.asker || ctx.agentSlug,
1608
- // the agent that actually asked (a delegate), for UI attribution
1609
- prompt: sp.prompt ?? "",
1610
- field: sp.field
1611
- })
1612
- );
1651
+ yield await emit(clientActionOrQuestion(ctx, ts++, followupId, toolCallId, sp));
1652
+ outcome = "suspended";
1613
1653
  return;
1614
1654
  }
1615
1655
  if (part?.type === "error") {
@@ -1627,7 +1667,7 @@ var SwarmEngine = class {
1627
1667
  );
1628
1668
  return;
1629
1669
  }
1630
- for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage)) {
1670
+ for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage, 0)) {
1631
1671
  if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
1632
1672
  lastOutputSlug = e.agentSlug;
1633
1673
  if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
@@ -1644,7 +1684,9 @@ var SwarmEngine = class {
1644
1684
  await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
1645
1685
  await this.opts.storage.runs.saveSnapshot(ctx.runId, {
1646
1686
  capHit: { message: userMessage, spentUsd: gov.costUsd() },
1647
- genIndex: generationIndex + 1
1687
+ genIndex: generationIndex + 1,
1688
+ state: runState.entries()
1689
+ // FR-003: persist per-run state across the cap-ask boundary
1648
1690
  });
1649
1691
  {
1650
1692
  const t = await emitTurn();
@@ -1652,6 +1694,7 @@ var SwarmEngine = class {
1652
1694
  }
1653
1695
  yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
1654
1696
  yield await emit(ev("swarm.question", base(ctx, ts++), capQuestion(ctx, followupId, gov)));
1697
+ outcome = "suspended";
1655
1698
  return;
1656
1699
  }
1657
1700
  await this.opts.storage.runs.setStatus(ctx.runId, "failed");
@@ -1700,6 +1743,7 @@ var SwarmEngine = class {
1700
1743
  if (t) yield t;
1701
1744
  }
1702
1745
  yield await emit(ev("swarm.status", base(ctx, ts++), { state: "done" }));
1746
+ outcome = "done";
1703
1747
  }
1704
1748
  } catch (err) {
1705
1749
  const stage = err instanceof ReserveDenied ? "reserve" : "exception";
@@ -1714,6 +1758,13 @@ var SwarmEngine = class {
1714
1758
  }
1715
1759
  yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage, message: errMessage(err), retryable: false }));
1716
1760
  } finally {
1761
+ if (this.opts.onRunEnd) {
1762
+ try {
1763
+ await this.opts.onRunEnd(ctx, { state: runState, outcome });
1764
+ } catch (err) {
1765
+ console.error(`[@nightowlsdev/core] onRunEnd threw for run ${ctx.runId}:`, err);
1766
+ }
1767
+ }
1717
1768
  floorAbort.abort();
1718
1769
  await releaseFloor?.();
1719
1770
  await exportSpans(this.opts.telemetry, collector);
@@ -1730,7 +1781,7 @@ var SwarmEngine = class {
1730
1781
  const driver = new StepDriver(wf, {
1731
1782
  nextTs: m.nextTs,
1732
1783
  runAgentStep: (slug, message, genIndex) => this.streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m),
1733
- runToolStep: (toolName, args) => this.runWorkflowToolStep(toolName, args, ctx),
1784
+ runToolStep: (toolName, args) => this.runWorkflowToolStep(toolName, args, ctx, m.state),
1734
1785
  saveState: (rid, st) => this.opts.storage.runs.saveSnapshot(rid, { workflow: st })
1735
1786
  });
1736
1787
  const it = driver.drive(state, ctx, input);
@@ -1749,7 +1800,7 @@ var SwarmEngine = class {
1749
1800
  const t = await m.emitTurn();
1750
1801
  if (t) yield t;
1751
1802
  }
1752
- return;
1803
+ return "suspended";
1753
1804
  }
1754
1805
  if (outcome.kind === "failed") {
1755
1806
  await this.opts.storage.runs.setStatus(ctx.runId, "failed");
@@ -1758,7 +1809,7 @@ var SwarmEngine = class {
1758
1809
  if (t) yield t;
1759
1810
  }
1760
1811
  yield await m.emit(ev("swarm.run_failed", base(ctx, m.nextTs()), { stage: outcome.stage, message: outcome.message, retryable: true }));
1761
- return;
1812
+ return "failed";
1762
1813
  }
1763
1814
  await this.mirrorDelegations(ctx);
1764
1815
  await this.attributeRun(ctx);
@@ -1768,19 +1819,20 @@ var SwarmEngine = class {
1768
1819
  if (t) yield t;
1769
1820
  }
1770
1821
  yield await m.emit(ev("swarm.status", base(ctx, m.nextTs()), { state: "done" }));
1822
+ return "done";
1771
1823
  }
1772
1824
  /** A workflow `agent` step: stream `slug` with `message` (a per-step requestContext so it inherits the agent's
1773
1825
  * persona/tools/gate/model), reserving + metering through the caller's machinery, returning the final text. */
1774
1826
  async *streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m) {
1775
1827
  await this.guardGeneration({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: slug, modelId: m.modelIdFor(slug), generationIndex: genIndex, kind: "run" });
1776
1828
  const sctx = { ...ctx, agentSlug: slug };
1777
- const stepRc = this.requestContext(sctx);
1829
+ const stepRc = this.requestContext(sctx, m.state);
1778
1830
  if (this.opts.pageContext) attachPageContext(stepRc, input.context);
1779
1831
  const result = await this.agent().stream(message, { runId: ctx.runId, requestContext: stepRc, ...this.memoryOpts(sctx) });
1780
1832
  let text = "";
1781
1833
  for await (const part of result.fullStream) {
1782
1834
  if (part?.type === "step-finish") m.gov.step();
1783
- for (const e of mapChunk(part, sctx, m.gov, m.modelIdFor, m.nextTs, m.streamed, m.delegateBudgets, m.activity, m.gatesApproval, m.turnUsage)) {
1835
+ 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)) {
1784
1836
  if (e.type === "swarm.message") text += e.data.delta ?? e.data.text ?? "";
1785
1837
  yield e;
1786
1838
  }
@@ -1788,11 +1840,11 @@ var SwarmEngine = class {
1788
1840
  return { text };
1789
1841
  }
1790
1842
  /** A workflow `tool` step: run the gate-free tool body through `executeToolWithGate` (the engine-owned gate). */
1791
- async runWorkflowToolStep(toolName, args, ctx) {
1843
+ async runWorkflowToolStep(toolName, args, ctx, state) {
1792
1844
  const skill = this.opts.resolveSkill?.(toolName);
1793
1845
  const exec = skill ? getToolExecutor(skill) : void 0;
1794
1846
  if (!skill || !exec) return { ok: false, error: `unknown tool "${toolName}"` };
1795
- const toolCtx = { tenantId: ctx.tenantId, userId: ctx.userId, runId: ctx.runId, secrets: bindSecrets(this.opts.secrets, ctx) };
1847
+ const toolCtx = { tenantId: ctx.tenantId, userId: ctx.userId, runId: ctx.runId, secrets: bindSecrets(this.opts.secrets, ctx), state };
1796
1848
  return executeToolWithGate({
1797
1849
  ev: toolPreCallEvent({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, toolName, origin: skill.origin ?? "first-party", needsApproval: this.gatesApproval(toolName), args }),
1798
1850
  gate: this.toolGate,
@@ -1802,6 +1854,7 @@ var SwarmEngine = class {
1802
1854
  async *resume(args, ctx) {
1803
1855
  const snap = await this.opts.storage.runs.loadSnapshot(ctx.tenantId, args.runId);
1804
1856
  if (!snap) throw new Error(`no suspended run: ${args.runId}`);
1857
+ const resumedState = createRunState(snap.state ?? void 0);
1805
1858
  const generationIndex = typeof snap.genIndex === "number" ? snap.genIndex : 1;
1806
1859
  const capHit = snap.capHit;
1807
1860
  await this.opts.storage.markFollowupAnswered?.(args.followupId, ctx.tenantId);
@@ -1835,6 +1888,7 @@ var SwarmEngine = class {
1835
1888
  turnEmitted = true;
1836
1889
  return emit(turnUsageEvent(rctx, ts++, turnUsage, generationIndex));
1837
1890
  };
1891
+ let resumeOutcome = "failed";
1838
1892
  try {
1839
1893
  if (!releaseFloor) {
1840
1894
  const held = await this.floor.holder(floorKey);
@@ -1866,7 +1920,7 @@ var SwarmEngine = class {
1866
1920
  wfState.outputs[wfState.pending.stepId] = args.answer;
1867
1921
  wfState.pending = void 0;
1868
1922
  }
1869
- yield* this.driveWorkflow(wf, wfState, rctx, { message: "", context: args.context }, {
1923
+ resumeOutcome = yield* this.driveWorkflow(wf, wfState, rctx, { message: "", context: args.context }, {
1870
1924
  gov,
1871
1925
  modelIdFor,
1872
1926
  streamed,
@@ -1876,7 +1930,11 @@ var SwarmEngine = class {
1876
1930
  turnUsage,
1877
1931
  nextTs: () => ts++,
1878
1932
  emit,
1879
- emitTurn
1933
+ emitTurn,
1934
+ segmentIndex: generationIndex,
1935
+ // FR-004: a resume segment starts at the snapshot's genIndex
1936
+ state: resumedState
1937
+ // FR-003: workflow steps' tools see the restored ctx.state
1880
1938
  });
1881
1939
  return;
1882
1940
  }
@@ -1895,7 +1953,7 @@ var SwarmEngine = class {
1895
1953
  );
1896
1954
  return;
1897
1955
  }
1898
- const rc = this.requestContext({ ...ctx, runId: args.runId });
1956
+ const rc = this.requestContext({ ...ctx, runId: args.runId }, resumedState);
1899
1957
  if (this.opts.pageContext) attachPageContext(rc, args.context);
1900
1958
  await this.guardGeneration({ runId: args.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, modelId, generationIndex, kind: "resume" });
1901
1959
  let resumeNudges = 0;
@@ -1927,22 +1985,14 @@ var SwarmEngine = class {
1927
1985
  const sp = payload.suspendPayload ?? {};
1928
1986
  await recordSuspend(this.opts.storage, rctx, followupId, toolCallId);
1929
1987
  await this.opts.storage.runs.setStatus(args.runId, "suspended");
1930
- await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId }, genIndex: generationIndex + 1 });
1988
+ await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId }, genIndex: generationIndex + 1, state: resumedState.entries() });
1931
1989
  {
1932
1990
  const t = await emitTurn();
1933
1991
  if (t) yield t;
1934
1992
  }
1935
1993
  yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
1936
- yield await emit(
1937
- ev("swarm.question", base(rctx, ts++), {
1938
- followupId,
1939
- toolCallId,
1940
- to: sp.to ?? "user",
1941
- from: sp.asker || rctx.agentSlug,
1942
- prompt: sp.prompt ?? "",
1943
- field: sp.field
1944
- })
1945
- );
1994
+ yield await emit(clientActionOrQuestion(rctx, ts++, followupId, toolCallId, sp));
1995
+ resumeOutcome = "suspended";
1946
1996
  return;
1947
1997
  }
1948
1998
  if (part?.type === "error") {
@@ -1961,7 +2011,7 @@ var SwarmEngine = class {
1961
2011
  return;
1962
2012
  }
1963
2013
  collectSpans(collector, part, modelId, gov);
1964
- for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage)) {
2014
+ for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage, generationIndex)) {
1965
2015
  if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
1966
2016
  lastOutputSlug = e.agentSlug;
1967
2017
  if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
@@ -1977,7 +2027,9 @@ var SwarmEngine = class {
1977
2027
  await this.opts.storage.runs.setStatus(args.runId, "suspended");
1978
2028
  await this.opts.storage.runs.saveSnapshot(args.runId, {
1979
2029
  capHit: { message: capHit.message, spentUsd: gov.costUsd() },
1980
- genIndex: generationIndex + 1
2030
+ genIndex: generationIndex + 1,
2031
+ state: resumedState.entries()
2032
+ // FR-003: persist per-run state across the cap-ask boundary
1981
2033
  });
1982
2034
  {
1983
2035
  const t = await emitTurn();
@@ -1985,6 +2037,7 @@ var SwarmEngine = class {
1985
2037
  }
1986
2038
  yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
1987
2039
  yield await emit(ev("swarm.question", base(rctx, ts++), capQuestion(rctx, followupId, gov)));
2040
+ resumeOutcome = "suspended";
1988
2041
  return;
1989
2042
  }
1990
2043
  await this.opts.storage.runs.setStatus(args.runId, "failed");
@@ -2031,6 +2084,7 @@ var SwarmEngine = class {
2031
2084
  if (t) yield t;
2032
2085
  }
2033
2086
  yield await emit(ev("swarm.status", base(rctx, ts++), { state: "done" }));
2087
+ resumeOutcome = "done";
2034
2088
  }
2035
2089
  } catch (err) {
2036
2090
  const stage = err instanceof ReserveDenied ? "reserve" : "exception";
@@ -2045,6 +2099,13 @@ var SwarmEngine = class {
2045
2099
  }
2046
2100
  yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage, message: errMessage(err), retryable: false }));
2047
2101
  } finally {
2102
+ if (this.opts.onRunEnd) {
2103
+ try {
2104
+ await this.opts.onRunEnd(ctx, { state: resumedState, outcome: resumeOutcome });
2105
+ } catch (err) {
2106
+ console.error(`[@nightowlsdev/core] onRunEnd threw for resume ${args.runId}:`, err);
2107
+ }
2108
+ }
2048
2109
  floorAbort.abort();
2049
2110
  await releaseFloor?.();
2050
2111
  await exportSpans(this.opts.telemetry, collector);
@@ -2064,6 +2125,26 @@ function errMessage(err) {
2064
2125
  function base(ctx, ts) {
2065
2126
  return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts };
2066
2127
  }
2128
+ function clientActionOrQuestion(ctx, ts, followupId, toolCallId, sp) {
2129
+ if (sp.clientAction) {
2130
+ return ev("swarm.client_action", base(ctx, ts), {
2131
+ followupId,
2132
+ toolCallId,
2133
+ tool: sp.clientAction.tool,
2134
+ input: sp.clientAction.input,
2135
+ needsApproval: sp.clientAction.needsApproval ?? false,
2136
+ from: sp.asker || ctx.agentSlug
2137
+ });
2138
+ }
2139
+ return ev("swarm.question", base(ctx, ts), {
2140
+ followupId,
2141
+ toolCallId,
2142
+ to: sp.to ?? "user",
2143
+ from: sp.asker || ctx.agentSlug,
2144
+ prompt: sp.prompt ?? "",
2145
+ field: sp.field
2146
+ });
2147
+ }
2067
2148
  var CAP_FOLLOWUP_SUFFIX = "cap";
2068
2149
  function capQuestion(ctx, followupId, gov) {
2069
2150
  const spent = gov.costUsd();
@@ -2159,7 +2240,7 @@ async function recordSuspend(storage, ctx, followupId, toolCallId) {
2159
2240
  }
2160
2241
  await storage.recordSuspend?.(ctx.runId, ctx.tenantId, followupId, toolCallId);
2161
2242
  }
2162
- function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage) {
2243
+ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage, segmentIndex) {
2163
2244
  const p = part.payload ?? {};
2164
2245
  const modelId = modelIdFor(ctx.agentSlug);
2165
2246
  const act = (slug) => {
@@ -2229,8 +2310,9 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets,
2229
2310
  gov.addUsage(modelId, u);
2230
2311
  delegateBudgets?.addUsage(ctx.agentSlug, modelId, u);
2231
2312
  const cost = gov.costOf(modelId, u);
2313
+ const generationId = `${ctx.runId}:${segmentIndex}:${turnUsage.length}`;
2232
2314
  turnUsage.push({ slug: ctx.agentSlug, breakdown: u, cost });
2233
- return [ev("swarm.usage", base(ctx, nextTs()), { slug: ctx.agentSlug, modelId, breakdown: u, cost })];
2315
+ return [ev("swarm.usage", base(ctx, nextTs()), { slug: ctx.agentSlug, modelId, breakdown: u, cost, generationId })];
2234
2316
  }
2235
2317
  return [];
2236
2318
  }
@@ -2241,7 +2323,7 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets,
2241
2323
  const inner = p.output;
2242
2324
  if (!inner || typeof inner.type !== "string") return [];
2243
2325
  if (inner.type === "text-delta") streamed.add(p.toolCallId);
2244
- return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage);
2326
+ return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage, segmentIndex);
2245
2327
  }
2246
2328
  case "tool-error": {
2247
2329
  const name = p.toolName ?? "";
@@ -2434,7 +2516,11 @@ function defineTool(spec) {
2434
2516
  tenantId,
2435
2517
  userId,
2436
2518
  runId,
2437
- secrets: bindSecrets(resolver, scopedCtx)
2519
+ secrets: bindSecrets(resolver, scopedCtx),
2520
+ // FR-003: the per-run state handle the engine put on the rc (same object across the run's tool calls +
2521
+ // delegated sub-agents). Absent on a raw test stream built without the engine — then `ctx.state` is
2522
+ // undefined, unchanged from prior behaviour.
2523
+ state: rc?.get?.(RUN_STATE_KEY)
2438
2524
  };
2439
2525
  const run = () => spec.execute(inputData, ctx);
2440
2526
  const agentCtx = context?.agent;
@@ -2476,6 +2562,48 @@ function deriveAsker(agentCtx, rc) {
2476
2562
  if (agentId.startsWith("swarm-sub-")) return agentId.slice("swarm-sub-".length);
2477
2563
  return rc?.get?.("agentSlug") ?? "";
2478
2564
  }
2565
+ var CLIENT_ACTION_SUSPEND_SCHEMA = import_zod4.z.object({
2566
+ clientAction: import_zod4.z.object({ tool: import_zod4.z.string(), input: import_zod4.z.any(), needsApproval: import_zod4.z.boolean().optional() }),
2567
+ asker: import_zod4.z.string().optional()
2568
+ });
2569
+ var CLIENT_ACTION_RESUME_SCHEMA = import_zod4.z.object({ answer: import_zod4.z.any() });
2570
+ var ClientToolError = class extends Error {
2571
+ constructor(toolName, reason) {
2572
+ super(reason ? `client tool ${toolName} failed: ${reason}` : `client tool ${toolName} failed`);
2573
+ this.name = "ClientToolError";
2574
+ }
2575
+ };
2576
+ function defineClientTool(spec) {
2577
+ const needsApproval = spec.needsApproval ?? false;
2578
+ const mastraTool = (0, import_tools4.createTool)({
2579
+ id: spec.name,
2580
+ description: spec.description ?? spec.name,
2581
+ inputSchema: spec.inputSchema,
2582
+ outputSchema: spec.outputSchema,
2583
+ suspendSchema: CLIENT_ACTION_SUSPEND_SCHEMA,
2584
+ resumeSchema: CLIENT_ACTION_RESUME_SCHEMA,
2585
+ execute: async (inputData, context) => {
2586
+ const rc = context?.requestContext;
2587
+ const agentCtx = context?.agent;
2588
+ if (agentCtx?.resumeData !== void 0 && agentCtx.resumeData !== null) {
2589
+ const answer = agentCtx.resumeData.answer;
2590
+ if (answer && typeof answer === "object" && "error" in answer && answer.error) {
2591
+ throw new ClientToolError(spec.name, String(answer.error));
2592
+ }
2593
+ return answer && typeof answer === "object" && "output" in answer ? answer.output : answer;
2594
+ }
2595
+ if (typeof agentCtx?.suspend !== "function") {
2596
+ throw new ClientToolError(spec.name, "client tools require an agent-driven run (no server execute)");
2597
+ }
2598
+ const asker = deriveAsker(agentCtx, rc);
2599
+ await agentCtx.suspend({ clientAction: { tool: spec.name, input: inputData, needsApproval }, asker });
2600
+ throw new ClientToolError(spec.name, "awaiting client action");
2601
+ }
2602
+ });
2603
+ const handle = { name: spec.name, needsApproval, origin: "first-party" };
2604
+ MASTRA.set(handle, mastraTool);
2605
+ return handle;
2606
+ }
2479
2607
  function __getMastraTool(t) {
2480
2608
  return MASTRA.get(t);
2481
2609
  }
@@ -2802,7 +2930,10 @@ function defineSwarm(cfg) {
2802
2930
  secrets: cfg.secrets,
2803
2931
  // SP3: best-effort per-event observer (metering debit/settle) — transport-agnostic, fired in run + resume.
2804
2932
  onEvent: cfg.onEvent,
2805
- verifyCompletion: cfg.verifyCompletion
2933
+ verifyCompletion: cfg.verifyCompletion,
2934
+ // FR-003: per-run lifecycle hooks (seed `ctx.state` at run start, drain at run end).
2935
+ onRunStart: cfg.onRunStart,
2936
+ onRunEnd: cfg.onRunEnd
2806
2937
  });
2807
2938
  return { engine };
2808
2939
  }
@@ -2826,6 +2957,8 @@ var InMemoryStorage = class {
2826
2957
  heads = /* @__PURE__ */ new Map();
2827
2958
  // key: tenantId:slug -> version
2828
2959
  pads = /* @__PURE__ */ new Map();
2960
+ threadRows = /* @__PURE__ */ new Map();
2961
+ // FR-009
2829
2962
  seedAgent(v, tenantId = "default") {
2830
2963
  this.agentRows.set(`${tenantId}:${v.slug}:${v.version}`, v);
2831
2964
  this.heads.set(`${tenantId}:${v.slug}`, v.version);
@@ -2909,6 +3042,18 @@ var InMemoryStorage = class {
2909
3042
  },
2910
3043
  getWaitpoint: async (followupId) => this.waitpoints.get(followupId) ?? null
2911
3044
  };
3045
+ // FR-009: idempotent thread-row creation. The dev store has no FK, so `messages.append` never threw `unknown
3046
+ // thread` here — this implements the contract so the engine's run-start ensure works against the in-memory store
3047
+ // too, and a host can read back the recorded thread (getThread) in tests.
3048
+ threads = {
3049
+ ensure: async ({ id, orgId, userId, projectId }) => {
3050
+ if (!this.threadRows.has(id)) this.threadRows.set(id, { id, orgId, userId, projectId });
3051
+ }
3052
+ };
3053
+ /** Test/host helper: read a recorded thread row. */
3054
+ getThread(id) {
3055
+ return this.threadRows.get(id);
3056
+ }
2912
3057
  messages = {
2913
3058
  append: async (m) => {
2914
3059
  this.msgs.push(m);
@@ -2948,6 +3093,69 @@ var InMemoryStorage = class {
2948
3093
  };
2949
3094
  };
2950
3095
 
3096
+ // src/run-agent.ts
3097
+ function uid() {
3098
+ return globalThis.crypto?.randomUUID?.() ?? `id-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
3099
+ }
3100
+ async function drainTrajectory(stream) {
3101
+ const events = [];
3102
+ let output = "";
3103
+ for await (const e of stream) {
3104
+ events.push(e);
3105
+ if (e.type === "swarm.message") {
3106
+ const d = e.data;
3107
+ if (d.role === "assistant") output += d.delta ?? d.text ?? "";
3108
+ }
3109
+ }
3110
+ return { events, output };
3111
+ }
3112
+ async function runToTrajectory(target, input, ctx) {
3113
+ const engine = "engine" in target ? target.engine : target;
3114
+ const runInput = typeof input === "string" ? { message: input } : input;
3115
+ const full = ephemeralCtx(ctx?.agentSlug ?? "agent", ctx);
3116
+ return drainTrajectory(engine.run(runInput, full));
3117
+ }
3118
+ function ephemeralCtx(agentSlug, over) {
3119
+ return {
3120
+ tenantId: over?.tenantId ?? "default",
3121
+ userId: over?.userId ?? "local",
3122
+ agentSlug: over?.agentSlug ?? agentSlug,
3123
+ runId: over?.runId ?? uid(),
3124
+ threadId: over?.threadId ?? uid(),
3125
+ ...over?.agentVersion !== void 0 ? { agentVersion: over.agentVersion } : {}
3126
+ };
3127
+ }
3128
+ function buildSingleAgentSwarm(def, opts) {
3129
+ const t = opts.models?.tier;
3130
+ const tierAllow = t ? [t.tiers.swift, ...t.tiers.genius ? [t.tiers.genius] : []] : [];
3131
+ const allow = opts.models?.allow ?? [def.head.modelId, ...tierAllow];
3132
+ const storage = opts.storage ?? new InMemoryStorage();
3133
+ const cfg = {
3134
+ storage,
3135
+ agents: [def],
3136
+ models: { allow, ...opts.models?.tier ? { tier: opts.models.tier } : {} },
3137
+ modelFactory: opts.modelFactory,
3138
+ cost: { maxSteps: 50, maxCostUsd: 10, ...opts.cost },
3139
+ ...opts.telemetry !== void 0 ? { telemetry: opts.telemetry } : {},
3140
+ ...opts.memory !== void 0 ? { memory: opts.memory } : {},
3141
+ ...opts.hooks !== void 0 ? { hooks: opts.hooks } : {},
3142
+ ...opts.toolApproval !== void 0 ? { toolApproval: opts.toolApproval } : {},
3143
+ ...opts.secrets !== void 0 ? { secrets: opts.secrets } : {},
3144
+ ...opts.onEvent !== void 0 ? { onEvent: opts.onEvent } : {},
3145
+ ...opts.onRunStart !== void 0 ? { onRunStart: opts.onRunStart } : {},
3146
+ ...opts.onRunEnd !== void 0 ? { onRunEnd: opts.onRunEnd } : {},
3147
+ ...opts.pageContext !== void 0 ? { pageContext: opts.pageContext } : {},
3148
+ ...opts.mastraStore !== void 0 ? { mastraStore: opts.mastraStore } : {}
3149
+ };
3150
+ return defineSwarm(cfg);
3151
+ }
3152
+ async function runAgent(def, input, opts) {
3153
+ const swarm = buildSingleAgentSwarm(def, opts);
3154
+ const runInput = typeof input === "string" ? { message: input } : input;
3155
+ const ctx = ephemeralCtx(def.head.slug, { ...opts.ctx, agentSlug: def.head.slug });
3156
+ return drainTrajectory(swarm.engine.run(runInput, ctx));
3157
+ }
3158
+
2951
3159
  // src/auth.ts
2952
3160
  var customAuth = (fn) => ({ authenticate: fn });
2953
3161
 
@@ -2988,6 +3196,7 @@ var VERSION = "0.0.0";
2988
3196
  ASK_TOOL_NAME,
2989
3197
  AgentMutationForbidden,
2990
3198
  CapturingExporter,
3199
+ ClientToolError,
2991
3200
  CostGovernor,
2992
3201
  DEFAULT_READ_ONLY_TOOLS,
2993
3202
  DelegateBudgets,
@@ -3006,6 +3215,7 @@ var VERSION = "0.0.0";
3006
3215
  allowListModelProvider,
3007
3216
  ask,
3008
3217
  assertActorMayMutateDefinition,
3218
+ buildSingleAgentSwarm,
3009
3219
  buildSkillResolver,
3010
3220
  composePolicyPrompt,
3011
3221
  composeSystemPrompt,
@@ -3013,11 +3223,13 @@ var VERSION = "0.0.0";
3013
3223
  containerFloor,
3014
3224
  createHookDispatcher,
3015
3225
  createInMemoryRateLimitStore,
3226
+ createRunState,
3016
3227
  customAuth,
3017
3228
  customTelemetry,
3018
3229
  decideFixedWindow,
3019
3230
  defineAgent,
3020
3231
  defineBundle,
3232
+ defineClientTool,
3021
3233
  defineHook,
3022
3234
  defineRule,
3023
3235
  defineSkill,
@@ -3025,6 +3237,7 @@ var VERSION = "0.0.0";
3025
3237
  defineTool,
3026
3238
  defineWorkflow,
3027
3239
  deny,
3240
+ drainTrajectory,
3028
3241
  ev,
3029
3242
  isEvent,
3030
3243
  isTierSentinel,
@@ -3033,6 +3246,8 @@ var VERSION = "0.0.0";
3033
3246
  rateConfig,
3034
3247
  resolveTelemetry,
3035
3248
  resolveTier,
3249
+ runAgent,
3250
+ runToTrajectory,
3036
3251
  sumBreakdowns,
3037
3252
  sumTurnUsage,
3038
3253
  tierModelId,