@nightowlsdev/engine-ai-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1360 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AI_SDK_ENGINE_CAPABILITIES: () => AI_SDK_ENGINE_CAPABILITIES,
24
+ AiSdkEngine: () => AiSdkEngine,
25
+ aiSdkEngine: () => aiSdkEngine,
26
+ nightOwlsPlugin: () => nightOwlsPlugin
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/engine.ts
31
+ var import_ai2 = require("ai");
32
+ var import_core4 = require("@nightowlsdev/core");
33
+ var import_engine_spi2 = require("@nightowlsdev/core/engine-spi");
34
+
35
+ // src/capabilities.ts
36
+ var AI_SDK_ENGINE_CAPABILITIES = {
37
+ contract: 1,
38
+ id: "ai-sdk",
39
+ kind: "native",
40
+ events: {
41
+ tier: 3,
42
+ emits: [
43
+ "swarm.message",
44
+ "swarm.status",
45
+ "swarm.run_failed",
46
+ "swarm.run_cancelled",
47
+ "swarm.question",
48
+ "swarm.answer",
49
+ "swarm.client_action",
50
+ "swarm.tool_call",
51
+ "swarm.tool_result",
52
+ "swarm.usage",
53
+ "swarm.turn_usage"
54
+ ]
55
+ },
56
+ streaming: { seq: "global", persistsUserMessage: true },
57
+ hitl: { ask: true, approval: true, clientTools: true, durableResume: false },
58
+ delegation: false,
59
+ workflows: false,
60
+ rules: true,
61
+ memory: { history: true, semanticRecall: false, workingMemory: false, observational: false },
62
+ scratchpad: true,
63
+ cancellation: "mid-stream",
64
+ governance: { preGeneration: true, preToolCall: "fail-closed", costCaps: true, secrets: true },
65
+ telemetry: true
66
+ };
67
+
68
+ // src/emit.ts
69
+ var import_core = require("@nightowlsdev/core");
70
+ function makeEmitter(opts) {
71
+ let ts = opts.startTs ?? 0;
72
+ return {
73
+ ev(type, data) {
74
+ return (0, import_core.ev)(type, { runId: opts.ctx.runId, agentSlug: opts.ctx.agentSlug, ts: ts++ }, data);
75
+ },
76
+ async emit(e) {
77
+ e.seq = await opts.storage.events.append(e);
78
+ if (opts.onEvent) {
79
+ try {
80
+ await opts.onEvent(e, opts.ctx);
81
+ } catch (err) {
82
+ console.error(`[@nightowlsdev/engine-ai-sdk] onEvent threw for ${e.type}`, err);
83
+ }
84
+ }
85
+ return e;
86
+ }
87
+ };
88
+ }
89
+
90
+ // src/tools.ts
91
+ var import_zod = require("zod");
92
+ var import_ai = require("ai");
93
+ var import_core2 = require("@nightowlsdev/core");
94
+ var import_engine_spi = require("@nightowlsdev/core/engine-spi");
95
+
96
+ // src/errors.ts
97
+ function errorMessage(err) {
98
+ return err instanceof Error ? err.message : String(err);
99
+ }
100
+
101
+ // src/internal/brand.ts
102
+ var GATED_DENIED = /* @__PURE__ */ Symbol("nightowls.gated_denied");
103
+
104
+ // src/map-chunks.ts
105
+ function isBrandedDenied(x) {
106
+ return typeof x === "object" && x !== null && x[GATED_DENIED] === true;
107
+ }
108
+ function mapChunks(part, ctx) {
109
+ switch (part.type) {
110
+ case "text-delta":
111
+ return [
112
+ { kind: "event", type: "swarm.message", agentSlug: ctx.agentSlug, data: { role: "assistant", delta: part.text } }
113
+ ];
114
+ case "tool-call":
115
+ return [
116
+ {
117
+ kind: "event",
118
+ type: "swarm.tool_call",
119
+ agentSlug: ctx.agentSlug,
120
+ data: {
121
+ toolCallId: part.toolCallId,
122
+ name: part.toolName,
123
+ args: part.input,
124
+ needsApproval: ctx.gatesApproval(part.toolName)
125
+ }
126
+ }
127
+ ];
128
+ case "tool-result": {
129
+ const { toolCallId } = part;
130
+ const output = part.output;
131
+ if (isBrandedDenied(output)) {
132
+ return [
133
+ {
134
+ kind: "event",
135
+ type: "swarm.tool_result",
136
+ agentSlug: ctx.agentSlug,
137
+ data: { toolCallId, ok: false, error: output.reason ?? output.error ?? "blocked" }
138
+ }
139
+ ];
140
+ }
141
+ return [
142
+ { kind: "event", type: "swarm.tool_result", agentSlug: ctx.agentSlug, data: { toolCallId, ok: true, result: output } }
143
+ ];
144
+ }
145
+ case "tool-output-denied":
146
+ return [
147
+ {
148
+ kind: "event",
149
+ type: "swarm.tool_result",
150
+ agentSlug: ctx.agentSlug,
151
+ data: {
152
+ toolCallId: part.toolCallId,
153
+ ok: false,
154
+ error: part.reason ?? "rejected by approver"
155
+ }
156
+ }
157
+ ];
158
+ case "tool-error":
159
+ return [
160
+ {
161
+ kind: "event",
162
+ type: "swarm.tool_result",
163
+ agentSlug: ctx.agentSlug,
164
+ data: { toolCallId: part.toolCallId, ok: false, error: errorMessage(part.error) }
165
+ }
166
+ ];
167
+ case "tool-approval-request":
168
+ return [
169
+ {
170
+ kind: "park",
171
+ approvalId: part.approvalId,
172
+ toolCallId: part.toolCall.toolCallId,
173
+ toolName: part.toolCall.toolName,
174
+ input: part.toolCall.input
175
+ }
176
+ ];
177
+ case "finish-step":
178
+ return [{ kind: "step-usage", usage: part.usage }];
179
+ case "abort":
180
+ return [{ kind: "cancel", reason: part.reason }];
181
+ case "error":
182
+ return [{ kind: "failure", error: part.error }];
183
+ default:
184
+ return [];
185
+ }
186
+ }
187
+
188
+ // src/internal/container.ts
189
+ function containerOf(threadId) {
190
+ return threadId.split(":")[0] || threadId;
191
+ }
192
+
193
+ // src/tools.ts
194
+ var RECALL_LANE_LIMIT = 20;
195
+ function buildServerTool(spec, handle, opts, runCtx, gateMemo) {
196
+ const toolCtx = {
197
+ tenantId: runCtx.ctx.tenantId,
198
+ userId: runCtx.ctx.userId,
199
+ runId: runCtx.ctx.runId,
200
+ secrets: (0, import_engine_spi.bindSecrets)(opts.secrets, runCtx.ctx),
201
+ state: runCtx.state
202
+ };
203
+ const runBody = (input) => spec.execute(input, toolCtx);
204
+ const resolveGate = async (input) => {
205
+ const ev = (0, import_engine_spi.toolPreCallEvent)({
206
+ runId: runCtx.ctx.runId,
207
+ tenantId: runCtx.ctx.tenantId,
208
+ agentSlug: runCtx.ctx.agentSlug,
209
+ toolName: spec.name,
210
+ origin: handle.origin,
211
+ needsApproval: handle.needsApproval,
212
+ args: input
213
+ });
214
+ let decision;
215
+ try {
216
+ decision = await opts.gate(ev);
217
+ } catch (err) {
218
+ return { action: "gate-error", error: errorMessage(err) };
219
+ }
220
+ if (decision.action === "deny") return { action: "deny", reason: decision.reason };
221
+ if (decision.action === "ask") return { action: "ask" };
222
+ return { action: "allow" };
223
+ };
224
+ return (0, import_ai.tool)({
225
+ description: spec.description ?? spec.name,
226
+ // spec.outputSchema is deliberately NOT forwarded: a branded GATED_DENIED return would fail output
227
+ // validation (the same reason core's define.ts THROWS ToolBlockedError instead of returning a sentinel).
228
+ inputSchema: spec.inputSchema,
229
+ needsApproval: async (input, { toolCallId }) => {
230
+ if (runCtx.approvedToolCallIds.has(toolCallId)) return false;
231
+ let memoPromise = gateMemo.get(toolCallId);
232
+ if (!memoPromise) {
233
+ memoPromise = resolveGate(input);
234
+ gateMemo.set(toolCallId, memoPromise);
235
+ }
236
+ const memo = await memoPromise;
237
+ return memo.action === "ask";
238
+ },
239
+ execute: async (input, options) => {
240
+ if (runCtx.approvedToolCallIds.has(options.toolCallId)) {
241
+ return runBody(input);
242
+ }
243
+ const memoPromise = gateMemo.get(options.toolCallId);
244
+ if (memoPromise) {
245
+ const memo = await memoPromise;
246
+ switch (memo.action) {
247
+ case "allow":
248
+ return runBody(input);
249
+ case "deny":
250
+ return { [GATED_DENIED]: true, reason: memo.reason };
251
+ case "gate-error":
252
+ return { [GATED_DENIED]: true, error: memo.error };
253
+ case "ask":
254
+ return runBody(input);
255
+ }
256
+ }
257
+ const late = await resolveGate(input);
258
+ switch (late.action) {
259
+ case "allow":
260
+ return runBody(input);
261
+ case "deny":
262
+ return { [GATED_DENIED]: true, reason: late.reason };
263
+ case "gate-error":
264
+ return { [GATED_DENIED]: true, error: late.error };
265
+ case "ask":
266
+ return {
267
+ [GATED_DENIED]: true,
268
+ reason: "approval required but the engine cannot suspend mid-execute"
269
+ };
270
+ }
271
+ }
272
+ });
273
+ }
274
+ var CLIENT_NEEDS_APPROVAL = /* @__PURE__ */ new WeakMap();
275
+ function clientNeedsApprovalOf(t) {
276
+ return (t && CLIENT_NEEDS_APPROVAL.get(t)) ?? false;
277
+ }
278
+ function buildClientTool(spec) {
279
+ const t = (0, import_ai.tool)({
280
+ description: spec.description ?? spec.name,
281
+ inputSchema: spec.inputSchema
282
+ });
283
+ CLIENT_NEEDS_APPROVAL.set(t, spec.needsApproval ?? false);
284
+ return t;
285
+ }
286
+ async function connectorToolsByName(resolve, ctx) {
287
+ if (!resolve) return {};
288
+ const out = {};
289
+ for (const t of await resolve(ctx)) out[t.name] = t;
290
+ return out;
291
+ }
292
+ var askFieldSchema = import_zod.z.object({
293
+ kind: import_zod.z.enum(["confirm", "buttons", "select", "multiselect", "number", "text", "textarea"]),
294
+ options: import_zod.z.array(import_zod.z.object({ label: import_zod.z.string(), value: import_zod.z.string(), description: import_zod.z.string().optional() })).optional(),
295
+ min: import_zod.z.number().optional(),
296
+ max: import_zod.z.number().optional(),
297
+ step: import_zod.z.number().optional(),
298
+ unit: import_zod.z.string().optional(),
299
+ placeholder: import_zod.z.string().optional(),
300
+ rows: import_zod.z.number().optional(),
301
+ confirmLabel: import_zod.z.string().optional(),
302
+ rejectLabel: import_zod.z.string().optional(),
303
+ submitLabel: import_zod.z.string().optional(),
304
+ default: import_zod.z.any().optional()
305
+ });
306
+ function buildAskTool() {
307
+ return (0, import_ai.tool)({
308
+ description: "Ask a follow-up question to the user (or another agent) when blocked. Prefer a RICH `field` so the user gets a fitting control instead of a text box: {kind:'confirm'} for yes/no; {kind:'buttons'|'select'|'multiselect', options:[{label,value}]} for choices; {kind:'number', min,max,step,unit} for a number; {kind:'text'|'textarea', placeholder} for free text. Omit `field` for a plain text answer. `prompt` may be markdown.",
309
+ inputSchema: import_zod.z.object({ to: import_zod.z.string().default("user"), prompt: import_zod.z.string(), field: askFieldSchema.optional() })
310
+ });
311
+ }
312
+ function buildScratchpadWriteTool(store, caps, runCtx) {
313
+ const maxEntryChars = caps?.maxEntryChars ?? import_core2.SCRATCHPAD_MAX_ENTRY_CHARS;
314
+ return (0, import_ai.tool)({
315
+ description: "Record a fact on the shared scratchpad under a short KEY. Re-use the same key to update that fact in place; different keys are independent so you never clobber a peer. Use `public` for facts the user should see, `meta` for internal agent notes. Record proactively as you work.",
316
+ inputSchema: import_zod.z.object({
317
+ section: import_zod.z.enum(["public", "meta"]),
318
+ key: import_zod.z.string().describe("a short stable slug for THIS fact, e.g. 'launch-date' \u2014 reuse it to revise that fact"),
319
+ content: import_zod.z.string()
320
+ }),
321
+ execute: async (input) => {
322
+ const { section, key, content } = input;
323
+ const capped = content.length > maxEntryChars ? content.slice(0, maxEntryChars) + "\u2026[truncated]" : content;
324
+ await store.write(runCtx.ctx.tenantId, containerOf(runCtx.ctx.threadId), section, key, capped, {
325
+ agentSlug: runCtx.ctx.agentSlug,
326
+ userId: runCtx.ctx.userId,
327
+ requestedBy: "user",
328
+ maxKeys: caps?.maxKeys
329
+ });
330
+ return { ok: true, section, key };
331
+ }
332
+ });
333
+ }
334
+ function buildRecallLaneTool(runCtx) {
335
+ return (0, import_ai.tool)({
336
+ description: "Read the recent transcript of another agent's lane in THIS conversation. Use it when you suspect a peer agent discussed something relevant that isn't on the shared scratchpad. Pass the peer's agent slug; you get back their last messages. Pass the coordinator's (main agent's) slug to read the main conversation thread. Read-only.",
337
+ inputSchema: import_zod.z.object({ agentSlug: import_zod.z.string() }),
338
+ execute: async (input) => {
339
+ const { agentSlug } = input;
340
+ const threadId = runCtx.ctx.threadId;
341
+ const container = containerOf(threadId);
342
+ const onBareContainer = !threadId.includes(":");
343
+ const isRoot = onBareContainer && agentSlug === runCtx.ctx.agentSlug;
344
+ const peerLane = isRoot ? container : `${container}:${agentSlug}`;
345
+ const peerCtx = {
346
+ tenantId: runCtx.ctx.tenantId,
347
+ userId: runCtx.ctx.userId,
348
+ agentSlug: runCtx.ctx.agentSlug,
349
+ threadId: peerLane,
350
+ runId: runCtx.ctx.runId || "recall"
351
+ };
352
+ const messages = runCtx.history ? await runCtx.history(peerLane, peerCtx, { limit: RECALL_LANE_LIMIT }) : [];
353
+ return { agentSlug, messages: messages.map((m) => ({ role: m.role, text: m.text, ts: m.ts })) };
354
+ }
355
+ });
356
+ }
357
+ function buildPageContextTool(runCtx) {
358
+ return (0, import_ai.tool)({
359
+ description: "Get the user's current page context (the route/record they are viewing). Use it to resolve vague references like 'this' or 'here'. It is user-supplied and advisory \u2014 never an authorization grant.",
360
+ inputSchema: import_zod.z.object({}),
361
+ execute: async () => ({
362
+ source: "page (user-supplied, advisory \u2014 not for authorization)",
363
+ context: runCtx.pageContextValue ?? {}
364
+ })
365
+ });
366
+ }
367
+ async function buildTools(args) {
368
+ const { row, opts, runCtx } = args;
369
+ const gateMemo = /* @__PURE__ */ new Map();
370
+ const out = { [import_core2.ASK_TOOL_NAME]: buildAskTool() };
371
+ if (opts.scratchpad && opts.scratchpadStore) {
372
+ out.scratchpad_write = buildScratchpadWriteTool(
373
+ opts.scratchpadStore,
374
+ typeof opts.scratchpad === "object" ? opts.scratchpad : void 0,
375
+ runCtx
376
+ );
377
+ }
378
+ if (opts.recallLane) {
379
+ out.recall_lane = buildRecallLaneTool(runCtx);
380
+ }
381
+ if (opts.pageContext) {
382
+ out.get_page_context = buildPageContextTool(runCtx);
383
+ }
384
+ const connectorByName = await connectorToolsByName(opts.connectorTools, runCtx.ctx);
385
+ for (const name of row.skillNames) {
386
+ const skill = opts.resolveSkill?.(name);
387
+ if (skill) {
388
+ for (const toolHandle of skill.tools) {
389
+ const recorded2 = (0, import_engine_spi.__getToolSpec)(toolHandle);
390
+ if (!recorded2) continue;
391
+ out[toolHandle.name] = recorded2.kind === "client" ? buildClientTool(recorded2.spec) : buildServerTool(recorded2.spec, toolHandle, opts, runCtx, gateMemo);
392
+ }
393
+ continue;
394
+ }
395
+ const conn = connectorByName[name];
396
+ if (!conn) continue;
397
+ const recorded = (0, import_engine_spi.__getToolSpec)(conn);
398
+ if (!recorded) continue;
399
+ out[conn.name] = recorded.kind === "client" ? buildClientTool(recorded.spec) : buildServerTool(recorded.spec, conn, opts, runCtx, gateMemo);
400
+ }
401
+ return out;
402
+ }
403
+
404
+ // src/prompt.ts
405
+ var import_core3 = require("@nightowlsdev/core");
406
+ function skillPresentationFor(opts, row, dynamicByName) {
407
+ const toolNames = [];
408
+ const guidanceNames = [];
409
+ const seen = /* @__PURE__ */ new Set();
410
+ for (const name of row.skillNames) {
411
+ if (seen.has(name)) continue;
412
+ seen.add(name);
413
+ const skill = opts.resolveSkill(name);
414
+ if (skill) {
415
+ if (skill.tools.length) for (const t of skill.tools) toolNames.push(t.name);
416
+ else guidanceNames.push(skill.name);
417
+ continue;
418
+ }
419
+ if (dynamicByName[name]) guidanceNames.push(name);
420
+ else toolNames.push(name);
421
+ }
422
+ return { toolNames: [...new Set(toolNames)], guidanceNames };
423
+ }
424
+ function grantedSkillsFor(opts, row, dynamicByName) {
425
+ const seen = /* @__PURE__ */ new Set();
426
+ const skills = [];
427
+ for (const name of row.skillNames) {
428
+ const s = opts.resolveSkill(name) ?? dynamicByName[name];
429
+ if (s && !seen.has(s.name)) {
430
+ seen.add(s.name);
431
+ skills.push(s);
432
+ }
433
+ }
434
+ return skills;
435
+ }
436
+ async function buildSystemMessages(args) {
437
+ const { row, opts, ctx, dynamicSkillsResolved, systemContextText } = args;
438
+ const presentation = skillPresentationFor(opts, row, dynamicSkillsResolved);
439
+ let messages = (0, import_core3.composeSystemPrompt)(row, presentation);
440
+ messages = [...messages, ...(0, import_core3.composePolicyPrompt)(opts.softPolicy?.(row.slug) ?? [])];
441
+ messages = [...messages, ...(0, import_core3.composeSkillInstructions)(grantedSkillsFor(opts, row, dynamicSkillsResolved))];
442
+ if (opts.scratchpad && opts.loadScratchpad) {
443
+ const container = ctx.threadId.split(":")[0] ?? "";
444
+ const entries = await opts.loadScratchpad(container, ctx.tenantId);
445
+ messages = [...messages, (0, import_core3.composeScratchpadPrompt)(entries)];
446
+ }
447
+ if (systemContextText) {
448
+ messages = [...messages, (0, import_core3.composeAdvisoryContext)(systemContextText)];
449
+ }
450
+ return messages;
451
+ }
452
+
453
+ // src/usage.ts
454
+ function extractUsage(u) {
455
+ const breakdown = {
456
+ inputTokens: u.inputTokens ?? 0,
457
+ outputTokens: u.outputTokens ?? 0
458
+ };
459
+ const cacheReadTokens = u.inputTokenDetails?.cacheReadTokens;
460
+ if (cacheReadTokens != null) breakdown.cacheReadTokens = cacheReadTokens;
461
+ const cacheWriteTokens = u.inputTokenDetails?.cacheWriteTokens;
462
+ if (cacheWriteTokens != null) breakdown.cacheWriteTokens = cacheWriteTokens;
463
+ const reasoningTokens = u.outputTokenDetails?.reasoningTokens;
464
+ if (reasoningTokens != null) breakdown.reasoningTokens = reasoningTokens;
465
+ return breakdown;
466
+ }
467
+
468
+ // src/engine.ts
469
+ function titleCase(slug) {
470
+ return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
471
+ }
472
+ var LEGACY_ASSISTANT_PREFIX_RE = /^\[([a-z0-9][a-z0-9-]*)\]\s([\s\S]*)$/;
473
+ var LEGACY_USER_PREFIX_RE = /^\[user:([^\]]*)\]\s([\s\S]*)$/;
474
+ function messagesFromEvents(threadId, events) {
475
+ const out = [];
476
+ let cur = null;
477
+ const flush = () => {
478
+ if (!cur) return;
479
+ const { role, ts, agentSlug } = cur;
480
+ let text = cur.parts.join("");
481
+ let userId;
482
+ if (role === "assistant") {
483
+ const m = LEGACY_ASSISTANT_PREFIX_RE.exec(text);
484
+ if (m) text = m[2];
485
+ } else {
486
+ const m = LEGACY_USER_PREFIX_RE.exec(text);
487
+ if (m) {
488
+ userId = m[1];
489
+ text = m[2];
490
+ }
491
+ }
492
+ out.push({
493
+ threadId,
494
+ role,
495
+ text,
496
+ ts,
497
+ ...role === "assistant" && agentSlug ? { agentSlug } : {},
498
+ ...userId ? { userId } : {}
499
+ });
500
+ cur = null;
501
+ };
502
+ for (const e of events) {
503
+ if (e.type === "swarm.message") {
504
+ const role = e.data.role;
505
+ const raw = e.data.delta ?? e.data.text ?? "";
506
+ if (cur && cur.role === role) {
507
+ cur.parts.push(raw);
508
+ } else {
509
+ flush();
510
+ cur = { role, parts: [raw], ts: e.ts, agentSlug: role === "assistant" ? e.agentSlug : void 0 };
511
+ }
512
+ } else {
513
+ flush();
514
+ }
515
+ }
516
+ flush();
517
+ return out;
518
+ }
519
+ function verifyNudge(missing) {
520
+ const gap = (missing ?? "").trim();
521
+ return gap ? `[automated] The task is NOT finished yet \u2014 still missing: ${gap}. Continue now and complete every remaining step. Do not end your turn until it is fully done.` : "[automated] Your previous turn ended without any message or tool call. If you have fully completed everything that was asked, give the final result now. Otherwise continue and finish every remaining step you intended \u2014 do not end your turn until the task is done.";
522
+ }
523
+ function isValidAiSdkSnapshot(snap) {
524
+ if (!snap || typeof snap !== "object") return false;
525
+ const s = snap;
526
+ if (s.v !== 1) return false;
527
+ if (s.kind !== "ask" && s.kind !== "approval" && s.kind !== "client") return false;
528
+ if (typeof s.toolCallId !== "string") return false;
529
+ if (!Array.isArray(s.messages)) return false;
530
+ if (s.kind === "approval" && typeof s.approvalId !== "string") return false;
531
+ return true;
532
+ }
533
+ var VERIFY_TRANSCRIPT_CAP = 6e3;
534
+ var HISTORY_PROMPT_CAP = 20;
535
+ function appendTranscript(t, add) {
536
+ return add ? (t + add).slice(-VERIFY_TRANSCRIPT_CAP) : t;
537
+ }
538
+ function isAbortLike(err) {
539
+ return err instanceof Error && err.name === "AbortError";
540
+ }
541
+ var warnedNoRecordSuspend = false;
542
+ async function recordSuspendSafe(storage, ctx, followupId, toolCallId) {
543
+ if (!storage.recordSuspend && !warnedNoRecordSuspend) {
544
+ warnedNoRecordSuspend = true;
545
+ console.warn(
546
+ "[@nightowlsdev/engine-ai-sdk] storage adapter does not implement recordSuspend() \u2014 human-in-the-loop resume will be forbidden (the followup index is never written). Implement recordSuspend on your StorageAdapter."
547
+ );
548
+ }
549
+ await storage.recordSuspend?.(ctx.runId, ctx.tenantId, followupId, toolCallId);
550
+ }
551
+ async function* finishDoneSeg(io) {
552
+ await io.runsStore.setStatus(io.ctx.runId, "done");
553
+ const t = await io.emitTurnUsage(io.segmentIndex);
554
+ if (t) yield t;
555
+ yield await io.emit(io.ev("swarm.status", { state: "done" }));
556
+ }
557
+ async function* finishFailedSeg(io, stage, message, retryable = false) {
558
+ try {
559
+ await io.runsStore.setStatus(io.ctx.runId, "failed");
560
+ } catch {
561
+ }
562
+ const t = await io.emitTurnUsage(io.segmentIndex);
563
+ if (t) yield t;
564
+ yield await io.emit(io.ev("swarm.run_failed", { stage, message, retryable }));
565
+ }
566
+ async function* finishCancelledSeg(io) {
567
+ await io.runsStore.setStatus(io.ctx.runId, "failed");
568
+ const t = await io.emitTurnUsage(io.segmentIndex);
569
+ if (t) yield t;
570
+ yield await io.emit(io.ev("swarm.run_cancelled", { reason: "aborted" }));
571
+ }
572
+ var AiSdkEngine = class {
573
+ constructor(opts, extra) {
574
+ this.opts = opts;
575
+ this.hooks = opts.hooks ?? new import_core4.HookDispatcher({}, opts.toolApproval ?? { mode: "flag" });
576
+ this.floor = opts.floor ?? import_core4.containerFloor;
577
+ this.telemetry = (0, import_core4.resolveTelemetry)(opts.telemetry ?? void 0);
578
+ this.capabilities = extra.durable ? { ...AI_SDK_ENGINE_CAPABILITIES, hitl: { ...AI_SDK_ENGINE_CAPABILITIES.hitl, durableResume: true } } : AI_SDK_ENGINE_CAPABILITIES;
579
+ }
580
+ opts;
581
+ capabilities;
582
+ hooks;
583
+ floor;
584
+ telemetry;
585
+ /**
586
+ * SP2 — the preGeneration DECISION seam, awaited immediately before every model launch. Verbatim mirror of
587
+ * the reference engine's private `guardGeneration` (`packages/core/src/engine.ts:543-553`): the dispatcher is
588
+ * fail-closed (a throwing hook ⇒ deny), so this only ever sees a clean allow/deny; a deny THROWS
589
+ * `ReserveDenied` (re-exported from core, not redefined here) so the caller's model launch never happens and
590
+ * the run/resume catch-all maps it to a terminal `run_failed` stage `"reserve"` (never the generic
591
+ * `"exception"`).
592
+ */
593
+ async guardGeneration(ev) {
594
+ const decision = await this.hooks.preGeneration(ev);
595
+ if (decision.action === "deny") throw new import_core4.ReserveDenied(decision.reason);
596
+ }
597
+ /**
598
+ * SP5 truth-fix — resolve whether a tool WILL require approval, for the `swarm.tool_call` event's advisory
599
+ * `needsApproval` field. Verbatim mirror of the reference engine's private `gatesApproval`
600
+ * (`packages/core/src/engine.ts:528-534`): the tool's resolved `needsApproval` (its own flag, defaulting by
601
+ * origin) run through the dispatcher's SYNC `policyDecision` (`ask` ⇒ true). The async `preToolCall` hook can
602
+ * still escalate a specific call at execute time (that's what actually drives the tool's native pause — see
603
+ * `tools.ts`'s `buildServerTool`); this is only the truthful baseline the event carries.
604
+ */
605
+ gatesApproval(toolName) {
606
+ const tool2 = this.opts.resolveSkill?.(toolName)?.tools.find((t) => t.name === toolName);
607
+ const origin = tool2?.origin ?? "first-party";
608
+ const needsApproval = tool2?.needsApproval ?? origin === "mcp";
609
+ const decision = this.hooks.policyDecision({ runId: "", agentSlug: "", toolName, origin, needsApproval });
610
+ return decision.action === "ask";
611
+ }
612
+ /**
613
+ * T9 (T8-review fix) — best-effort recall of the thread's request text for the completion verifier's
614
+ * `request` param on RESUME (the engine doesn't hold the original prompt there — `resume()` only carries the
615
+ * suspended tool's answer). Mirrors the reference engine's `recallRequest`, but deliberately the LAST user
616
+ * turn (not the first): a resume continues whichever prompt MOST RECENTLY triggered this run, not the
617
+ * thread's opening message. Fail-safe: gated behind `verifyCompletion` (a no-verifier config never pays for
618
+ * the read) and never throws — any failure yields `""`, matching the reference's own fallback.
619
+ */
620
+ async recallRequest(ctx) {
621
+ if (!this.opts.verifyCompletion) return "";
622
+ try {
623
+ const msgs = await this.history(ctx.threadId, ctx, { limit: 50 });
624
+ for (let i = msgs.length - 1; i >= 0; i--) {
625
+ if (msgs[i].role === "user") return msgs[i].text;
626
+ }
627
+ return "";
628
+ } catch {
629
+ return "";
630
+ }
631
+ }
632
+ /** Run the completion supervisor (`EngineOpts.verifyCompletion`), FAIL-OPEN: no verifier, or a throwing one,
633
+ * yields `{ complete: true }` so a missing/broken judge never traps a run in a verify loop. Mirrors the
634
+ * reference engine's private `safeVerify`. */
635
+ async safeVerify(request, transcript, ctx) {
636
+ if (!this.opts.verifyCompletion) return { complete: true };
637
+ try {
638
+ return await this.opts.verifyCompletion({ request, transcript, ctx });
639
+ } catch (err) {
640
+ console.error(`[@nightowlsdev/engine-ai-sdk] verifyCompletion threw for run ${ctx.runId} \u2014 treating as complete:`, err);
641
+ return { complete: true };
642
+ }
643
+ }
644
+ /** FR-024 — resolve the host's per-run advisory system-context block. FAIL-SAFE: a throwing/rejecting resolver
645
+ * is swallowed (logged) and yields no block, so a broken context source never breaks the run. Mirrors the
646
+ * reference engine's private `safeSystemContext`. */
647
+ async safeSystemContext(ctx, input) {
648
+ if (!this.opts.systemContext) return void 0;
649
+ try {
650
+ const s = await this.opts.systemContext(ctx, input);
651
+ return s && s.trim() ? s : void 0;
652
+ } catch (err) {
653
+ console.error(`[@nightowlsdev/engine-ai-sdk] systemContext threw for run ${ctx.runId} \u2014 skipping the block:`, err);
654
+ return void 0;
655
+ }
656
+ }
657
+ /** FR-027 — resolve the host's opt-in per-request instruction-only (dynamic/imported) skills, by name.
658
+ * FAIL-SAFE: a throwing/rejecting resolver yields no dynamic skills (never breaks the run). No memoization —
659
+ * v1 has no delegation, so there is only ever ONE resolution per segment (no sub-agent to share it with);
660
+ * re-resolved fresh on every `resume()` segment too (never carried over the suspend boundary). */
661
+ async safeDynamicSkills(ctx) {
662
+ if (!this.opts.dynamicSkills) return {};
663
+ try {
664
+ const skills = await this.opts.dynamicSkills(ctx);
665
+ const out = {};
666
+ for (const s of skills) out[s.name] = s;
667
+ return out;
668
+ } catch (err) {
669
+ console.error(`[@nightowlsdev/engine-ai-sdk] dynamicSkills threw for run ${ctx.runId} \u2014 skipping:`, err);
670
+ return {};
671
+ }
672
+ }
673
+ /**
674
+ * Resolve the CONCRETE model instance for one generation launch: tier routing (a sentinel `modelId` routes to
675
+ * the configured tier; a concrete pin passes verbatim), THEN the allow-list `opts.model.resolve` (a deny
676
+ * throws — this is the security-critical re-validation the plan calls out: EVERY resolved id, tier-routed or
677
+ * not, goes through the allow-list, never just the stored one), THEN `opts.modelFactory`. Mirrors the
678
+ * reference engine's private `modelFor` (`packages/core/src/mastra-map.ts:284-292`). Called once for the
679
+ * `streamText` call's outer `model` field AND again from `prepareStep` for EVERY step (including the first) —
680
+ * so a revoked/rotated allow-list entry is caught mid-run, not just at the top of the segment.
681
+ */
682
+ async resolveModel(row, ctx) {
683
+ const effective = (0, import_core4.tierModelId)(row.modelId ?? "unknown", this.opts.tier, {
684
+ tenantId: ctx.tenantId,
685
+ agentSlug: ctx.agentSlug,
686
+ pinnedModelId: row.modelId ?? "unknown"
687
+ });
688
+ const resolvedId = await this.opts.model.resolve(effective, { tenantId: ctx.tenantId });
689
+ return this.opts.modelFactory(resolvedId, ctx.agentSlug);
690
+ }
691
+ /** Build this segment's `ToolAssemblyOpts` — identical wiring for `run()` and `resume()` (both rebuild tools
692
+ * fresh from the CURRENT agent row; only `runCtx` differs). */
693
+ toolAssemblyOpts() {
694
+ return {
695
+ resolveSkill: this.opts.resolveSkill,
696
+ connectorTools: this.opts.connectorTools,
697
+ secrets: this.opts.secrets,
698
+ scratchpad: this.opts.scratchpad,
699
+ scratchpadStore: this.opts.storage.scratchpad,
700
+ recallLane: this.opts.recallLane,
701
+ pageContext: this.opts.pageContext,
702
+ gate: (event) => this.hooks.preToolCall(event)
703
+ };
704
+ }
705
+ /**
706
+ * The shared governed `streamText` drive — Task 6's loop, generalized so both `run()` (segmentIndex 0) and
707
+ * `resume()` (segmentIndex = the snapshot's genIndex) call the SAME implementation: per-step metering + cost
708
+ * caps, mid-stream cancellation, suspend-parking (ask / native tool approvals / client actions), and
709
+ * completion-supervisor nudging. Returns the segment's terminal outcome as the generator's return value (the
710
+ * caller captures it via `const outcome = yield* this.drive(...)`); every terminal event (`done`/`run_failed`/
711
+ * `run_cancelled`/the park's `waiting`+`question`/`client_action`) is yielded from INSIDE this loop — the
712
+ * caller only needs the returned outcome for its own `onRunEnd` reporting.
713
+ */
714
+ async *drive(d) {
715
+ const { ctx, ev, emit, emitTurnUsage, segmentIndex, row, modelId, tools, systemMessages, gov, collector, signal, turnUsage, runState, requestText } = d;
716
+ let messages = d.initialMessages;
717
+ let continueNudges = 0;
718
+ const maxContinueNudges = Math.max(0, Math.floor(this.opts.maxContinueNudges ?? 2));
719
+ let transcript = "";
720
+ let incompleteMissing = null;
721
+ try {
722
+ for (; ; ) {
723
+ if (signal?.aborted) {
724
+ yield* finishCancelledSeg(d);
725
+ return "cancelled";
726
+ }
727
+ const initialModel = await this.resolveModel(row, ctx);
728
+ const remainingSteps = Math.max(1, this.opts.cost.maxSteps - turnUsage.length);
729
+ const result = (0, import_ai2.streamText)({
730
+ model: initialModel,
731
+ system: systemMessages,
732
+ messages,
733
+ tools,
734
+ stopWhen: (0, import_ai2.stepCountIs)(remainingSteps),
735
+ prepareStep: async () => ({ model: await this.resolveModel(row, ctx) }),
736
+ maxRetries: 0,
737
+ // E11 — no engine-level retries; the durable backend owns retries.
738
+ ...signal ? { abortSignal: signal } : {}
739
+ });
740
+ const pendingToolCalls = /* @__PURE__ */ new Map();
741
+ let parkedApproval = null;
742
+ for await (const part of result.fullStream) {
743
+ if (signal?.aborted) {
744
+ yield* finishCancelledSeg(d);
745
+ return "cancelled";
746
+ }
747
+ for (const mapped of mapChunks(part, { agentSlug: ctx.agentSlug, gatesApproval: (name) => this.gatesApproval(name) })) {
748
+ if (mapped.kind === "event") {
749
+ switch (mapped.type) {
750
+ case "swarm.message":
751
+ collector?.openGeneration(modelId);
752
+ if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, mapped.data.delta ?? "");
753
+ break;
754
+ case "swarm.tool_call":
755
+ collector?.openGeneration(modelId);
756
+ collector?.openTool(mapped.data.toolCallId, mapped.data.name);
757
+ pendingToolCalls.set(mapped.data.toolCallId, { name: mapped.data.name, input: mapped.data.args });
758
+ if (this.opts.verifyCompletion) {
759
+ transcript = appendTranscript(transcript, `
760
+ \xAB${ctx.agentSlug} \u2192 ${mapped.data.name}\xBB
761
+ `);
762
+ }
763
+ break;
764
+ case "swarm.tool_result":
765
+ collector?.closeTool(mapped.data.toolCallId, mapped.data.ok);
766
+ pendingToolCalls.delete(mapped.data.toolCallId);
767
+ break;
768
+ }
769
+ yield await emit(ev(mapped.type, mapped.data));
770
+ continue;
771
+ }
772
+ switch (mapped.kind) {
773
+ case "park":
774
+ parkedApproval = {
775
+ approvalId: mapped.approvalId,
776
+ toolCallId: mapped.toolCallId,
777
+ toolName: mapped.toolName,
778
+ input: mapped.input
779
+ };
780
+ break;
781
+ case "step-usage": {
782
+ gov.step();
783
+ const u = extractUsage(mapped.usage);
784
+ collector?.openGeneration(modelId);
785
+ collector?.closeGeneration(u, gov.priceOf(modelId, u));
786
+ gov.addUsage(modelId, u);
787
+ const cost = gov.costOf(modelId, u);
788
+ const generationId = `${ctx.runId}:${segmentIndex}:${turnUsage.length}`;
789
+ turnUsage.push({ slug: ctx.agentSlug, breakdown: u, cost });
790
+ yield await emit(ev("swarm.usage", { slug: ctx.agentSlug, modelId, breakdown: u, cost, generationId }));
791
+ break;
792
+ }
793
+ case "cancel":
794
+ yield* finishCancelledSeg(d);
795
+ return "cancelled";
796
+ case "failure":
797
+ yield* finishFailedSeg(d, "model", errorMessage(mapped.error), false);
798
+ return "failed";
799
+ }
800
+ }
801
+ const stop = gov.shouldStop();
802
+ if (stop.stop) {
803
+ yield* finishFailedSeg(d, "cost", stop.reason ?? "cost cap reached", false);
804
+ return "failed";
805
+ }
806
+ }
807
+ if (signal?.aborted) {
808
+ yield* finishCancelledSeg(d);
809
+ return "cancelled";
810
+ }
811
+ if (parkedApproval || pendingToolCalls.size > 0) {
812
+ let parkKind;
813
+ let toolCallId;
814
+ let toolName;
815
+ let toolInput;
816
+ let approvalId;
817
+ if (parkedApproval) {
818
+ ({ approvalId, toolCallId, toolName, input: toolInput } = parkedApproval);
819
+ parkKind = "approval";
820
+ } else {
821
+ const [id, info] = [...pendingToolCalls.entries()][0];
822
+ toolCallId = id;
823
+ toolName = info.name;
824
+ toolInput = info.input;
825
+ parkKind = toolName === import_core4.ASK_TOOL_NAME ? "ask" : "client";
826
+ }
827
+ const responseMessages = (await result.response).messages;
828
+ const snapshotMessages = [...messages, ...responseMessages];
829
+ const followupId = `${ctx.runId}:${toolCallId}`;
830
+ const genIndex = segmentIndex + 1;
831
+ const snapshot = {
832
+ v: 1,
833
+ kind: parkKind,
834
+ messages: snapshotMessages,
835
+ genIndex,
836
+ toolCallId,
837
+ toolName,
838
+ state: runState.entries(),
839
+ ...approvalId ? { approvalId } : {}
840
+ };
841
+ await recordSuspendSafe(this.opts.storage, ctx, followupId, toolCallId);
842
+ await d.runsStore.setStatus(ctx.runId, "suspended");
843
+ await d.runsStore.saveSnapshot(ctx.runId, snapshot);
844
+ if (signal?.aborted) {
845
+ yield* finishCancelledSeg(d);
846
+ return "cancelled";
847
+ }
848
+ const t = await emitTurnUsage(segmentIndex);
849
+ if (t) yield t;
850
+ yield await emit(ev("swarm.status", { state: "waiting" }));
851
+ if (parkKind === "client") {
852
+ const needsApproval = clientNeedsApprovalOf(tools[toolName]);
853
+ yield await emit(
854
+ ev("swarm.client_action", {
855
+ followupId,
856
+ toolCallId,
857
+ tool: toolName,
858
+ input: toolInput,
859
+ needsApproval,
860
+ from: ctx.agentSlug
861
+ })
862
+ );
863
+ } else if (parkKind === "approval") {
864
+ yield await emit(
865
+ ev("swarm.question", {
866
+ followupId,
867
+ toolCallId,
868
+ to: "user",
869
+ from: ctx.agentSlug,
870
+ prompt: `Approve running \`${toolName}\`? args: ${JSON.stringify(toolInput)}`,
871
+ field: { kind: "confirm", confirmLabel: "Approve", rejectLabel: "Reject" }
872
+ })
873
+ );
874
+ } else {
875
+ const askInput = toolInput ?? {};
876
+ yield await emit(
877
+ ev("swarm.question", {
878
+ followupId,
879
+ toolCallId,
880
+ to: askInput.to ?? "user",
881
+ from: ctx.agentSlug,
882
+ prompt: askInput.prompt ?? "",
883
+ field: askInput.field
884
+ })
885
+ );
886
+ }
887
+ return "suspended";
888
+ }
889
+ if (this.opts.verifyCompletion) {
890
+ const verdict = await this.safeVerify(requestText, transcript, ctx);
891
+ if (!verdict.complete && continueNudges < maxContinueNudges) {
892
+ continueNudges++;
893
+ messages = [...messages, ...(await result.response).messages, { role: "user", content: verifyNudge(verdict.missing) }];
894
+ continue;
895
+ }
896
+ incompleteMissing = verdict.complete ? null : verdict.missing ?? "The task was not completed.";
897
+ }
898
+ break;
899
+ }
900
+ } catch (err) {
901
+ if (signal?.aborted || isAbortLike(err)) {
902
+ yield* finishCancelledSeg(d);
903
+ return "cancelled";
904
+ }
905
+ yield* finishFailedSeg(d, "model", errorMessage(err), false);
906
+ return "failed";
907
+ }
908
+ if (incompleteMissing != null) {
909
+ yield* finishFailedSeg(d, "incomplete", incompleteMissing, true);
910
+ return "failed";
911
+ }
912
+ yield* finishDoneSeg(d);
913
+ return "done";
914
+ }
915
+ async *run(input, ctx, signal) {
916
+ const row = await this.opts.storage.agents.head(ctx.tenantId, ctx.agentSlug);
917
+ if (!row) throw new Error(`unknown agent: ${ctx.agentSlug}`);
918
+ const modelId = (0, import_core4.tierModelId)(row.modelId ?? "unknown", this.opts.tier, {
919
+ tenantId: ctx.tenantId,
920
+ agentSlug: ctx.agentSlug,
921
+ pinnedModelId: row.modelId ?? "unknown"
922
+ });
923
+ if (this.opts.storage.threads) {
924
+ await this.opts.storage.threads.ensure({ id: ctx.threadId, orgId: ctx.tenantId, userId: ctx.userId });
925
+ }
926
+ await this.opts.storage.runs.create({
927
+ runId: ctx.runId,
928
+ tenantId: ctx.tenantId,
929
+ userId: ctx.userId,
930
+ threadId: ctx.threadId,
931
+ agentSlug: ctx.agentSlug
932
+ });
933
+ const runsStore = this.opts.storage.runs;
934
+ const { ev, emit } = makeEmitter({ storage: this.opts.storage, onEvent: this.opts.onEvent, ctx });
935
+ const turnUsage = [];
936
+ let turnEmitted = false;
937
+ const emitTurnUsage = async (segmentIndex) => {
938
+ if (turnEmitted) return null;
939
+ turnEmitted = true;
940
+ const total = (0, import_core4.sumTurnUsage)(turnUsage);
941
+ return emit(
942
+ ev("swarm.turn_usage", {
943
+ breakdown: total.breakdown,
944
+ cost: total.cost,
945
+ bySlug: total.bySlug,
946
+ generations: turnUsage.length,
947
+ segmentIndex
948
+ })
949
+ );
950
+ };
951
+ const collector = this.telemetry ? new import_core4.SpanCollector(ctx.runId, () => Date.now(), "run", { agentSlug: ctx.agentSlug }) : null;
952
+ const runState = (0, import_core4.createRunState)();
953
+ let outcome = "failed";
954
+ if (this.opts.onRunStart) {
955
+ try {
956
+ await this.opts.onRunStart(ctx, { input, state: runState });
957
+ } catch (err) {
958
+ console.error(`[@nightowlsdev/engine-ai-sdk] onRunStart threw for run ${ctx.runId}:`, err);
959
+ }
960
+ }
961
+ const io = { ctx, runsStore, ev, emit, emitTurnUsage, segmentIndex: 0 };
962
+ const floorKey = ctx.threadId;
963
+ const me = { label: titleCase(ctx.agentSlug), runId: ctx.runId };
964
+ const floorAbort = new AbortController();
965
+ let releaseFloor = await this.floor.tryAcquire(floorKey, me);
966
+ try {
967
+ if (!releaseFloor) {
968
+ const held = await this.floor.holder(floorKey);
969
+ const position = await this.floor.queueDepth(floorKey) + 1;
970
+ yield await emit(ev("swarm.status", { state: "blocked", note: held?.label ?? "another agent", position }));
971
+ releaseFloor = await this.floor.acquire(floorKey, me, floorAbort.signal);
972
+ if (floorAbort.signal.aborted) return;
973
+ }
974
+ if (signal?.aborted) {
975
+ yield* finishCancelledSeg(io);
976
+ outcome = "cancelled";
977
+ return;
978
+ }
979
+ yield await emit(ev("swarm.status", { state: "thinking" }));
980
+ try {
981
+ await this.guardGeneration({
982
+ runId: ctx.runId,
983
+ tenantId: ctx.tenantId,
984
+ agentSlug: ctx.agentSlug,
985
+ modelId,
986
+ generationIndex: 0,
987
+ kind: "run"
988
+ });
989
+ } catch (err) {
990
+ const stage = err instanceof import_core4.ReserveDenied ? "reserve" : "exception";
991
+ yield* finishFailedSeg(io, stage, errorMessage(err), false);
992
+ outcome = "failed";
993
+ return;
994
+ }
995
+ const priorHistory = await this.history(ctx.threadId, ctx);
996
+ const userMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
997
+ await emit(ev("swarm.message", { role: "user", text: userMessage }));
998
+ const approvedToolCallIds = /* @__PURE__ */ new Set();
999
+ const runToolCtx = {
1000
+ ctx,
1001
+ state: runState,
1002
+ approvedToolCallIds,
1003
+ pageContextValue: input.context,
1004
+ // T9: the real events-derived `history()` — `recall_lane` now returns a real peer-lane transcript.
1005
+ history: (threadId, histCtx, historyOpts) => this.history(threadId, histCtx, historyOpts)
1006
+ };
1007
+ const tools = await buildTools({ row, opts: this.toolAssemblyOpts(), runCtx: runToolCtx });
1008
+ const dynamicSkillsResolved = await this.safeDynamicSkills(ctx);
1009
+ const systemMessages = await buildSystemMessages({
1010
+ row,
1011
+ opts: {
1012
+ resolveSkill: this.opts.resolveSkill ?? (() => void 0),
1013
+ softPolicy: this.opts.softPolicy,
1014
+ scratchpad: !!this.opts.scratchpad,
1015
+ loadScratchpad: this.opts.storage.scratchpad ? (container, tenantId) => this.opts.storage.scratchpad.list(tenantId, container) : void 0
1016
+ },
1017
+ ctx: { tenantId: ctx.tenantId, threadId: ctx.threadId },
1018
+ dynamicSkillsResolved,
1019
+ systemContextText: await this.safeSystemContext(ctx, input)
1020
+ });
1021
+ const gov = new import_core4.CostGovernor(this.opts.cost);
1022
+ const historyMessages = priorHistory.slice(-HISTORY_PROMPT_CAP).map((m) => m.role === "user" ? { role: "user", content: m.text } : { role: "assistant", content: m.text });
1023
+ const messages = [...historyMessages, { role: "user", content: userMessage }];
1024
+ const result = yield* this.drive({
1025
+ ...io,
1026
+ row,
1027
+ modelId,
1028
+ tools,
1029
+ systemMessages,
1030
+ initialMessages: messages,
1031
+ gov,
1032
+ collector,
1033
+ signal,
1034
+ turnUsage,
1035
+ runState,
1036
+ requestText: userMessage
1037
+ });
1038
+ outcome = result;
1039
+ } finally {
1040
+ if (this.opts.onRunEnd) {
1041
+ try {
1042
+ await this.opts.onRunEnd(ctx, { state: runState, outcome });
1043
+ } catch (err) {
1044
+ console.error(`[@nightowlsdev/engine-ai-sdk] onRunEnd threw for run ${ctx.runId}:`, err);
1045
+ }
1046
+ }
1047
+ floorAbort.abort();
1048
+ await releaseFloor?.();
1049
+ if (this.telemetry && collector) {
1050
+ const spans = collector.finish();
1051
+ try {
1052
+ await this.telemetry.export(spans);
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+ /**
1059
+ * Resume a parked run: `markFollowupAnswered` EARLY (CAS, replay-safe) → `loadSnapshot` → reload the agent
1060
+ * row fresh (same as `run()`) → re-acquire the floor → emit `swarm.answer` → preGeneration re-fire (kind
1061
+ * 'resume', at the snapshot's genIndex) → per-kind append the answer to the snapshot's messages → re-enter
1062
+ * the SHARED `drive()` with rebuilt tools (`approvedToolCallIds` pre-populated on an approval-approve, BEFORE
1063
+ * `buildTools`/`streamText` — Task 4's blocker-fix contract) + restored messages. A re-park persists
1064
+ * `genIndex = (this segment's own segmentIndex) + 1` — the same bookkeeping `drive()` already does for
1065
+ * `run()`'s park, generalized.
1066
+ *
1067
+ * ⚠ `onRunStart` deliberately does NOT fire here — this is a CONTINUATION of the run `onRunStart` already
1068
+ * announced (in `run()`), not a new one. Mirrors the reference (Mastra) engine exactly: `packages/core/src/
1069
+ * engine.ts`'s own `resume()` never calls `this.opts.onRunStart` either (only `run()` does — see that file's
1070
+ * `onRunStart` doc comment: "fires on `run()` only"). A host that seeds per-run state in `onRunStart` gets it
1071
+ * back via `snap.state` → `resumedState` below, so nothing is lost across the suspend/resume boundary.
1072
+ */
1073
+ async *resume(args, ctx, signal) {
1074
+ const rctx = { ...ctx, runId: args.runId };
1075
+ const runsStore = this.opts.storage.runs;
1076
+ const { ev, emit } = makeEmitter({ storage: this.opts.storage, onEvent: this.opts.onEvent, ctx: rctx, startTs: 1e3 });
1077
+ const snap = await runsStore.loadSnapshot(rctx.tenantId, args.runId);
1078
+ if (!snap) {
1079
+ yield await emit(ev("swarm.run_failed", { stage: "resume", message: `no suspended run: ${args.runId}`, retryable: false }));
1080
+ return;
1081
+ }
1082
+ if (!isValidAiSdkSnapshot(snap)) {
1083
+ yield await emit(
1084
+ ev("swarm.run_failed", { stage: "resume", message: "incompatible or corrupt snapshot", retryable: false })
1085
+ );
1086
+ return;
1087
+ }
1088
+ const resumedState = (0, import_core4.createRunState)(snap.state ?? void 0);
1089
+ const generationIndex = typeof snap.genIndex === "number" ? snap.genIndex : 1;
1090
+ const answered = await this.opts.storage.markFollowupAnswered?.(args.followupId, rctx.tenantId);
1091
+ if (this.opts.storage.markFollowupAnswered && answered === false) {
1092
+ yield await emit(
1093
+ ev("swarm.run_failed", { stage: "resume", message: `followup already answered: ${args.followupId}`, retryable: false })
1094
+ );
1095
+ return;
1096
+ }
1097
+ await runsStore.setStatus(args.runId, "running");
1098
+ const row = await this.opts.storage.agents.head(rctx.tenantId, rctx.agentSlug);
1099
+ if (!row) {
1100
+ try {
1101
+ await runsStore.setStatus(args.runId, "failed");
1102
+ } catch {
1103
+ }
1104
+ yield await emit(ev("swarm.run_failed", { stage: "resume", message: `unknown agent: ${rctx.agentSlug}`, retryable: false }));
1105
+ return;
1106
+ }
1107
+ const modelId = (0, import_core4.tierModelId)(row.modelId ?? "unknown", this.opts.tier, {
1108
+ tenantId: rctx.tenantId,
1109
+ agentSlug: rctx.agentSlug,
1110
+ pinnedModelId: row.modelId ?? "unknown"
1111
+ });
1112
+ const turnUsage = [];
1113
+ let turnEmitted = false;
1114
+ const emitTurnUsage = async (segmentIndex) => {
1115
+ if (turnEmitted) return null;
1116
+ turnEmitted = true;
1117
+ const total = (0, import_core4.sumTurnUsage)(turnUsage);
1118
+ return emit(
1119
+ ev("swarm.turn_usage", {
1120
+ breakdown: total.breakdown,
1121
+ cost: total.cost,
1122
+ bySlug: total.bySlug,
1123
+ generations: turnUsage.length,
1124
+ segmentIndex
1125
+ })
1126
+ );
1127
+ };
1128
+ const collector = this.telemetry ? new import_core4.SpanCollector(args.runId, () => Date.now(), "resume", { agentSlug: rctx.agentSlug }) : null;
1129
+ let outcome = "failed";
1130
+ const io = { ctx: rctx, runsStore, ev, emit, emitTurnUsage, segmentIndex: generationIndex };
1131
+ const floorKey = rctx.threadId;
1132
+ const me = { label: titleCase(rctx.agentSlug), runId: args.runId };
1133
+ const floorAbort = new AbortController();
1134
+ let releaseFloor = await this.floor.tryAcquire(floorKey, me);
1135
+ try {
1136
+ if (!releaseFloor) {
1137
+ const held = await this.floor.holder(floorKey);
1138
+ const position = await this.floor.queueDepth(floorKey) + 1;
1139
+ yield await emit(ev("swarm.status", { state: "blocked", note: held?.label ?? "another agent", position }));
1140
+ releaseFloor = await this.floor.acquire(floorKey, me, floorAbort.signal);
1141
+ if (floorAbort.signal.aborted) return;
1142
+ }
1143
+ if (signal?.aborted) {
1144
+ yield* finishCancelledSeg(io);
1145
+ outcome = "cancelled";
1146
+ return;
1147
+ }
1148
+ yield await emit(ev("swarm.answer", { followupId: args.followupId, from: "user", answer: args.answer }));
1149
+ try {
1150
+ await this.guardGeneration({ runId: args.runId, tenantId: rctx.tenantId, agentSlug: rctx.agentSlug, modelId, generationIndex, kind: "resume" });
1151
+ } catch (err) {
1152
+ const stage = err instanceof import_core4.ReserveDenied ? "reserve" : "exception";
1153
+ yield* finishFailedSeg(io, stage, errorMessage(err), false);
1154
+ outcome = "failed";
1155
+ return;
1156
+ }
1157
+ yield await emit(ev("swarm.status", { state: "thinking" }));
1158
+ const approvedToolCallIds = /* @__PURE__ */ new Set();
1159
+ let messages = [...snap.messages];
1160
+ if (snap.kind === "ask") {
1161
+ const toolResult = {
1162
+ type: "tool-result",
1163
+ toolCallId: snap.toolCallId,
1164
+ toolName: snap.toolName,
1165
+ output: { type: "json", value: { answer: args.answer } }
1166
+ };
1167
+ const toolMessage = { role: "tool", content: [toolResult] };
1168
+ messages = [...messages, toolMessage];
1169
+ } else if (snap.kind === "approval") {
1170
+ const approved = (0, import_engine_spi2.isApproved)(args.answer);
1171
+ if (approved) approvedToolCallIds.add(snap.toolCallId);
1172
+ const approvalResponse = { type: "tool-approval-response", approvalId: snap.approvalId ?? "", approved };
1173
+ const toolMessage = { role: "tool", content: [approvalResponse] };
1174
+ messages = [...messages, toolMessage];
1175
+ } else {
1176
+ const clientAnswer = args.answer;
1177
+ const isError = !!(clientAnswer && typeof clientAnswer === "object" && "error" in clientAnswer && clientAnswer.error);
1178
+ const outputValue = !isError && clientAnswer && typeof clientAnswer === "object" && "output" in clientAnswer ? clientAnswer.output : clientAnswer;
1179
+ const output = isError ? { type: "error-text", value: String(clientAnswer.error) } : { type: "json", value: outputValue };
1180
+ const toolResult = { type: "tool-result", toolCallId: snap.toolCallId, toolName: snap.toolName, output };
1181
+ const toolMessage = { role: "tool", content: [toolResult] };
1182
+ messages = [...messages, toolMessage];
1183
+ }
1184
+ const runToolCtx = {
1185
+ ctx: rctx,
1186
+ state: resumedState,
1187
+ approvedToolCallIds,
1188
+ pageContextValue: args.context,
1189
+ // R6: re-attach the host's (possibly refreshed) page context on resume.
1190
+ // T9: wired to the real events-derived `history()` (same binding as run()).
1191
+ history: (threadId, histCtx, historyOpts) => this.history(threadId, histCtx, historyOpts)
1192
+ };
1193
+ const tools = await buildTools({ row, opts: this.toolAssemblyOpts(), runCtx: runToolCtx });
1194
+ const dynamicSkillsResolved = await this.safeDynamicSkills(rctx);
1195
+ const systemMessages = await buildSystemMessages({
1196
+ row,
1197
+ opts: {
1198
+ resolveSkill: this.opts.resolveSkill ?? (() => void 0),
1199
+ softPolicy: this.opts.softPolicy,
1200
+ scratchpad: !!this.opts.scratchpad,
1201
+ loadScratchpad: this.opts.storage.scratchpad ? (container, tenantId) => this.opts.storage.scratchpad.list(tenantId, container) : void 0
1202
+ },
1203
+ ctx: { tenantId: rctx.tenantId, threadId: rctx.threadId },
1204
+ dynamicSkillsResolved,
1205
+ // FR-024 gotcha: systemContext is NEVER re-resolved on resume (only run() sets it) — mirrors the
1206
+ // reference engine's own resume rc, which never attaches SYSTEM_CONTEXT_KEY.
1207
+ systemContextText: void 0
1208
+ });
1209
+ const gov = new import_core4.CostGovernor(this.opts.cost);
1210
+ const requestText = await this.recallRequest(rctx);
1211
+ const result = yield* this.drive({
1212
+ ...io,
1213
+ row,
1214
+ modelId,
1215
+ tools,
1216
+ systemMessages,
1217
+ initialMessages: messages,
1218
+ gov,
1219
+ collector,
1220
+ signal,
1221
+ turnUsage,
1222
+ runState: resumedState,
1223
+ requestText
1224
+ });
1225
+ outcome = result;
1226
+ } finally {
1227
+ if (this.opts.onRunEnd) {
1228
+ try {
1229
+ await this.opts.onRunEnd(rctx, { state: resumedState, outcome });
1230
+ } catch (err) {
1231
+ console.error(`[@nightowlsdev/engine-ai-sdk] onRunEnd threw for resume ${args.runId}:`, err);
1232
+ }
1233
+ }
1234
+ floorAbort.abort();
1235
+ await releaseFloor?.();
1236
+ if (this.telemetry && collector) {
1237
+ const spans = collector.finish();
1238
+ try {
1239
+ await this.telemetry.export(spans);
1240
+ } catch {
1241
+ }
1242
+ }
1243
+ }
1244
+ }
1245
+ /**
1246
+ * T9 — events-derived thread history (no Mastra memory in this engine: the events log IS the store).
1247
+ * `container(threadId)` resolves the root container; `events.listForContainer` (optional on `StorageAdapter`
1248
+ * — absent ⇒ `[]`, matching the reference's "no memory ⇒ []" degradation) returns the FULL container log
1249
+ * (root + any lane sub-threads), each event tagged with its OWN run's `threadId`. Filtered LANE-EXACT to the
1250
+ * REQUESTED `threadId` (so a root read never leaks a lane's side-chat and vice versa), then folded into
1251
+ * `SwarmMessage[]` by `messagesFromEvents` (structural attribution; decode-only legacy prefix stripping — see
1252
+ * its doc comment). Hard-capped to the most recent `limit` (default 200, mirrors the reference).
1253
+ */
1254
+ async history(threadId, ctx, opts) {
1255
+ const limit = opts?.limit ?? 200;
1256
+ const container = containerOf(threadId);
1257
+ const events = await this.opts.storage.events.listForContainer?.(ctx.tenantId, container) ?? [];
1258
+ const laneEvents = events.filter((e) => e.threadId === threadId);
1259
+ const mapped = messagesFromEvents(threadId, laneEvents);
1260
+ return mapped.length > limit ? mapped.slice(-limit) : mapped;
1261
+ }
1262
+ /** T9 helper — one container's `ThreadSummary`: title = its first (oldest) user message's text, falling back
1263
+ * to the bare container id (an empty/title-less container, matching the reference's "no Mastra row" fallback
1264
+ * shape: `{ threadId: container, title: container, lastActivityAt: 0 }`). `lastActivityAt` = the max `seq`
1265
+ * seen on the container's BARE (non-lane) events (T9-review fix): `seq` is the storage-assigned, GLOBALLY
1266
+ * monotonic append counter, so a newer run's events always rank above an older run's — whereas `ts` is a
1267
+ * per-segment ordinal (restarts at 0/1000 every segment, see `emit.ts`) and is meaningless across runs. Same
1268
+ * seq-over-ts preference the react timeline established (react/timeline.ts). NOT a wall-clock epoch (no
1269
+ * adapter in the `StorageAdapter` contract exposes one for runs/events) — a recency-ORDERING proxy only.
1270
+ */
1271
+ async threadSummaryFor(container, ctx) {
1272
+ const events = await this.opts.storage.events.listForContainer?.(ctx.tenantId, container) ?? [];
1273
+ const bare = events.filter((e) => e.threadId === container);
1274
+ if (bare.length === 0) return { threadId: container, title: container, lastActivityAt: 0 };
1275
+ const firstUser = bare.find(
1276
+ (e) => e.type === "swarm.message" && e.data.role === "user"
1277
+ );
1278
+ const title = (firstUser?.data.text ?? "").trim() || container;
1279
+ const lastActivityAt = Math.max(...bare.map((e) => e.seq ?? 0));
1280
+ return { threadId: container, title, lastActivityAt };
1281
+ }
1282
+ /**
1283
+ * T9 — R14 participation-based thread listing (mirrors the reference `listThreads`, minus the Mastra memory
1284
+ * dependency): `runs.listUserContainers` (optional) gives the containers this user has a run in,
1285
+ * newest-active first; each is summarized via `threadSummaryFor` (concurrently, order preserved).
1286
+ * FALLBACK (an adapter without `listUserContainers`): the reference degrades to the CURRENT container under
1287
+ * multi-user (no cross-container Mastra listing to fall back on here either) — a single-entry list for
1288
+ * `ctx.threadId`'s own container.
1289
+ */
1290
+ async listThreads(ctx, opts) {
1291
+ const limit = opts?.limit ?? 50;
1292
+ const lister = this.opts.storage.runs.listUserContainers;
1293
+ if (lister) {
1294
+ const containers = await lister.call(this.opts.storage.runs, ctx.tenantId, ctx.userId, limit);
1295
+ const out = await Promise.all(containers.map((c) => this.threadSummaryFor(c, ctx)));
1296
+ return out.slice(0, limit);
1297
+ }
1298
+ const container = containerOf(ctx.threadId);
1299
+ return [await this.threadSummaryFor(container, ctx)];
1300
+ }
1301
+ /** The PUBLIC entries of a conversation's scratchpad (empty array when the feature is off or unset).
1302
+ * Mirrors the reference engine's `scratchpadPublic` verbatim (storage-backed, engine-neutral). */
1303
+ async scratchpadPublic(container, ctx) {
1304
+ const all = await this.opts.storage.scratchpad?.list(ctx.tenantId, container) ?? [];
1305
+ return all.filter((e) => e.section === "public");
1306
+ }
1307
+ /** In-flight runs (running|suspended) for a container + its lanes. Mirrors the reference engine's
1308
+ * `activeRuns` verbatim (storage-backed, engine-neutral). */
1309
+ async activeRuns(container, ctx) {
1310
+ return this.opts.storage.runs.listActive(ctx.tenantId, container);
1311
+ }
1312
+ /** The full, globally-ordered event log for a thread's CONTAINER (all its runs + lane sub-threads) — unlike
1313
+ * `history()`, NOT lane-filtered: this is the rich raw timeline (tool calls + status, not just messages).
1314
+ * Mirrors the reference engine's `threadEvents` verbatim (storage-backed, engine-neutral). Returns `[]` when
1315
+ * the store has no `events.listForContainer`. */
1316
+ async threadEvents(threadId, ctx) {
1317
+ const container = containerOf(threadId);
1318
+ return await this.opts.storage.events.listForContainer?.(ctx.tenantId, container) ?? [];
1319
+ }
1320
+ /**
1321
+ * T9 (REQUIRED on `Engine`) — the tenant's agent roster as wall-safe `AgentSummary[]`: `agents.listSlugs` →
1322
+ * `head` per slug (dropping any slug whose head resolves to `null`, a race with a mid-listing deletion).
1323
+ * `delegateSlugs` is ALWAYS `[]` — a deliberate divergence from the reference engine (which surfaces the
1324
+ * stored `AgentVersion.delegateSlugs`): this engine's `capabilities.delegation` is `false` (v1, single-agent),
1325
+ * so there is no addressable delegate graph to report regardless of what a row happens to store.
1326
+ */
1327
+ async listAgents(ctx) {
1328
+ const slugs = await this.opts.storage.agents.listSlugs(ctx.tenantId);
1329
+ const rows = await Promise.all(slugs.map((s) => this.opts.storage.agents.head(ctx.tenantId, s)));
1330
+ return rows.filter((r) => !!r).map((r) => ({ slug: r.slug, name: titleCase(r.slug), role: r.role, delegateSlugs: [] }));
1331
+ }
1332
+ };
1333
+
1334
+ // src/index.ts
1335
+ function aiSdkEngine({ durable = false } = {}) {
1336
+ return (opts) => new AiSdkEngine(opts, { durable });
1337
+ }
1338
+ var nightOwlsPlugin = {
1339
+ name: "engine-ai-sdk",
1340
+ version: "0.0.0",
1341
+ kind: "engine",
1342
+ pkg: "@nightowlsdev/engine-ai-sdk",
1343
+ description: "The AI-SDK-native engine as a named engine package (single-agent v1, governed ai@6 streamText loop).",
1344
+ env: [],
1345
+ config: {
1346
+ import: `import { aiSdkEngine } from "@nightowlsdev/engine-ai-sdk";`,
1347
+ snippet: `engine = aiSdkEngine();`,
1348
+ marker: "engine"
1349
+ },
1350
+ // Print-only (idempotent) — runs on init/install + `owl init engine-ai-sdk`. NEVER connects anywhere.
1351
+ init: (ctx) => ctx.log("engine-ai-sdk wired \u2014 defineSwarm({ engine: aiSdkEngine() })."),
1352
+ commands: []
1353
+ };
1354
+ // Annotate the CommonJS export names for ESM import in node:
1355
+ 0 && (module.exports = {
1356
+ AI_SDK_ENGINE_CAPABILITIES,
1357
+ AiSdkEngine,
1358
+ aiSdkEngine,
1359
+ nightOwlsPlugin
1360
+ });