@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/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/index.cjs +1360 -0
- package/dist/index.d.cts +210 -0
- package/dist/index.d.ts +210 -0
- package/dist/index.js +1349 -0
- package/package.json +53 -0
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
|
+
});
|