@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 +261 -46
- package/dist/index.d.cts +176 -1
- package/dist/index.d.ts +176 -1
- package/dist/index.js +254 -46
- package/package.json +3 -3
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
51
|
-
"@nightowlsdev/
|
|
50
|
+
"@nightowlsdev/tsconfig": "0.0.0",
|
|
51
|
+
"@nightowlsdev/eslint-config": "0.0.0"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "tsup",
|