@openbkn/bkn-sdk 0.1.2 → 0.1.3

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.
@@ -20,6 +20,14 @@ var HttpError = class extends Error {
20
20
  this.hint = hint;
21
21
  }
22
22
  };
23
+ var ToolError = class extends Error {
24
+ code;
25
+ constructor(message, code) {
26
+ super(message);
27
+ this.name = "ToolError";
28
+ if (code) this.code = code;
29
+ }
30
+ };
23
31
  var InputError = class extends Error {
24
32
  constructor(message) {
25
33
  super(message);
@@ -38,14 +46,15 @@ function formatError(err2) {
38
46
  if (err2 instanceof HttpError) {
39
47
  const serverMsg = serverError(err2.body);
40
48
  if (err2.status === 401) {
41
- const next = err2.hint ?? "Run `openbkn auth login` and retry.";
42
- return `Not authorized (HTTP 401)${serverMsg ? `: ${serverMsg}` : ""}. ${next}`;
49
+ const next2 = err2.hint ?? "Run `openbkn auth login` and retry.";
50
+ return `Not authorized (HTTP 401)${serverMsg ? `: ${serverMsg}` : ""}. ${next2}`;
43
51
  }
44
52
  if (err2.status === 403) {
45
53
  return `Forbidden (HTTP 403)${serverMsg ? `: ${serverMsg}` : " \u2014 admin privileges required"}.`;
46
54
  }
47
55
  const detail = err2.body ? `: ${truncate(err2.body, 500)}` : "";
48
- return `Request failed (HTTP ${err2.status} ${err2.statusText})${detail}`;
56
+ const next = err2.hint ? ` ${err2.hint}` : "";
57
+ return `Request failed (HTTP ${err2.status} ${err2.statusText})${detail}${next}`;
49
58
  }
50
59
  if (err2 instanceof Error) {
51
60
  const cause = err2.cause;
@@ -81,10 +90,23 @@ import { spawn } from "child_process";
81
90
 
82
91
  // src/api/tls.ts
83
92
  import { Agent, FormData as UndiciFormData, fetch as undiciFetch } from "undici";
84
- var insecureAgent;
85
- function insecureDispatcher() {
86
- insecureAgent ??= new Agent({ connect: { rejectUnauthorized: false } });
87
- return insecureAgent;
93
+ var UNDICI_HEADERS_TIMEOUT_MS = 3e5;
94
+ var agents = /* @__PURE__ */ new Map();
95
+ function dispatcherFor(insecure, headersTimeoutMs) {
96
+ const key = `${insecure}|${headersTimeoutMs ?? ""}`;
97
+ let agent = agents.get(key);
98
+ if (!agent) {
99
+ agent = new Agent({
100
+ ...insecure ? { connect: { rejectUnauthorized: false } } : {},
101
+ ...headersTimeoutMs === void 0 ? {} : (
102
+ // undici also enforces a body deadline; a request that waits this
103
+ // long for headers is not going to stream its body any faster.
104
+ { headersTimeout: headersTimeoutMs, bodyTimeout: headersTimeoutMs }
105
+ )
106
+ });
107
+ agents.set(key, agent);
108
+ }
109
+ return agent;
88
110
  }
89
111
  function isFormData(body) {
90
112
  return typeof body === "object" && body !== null && body[Symbol.toStringTag] === "FormData";
@@ -98,12 +120,13 @@ function toUndiciBody(body) {
98
120
  }
99
121
  return form2;
100
122
  }
101
- function tlsFetch(insecure, url, init) {
102
- if (!insecure) return fetch(url, init);
123
+ function tlsFetch(insecure, url, init, headersTimeoutMs) {
124
+ const needsAgent = headersTimeoutMs !== void 0 && headersTimeoutMs > UNDICI_HEADERS_TIMEOUT_MS;
125
+ if (!insecure && !needsAgent) return fetch(url, init);
103
126
  return undiciFetch(url, {
104
127
  ...init,
105
128
  ...init?.body === void 0 || init?.body === null ? {} : { body: toUndiciBody(init.body) },
106
- dispatcher: insecureDispatcher()
129
+ dispatcher: dispatcherFor(insecure === true, needsAgent ? headersTimeoutMs : void 0)
107
130
  });
108
131
  }
109
132
 
@@ -426,33 +449,59 @@ async function request(ctx, path, init = {}) {
426
449
  const hasBody = init.body !== void 0;
427
450
  const controller = new AbortController();
428
451
  const timer = setTimeout(() => controller.abort(), init.timeoutMs ?? DEFAULT_TIMEOUT_MS);
429
- const send = () => tlsFetch(ctx.insecure, url, {
430
- method: init.method ?? (hasBody ? "POST" : "GET"),
431
- headers: buildHeaders(ctx, {
432
- ...hasBody ? { "content-type": "application/json" } : {},
433
- ...init.headers
434
- }),
435
- body: hasBody ? JSON.stringify(init.body) : void 0,
436
- redirect: init.redirect,
437
- signal: controller.signal
438
- });
452
+ const send = () => tlsFetch(
453
+ ctx.insecure,
454
+ url,
455
+ {
456
+ method: init.method ?? (hasBody ? "POST" : "GET"),
457
+ headers: buildHeaders(ctx, {
458
+ ...hasBody ? { "content-type": "application/json" } : {},
459
+ ...init.headers
460
+ }),
461
+ body: hasBody ? JSON.stringify(init.body) : void 0,
462
+ redirect: init.redirect,
463
+ signal: controller.signal
464
+ },
465
+ init.headersTimeoutMs
466
+ );
439
467
  try {
440
468
  let res = await send();
441
469
  if (res.status === 401 && ctx.refresh && await tryRefresh(ctx)) {
442
470
  res = await send();
443
471
  }
444
472
  const text = await res.text();
445
- if (!res.ok) throw new HttpError(res.status, res.statusText, text, hintFor(ctx, res.status));
473
+ if (!res.ok) {
474
+ throw new HttpError(res.status, res.statusText, text, hintFor(ctx, res.status, text));
475
+ }
446
476
  return text ? JSON.parse(text) : void 0;
447
477
  } finally {
448
478
  clearTimeout(timer);
449
479
  }
450
480
  }
451
- function hintFor(ctx, status2) {
481
+ function hintFor(ctx, status2, body) {
452
482
  if (status2 === 401 && ctx.token.startsWith("bak_")) {
453
483
  return "AppKey invalid / expired / revoked / owner disabled \u2014 re-issue with `openbkn appkey create` (or `appkey regenerate <id>`). Do not auto-retry.";
454
484
  }
455
- return void 0;
485
+ return lifecycleHint(body);
486
+ }
487
+ var LIFECYCLE_ACTIONS = /* @__PURE__ */ new Set([
488
+ "create_conversation",
489
+ "start_interaction",
490
+ "ensure_operation",
491
+ "bkn_start_interaction"
492
+ ]);
493
+ function lifecycleHint(body) {
494
+ if (!LIFECYCLE_ACTIONS.has(requiredAction(body) ?? "")) return void 0;
495
+ return "This deploy requires a managed lifecycle session: the request needs a `bkn_context` with conversation_id and interaction_id. Easiest fix: use the `openbkn bkn` / `openbkn context` commands, which open and release one for you. To do it by hand, check `openbkn context info` for the deploy's lifecycle tools \u2014 where it lists `bkn_create_conversation`, call that first and pass the conversation_id it returns to `bkn_start_interaction`; where it does not, `bkn_start_interaction` alone returns both ids. Either way: `openbkn context tool-call <kn-id> <tool> --args '{...}'`.";
496
+ }
497
+ function requiredAction(body) {
498
+ try {
499
+ const parsed = JSON.parse(body);
500
+ const action = parsed.error?.required_action;
501
+ return typeof action === "string" ? action : void 0;
502
+ } catch {
503
+ return void 0;
504
+ }
456
505
  }
457
506
  async function tryRefresh(ctx) {
458
507
  if (!ctx.refresh) return false;
@@ -1194,7 +1243,7 @@ function setSkillMembers(agent, members) {
1194
1243
  if (!config.skills || typeof config.skills !== "object") config.skills = {};
1195
1244
  config.skills.skills = members;
1196
1245
  }
1197
- function agents(ctx) {
1246
+ function agents2(ctx) {
1198
1247
  return {
1199
1248
  list: (opts) => listAgents(ctx, opts),
1200
1249
  get: (agentId) => getAgent(ctx, agentId),
@@ -1243,6 +1292,9 @@ function agents(ctx) {
1243
1292
  };
1244
1293
  }
1245
1294
 
1295
+ // src/api/lifecycle.ts
1296
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
1297
+
1246
1298
  // src/api/context-loader.ts
1247
1299
  var MCP_PATH = "/api/agent-retrieval/v1/mcp";
1248
1300
  var PROTOCOL = "2024-11-05";
@@ -1280,18 +1332,25 @@ function parseBody(text) {
1280
1332
  throw new Error(`Context-loader returned invalid JSON: ${text.slice(0, 200)}`);
1281
1333
  }
1282
1334
  }
1283
- async function post(ctx, knId, sessionId, body) {
1284
- const res = await authFetch(
1285
- ctx,
1286
- () => tlsFetch(ctx.insecure, mcpUrl(ctx), {
1287
- method: "POST",
1288
- headers: headers(ctx, knId, sessionId),
1289
- body: JSON.stringify(body)
1290
- })
1291
- );
1292
- const text = await res.text();
1293
- if (!res.ok) throw new HttpError(res.status, res.statusText, text);
1294
- return { res, text };
1335
+ async function post(ctx, knId, sessionId, body, timeoutMs) {
1336
+ const controller = timeoutMs === void 0 ? void 0 : new AbortController();
1337
+ const timer = controller === void 0 ? void 0 : setTimeout(() => controller.abort(), timeoutMs);
1338
+ try {
1339
+ const res = await authFetch(
1340
+ ctx,
1341
+ () => tlsFetch(ctx.insecure, mcpUrl(ctx), {
1342
+ method: "POST",
1343
+ headers: headers(ctx, knId, sessionId),
1344
+ body: JSON.stringify(body),
1345
+ ...controller ? { signal: controller.signal } : {}
1346
+ })
1347
+ );
1348
+ const text = await res.text();
1349
+ if (!res.ok) throw new HttpError(res.status, res.statusText, text);
1350
+ return { res, text };
1351
+ } finally {
1352
+ if (timer) clearTimeout(timer);
1353
+ }
1295
1354
  }
1296
1355
  async function ensureSession(ctx, knId) {
1297
1356
  const key = `${mcpUrl(ctx)}:${knId}`;
@@ -1314,31 +1373,103 @@ async function ensureSession(ctx, knId) {
1314
1373
  sessions.set(key, { id: sessionId, at: Date.now() });
1315
1374
  return sessionId;
1316
1375
  }
1317
- function unwrap(parsed) {
1376
+ function toolErrorCode(structuredContent) {
1377
+ const code = structuredContent?.error?.code;
1378
+ return typeof code === "string" ? code : void 0;
1379
+ }
1380
+ function unwrapToolResult(parsed) {
1318
1381
  const rpc = parsed;
1319
1382
  if (rpc.error) throw new Error(`Context-loader error: ${rpc.error.message}`);
1320
1383
  const result = rpc.result;
1321
- if (result === void 0) return parsed;
1384
+ if (result === void 0) return { value: parsed };
1385
+ const structuredContent = result.structuredContent;
1386
+ const receipt = structuredContent?.bkn_receipt;
1322
1387
  const content = result.content;
1388
+ if (result.isError === true) {
1389
+ const message = Array.isArray(content) && content[0] && typeof content[0].text === "string" ? content[0].text : "tool call failed";
1390
+ throw new ToolError(`Context-loader error: ${message}`, toolErrorCode(structuredContent));
1391
+ }
1323
1392
  if (Array.isArray(content) && content[0] && typeof content[0].text === "string") {
1324
1393
  try {
1325
- return JSON.parse(content[0].text);
1394
+ return { value: JSON.parse(content[0].text), receipt };
1326
1395
  } catch {
1327
- return { raw: content[0].text };
1396
+ if (structuredContent !== void 0) {
1397
+ return { value: structuredContent, receipt };
1398
+ }
1399
+ return { value: { raw: content[0].text }, receipt };
1328
1400
  }
1329
1401
  }
1330
- return result;
1402
+ return { value: result, receipt };
1403
+ }
1404
+ function toolCallParams(name, args, options) {
1405
+ const meta = {
1406
+ ...options?.hostConversationKey ? { "openbkn.ai/host-conversation-key": options.hostConversationKey } : {},
1407
+ ...options?.clientInvocationId ? { "openbkn.ai/client-invocation-id": options.clientInvocationId } : {}
1408
+ };
1409
+ return {
1410
+ name,
1411
+ arguments: args,
1412
+ ...Object.keys(meta).length > 0 ? { _meta: meta } : {}
1413
+ };
1414
+ }
1415
+ async function callToolRaw(ctx, knId, name, args, options, timeoutMs) {
1416
+ const operationCtx = operationContext(ctx);
1417
+ const sessionId = await ensureSession(operationCtx, knId);
1418
+ const { text } = await post(
1419
+ operationCtx,
1420
+ knId,
1421
+ sessionId,
1422
+ {
1423
+ jsonrpc: "2.0",
1424
+ method: "tools/call",
1425
+ params: toolCallParams(name, args, options),
1426
+ id: nextId()
1427
+ },
1428
+ timeoutMs
1429
+ );
1430
+ return unwrapToolResult(parseBody(text)).value;
1431
+ }
1432
+ var LIFECYCLE_TOOLS = /* @__PURE__ */ new Set([
1433
+ "bkn_create_conversation",
1434
+ "bkn_resume_conversation",
1435
+ "bkn_start_interaction",
1436
+ "bkn_complete_interaction",
1437
+ "bkn_finish_interaction",
1438
+ "bkn_fail_interaction",
1439
+ "bkn_cancel_interaction",
1440
+ "bkn_handoff_interaction",
1441
+ "bkn_close_conversation",
1442
+ "bkn_get_operation",
1443
+ "bkn_retry_operation",
1444
+ "bkn_get_receipt"
1445
+ ]);
1446
+ function callTool(ctx, knId, name, args, options) {
1447
+ if (LIFECYCLE_TOOLS.has(name)) return callToolRaw(ctx, knId, name, args, options);
1448
+ if (args.bkn_context !== void 0) return callToolRaw(ctx, knId, name, args, options);
1449
+ return withManagedLifecycle(
1450
+ ctx,
1451
+ knId,
1452
+ questionFor(name, args),
1453
+ (bknContext) => callToolRaw(ctx, knId, name, bknContext ? { ...args, bkn_context: bknContext } : args, options)
1454
+ );
1455
+ }
1456
+ function questionFor(name, args) {
1457
+ return typeof args.query === "string" && args.query ? args.query : name;
1331
1458
  }
1332
- async function callTool(ctx, knId, name, args) {
1459
+ async function callManagedTool(ctx, knId, name, args, options) {
1333
1460
  const operationCtx = operationContext(ctx);
1334
1461
  const sessionId = await ensureSession(operationCtx, knId);
1335
1462
  const { text } = await post(operationCtx, knId, sessionId, {
1336
1463
  jsonrpc: "2.0",
1337
1464
  method: "tools/call",
1338
- params: { name, arguments: args },
1465
+ params: toolCallParams(name, args, options),
1339
1466
  id: nextId()
1340
1467
  });
1341
- return unwrap(parseBody(text));
1468
+ const result = unwrapToolResult(parseBody(text));
1469
+ if (!result.receipt) {
1470
+ throw new Error("Context-loader managed tool response did not include bkn_receipt");
1471
+ }
1472
+ return { value: result.value, receipt: result.receipt };
1342
1473
  }
1343
1474
  async function callMethod(ctx, knId, method, params = {}) {
1344
1475
  const operationCtx = operationContext(ctx);
@@ -1406,6 +1537,225 @@ function getPrompt(ctx, knId, name, args = {}) {
1406
1537
  return callMethod(ctx, knId, "prompts/get", { name, arguments: args });
1407
1538
  }
1408
1539
 
1540
+ // src/api/lifecycle.ts
1541
+ var V1_MARKER = "bkn_create_conversation";
1542
+ var V2_MARKER = "bkn_start_interaction";
1543
+ var STALE_SESSION_CODES = /* @__PURE__ */ new Set([
1544
+ "conversation_required",
1545
+ "interaction_required",
1546
+ "interaction_terminal",
1547
+ "interaction_in_progress",
1548
+ "lease_expired",
1549
+ "lease_invalid",
1550
+ "lease_superseded"
1551
+ ]);
1552
+ var PROCESS_ID = randomUUID2();
1553
+ var generation = 0;
1554
+ var AGENT_NAME = "openbkn-sdk";
1555
+ var RELEASE_TIMEOUT_MS = 3e3;
1556
+ var contracts = /* @__PURE__ */ new Map();
1557
+ var PROBE_FAILURE_TTL_MS = 3e4;
1558
+ var probeFailures = /* @__PURE__ */ new Map();
1559
+ var sessions2 = /* @__PURE__ */ new Map();
1560
+ function lifecycleContract(ctx) {
1561
+ const failureKey = `${ctx.baseUrl}\0${identityOf(ctx)}`;
1562
+ const failedAt = probeFailures.get(failureKey);
1563
+ if (failedAt !== void 0) {
1564
+ if (Date.now() - failedAt < PROBE_FAILURE_TTL_MS) return Promise.resolve("none");
1565
+ probeFailures.delete(failureKey);
1566
+ }
1567
+ let pending = contracts.get(ctx.baseUrl);
1568
+ if (!pending) {
1569
+ pending = mcpInfo(ctx).then((info) => {
1570
+ const names = toolNames(info);
1571
+ if (names.includes(V1_MARKER)) return "managed-v1";
1572
+ return names.includes(V2_MARKER) ? "managed-v2" : "none";
1573
+ });
1574
+ contracts.set(ctx.baseUrl, pending);
1575
+ pending.catch((err2) => {
1576
+ contracts.delete(ctx.baseUrl);
1577
+ if (!isAuthFailure(err2)) probeFailures.set(failureKey, Date.now());
1578
+ });
1579
+ }
1580
+ return pending.catch(() => "none");
1581
+ }
1582
+ function isAuthFailure(err2) {
1583
+ return err2 instanceof HttpError && (err2.status === 401 || err2.status === 403);
1584
+ }
1585
+ function toolNames(info) {
1586
+ const tools = info?.tools;
1587
+ if (!Array.isArray(tools)) return [];
1588
+ return tools.flatMap((tool) => typeof tool?.name === "string" ? [tool.name] : []);
1589
+ }
1590
+ function callerOwnedSession(ctx) {
1591
+ const conversationId = ctx.trace?.conversationId;
1592
+ const interactionId = ctx.trace?.interactionId;
1593
+ return conversationId && interactionId ? { conversationId, interactionId } : void 0;
1594
+ }
1595
+ function readId(result, field, tool) {
1596
+ const value = result?.[field];
1597
+ if (typeof value !== "string" || !value) {
1598
+ throw new Error(`Managed lifecycle: ${tool} returned no ${field}.`);
1599
+ }
1600
+ return value;
1601
+ }
1602
+ function callerNamedConversation(ctx) {
1603
+ return ctx.trace?.interactionId ? void 0 : ctx.trace?.conversationId;
1604
+ }
1605
+ async function openSession(ctx, knId, contract, question) {
1606
+ generation += 1;
1607
+ const named = callerNamedConversation(ctx);
1608
+ if (contract === "managed-v2") {
1609
+ const started2 = await callToolRaw(ctx, knId, V2_MARKER, {
1610
+ question,
1611
+ // The name is fixed when the conversation is created, so it belongs only
1612
+ // on the call that creates one. Joining a conversation the caller named
1613
+ // and relabelling it `openbkn-sdk` would rewrite their attribution — the
1614
+ // v1 join below has always omitted it, and this is the same decision.
1615
+ ...named ? { conversation_id: named } : (
1616
+ // Display-only, but the only thing separating a session the SDK
1617
+ // opened from a real agent's in a Trace listing.
1618
+ { agent_name: AGENT_NAME }
1619
+ )
1620
+ });
1621
+ return {
1622
+ contract,
1623
+ ctx,
1624
+ knId,
1625
+ conversationId: readId(started2, "conversation_id", V2_MARKER),
1626
+ interactionId: readId(started2, "interaction_id", V2_MARKER)
1627
+ };
1628
+ }
1629
+ if (named) {
1630
+ const started2 = await callToolRaw(ctx, knId, V2_MARKER, {
1631
+ conversation_id: named,
1632
+ idempotency_key: `start:${PROCESS_ID}:${generation}`,
1633
+ question
1634
+ });
1635
+ return {
1636
+ contract,
1637
+ ctx,
1638
+ knId,
1639
+ conversationId: named,
1640
+ interactionId: readId(started2, "interaction_id", V2_MARKER)
1641
+ };
1642
+ }
1643
+ const conversation = await callToolRaw(ctx, knId, V1_MARKER, {
1644
+ external_conversation_key: `cli:${PROCESS_ID}:${generation}`,
1645
+ // Nothing closes this conversation: a CLI invocation has no answer to close
1646
+ // over, and v1's closure manifest must enumerate every operation it
1647
+ // produced. one_shot hands it to the server's idle sweeper instead.
1648
+ one_shot: true
1649
+ });
1650
+ const conversationId = readId(conversation, "conversation_id", V1_MARKER);
1651
+ const started = await callToolRaw(ctx, knId, V2_MARKER, {
1652
+ conversation_id: conversationId,
1653
+ idempotency_key: `start:${PROCESS_ID}:${generation}`,
1654
+ question
1655
+ });
1656
+ return {
1657
+ contract,
1658
+ ctx,
1659
+ knId,
1660
+ conversationId,
1661
+ interactionId: readId(started, "interaction_id", V2_MARKER)
1662
+ };
1663
+ }
1664
+ function identityOf(ctx) {
1665
+ return createHash("sha256").update(`${ctx.token}\0${ctx.businessDomain}`).digest("hex").slice(0, 16);
1666
+ }
1667
+ function sessionKey(ctx, knId) {
1668
+ return `${ctx.baseUrl}\0${knId}\0${identityOf(ctx)}\0${ctx.trace?.conversationId ?? ""}`;
1669
+ }
1670
+ function ensureSession2(ctx, knId, contract, question) {
1671
+ const key = sessionKey(ctx, knId);
1672
+ const cached = sessions2.get(key);
1673
+ if (cached) return cached;
1674
+ const opening = openSession(ctx, knId, contract, question);
1675
+ sessions2.set(key, opening);
1676
+ opening.catch(() => sessions2.delete(key));
1677
+ return opening;
1678
+ }
1679
+ function newOperationKey() {
1680
+ return `op:${randomUUID2()}`;
1681
+ }
1682
+ function contextFor(session, contract) {
1683
+ return {
1684
+ conversation_id: session.conversationId,
1685
+ interaction_id: session.interactionId,
1686
+ // v2 validates bkn_context strictly and rejects the field.
1687
+ ...contract === "managed-v1" ? { operation_key: newOperationKey() } : {}
1688
+ };
1689
+ }
1690
+ async function bknContextFor(ctx, knId, question) {
1691
+ const contract = await lifecycleContract(ctx);
1692
+ if (contract === "none") return void 0;
1693
+ const owned = callerOwnedSession(ctx);
1694
+ if (owned) return contextFor(owned, contract);
1695
+ try {
1696
+ return contextFor(await ensureSession2(ctx, knId, contract, question), contract);
1697
+ } catch {
1698
+ return void 0;
1699
+ }
1700
+ }
1701
+ function serverErrorCode(err2) {
1702
+ if (err2 instanceof ToolError) return err2.code;
1703
+ if (!(err2 instanceof HttpError)) return void 0;
1704
+ try {
1705
+ const parsed = JSON.parse(err2.body);
1706
+ return typeof parsed.error?.code === "string" ? parsed.error.code : void 0;
1707
+ } catch {
1708
+ return void 0;
1709
+ }
1710
+ }
1711
+ async function withManagedLifecycle(ctx, knId, question, send) {
1712
+ const first = await bknContextFor(ctx, knId, question);
1713
+ try {
1714
+ return await send(first);
1715
+ } catch (err2) {
1716
+ const code = serverErrorCode(err2);
1717
+ if (!first || callerOwnedSession(ctx) || !code || !STALE_SESSION_CODES.has(code)) throw err2;
1718
+ sessions2.delete(sessionKey(ctx, knId));
1719
+ const reopened = await bknContextFor(ctx, knId, question);
1720
+ if (!reopened) throw err2;
1721
+ return await send(reopened);
1722
+ }
1723
+ }
1724
+ async function releaseLifecycleSessions() {
1725
+ const pending = [...sessions2.values()];
1726
+ sessions2.clear();
1727
+ await Promise.all(pending.map((opening) => withDeadline(releaseOne(opening))));
1728
+ }
1729
+ function withDeadline(work) {
1730
+ return new Promise((resolve7) => {
1731
+ const timer = setTimeout(resolve7, RELEASE_TIMEOUT_MS);
1732
+ timer.unref?.();
1733
+ work.finally(() => {
1734
+ clearTimeout(timer);
1735
+ resolve7();
1736
+ });
1737
+ });
1738
+ }
1739
+ async function releaseOne(opening) {
1740
+ try {
1741
+ const session = await opening;
1742
+ if (session.contract !== "managed-v2") return;
1743
+ await callToolRaw(
1744
+ session.ctx,
1745
+ session.knId,
1746
+ "bkn_finish_interaction",
1747
+ {
1748
+ interaction_id: session.interactionId,
1749
+ outcome: "cancelled",
1750
+ reason: "client session ended"
1751
+ },
1752
+ void 0,
1753
+ RELEASE_TIMEOUT_MS
1754
+ );
1755
+ } catch {
1756
+ }
1757
+ }
1758
+
1409
1759
  // src/resources/context-loader.ts
1410
1760
  function context(ctx) {
1411
1761
  return {
@@ -1418,7 +1768,8 @@ function context(ctx) {
1418
1768
  relationTypes: (knId, ids) => getRelationTypes(ctx, knId, ids),
1419
1769
  info: () => mcpInfo(ctx),
1420
1770
  tools: (knId) => listTools(ctx, knId),
1421
- toolCall: (knId, name, args) => callTool(ctx, knId, name, args),
1771
+ toolCall: (knId, name, args, options) => callTool(ctx, knId, name, args, options),
1772
+ managedToolCall: (knId, name, args, options) => callManagedTool(ctx, knId, name, args, options),
1422
1773
  // Generic MCP method passthrough — covers methods not yet wrapped, so the
1423
1774
  // surface doesn't have to grow every time the server adds one.
1424
1775
  callMethod: (knId, method, params) => callMethod(ctx, knId, method, params),
@@ -1715,22 +2066,39 @@ function searchMetrics(ctx, knId, body) {
1715
2066
  });
1716
2067
  }
1717
2068
  function validateMetric(ctx, knId, body) {
1718
- return request(ctx, `${ONTOLOGY_BASE}/${encodeURIComponent(knId)}/metrics/validate`, {
2069
+ return request(ctx, `${ONTOLOGY_BASE}/${encodeURIComponent(knId)}/metrics/validation`, {
1719
2070
  method: "POST",
1720
2071
  body
1721
2072
  });
1722
2073
  }
2074
+ function searchBody(knId, query, opts) {
2075
+ return {
2076
+ kn_id: knId,
2077
+ query,
2078
+ mode: opts.mode ?? "keyword_vector_retrieval",
2079
+ max_concepts: opts.maxConcepts ?? 10,
2080
+ return_query_understanding: opts.returnQueryUnderstanding ?? false
2081
+ };
2082
+ }
1723
2083
  function semanticSearch(ctx, knId, query, opts = {}) {
1724
- return request(ctx, `${RETRIEVAL_BASE}/semantic-search`, {
1725
- method: "POST",
1726
- body: {
1727
- kn_id: knId,
1728
- query,
1729
- mode: opts.mode ?? "keyword_vector_retrieval",
1730
- max_concepts: opts.maxConcepts ?? 10,
1731
- return_query_understanding: opts.returnQueryUnderstanding ?? false
1732
- }
1733
- });
2084
+ if (opts.bknContext) {
2085
+ return request(ctx, `${RETRIEVAL_BASE}/semantic-search`, {
2086
+ method: "POST",
2087
+ body: { ...searchBody(knId, query, opts), bkn_context: opts.bknContext }
2088
+ });
2089
+ }
2090
+ return withManagedLifecycle(
2091
+ ctx,
2092
+ knId,
2093
+ query,
2094
+ (bknContext) => request(ctx, `${RETRIEVAL_BASE}/semantic-search`, {
2095
+ method: "POST",
2096
+ body: {
2097
+ ...searchBody(knId, query, opts),
2098
+ ...bknContext ? { bkn_context: bknContext } : {}
2099
+ }
2100
+ })
2101
+ );
1734
2102
  }
1735
2103
 
1736
2104
  // src/api/resources.ts
@@ -3194,28 +3562,102 @@ function deleteActionSchedules(ctx, knId, ids) {
3194
3562
  import { z } from "zod";
3195
3563
  var VEGA_BASE = "/api/vega-backend/v1";
3196
3564
  var BuildMode = z.enum(["batch", "streaming"]);
3197
- var CreateBuildTaskRequest = z.object({
3198
- resource_id: z.string().min(1),
3199
- mode: BuildMode,
3200
- execute_type: z.enum(["incremental", "full"]).optional()
3201
- });
3565
+ var BuildTaskExecuteType = z.enum(["incremental", "full"]);
3566
+ var BuildTaskStatus = z.enum([
3567
+ "init",
3568
+ "running",
3569
+ "stopping",
3570
+ "stopped",
3571
+ "completed",
3572
+ "failed"
3573
+ ]);
3574
+ var CatalogHealthCheckScheduleMode = z.enum(["inherit", "enabled", "disabled"]);
3575
+ var CatalogHealthCheckStatus = z.enum([
3576
+ "healthy",
3577
+ "degraded",
3578
+ "unhealthy",
3579
+ "offline",
3580
+ "unchecked"
3581
+ ]);
3582
+ var CatalogHealthStatus = z.object({
3583
+ id: z.string(),
3584
+ health_check_status: CatalogHealthCheckStatus,
3585
+ last_check_time: z.number().optional(),
3586
+ health_check_result: z.string().optional()
3587
+ }).passthrough();
3588
+ var CatalogHealthCheckSchedule = z.object({
3589
+ catalog_id: z.string(),
3590
+ mode: CatalogHealthCheckScheduleMode,
3591
+ cron_expr: z.string().optional(),
3592
+ last_run: z.number(),
3593
+ next_run: z.number()
3594
+ }).passthrough();
3595
+ var CatalogConnectionTestResult = z.object({
3596
+ success: z.boolean(),
3597
+ message: z.string().optional()
3598
+ }).passthrough();
3599
+ var CreateBuildTaskRequest = z.discriminatedUnion("mode", [
3600
+ z.object({
3601
+ resource_id: z.string().min(1),
3602
+ mode: z.literal("batch"),
3603
+ execute_type: BuildTaskExecuteType.optional()
3604
+ }),
3605
+ z.object({
3606
+ resource_id: z.string().min(1),
3607
+ mode: z.literal("streaming"),
3608
+ // Streaming tasks do not have an execution type.
3609
+ execute_type: z.never().optional()
3610
+ })
3611
+ ]);
3202
3612
  var BuildTask = z.object({
3203
3613
  id: z.string(),
3204
3614
  resource_id: z.string().optional(),
3205
3615
  mode: BuildMode.optional(),
3206
- status: z.string().optional(),
3616
+ status: BuildTaskStatus.optional(),
3207
3617
  state: z.string().optional(),
3208
3618
  total_count: z.number().optional(),
3209
3619
  synced_count: z.number().optional(),
3210
3620
  vectorized_count: z.number().optional(),
3211
3621
  index_config: z.unknown().optional(),
3212
3622
  catalog_id: z.string().optional(),
3623
+ execute_type: BuildTaskExecuteType.optional(),
3624
+ index_health: z.object({
3625
+ embedding: z.string(),
3626
+ fulltext: z.string(),
3627
+ usable: z.boolean()
3628
+ }).passthrough().optional()
3629
+ }).passthrough();
3630
+ var BuildTaskSummary = z.object({
3631
+ id: z.string(),
3632
+ resource_id: z.string(),
3633
+ resource_name: z.string().optional(),
3634
+ catalog_id: z.string(),
3635
+ catalog_name: z.string().optional(),
3636
+ status: BuildTaskStatus,
3637
+ mode: BuildMode,
3638
+ execute_type: BuildTaskExecuteType.optional(),
3639
+ total_count: z.number(),
3640
+ synced_count: z.number(),
3641
+ vectorized_count: z.number(),
3642
+ synced_mark: z.string(),
3643
+ error_msg: z.string().optional(),
3644
+ creator: z.object({
3645
+ id: z.string(),
3646
+ name: z.string().optional(),
3647
+ type: z.string()
3648
+ }),
3649
+ create_time: z.number(),
3650
+ update_time: z.number(),
3213
3651
  index_health: z.object({
3214
3652
  embedding: z.string(),
3215
3653
  fulltext: z.string(),
3216
3654
  usable: z.boolean()
3217
3655
  }).passthrough().optional()
3218
3656
  }).passthrough();
3657
+ var ListBuildTasksResponse = z.object({
3658
+ entries: z.array(BuildTaskSummary),
3659
+ total_count: z.number()
3660
+ }).passthrough();
3219
3661
  async function createBuildTask(ctx, req) {
3220
3662
  const p = CreateBuildTaskRequest.parse(req);
3221
3663
  const body = {
@@ -3226,8 +3668,13 @@ async function createBuildTask(ctx, req) {
3226
3668
  const res = await request(ctx, `${VEGA_BASE}/build-tasks`, { method: "POST", body });
3227
3669
  return BuildTask.parse(res);
3228
3670
  }
3229
- function listBuildTasks(ctx, opts = {}) {
3230
- return request(ctx, `${VEGA_BASE}/build-tasks`, {
3671
+ async function listBuildTasks(ctx, opts = {}) {
3672
+ if (opts.orderBy === "default") {
3673
+ throw new InputError(
3674
+ 'orderBy "default" is no longer supported; use "created_at" or "updated_at"'
3675
+ );
3676
+ }
3677
+ const res = await request(ctx, `${VEGA_BASE}/build-tasks`, {
3231
3678
  query: {
3232
3679
  limit: opts.limit,
3233
3680
  offset: opts.offset,
@@ -3240,6 +3687,7 @@ function listBuildTasks(ctx, opts = {}) {
3240
3687
  order: opts.order
3241
3688
  }
3242
3689
  });
3690
+ return ListBuildTasksResponse.parse(res);
3243
3691
  }
3244
3692
  async function getBuildTask(ctx, taskId) {
3245
3693
  const res = await request(ctx, `${VEGA_BASE}/build-tasks/${encodeURIComponent(taskId)}`);
@@ -3290,35 +3738,46 @@ async function listCatalogs(ctx, opts = {}) {
3290
3738
  function getCatalog(ctx, id) {
3291
3739
  return request(ctx, `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}`);
3292
3740
  }
3293
- function createCatalog(ctx, req) {
3741
+ function createCatalog(ctx, req, opts = {}) {
3294
3742
  return request(ctx, `${VEGA_BASE}/catalogs`, {
3295
3743
  method: "POST",
3744
+ query: {
3745
+ allow_unhealthy: opts.allowUnhealthy === void 0 ? void 0 : String(opts.allowUnhealthy)
3746
+ },
3296
3747
  body: {
3297
3748
  ...req.id ? { id: req.id } : {},
3298
3749
  name: req.name,
3299
3750
  connector_type: req.connectorType,
3300
3751
  connector_config: req.connectorConfig,
3301
- ...req.tags ? { tags: req.tags } : {},
3302
- ...req.description ? { description: req.description } : {},
3752
+ ...req.tags !== void 0 ? { tags: req.tags } : {},
3753
+ ...req.description !== void 0 ? { description: req.description } : {},
3303
3754
  ...req.enabled !== void 0 ? { enabled: req.enabled } : {},
3304
3755
  ...req.internal !== void 0 ? { internal: req.internal } : {},
3305
- ...req.extensions ? { extensions: req.extensions } : {}
3306
- }
3756
+ ...req.extensions !== void 0 ? { extensions: req.extensions } : {},
3757
+ ...req.healthCheckSchedule !== void 0 ? {
3758
+ health_check_schedule: req.healthCheckSchedule === null ? null : mapCatalogHealthCheckScheduleRequest(req.healthCheckSchedule)
3759
+ } : {}
3760
+ },
3761
+ timeoutMs: 6e4
3307
3762
  });
3308
3763
  }
3309
- function updateCatalog(ctx, id, req) {
3764
+ function updateCatalog(ctx, id, req, opts = {}) {
3310
3765
  return request(ctx, `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}`, {
3311
3766
  method: "PUT",
3767
+ query: {
3768
+ allow_unhealthy: opts.allowUnhealthy === void 0 ? void 0 : String(opts.allowUnhealthy)
3769
+ },
3312
3770
  body: {
3313
- ...req.id ? { id: req.id } : {},
3314
- ...req.name ? { name: req.name } : {},
3315
- ...req.connectorType ? { connector_type: req.connectorType } : {},
3771
+ id,
3772
+ name: req.name,
3773
+ connector_type: req.connectorType,
3774
+ enabled: req.enabled,
3316
3775
  ...req.connectorConfig !== void 0 ? { connector_config: req.connectorConfig } : {},
3317
- ...req.tags ? { tags: req.tags } : {},
3776
+ ...req.tags !== void 0 ? { tags: req.tags } : {},
3318
3777
  ...req.description !== void 0 ? { description: req.description } : {},
3319
- ...req.enabled !== void 0 ? { enabled: req.enabled } : {},
3320
- ...req.extensions ? { extensions: req.extensions } : {}
3321
- }
3778
+ ...req.extensions !== void 0 ? { extensions: req.extensions } : {}
3779
+ },
3780
+ timeoutMs: 6e4
3322
3781
  });
3323
3782
  }
3324
3783
  function enableCatalog(ctx, id) {
@@ -3332,10 +3791,51 @@ function disableCatalog(ctx, id) {
3332
3791
  function deleteCatalog(ctx, id) {
3333
3792
  return request(ctx, `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}`, { method: "DELETE" });
3334
3793
  }
3335
- function testCatalogConnection(ctx, id) {
3336
- return request(ctx, `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}/test-connection`, {
3337
- method: "POST"
3794
+ async function testCatalogConnectionConfig(ctx, req) {
3795
+ const result = await request(ctx, `${VEGA_BASE}/catalogs/test-connection`, {
3796
+ method: "POST",
3797
+ body: {
3798
+ connector_type: req.connectorType,
3799
+ connector_config: req.connectorConfig
3800
+ },
3801
+ timeoutMs: 6e4
3338
3802
  });
3803
+ return CatalogConnectionTestResult.parse(result);
3804
+ }
3805
+ async function testCatalogConnection(ctx, id) {
3806
+ const result = await request(
3807
+ ctx,
3808
+ `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}/test-connection`,
3809
+ {
3810
+ method: "POST",
3811
+ timeoutMs: 6e4
3812
+ }
3813
+ );
3814
+ return CatalogConnectionTestResult.parse(result);
3815
+ }
3816
+ async function getCatalogHealthCheckSchedule(ctx, id) {
3817
+ const result = await request(
3818
+ ctx,
3819
+ `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}/health-check-schedule`
3820
+ );
3821
+ return CatalogHealthCheckSchedule.parse(result);
3822
+ }
3823
+ async function updateCatalogHealthCheckSchedule(ctx, id, req) {
3824
+ const result = await request(
3825
+ ctx,
3826
+ `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}/health-check-schedule`,
3827
+ {
3828
+ method: "PUT",
3829
+ body: mapCatalogHealthCheckScheduleRequest(req)
3830
+ }
3831
+ );
3832
+ return CatalogHealthCheckSchedule.parse(result);
3833
+ }
3834
+ function mapCatalogHealthCheckScheduleRequest(req) {
3835
+ return {
3836
+ mode: req.mode,
3837
+ ...req.mode === "enabled" ? { cron_expr: req.cronExpr } : {}
3838
+ };
3339
3839
  }
3340
3840
  function discoverCatalog(ctx, id, wait = true) {
3341
3841
  return request(ctx, `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}/discover`, {
@@ -3355,11 +3855,12 @@ function listCatalogResources(ctx, id, category, limit, offset) {
3355
3855
  }
3356
3856
  });
3357
3857
  }
3358
- function catalogHealthStatus(ctx, ids) {
3359
- return request(
3858
+ async function catalogHealthStatus(ctx, id) {
3859
+ const result = await request(
3360
3860
  ctx,
3361
- `${VEGA_BASE}/catalogs/${ids.map(encodeURIComponent).join(",")}/health-status`
3861
+ `${VEGA_BASE}/catalogs/${encodeURIComponent(id)}/health-status`
3362
3862
  );
3863
+ return CatalogHealthStatus.parse(result);
3363
3864
  }
3364
3865
  function listConnectorTypes(ctx) {
3365
3866
  return request(ctx, `${VEGA_BASE}/connector-types`, { query: { sort: "name", order: "asc" } });
@@ -4273,16 +4774,45 @@ async function updateSkillPackageZip(ctx, skillId, bytes, filename = "skill.zip"
4273
4774
  if (!res.ok) throw new HttpError(res.status, res.statusText, text);
4274
4775
  return text ? JSON.parse(text) : void 0;
4275
4776
  }
4276
- async function downloadSkill(ctx, skillId) {
4777
+ function skillPath(skillId, view, path) {
4778
+ const seg = view === "draft" ? "management/" : "";
4779
+ return `${BASE5}/skills/${encodeURIComponent(skillId)}/${seg}${path}`;
4780
+ }
4781
+ async function downloadSkill(ctx, skillId, view = "published") {
4277
4782
  const res = await authFetch(
4278
4783
  ctx,
4279
- () => tlsFetch(ctx.insecure, `${ctx.baseUrl}${BASE5}/skills/${encodeURIComponent(skillId)}/download`, {
4784
+ () => tlsFetch(ctx.insecure, `${ctx.baseUrl}${skillPath(skillId, view, "download")}`, {
4280
4785
  headers: buildHeaders(ctx)
4281
4786
  })
4282
4787
  );
4283
4788
  if (!res.ok) throw new HttpError(res.status, res.statusText, await res.text());
4284
4789
  return new Uint8Array(await res.arrayBuffer());
4285
4790
  }
4791
+ function executeSkill(ctx, skillId, opts) {
4792
+ return request(ctx, `${BASE5}/skills/${encodeURIComponent(skillId)}/execute`, {
4793
+ method: "POST",
4794
+ body: {
4795
+ entry_shell: opts.entryShell,
4796
+ ...opts.timeout === void 0 ? {} : { timeout: opts.timeout }
4797
+ },
4798
+ // Outlast the sandbox: the default client timeout is shorter than the run
4799
+ // budget, so without this a long run aborts locally mid-execution and the
4800
+ // caller never learns the exit code. With no stated limit the sandbox
4801
+ // applies its own — 300s by default, 3600s at most — so the budget has to
4802
+ // cover that rather than a number of ours.
4803
+ timeoutMs: executeBudgetMs(opts.timeout),
4804
+ // The abort deadline alone tops out at undici's 300s header deadline,
4805
+ // because `execute-sync` blocks and sends no headers until the run is over.
4806
+ headersTimeoutMs: executeBudgetMs(opts.timeout)
4807
+ });
4808
+ }
4809
+ var SANDBOX_MAX_TIMEOUT_SEC = 3600;
4810
+ function executeBudgetMs(timeoutSec) {
4811
+ return (timeoutSec ?? SANDBOX_MAX_TIMEOUT_SEC) * 1e3 + 15e3;
4812
+ }
4813
+ function getSkillNames(ctx, ids) {
4814
+ return request(ctx, `${BASE5}/skills/names`, { method: "POST", body: { ids } });
4815
+ }
4286
4816
  function updateSkillMetadata(ctx, skillId, body) {
4287
4817
  return request(ctx, `${BASE5}/skills/${encodeURIComponent(skillId)}`, { method: "PUT", body });
4288
4818
  }
@@ -4323,12 +4853,15 @@ function getSkillMarket(ctx, skillId) {
4323
4853
  function deleteSkill(ctx, skillId) {
4324
4854
  return request(ctx, `${BASE5}/skills/${encodeURIComponent(skillId)}`, { method: "DELETE" });
4325
4855
  }
4326
- function getSkillContent(ctx, skillId) {
4327
- return request(ctx, `${BASE5}/skills/${encodeURIComponent(skillId)}/content`);
4856
+ function getSkillContent(ctx, skillId, opts = {}) {
4857
+ return request(ctx, skillPath(skillId, opts.view ?? "published", "content"), {
4858
+ query: { response_mode: opts.responseMode }
4859
+ });
4328
4860
  }
4329
- function readSkillFile(ctx, skillId, relPath) {
4330
- return request(ctx, `${BASE5}/skills/${encodeURIComponent(skillId)}/files/read`, {
4861
+ function readSkillFile(ctx, skillId, relPath, opts = {}) {
4862
+ return request(ctx, skillPath(skillId, opts.view ?? "published", "files/read"), {
4331
4863
  method: "POST",
4864
+ query: { response_mode: opts.responseMode },
4332
4865
  body: { rel_path: relPath }
4333
4866
  });
4334
4867
  }
@@ -4366,6 +4899,15 @@ async function zipDirectory(dir) {
4366
4899
  }
4367
4900
  return new Uint8Array(await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" }));
4368
4901
  }
4902
+ async function unzipToMap(bytes) {
4903
+ const zip = await JSZip.loadAsync(bytes);
4904
+ const out = /* @__PURE__ */ new Map();
4905
+ for (const entry of Object.values(zip.files)) {
4906
+ if (entry.dir) continue;
4907
+ out.set(entry.name, new Uint8Array(await entry.async("uint8array")));
4908
+ }
4909
+ return out;
4910
+ }
4369
4911
  async function unzipToDirectory(bytes, dir) {
4370
4912
  const abs = resolve4(dir);
4371
4913
  mkdirSync3(abs, { recursive: true });
@@ -4382,16 +4924,202 @@ async function unzipToDirectory(bytes, dir) {
4382
4924
  return written;
4383
4925
  }
4384
4926
 
4927
+ // src/utils/skill-tree.ts
4928
+ function normalize(path) {
4929
+ return (path ?? "").replace(/^\/+|\/+$/g, "");
4930
+ }
4931
+ function segments(relPath) {
4932
+ return relPath.split("/").filter(Boolean);
4933
+ }
4934
+ function classifyPath(files, path) {
4935
+ const target = normalize(path);
4936
+ if (!target) return "root";
4937
+ if (files.some((f) => normalize(f.rel_path) === target)) return "file";
4938
+ const prefix = `${target}/`;
4939
+ return files.some((f) => normalize(f.rel_path).startsWith(prefix)) ? "dir" : "missing";
4940
+ }
4941
+ function listChildren(files, path) {
4942
+ const target = normalize(path);
4943
+ const prefix = target ? `${target}/` : "";
4944
+ const dirs = /* @__PURE__ */ new Map();
4945
+ const leaves = [];
4946
+ for (const file of files) {
4947
+ const rel = normalize(file.rel_path);
4948
+ if (prefix && !rel.startsWith(prefix)) continue;
4949
+ const rest = segments(rel.slice(prefix.length));
4950
+ if (rest.length === 0) continue;
4951
+ const [head] = rest;
4952
+ if (!head) continue;
4953
+ if (rest.length === 1) {
4954
+ leaves.push({
4955
+ name: head,
4956
+ type: "file",
4957
+ relPath: rel,
4958
+ fileType: file.file_type,
4959
+ size: file.size,
4960
+ mime: file.mime_type
4961
+ });
4962
+ continue;
4963
+ }
4964
+ const dir = dirs.get(head) ?? { name: head, type: "dir", files: 0, size: 0 };
4965
+ dir.files += 1;
4966
+ dir.size += file.size ?? 0;
4967
+ dirs.set(head, dir);
4968
+ }
4969
+ const byName = (a, b) => a.name.localeCompare(b.name);
4970
+ return [...[...dirs.values()].sort(byName), ...leaves.sort(byName)];
4971
+ }
4972
+ function filesUnder(files, path) {
4973
+ const target = normalize(path);
4974
+ if (!target) return files;
4975
+ const prefix = `${target}/`;
4976
+ return files.filter((f) => normalize(f.rel_path).startsWith(prefix));
4977
+ }
4978
+ function emptyNode() {
4979
+ return { dirs: /* @__PURE__ */ new Map(), files: [] };
4980
+ }
4981
+ function buildTree(files) {
4982
+ const root = emptyNode();
4983
+ for (const file of files) {
4984
+ const parts = segments(normalize(file.rel_path));
4985
+ const name = parts.pop();
4986
+ if (!name) continue;
4987
+ let node = root;
4988
+ for (const part of parts) {
4989
+ const next = node.dirs.get(part) ?? emptyNode();
4990
+ node.dirs.set(part, next);
4991
+ node = next;
4992
+ }
4993
+ node.files.push({
4994
+ name,
4995
+ type: "file",
4996
+ relPath: normalize(file.rel_path),
4997
+ fileType: file.file_type,
4998
+ size: file.size,
4999
+ mime: file.mime_type
5000
+ });
5001
+ }
5002
+ return root;
5003
+ }
5004
+ function renderNode(node, indent, out) {
5005
+ const dirs = [...node.dirs.entries()].sort(([a], [b]) => a.localeCompare(b));
5006
+ const files = [...node.files].sort((a, b) => a.name.localeCompare(b.name));
5007
+ const total = dirs.length + files.length;
5008
+ let index = 0;
5009
+ for (const [name, child] of dirs) {
5010
+ index += 1;
5011
+ const last = index === total;
5012
+ out.push(`${indent}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${name}/`);
5013
+ renderNode(child, `${indent}${last ? " " : "\u2502 "}`, out);
5014
+ }
5015
+ for (const file of files) {
5016
+ index += 1;
5017
+ const last = index === total;
5018
+ const meta = [file.fileType, file.size === void 0 ? void 0 : `${file.size} B`].filter(Boolean).join(", ");
5019
+ out.push(`${indent}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${file.name}${meta ? ` (${meta})` : ""}`);
5020
+ }
5021
+ }
5022
+ function renderTree(files) {
5023
+ const out = [];
5024
+ renderNode(buildTree(files), "", out);
5025
+ return out.join("\n");
5026
+ }
5027
+
4385
5028
  // src/resources/skills.ts
5029
+ function viewOf(opts) {
5030
+ return opts?.draft ? "draft" : "published";
5031
+ }
5032
+ function cachedArchive(cache, ctx, skillId, view) {
5033
+ const key = `${skillId}|${view}`;
5034
+ const hit = cache.get(key);
5035
+ if (hit) return hit;
5036
+ const pending = downloadSkill(ctx, skillId, view).then(unzipToMap);
5037
+ cache.set(key, pending);
5038
+ pending.catch(() => cache.delete(key));
5039
+ return pending;
5040
+ }
5041
+ function binaryError(relPath) {
5042
+ return new InputError(
5043
+ `'${relPath}' is a binary file \u2014 use \`openbkn skill install\` or \`skill download\` to fetch it.`
5044
+ );
5045
+ }
5046
+ function decodeStrict(bytes, relPath) {
5047
+ let text;
5048
+ try {
5049
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
5050
+ } catch {
5051
+ throw binaryError(relPath);
5052
+ }
5053
+ if (text.includes("\0")) throw binaryError(relPath);
5054
+ return text;
5055
+ }
5056
+ function looksLossy(text) {
5057
+ return text.includes("\0") || text.includes("\uFFFD");
5058
+ }
5059
+ async function readFileText(cache, ctx, skillId, relPath, view) {
5060
+ const res = await readSkillFile(ctx, skillId, normalize(relPath), {
5061
+ view,
5062
+ responseMode: "content"
5063
+ });
5064
+ if (typeof res?.content === "string" && res.content.length > 0) {
5065
+ if (looksLossy(res.content)) throw binaryError(relPath);
5066
+ return res.content;
5067
+ }
5068
+ const archive = await cachedArchive(cache, ctx, skillId, view);
5069
+ const bytes = archive.get(normalize(relPath));
5070
+ if (!bytes) {
5071
+ throw new InputError(`'${relPath}' not found in skill ${skillId}.`);
5072
+ }
5073
+ return decodeStrict(bytes, relPath);
5074
+ }
4386
5075
  function skills(ctx) {
5076
+ const archives = /* @__PURE__ */ new Map();
5077
+ const manifest = async (skillId, opts) => {
5078
+ const res = await getSkillContent(ctx, skillId, { view: viewOf(opts) });
5079
+ return res?.files ?? [];
5080
+ };
4387
5081
  return {
4388
5082
  list: (opts) => listSkills(ctx, opts),
4389
5083
  get: (skillId) => getSkill(ctx, skillId),
4390
5084
  market: (opts) => listSkillMarket(ctx, opts),
4391
5085
  marketGet: (skillId) => getSkillMarket(ctx, skillId),
4392
5086
  delete: (skillId) => deleteSkill(ctx, skillId),
4393
- content: (skillId) => getSkillContent(ctx, skillId),
4394
- readFile: (skillId, relPath) => readSkillFile(ctx, skillId, relPath),
5087
+ content: (skillId, opts) => getSkillContent(ctx, skillId, { view: viewOf(opts) }),
5088
+ // Normalized like every other path entry point, so `/a/b.md` addresses the
5089
+ // same file here as it does under `--raw` and in `skill files`.
5090
+ readFile: (skillId, relPath, opts) => readSkillFile(ctx, skillId, normalize(relPath), { view: viewOf(opts) }),
5091
+ /** SKILL.md's own text, for callers that want the document rather than a link. */
5092
+ contentRaw: (skillId, opts) => readFileText(archives, ctx, skillId, "SKILL.md", viewOf(opts)),
5093
+ /** A bundled file's text. */
5094
+ readFileRaw: (skillId, relPath, opts) => readFileText(archives, ctx, skillId, relPath, viewOf(opts)),
5095
+ /** Run the skill in the platform sandbox. */
5096
+ execute: (skillId, opts) => executeSkill(ctx, skillId, opts),
5097
+ /** Resolve skill ids to names; unknown ids are simply absent from the result. */
5098
+ names: (ids) => getSkillNames(ctx, ids),
5099
+ /**
5100
+ * One level of the skill's file tree. Directories are inferred from the
5101
+ * manifest's paths — see utils/skill-tree.
5102
+ */
5103
+ files: async (skillId, path, opts) => {
5104
+ const files = await manifest(skillId, opts);
5105
+ const kind = classifyPath(files, path);
5106
+ if (kind === "file") {
5107
+ throw new InputError(`'${path}' is a file, not a directory \u2014 use \`skill read-file\`.`);
5108
+ }
5109
+ if (kind === "missing") {
5110
+ throw new InputError(`'${path}' not found in skill ${skillId}.`);
5111
+ }
5112
+ const subtree = filesUnder(files, path);
5113
+ return {
5114
+ skillId,
5115
+ path: path ?? "",
5116
+ entries: listChildren(files, path),
5117
+ totalFiles: subtree.length,
5118
+ totalSize: subtree.reduce((sum, f) => sum + (f.size ?? 0), 0)
5119
+ };
5120
+ },
5121
+ /** The manifest itself, for callers that want to render the whole tree. */
5122
+ fileManifest: manifest,
4395
5123
  history: (skillId) => getSkillHistory(ctx, skillId),
4396
5124
  setStatus: (skillId, status2) => setSkillStatus(ctx, skillId, status2),
4397
5125
  updateMetadata: (skillId, body) => updateSkillMetadata(ctx, skillId, body),
@@ -4405,16 +5133,16 @@ function skills(ctx) {
4405
5133
  /** Replace a skill's package from a local directory. */
4406
5134
  updatePackage: async (skillId, dir) => updateSkillPackageZip(ctx, skillId, await zipDirectory(dir), `${basename2(resolve5(dir))}.zip`),
4407
5135
  /** Download a skill archive to a local .zip file. */
4408
- download: async (skillId, outPath) => {
4409
- const bytes = await downloadSkill(ctx, skillId);
5136
+ download: async (skillId, outPath, opts) => {
5137
+ const bytes = await downloadSkill(ctx, skillId, viewOf(opts));
4410
5138
  const dest = resolve5(outPath ?? `${skillId}.zip`);
4411
5139
  mkdirSync4(dirname3(dest), { recursive: true });
4412
5140
  writeFileSync3(dest, bytes);
4413
5141
  return { skillId, path: dest, bytes: bytes.length };
4414
5142
  },
4415
5143
  /** Download a skill archive and extract it into a directory. */
4416
- install: async (skillId, dir) => {
4417
- const bytes = await downloadSkill(ctx, skillId);
5144
+ install: async (skillId, dir, opts) => {
5145
+ const bytes = await downloadSkill(ctx, skillId, viewOf(opts));
4418
5146
  const target = resolve5(dir ?? skillId);
4419
5147
  const files = await unzipToDirectory(bytes, target);
4420
5148
  return { skillId, dir: target, files: files.length };
@@ -4576,727 +5304,384 @@ function toolboxes(ctx) {
4576
5304
  };
4577
5305
  }
4578
5306
 
4579
- // src/trace-session.ts
4580
- import { randomUUID as randomUUID2 } from "crypto";
4581
- var PAYLOAD_FIELDS = {
4582
- "agent.interaction.started": /* @__PURE__ */ new Set([
4583
- "intent_hash",
4584
- "mode",
4585
- "agent_id",
4586
- "app_ref",
4587
- "question_artifact_ref"
4588
- ]),
4589
- "retrieval.completed": /* @__PURE__ */ new Set([
4590
- "query_hash",
4591
- "candidate_count",
4592
- "truncated",
4593
- "version_status",
4594
- "source_refs"
4595
- ]),
4596
- "knowledge.read.observed": /* @__PURE__ */ new Set([
4597
- "kn_id",
4598
- "read_kind",
4599
- "version_status",
4600
- "schema_version",
4601
- "business_refs"
4602
- ]),
4603
- "data.query.observed": /* @__PURE__ */ new Set([
4604
- "query_hash",
4605
- "query_type",
4606
- "row_count",
4607
- "truncated",
4608
- "as_of",
4609
- "version_status",
4610
- "resource_refs",
4611
- "field_refs",
4612
- "query_artifact_ref",
4613
- "result_artifact_ref"
4614
- ]),
4615
- "logic.execution.observed": /* @__PURE__ */ new Set([
4616
- "logic_ref",
4617
- "input_artifact_ref",
4618
- "result_artifact_ref",
4619
- "status"
4620
- ]),
4621
- "model.call.observed": /* @__PURE__ */ new Set([
4622
- "model_name",
4623
- "model_provider",
4624
- "status",
4625
- "input_token_count",
4626
- "output_token_count",
4627
- "prompt_hash",
4628
- "output_hash",
4629
- "error_category",
4630
- "error_hash"
4631
- ]),
4632
- "tool.called": /* @__PURE__ */ new Set(["tool_id", "tool_name", "args_hash", "visibility", "version_status"]),
4633
- "tool.result.observed": /* @__PURE__ */ new Set([
4634
- "tool_id",
4635
- "tool_name",
4636
- "status",
4637
- "result_hash",
4638
- "result_length",
4639
- "result_count",
4640
- "error_hash",
4641
- "error_category",
4642
- "visibility",
4643
- "version_status"
4644
- ]),
4645
- "claim.created": /* @__PURE__ */ new Set([
4646
- "claim_id",
4647
- "claim_type",
4648
- "claim_hash",
4649
- "source_event_ids",
4650
- "operation_ids",
4651
- "visibility",
4652
- "version_status",
4653
- "result_artifact_ref"
4654
- ]),
4655
- "evidence.refs.created": /* @__PURE__ */ new Set(["claim_id", "evidence_refs"]),
4656
- "business.refs.resolved": /* @__PURE__ */ new Set(["claim_id", "resolver_status", "business_refs"]),
4657
- "action.recommended": /* @__PURE__ */ new Set([
4658
- "action_instance_id",
4659
- "action_type",
4660
- "target_refs",
4661
- "reason_hash",
4662
- "status",
4663
- "reason_artifact_ref",
4664
- "input_artifact_ref"
4665
- ]),
4666
- "action.approval_requested": /* @__PURE__ */ new Set(["action_instance_id", "policy_ref", "status"]),
4667
- "action.approved": /* @__PURE__ */ new Set(["action_instance_id", "actor_ref", "policy_decision_ref", "status"]),
4668
- "action.rejected": /* @__PURE__ */ new Set(["action_instance_id", "actor_ref", "policy_decision_ref", "status"]),
4669
- "action.executed": /* @__PURE__ */ new Set([
4670
- "action_instance_id",
4671
- "status",
4672
- "invocation_ref",
4673
- "error_category",
4674
- "error_hash"
4675
- ]),
4676
- "action.result_recorded": /* @__PURE__ */ new Set([
4677
- "action_instance_id",
4678
- "status",
4679
- "result_hash",
4680
- "task_ref",
4681
- "artifact_ref",
4682
- "result_artifact_ref"
4683
- ])
4684
- };
4685
- var REQUIRED_PAYLOAD_FIELDS = {
4686
- "agent.interaction.started": ["intent_hash", "mode"],
4687
- "retrieval.completed": ["query_hash", "candidate_count", "truncated"],
4688
- "knowledge.read.observed": ["kn_id", "read_kind", "version_status"],
4689
- "data.query.observed": ["query_hash", "query_type", "row_count"],
4690
- "logic.execution.observed": ["logic_ref", "input_artifact_ref", "result_artifact_ref", "status"],
4691
- "model.call.observed": [
4692
- "model_name",
4693
- "model_provider",
4694
- "status",
4695
- "input_token_count",
4696
- "output_token_count",
4697
- "prompt_hash",
4698
- "output_hash"
4699
- ],
4700
- "tool.called": ["tool_id", "tool_name", "args_hash", "visibility", "version_status"],
4701
- "tool.result.observed": ["tool_id", "tool_name", "status", "visibility", "version_status"],
4702
- "claim.created": [
4703
- "claim_id",
4704
- "claim_type",
4705
- "claim_hash",
4706
- "source_event_ids",
4707
- "operation_ids",
4708
- "visibility",
4709
- "version_status"
4710
- ],
4711
- "evidence.refs.created": ["claim_id", "evidence_refs"],
4712
- "business.refs.resolved": ["claim_id", "resolver_status", "business_refs"],
4713
- "action.recommended": [
4714
- "action_instance_id",
4715
- "action_type",
4716
- "target_refs",
4717
- "reason_hash",
4718
- "status"
4719
- ],
4720
- "action.approval_requested": ["action_instance_id", "policy_ref", "status"],
4721
- "action.approved": ["action_instance_id", "actor_ref", "policy_decision_ref", "status"],
4722
- "action.rejected": ["action_instance_id", "actor_ref", "policy_decision_ref", "status"],
4723
- "action.executed": ["action_instance_id", "status", "invocation_ref"],
4724
- "action.result_recorded": ["action_instance_id", "status", "result_hash"]
4725
- };
4726
- var REF_FIELDS = /* @__PURE__ */ new Set([
4727
- "ref_id",
4728
- "ref_type",
4729
- "source_system",
4730
- "validity",
4731
- "version_status",
4732
- "visibility",
4733
- "summary_hash"
5307
+ // src/api/trace-lifecycle.ts
5308
+ var LIFECYCLE = "/api/agent-observability/v1";
5309
+ var FORBIDDEN_INPUT_FIELDS = /* @__PURE__ */ new Set([
5310
+ "generation",
5311
+ "on_behalf_of",
5312
+ "onBehalfOf",
5313
+ "owner",
5314
+ "tenant_id",
5315
+ "application_principal_id",
5316
+ "actor_subject",
5317
+ "actor_subject_type",
5318
+ "actor_subject_id",
5319
+ "effective_subject",
5320
+ "effective_subject_type",
5321
+ "effective_subject_id",
5322
+ "delegation_id"
4734
5323
  ]);
4735
- var RAW_KEYS = /* @__PURE__ */ new Set([
4736
- "authorization",
4737
- "cookie",
4738
- "access_token",
4739
- "refresh_token",
4740
- "id_token",
4741
- "api_key",
4742
- "password",
4743
- "private_key",
4744
- "prompt",
4745
- "user_question",
4746
- "approval_comment",
4747
- "sql",
4748
- "query_params",
4749
- "rows"
4750
- ]);
4751
- var HASH_RE = /^sha256:[0-9a-f]{64}$/;
4752
- var RAW_VALUE_PATTERNS = [
4753
- /bearer\s+[A-Za-z0-9._-]+/i,
4754
- /\bselect\s+.+\s+from\b/is,
4755
- /\binsert\s+into\b/i,
4756
- /\bupdate\s+\S+\s+set\b/i,
4757
- /\bdelete\s+from\b/i,
4758
- /[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}/,
4759
- /https?:\/\/[^\s"']+/i
4760
- ];
4761
- function defaultNow() {
4762
- return (/* @__PURE__ */ new Date()).toISOString();
4763
- }
4764
- function assertSessionOptions(options) {
4765
- const trace2 = options.trace;
4766
- if (!/^[0-9a-f]{32}$/.test(trace2.trace_id)) throw new Error("trace_id must be 32 hex characters");
4767
- const traceparent = /^00-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/.exec(trace2.traceparent);
4768
- if (!traceparent || traceparent[1] !== trace2.trace_id) {
4769
- throw new Error("traceparent must be valid and match trace_id");
4770
- }
4771
- if (!/^req_[0-9A-Za-z_.-]+$/.test(trace2["bkn.request.id"])) {
4772
- throw new Error("bkn.request.id must start with req_");
4773
- }
4774
- const conversationId = options.conversationId ?? trace2["bkn.conversation.id"];
4775
- if (conversationId && !/^[0-9A-Za-z_.:-]{1,128}$/.test(conversationId)) {
4776
- throw new Error("conversationId must be an opaque correlation identifier");
4777
- }
4778
- if (options.conversationId && trace2["bkn.conversation.id"] && options.conversationId !== trace2["bkn.conversation.id"]) {
4779
- throw new Error("conversationId conflicts with trace bkn.conversation.id");
4780
- }
4781
- if (!trace2["bkn.tenant.id"] && !trace2.business_domain) {
4782
- throw new Error("trace requires bkn.tenant.id or business_domain");
4783
- }
4784
- if (!trace2["bkn.account.id"] || !trace2["bkn.account.type"]) {
4785
- throw new Error("trace requires account id and type");
4786
- }
4787
- if (!/^[0-9a-f]{16}$/.test(options.spanId)) throw new Error("spanId must be 16 hex characters");
4788
- if (!options.producerModule.trim()) throw new Error("producerModule is required");
4789
- }
4790
- function clone(value) {
4791
- return JSON.parse(JSON.stringify(value));
4792
- }
4793
- function assertSafePayload(eventType, payload) {
4794
- const allowed = PAYLOAD_FIELDS[eventType];
4795
- for (const key of Object.keys(payload)) {
4796
- if (!allowed.has(key)) throw new Error(`${eventType} payload field is not registered: ${key}`);
4797
- }
4798
- for (const key of REQUIRED_PAYLOAD_FIELDS[eventType] ?? []) {
4799
- if (payload[key] === void 0 || payload[key] === "") {
4800
- throw new Error(`${eventType} payload requires ${key}`);
4801
- }
4802
- }
4803
- if (eventType === "agent.interaction.started" && !payload.agent_id && !payload.app_ref) {
4804
- throw new Error("agent.interaction.started requires agent_id or app_ref");
4805
- }
4806
- if (eventType === "agent.interaction.started") {
4807
- assertEnum(payload, "mode", ["chat", "task", "background"]);
4808
- }
4809
- if (eventType === "model.call.observed" || eventType === "action.executed") {
4810
- assertEnum(payload, "status", ["ok", "error"]);
4811
- }
4812
- if (eventType === "model.call.observed" && payload.status === "error") {
4813
- for (const key of ["error_category", "error_hash"]) {
4814
- if (!payload[key]) throw new Error(`model.call.observed error requires ${key}`);
4815
- }
4816
- }
4817
- if (eventType === "tool.result.observed") {
4818
- assertEnum(payload, "status", ["success", "error"]);
4819
- if (payload.status === "success" && !payload.result_hash) {
4820
- throw new Error("tool.result.observed success requires result_hash");
4821
- }
4822
- if (payload.status === "error" && !payload.error_hash) {
4823
- throw new Error("tool.result.observed error requires error_hash");
4824
- }
4825
- }
4826
- if (eventType === "business.refs.resolved") {
4827
- assertEnum(payload, "resolver_status", ["resolved", "partial", "unresolved"]);
4828
- }
4829
- for (const key of ["source_event_ids", "operation_ids", "target_refs"]) {
4830
- if (key in payload && (!Array.isArray(payload[key]) || payload[key].length === 0)) {
4831
- throw new Error(`${eventType} payload requires non-empty ${key}`);
4832
- }
4833
- }
4834
- if (eventType === "action.recommended") {
4835
- for (const ref of payload.target_refs) assertQualifiedReference(ref);
4836
- }
4837
- if (eventType === "evidence.refs.created") {
4838
- assertRefs(payload.evidence_refs, false);
4839
- }
4840
- if (eventType === "business.refs.resolved") {
4841
- const unresolved = payload.resolver_status === "unresolved";
4842
- assertRefs(payload.business_refs, unresolved);
4843
- }
4844
- if (eventType === "action.result_recorded" && !payload.task_ref && !payload.artifact_ref && !payload.result_artifact_ref) {
4845
- throw new Error(
4846
- "action.result_recorded requires task_ref, artifact_ref, or result_artifact_ref"
4847
- );
4848
- }
4849
- if (eventType === "action.executed" && payload.status === "error") {
4850
- for (const key of ["error_category", "error_hash"]) {
4851
- if (!payload[key]) throw new Error(`action.executed error requires ${key}`);
4852
- }
4853
- }
4854
- const fixedStatus = {
4855
- "action.recommended": "recommended",
4856
- "action.approval_requested": "approval_requested",
4857
- "action.approved": "approved",
4858
- "action.rejected": "rejected"
5324
+ function traceLifecycleApi(ctx) {
5325
+ const post2 = async (path, input) => {
5326
+ assertNoForbiddenInputFields(input);
5327
+ return await request(ctx, `${LIFECYCLE}${path}`, { method: "POST", body: input });
5328
+ };
5329
+ const get = (path) => request(ctx, `${LIFECYCLE}${path}`, { method: "GET" });
5330
+ const interactionTerminal = (interactionId, action, input) => post2(`/interactions/${encodeURIComponent(interactionId)}/${action}`, input);
5331
+ const finishAttempt = (operationId, attempt, action, input) => post2(
5332
+ `/operations/${encodeURIComponent(operationId)}/attempts/${encodeURIComponent(String(attempt))}:${action}`,
5333
+ input
5334
+ );
5335
+ return {
5336
+ listConversations: (query = {}) => {
5337
+ const params = new URLSearchParams();
5338
+ if (query.limit !== void 0 && Number.isFinite(query.limit)) {
5339
+ params.set("limit", String(query.limit));
5340
+ }
5341
+ const suffix = params.size > 0 ? `?${params.toString()}` : "";
5342
+ return get(`/conversations${suffix}`);
5343
+ },
5344
+ ensureConversation: (input) => post2("/conversations:ensure-current", input),
5345
+ createNewConversationGeneration: (input) => post2("/conversations:create-new-generation", input),
5346
+ resumeConversation: (input) => post2("/conversations:resume-by-id", input),
5347
+ getConversation: (conversationId) => get(`/conversations/${encodeURIComponent(conversationId)}`),
5348
+ closeConversation: (conversationId, input) => post2(`/conversations/${encodeURIComponent(conversationId)}/close`, input),
5349
+ startInteraction: (conversationId, input) => post2(`/conversations/${encodeURIComponent(conversationId)}/interactions`, input),
5350
+ getInteraction: (interactionId) => get(`/interactions/${encodeURIComponent(interactionId)}`),
5351
+ completeInteraction: (interactionId, input) => interactionTerminal(interactionId, "complete", input),
5352
+ failInteraction: (interactionId, input) => interactionTerminal(interactionId, "fail", input),
5353
+ cancelInteraction: (interactionId, input) => interactionTerminal(interactionId, "cancel", input),
5354
+ handoffInteraction: (interactionId, input) => interactionTerminal(interactionId, "handoff", input),
5355
+ ensureOperation: (conversationId, interactionId, input) => post2(
5356
+ `/conversations/${encodeURIComponent(conversationId)}/interactions/${encodeURIComponent(interactionId)}/operations:ensure`,
5357
+ input
5358
+ ),
5359
+ getOperation: (operationId) => get(`/operations/${encodeURIComponent(operationId)}`),
5360
+ retryOperationAttempt: (operationId, input) => post2(`/operations/${encodeURIComponent(operationId)}/attempts`, input),
5361
+ completeOperationAttempt: (operationId, attempt, input) => finishAttempt(operationId, attempt, "complete", input),
5362
+ failOperationAttempt: (operationId, attempt, input) => finishAttempt(operationId, attempt, "fail", input),
5363
+ getReceipt: (receiptId) => get(`/receipts/${encodeURIComponent(receiptId)}`)
4859
5364
  };
4860
- if (fixedStatus[eventType] && payload.status !== fixedStatus[eventType]) {
4861
- throw new Error(`${eventType} requires status=${fixedStatus[eventType]}`);
4862
- }
4863
- scanSafeValue(payload, "payload");
4864
- }
4865
- function assertRefs(value, allowEmpty) {
4866
- if (!Array.isArray(value) || !allowEmpty && value.length === 0) {
4867
- throw new Error("reference list must be a non-empty array");
4868
- }
4869
- for (const item of value) {
4870
- if (!item || typeof item !== "object" || Array.isArray(item)) {
4871
- throw new Error("reference must be an object");
4872
- }
4873
- const ref = item;
4874
- for (const key of Object.keys(ref)) {
4875
- if (!REF_FIELDS.has(key)) throw new Error(`reference field is not registered: ${key}`);
4876
- }
4877
- for (const key of [
4878
- "ref_id",
4879
- "ref_type",
4880
- "source_system",
4881
- "validity",
4882
- "version_status",
4883
- "visibility"
4884
- ]) {
4885
- if (!ref[key]) throw new Error(`reference requires ${key}`);
4886
- }
4887
- assertQualifiedReference(String(ref.ref_id));
4888
- assertEnum(ref, "validity", ["observed", "available", "unavailable", "expired", "partial"]);
4889
- assertEnum(ref, "version_status", ["versioned", "unversioned", "not_auditable"]);
4890
- assertEnum(ref, "visibility", [
4891
- "visible",
4892
- "redacted",
4893
- "hidden",
4894
- "omitted",
4895
- "unresolved",
4896
- "unauthorized"
4897
- ]);
4898
- }
4899
- }
4900
- function assertQualifiedReference(value) {
4901
- const parts = value.trim().split(":");
4902
- const namespace = parts[0] ?? "";
4903
- let valid = parts.every((part) => part.length > 0);
4904
- if (["kn", "resource"].includes(namespace)) valid = valid && parts.length === 2;
4905
- if (["object", "relation", "action_type", "metric", "field"].includes(namespace)) {
4906
- valid = valid && parts.length === 3;
4907
- }
4908
- if (namespace === "property") valid = valid && parts.length === 4;
4909
- if (!valid) {
4910
- throw new Error("business reference id must include its knowledge-network or resource scope");
4911
- }
4912
- }
4913
- function assertEnum(value, key, allowed) {
4914
- if (!allowed.includes(String(value[key]))) {
4915
- throw new Error(`${key} must be one of ${allowed.join(", ")}`);
4916
- }
4917
5365
  }
4918
- function scanSafeValue(value, path) {
4919
- if (Array.isArray(value)) {
4920
- value.forEach((child, index) => scanSafeValue(child, `${path}[${index}]`));
4921
- return;
4922
- }
4923
- if (value && typeof value === "object") {
4924
- for (const [key, child] of Object.entries(value)) {
4925
- if (RAW_KEYS.has(key.toLowerCase())) throw new Error(`raw sensitive payload field: ${key}`);
4926
- if (key.endsWith("_hash") && child !== "" && !HASH_RE.test(String(child))) {
4927
- throw new Error(`${path}.${key} must be a sha256 hash`);
5366
+ function assertNoForbiddenInputFields(input) {
5367
+ const pending = [input];
5368
+ const seen = /* @__PURE__ */ new Set();
5369
+ while (pending.length > 0) {
5370
+ const value = pending.pop();
5371
+ if (typeof value !== "object" || value === null || seen.has(value)) continue;
5372
+ seen.add(value);
5373
+ for (const [field, nested] of Object.entries(value)) {
5374
+ if (FORBIDDEN_INPUT_FIELDS.has(field)) {
5375
+ throw new InputError(`Lifecycle input field "${field}" is not allowed`);
4928
5376
  }
4929
- scanSafeValue(child, `${path}.${key}`);
5377
+ pending.push(nested);
4930
5378
  }
4931
- return;
4932
- }
4933
- if (typeof value === "string" && RAW_VALUE_PATTERNS.some((pattern) => pattern.test(value))) {
4934
- throw new Error(`raw sensitive payload value at ${path}`);
4935
5379
  }
4936
5380
  }
4937
- var TraceSession = class {
4938
- interactionId;
4939
- trace;
4940
- producerModule;
4941
- spanId;
4942
- emit;
4943
- contractVersion;
5381
+
5382
+ // src/managed-trace.ts
5383
+ import { randomUUID as randomUUID3 } from "crypto";
5384
+ var FORBIDDEN_INPUT_FIELDS2 = ["generation", "on_behalf_of", "onBehalfOf"];
5385
+ var CompletionMissingError = class extends InputError {
5386
+ };
5387
+ var OperationFailedError = class extends InputError {
5388
+ };
5389
+ var ManagedTrace = class {
5390
+ constructor(api, options = {}) {
5391
+ this.api = api;
5392
+ this.idFactory = options.idFactory ?? randomUUID3;
5393
+ }
5394
+ api;
4944
5395
  idFactory;
4945
- now;
4946
- events = [];
4947
- eventIDs = /* @__PURE__ */ new Set();
4948
- operationIDs = /* @__PURE__ */ new Set();
4949
- claimEventIDs = /* @__PURE__ */ new Map();
4950
- actions = /* @__PURE__ */ new WeakMap();
4951
- flushTail = Promise.resolve();
4952
- constructor(options) {
4953
- assertSessionOptions(options);
4954
- this.trace = clone(options.trace);
4955
- if (options.conversationId) {
4956
- this.trace["bkn.conversation.id"] = options.conversationId;
4957
- }
4958
- this.producerModule = options.producerModule;
4959
- this.spanId = options.spanId;
4960
- this.emit = options.emit;
4961
- this.contractVersion = options.contractVersion ?? "2.1.0";
4962
- this.idFactory = options.idFactory ?? randomUUID2;
4963
- this.now = options.now ?? defaultNow;
4964
- this.interactionId = options.interactionId ?? this.idFactory();
4965
- }
4966
- startInteraction(input) {
4967
- return this.append("agent.interaction.started", {
4968
- operationName: input.operationName,
4969
- payload: {
4970
- intent_hash: input.intentHash,
4971
- mode: input.mode,
4972
- ...input.agentId ? { agent_id: input.agentId } : {},
4973
- ...input.appRef ? { app_ref: input.appRef } : {},
4974
- ...input.questionArtifactRef ? { question_artifact_ref: input.questionArtifactRef } : {}
5396
+ pendingConversations = /* @__PURE__ */ new Map();
5397
+ activeConversationIds = /* @__PURE__ */ new Set();
5398
+ async withInteraction(strategy, callback) {
5399
+ assertSafeStrategy(strategy);
5400
+ const conversation = await this.resolveConversation(strategy);
5401
+ if (this.activeConversationIds.has(conversation.conversation_id)) {
5402
+ throw new InputError(
5403
+ `Conversation "${conversation.conversation_id}" already has an active interaction`
5404
+ );
5405
+ }
5406
+ this.activeConversationIds.add(conversation.conversation_id);
5407
+ try {
5408
+ const interaction = await this.api.startInteraction(conversation.conversation_id, {
5409
+ idempotency_key: this.idFactory()
5410
+ });
5411
+ const receipts = /* @__PURE__ */ new Map();
5412
+ let terminal;
5413
+ let terminalAction;
5414
+ const terminalInput = (reason) => ({
5415
+ terminal_idempotency_key: this.idFactory(),
5416
+ lease_token: interaction.lease_token,
5417
+ lease_epoch: interaction.lease_epoch,
5418
+ completion_manifest_version: "3.0.0",
5419
+ completion_reason: reason,
5420
+ claims: [],
5421
+ expected_operations: expectedOperations(receipts.values()),
5422
+ expected_receipts: expectedReceipts(receipts.values())
5423
+ });
5424
+ const scope = {
5425
+ conversation,
5426
+ interaction,
5427
+ bknContext: (operationKey, parentOperationId, causationEventIds) => ({
5428
+ bkn_context: {
5429
+ conversation_id: conversation.conversation_id,
5430
+ interaction_id: interaction.interaction_id,
5431
+ operation_key: operationKey,
5432
+ ...parentOperationId ? { parent_operation_id: parentOperationId } : {},
5433
+ ...causationEventIds?.length ? { causation_event_ids: causationEventIds } : {}
5434
+ }
5435
+ }),
5436
+ recordReceipt: (receipt) => {
5437
+ if (receipt.conversation_id !== conversation.conversation_id || receipt.interaction_id !== interaction.interaction_id) {
5438
+ throw new InputError("Receipt does not belong to the active managed interaction");
5439
+ }
5440
+ receipts.set(receipt.receipt_id, receipt);
5441
+ },
5442
+ supportCandidates: () => [...receipts.values()].flatMap(
5443
+ (receipt) => receipt.observed_evidence_refs.map((ref) => ({
5444
+ ref,
5445
+ state: "observed",
5446
+ adopted: false
5447
+ }))
5448
+ ),
5449
+ runOperation: async (input, execute) => this.runOperation(
5450
+ conversation,
5451
+ interaction,
5452
+ input,
5453
+ execute,
5454
+ scope.bknContext,
5455
+ scope.recordReceipt
5456
+ ),
5457
+ cancel: async (reason) => {
5458
+ terminalAction ??= this.api.cancelInteraction(
5459
+ interaction.interaction_id,
5460
+ terminalInput(reason)
5461
+ );
5462
+ terminal = await terminalAction;
5463
+ return terminal;
5464
+ },
5465
+ handoff: async (reason) => {
5466
+ terminalAction ??= this.api.handoffInteraction(
5467
+ interaction.interaction_id,
5468
+ terminalInput(reason)
5469
+ );
5470
+ terminal = await terminalAction;
5471
+ return terminal;
5472
+ }
5473
+ };
5474
+ let completion;
5475
+ try {
5476
+ completion = await callback(scope);
5477
+ if (terminalAction) return await terminalAction;
5478
+ if (terminal) return terminal;
5479
+ if (!completion || typeof completion !== "object") {
5480
+ throw new CompletionMissingError(
5481
+ "Interaction callback must return a completion manifest"
5482
+ );
5483
+ }
5484
+ } catch (error) {
5485
+ if (!terminalAction && !terminal) {
5486
+ try {
5487
+ terminal = await this.api.failInteraction(
5488
+ interaction.interaction_id,
5489
+ terminalInput(
5490
+ error instanceof CompletionMissingError ? "completion_missing" : "callback_failed"
5491
+ )
5492
+ );
5493
+ } catch {
5494
+ }
5495
+ }
5496
+ throw error;
4975
5497
  }
4976
- });
4977
- }
4978
- observeOperation(eventType, input) {
4979
- const operationId = input.operationId ?? this.idFactory();
4980
- this.operationIDs.add(operationId);
4981
- return this.append(eventType, { ...input, operationId });
4982
- }
4983
- createClaim(input) {
4984
- if (input.sourceEventIds.length === 0 || input.operationIds.length === 0) {
4985
- throw new Error("claim requires at least one source event and operation");
4986
- }
4987
- this.assertKnownRefs(input.sourceEventIds, this.eventIDs, "event");
4988
- this.assertKnownRefs(input.operationIds, this.operationIDs, "operation");
4989
- const event = this.append("claim.created", {
4990
- operationName: input.operationName,
4991
- causationEventId: input.causationEventId,
4992
- claimId: input.claimId,
4993
- payload: {
4994
- claim_id: input.claimId,
4995
- claim_type: input.claimType,
4996
- claim_hash: input.claimHash,
4997
- source_event_ids: input.sourceEventIds,
4998
- operation_ids: input.operationIds,
4999
- visibility: input.visibility ?? "visible",
5000
- version_status: input.versionStatus ?? "unversioned",
5001
- ...input.resultArtifactRef ? { result_artifact_ref: input.resultArtifactRef } : {}
5498
+ const completionInput = {
5499
+ ...completion,
5500
+ terminal_idempotency_key: this.idFactory(),
5501
+ lease_token: interaction.lease_token,
5502
+ lease_epoch: interaction.lease_epoch,
5503
+ expected_operations: completion.expected_operations ?? expectedOperations(receipts.values()),
5504
+ expected_receipts: completion.expected_receipts ?? expectedReceipts(receipts.values())
5505
+ };
5506
+ try {
5507
+ return await this.api.completeInteraction(interaction.interaction_id, completionInput);
5508
+ } catch (completeError) {
5509
+ let current;
5510
+ try {
5511
+ current = await this.api.getInteraction(interaction.interaction_id);
5512
+ } catch {
5513
+ throw completeError;
5514
+ }
5515
+ if (current.execution_status === "completed") return current;
5516
+ if (current.execution_status !== "active") {
5517
+ throw new InputError(
5518
+ `Interaction terminal state "${current.execution_status}" conflicts with complete`
5519
+ );
5520
+ }
5521
+ return await this.api.completeInteraction(interaction.interaction_id, completionInput);
5002
5522
  }
5523
+ } finally {
5524
+ this.activeConversationIds.delete(conversation.conversation_id);
5525
+ }
5526
+ }
5527
+ async resolveConversation(strategy) {
5528
+ switch (strategy.mode) {
5529
+ case "resume_by_id":
5530
+ return await this.api.resumeConversation({ conversation_id: strategy.conversationId });
5531
+ case "create_new_generation":
5532
+ return await this.api.createNewConversationGeneration({
5533
+ external_conversation_key: strategy.externalConversationKey,
5534
+ idempotency_key: this.idFactory()
5535
+ });
5536
+ case "one_shot":
5537
+ return await this.api.ensureConversation({
5538
+ external_conversation_key: strategy.externalConversationKey ?? `one-shot-${this.idFactory()}`,
5539
+ idempotency_key: this.idFactory(),
5540
+ one_shot: true
5541
+ });
5542
+ case "ensure_current":
5543
+ return await this.ensureCurrent(strategy.externalConversationKey);
5544
+ }
5545
+ }
5546
+ async ensureCurrent(externalConversationKey) {
5547
+ const pending = this.pendingConversations.get(externalConversationKey);
5548
+ if (pending) return await pending;
5549
+ const request2 = this.api.ensureConversation({
5550
+ external_conversation_key: externalConversationKey,
5551
+ idempotency_key: this.idFactory()
5003
5552
  });
5004
- this.claimEventIDs.set(input.claimId, event.event_id);
5005
- return event;
5006
- }
5007
- createEvidenceRefs(input) {
5008
- const claimEventID = this.requireClaim(input.claimId);
5009
- if (input.refs.length === 0) throw new Error("evidence refs must not be empty");
5010
- const operationId = this.idFactory();
5011
- this.operationIDs.add(operationId);
5012
- const event = this.append("evidence.refs.created", {
5013
- operationName: input.operationName,
5014
- operationId,
5015
- causationEventId: input.causationEventId ?? claimEventID,
5016
- claimId: input.claimId,
5017
- payload: {
5018
- claim_id: input.claimId,
5019
- evidence_refs: input.refs.map((ref) => ({
5020
- ref_id: ref.refId,
5021
- ref_type: ref.refType,
5022
- source_system: ref.sourceSystem,
5023
- validity: ref.validity,
5024
- version_status: ref.versionStatus,
5025
- visibility: ref.visibility,
5026
- ...ref.summaryHash ? { summary_hash: ref.summaryHash } : {}
5027
- }))
5553
+ this.pendingConversations.set(externalConversationKey, request2);
5554
+ try {
5555
+ return await request2;
5556
+ } finally {
5557
+ if (this.pendingConversations.get(externalConversationKey) === request2) {
5558
+ this.pendingConversations.delete(externalConversationKey);
5028
5559
  }
5029
- });
5030
- this.claimEventIDs.set(input.claimId, event.event_id);
5031
- return event;
5032
- }
5033
- resolveBusinessRefs(input) {
5034
- const claimEventID = this.requireClaim(input.claimId);
5035
- if (input.resolverStatus === "resolved" && input.refs.length === 0) {
5036
- throw new Error("resolved business refs must not be empty");
5037
- }
5038
- const operationId = this.idFactory();
5039
- this.operationIDs.add(operationId);
5040
- const event = this.append("business.refs.resolved", {
5041
- operationName: input.operationName,
5042
- operationId,
5043
- causationEventId: input.causationEventId ?? claimEventID,
5044
- claimId: input.claimId,
5045
- payload: {
5046
- claim_id: input.claimId,
5047
- resolver_status: input.resolverStatus,
5048
- business_refs: input.refs.map((ref) => ({
5049
- ref_id: ref.refId,
5050
- ref_type: ref.refType,
5051
- source_system: ref.sourceSystem,
5052
- validity: ref.validity,
5053
- version_status: ref.versionStatus,
5054
- visibility: ref.visibility
5055
- }))
5560
+ }
5561
+ }
5562
+ async runOperation(conversation, interaction, input, execute, bknContext, recordReceipt) {
5563
+ const operationKey = input.operationKey ?? this.idFactory();
5564
+ let current = await this.api.ensureOperation(
5565
+ conversation.conversation_id,
5566
+ interaction.interaction_id,
5567
+ {
5568
+ operation_key: operationKey,
5569
+ tool_name: input.toolName,
5570
+ normalized_input_hash: input.normalizedInputHash,
5571
+ parent_operation_id: input.parentOperationId,
5572
+ causation_event_ids: input.causationEventIds,
5573
+ required: input.required ?? true,
5574
+ lease_token: interaction.lease_token,
5575
+ lease_epoch: interaction.lease_epoch
5056
5576
  }
5057
- });
5058
- this.claimEventIDs.set(input.claimId, event.event_id);
5059
- return event;
5060
- }
5061
- recommendAction(input) {
5062
- const claimEventID = this.requireClaim(input.claimId);
5063
- if (input.targetRefs.length === 0) throw new Error("action target refs must not be empty");
5064
- const operationId = this.idFactory();
5065
- const actionInstanceId = this.idFactory();
5066
- this.operationIDs.add(operationId);
5067
- const event = this.append("action.recommended", {
5068
- operationName: input.operationName,
5069
- operationId,
5070
- causationEventId: input.causationEventId ?? claimEventID,
5071
- claimId: input.claimId,
5072
- payload: {
5073
- action_instance_id: actionInstanceId,
5074
- action_type: input.actionType,
5075
- target_refs: input.targetRefs,
5076
- reason_hash: input.reasonHash,
5077
- ...input.reasonArtifactRef ? { reason_artifact_ref: input.reasonArtifactRef } : {},
5078
- ...input.inputArtifactRef ? { input_artifact_ref: input.inputArtifactRef } : {},
5079
- status: "recommended"
5577
+ );
5578
+ const maxAttempts = input.maxAttempts ?? 2;
5579
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 10) {
5580
+ throw new InputError("maxAttempts must be an integer between 1 and 10");
5581
+ }
5582
+ while (true) {
5583
+ if (isCompletedReceipt(current.receipt)) {
5584
+ recordReceipt(current.receipt);
5585
+ return { value: void 0, receipt: current.receipt, recovered: true };
5080
5586
  }
5081
- });
5082
- const internal = {
5083
- actionInstanceId,
5084
- claimId: input.claimId,
5085
- operationId,
5086
- lastEventId: event.event_id,
5087
- state: "recommended"
5088
- };
5089
- const handle = Object.freeze({
5090
- get actionInstanceId() {
5091
- return internal.actionInstanceId;
5092
- },
5093
- get claimId() {
5094
- return internal.claimId;
5095
- },
5096
- get operationId() {
5097
- return internal.operationId;
5098
- },
5099
- get lastEventId() {
5100
- return internal.lastEventId;
5101
- },
5102
- get state() {
5103
- return internal.state;
5587
+ if (isFailedReceipt(current.receipt)) {
5588
+ recordReceipt(current.receipt);
5589
+ if (!current.operation.retryable) {
5590
+ throw new OperationFailedError(
5591
+ `Operation "${current.operation.operation_id}" failed and is not retryable`
5592
+ );
5593
+ }
5594
+ if (current.operation.attempt >= maxAttempts) {
5595
+ throw new OperationFailedError(
5596
+ `Operation "${current.operation.operation_id}" reached maximum attempt count ${maxAttempts}`
5597
+ );
5598
+ }
5599
+ current = await this.api.retryOperationAttempt(current.operation.operation_id, {
5600
+ lease_token: interaction.lease_token,
5601
+ lease_epoch: interaction.lease_epoch
5602
+ });
5603
+ continue;
5104
5604
  }
5105
- });
5106
- this.actions.set(handle, internal);
5107
- return handle;
5108
- }
5109
- requestActionApproval(action, input) {
5110
- const internal = this.expectActionState(action, "recommended");
5111
- const event = this.appendAction(internal, "action.approval_requested", {
5112
- action_instance_id: internal.actionInstanceId,
5113
- policy_ref: input.policyRef,
5114
- status: "approval_requested"
5115
- });
5116
- internal.state = "approval_requested";
5117
- internal.lastEventId = event.event_id;
5118
- return event;
5119
- }
5120
- approveAction(action, input) {
5121
- const internal = this.expectActionState(action, "approval_requested");
5122
- const event = this.appendAction(internal, "action.approved", {
5123
- action_instance_id: internal.actionInstanceId,
5124
- actor_ref: input.actorRef,
5125
- policy_decision_ref: input.policyDecisionRef,
5126
- status: "approved"
5127
- });
5128
- internal.state = "approved";
5129
- internal.lastEventId = event.event_id;
5130
- return event;
5131
- }
5132
- rejectAction(action, input) {
5133
- const internal = this.expectActionState(action, "approval_requested");
5134
- const event = this.appendAction(internal, "action.rejected", {
5135
- action_instance_id: internal.actionInstanceId,
5136
- actor_ref: input.actorRef,
5137
- policy_decision_ref: input.policyDecisionRef,
5138
- status: "rejected"
5139
- });
5140
- internal.state = "rejected";
5141
- internal.lastEventId = event.event_id;
5142
- return event;
5143
- }
5144
- executeAction(action, input) {
5145
- const internal = this.expectActionState(
5146
- action,
5147
- "approved",
5148
- "requires approval before execution"
5149
- );
5150
- const event = this.appendAction(internal, "action.executed", {
5151
- action_instance_id: internal.actionInstanceId,
5152
- status: input.status,
5153
- invocation_ref: input.invocationRef,
5154
- ...input.status === "error" ? { error_category: input.errorCategory, error_hash: input.errorHash } : {}
5155
- });
5156
- internal.state = "executed";
5157
- internal.lastEventId = event.event_id;
5158
- return event;
5159
- }
5160
- recordActionResult(action, input) {
5161
- const internal = this.expectActionState(action, "executed");
5162
- const event = this.appendAction(internal, "action.result_recorded", {
5163
- action_instance_id: internal.actionInstanceId,
5164
- result_hash: input.resultHash,
5165
- status: input.status,
5166
- ...input.taskRef ? { task_ref: input.taskRef } : {},
5167
- ...input.artifactRef ? { artifact_ref: input.artifactRef } : {},
5168
- ...input.resultArtifactRef ? { result_artifact_ref: input.resultArtifactRef } : {}
5169
- });
5170
- internal.state = "result_recorded";
5171
- internal.lastEventId = event.event_id;
5172
- return event;
5173
- }
5174
- pendingEvents() {
5175
- return clone(this.events);
5176
- }
5177
- flush() {
5178
- const requestedIDs = new Set(this.events.map((event) => event.event_id));
5179
- const operation = this.flushTail.then(() => this.flushEvents(requestedIDs));
5180
- this.flushTail = operation.then(
5181
- () => void 0,
5182
- () => void 0
5183
- );
5184
- return operation;
5185
- }
5186
- appendAction(action, eventType, payload) {
5187
- return this.append(eventType, {
5188
- operationName: eventType,
5189
- operationId: action.operationId,
5190
- causationEventId: action.lastEventId,
5191
- claimId: action.claimId,
5192
- payload
5193
- });
5194
- }
5195
- append(eventType, input) {
5196
- assertSafePayload(eventType, input.payload);
5197
- this.assertContractPayload(eventType, input.payload);
5198
- if (eventType !== "agent.interaction.started") {
5199
- if (!input.causationEventId) throw new Error(`${eventType} requires causation_event_id`);
5200
- if (!this.eventIDs.has(input.causationEventId)) {
5201
- throw new Error(`unknown event reference: ${input.causationEventId}`);
5605
+ let result;
5606
+ try {
5607
+ result = await execute({
5608
+ context: bknContext(operationKey, input.parentOperationId, input.causationEventIds),
5609
+ operation: current.operation,
5610
+ receipt: current.receipt
5611
+ });
5612
+ } catch (executeError) {
5613
+ const recovered = await this.api.getReceipt(current.receipt.receipt_id);
5614
+ if (!isTerminalReceipt(recovered)) throw executeError;
5615
+ assertSameReceipt(current.receipt, recovered);
5616
+ current = {
5617
+ ...current,
5618
+ operation: isFailedReceipt(recovered) ? await this.api.getOperation(current.operation.operation_id) : current.operation,
5619
+ receipt: recovered
5620
+ };
5621
+ continue;
5202
5622
  }
5203
- }
5204
- if (eventType !== "agent.interaction.started" && eventType !== "claim.created" && !input.operationId) {
5205
- throw new Error(`${eventType} requires operation_id`);
5206
- }
5207
- const eventID = this.idFactory();
5208
- if (this.eventIDs.has(eventID)) throw new Error(`duplicate event id: ${eventID}`);
5209
- const timestamp = this.now();
5210
- const event = {
5211
- event_id: eventID,
5212
- event_type: eventType,
5213
- "bkn.trace.schema.version": this.contractVersion,
5214
- observed_at: timestamp,
5215
- emitted_at: timestamp,
5216
- producer_module: this.producerModule,
5217
- trace_id: this.trace.trace_id,
5218
- span_id: this.spanId,
5219
- "bkn.request.id": this.trace["bkn.request.id"],
5220
- "bkn.operation.name": input.operationName,
5221
- interaction_id: this.interactionId,
5222
- ...input.operationId ? { operation_id: input.operationId } : {},
5223
- ...input.causationEventId ? { causation_event_id: input.causationEventId } : {},
5224
- ...input.claimId ? { claim_id: input.claimId } : {},
5225
- ...input.attempt ? { attempt: input.attempt } : {},
5226
- payload: clone(input.payload)
5227
- };
5228
- this.events.push(clone(event));
5229
- this.eventIDs.add(eventID);
5230
- return clone(event);
5231
- }
5232
- async flushEvents(requestedIDs) {
5233
- const batch = this.events.filter((event) => requestedIDs.has(event.event_id));
5234
- if (batch.length === 0) return void 0;
5235
- const response = await this.emit({
5236
- "bkn.trace.schema.version": this.contractVersion,
5237
- trace: clone(this.trace),
5238
- events: clone(batch)
5239
- });
5240
- const remaining = this.events.filter((event) => !requestedIDs.has(event.event_id));
5241
- this.events.splice(0, this.events.length, ...remaining);
5242
- return response;
5243
- }
5244
- assertContractPayload(eventType, payload) {
5245
- if (this.contractVersion !== "2.2.0") return;
5246
- const requiredArtifactFields = {
5247
- "agent.interaction.started": ["question_artifact_ref"],
5248
- "data.query.observed": ["query_artifact_ref", "result_artifact_ref"],
5249
- "logic.execution.observed": ["input_artifact_ref", "result_artifact_ref"],
5250
- "claim.created": ["result_artifact_ref"],
5251
- "action.recommended": ["input_artifact_ref"],
5252
- "action.result_recorded": ["result_artifact_ref"]
5253
- };
5254
- for (const field of requiredArtifactFields[eventType] ?? []) {
5255
- const value = payload[field];
5256
- if (typeof value !== "string" || !/^artifact:[0-9A-Za-z][0-9A-Za-z_.:-]{0,127}$/.test(value)) {
5257
- throw new Error(`${eventType} payload requires valid ${field}`);
5623
+ assertSameReceipt(current.receipt, result.receipt);
5624
+ recordReceipt(result.receipt);
5625
+ if (isCompletedReceipt(result.receipt)) {
5626
+ return { ...result, recovered: false };
5258
5627
  }
5259
- }
5260
- for (const [field, value] of Object.entries(payload)) {
5261
- if (field.endsWith("_artifact_ref") && value !== void 0) {
5262
- if (typeof value !== "string" || !/^artifact:[0-9A-Za-z][0-9A-Za-z_.:-]{0,127}$/.test(value)) {
5263
- throw new Error(`${eventType} payload requires valid ${field}`);
5264
- }
5628
+ current = {
5629
+ ...current,
5630
+ operation: isFailedReceipt(result.receipt) ? await this.api.getOperation(current.operation.operation_id) : current.operation,
5631
+ receipt: result.receipt
5632
+ };
5633
+ if (!isTerminalReceipt(result.receipt)) {
5634
+ throw new InputError(
5635
+ `Operation "${current.operation.operation_id}" returned a pending receipt`
5636
+ );
5265
5637
  }
5266
5638
  }
5267
- if (eventType === "action.result_recorded" && payload.artifact_ref !== void 0) {
5268
- throw new Error("action.result_recorded 2.2 does not accept legacy artifact_ref");
5269
- }
5270
5639
  }
5271
- assertKnownRefs(values, known, kind) {
5272
- for (const value of values) {
5273
- if (!known.has(value)) throw new Error(`unknown ${kind} reference: ${value}`);
5274
- }
5640
+ };
5641
+ function assertSafeStrategy(strategy) {
5642
+ if (!strategy || typeof strategy !== "object") {
5643
+ throw new InputError("A conversation strategy is required");
5275
5644
  }
5276
- requireClaim(claimID) {
5277
- const eventID = this.claimEventIDs.get(claimID);
5278
- if (!eventID) throw new Error(`unknown claim reference: ${claimID}`);
5279
- return eventID;
5645
+ for (const field of FORBIDDEN_INPUT_FIELDS2) {
5646
+ if (field in strategy) throw new InputError(`Lifecycle input field "${field}" is not allowed`);
5280
5647
  }
5281
- expectActionState(action, expected, message) {
5282
- const internal = this.actions.get(action);
5283
- if (!internal) throw new Error("action handle does not belong to this trace session");
5284
- if (internal.state !== expected) {
5285
- if (message) throw new Error(`action ${internal.actionInstanceId} ${message}`);
5286
- throw new Error(
5287
- `action ${internal.actionInstanceId} must be ${expected}, got ${internal.state}`
5288
- );
5289
- }
5290
- return internal;
5648
+ }
5649
+ function expectedOperations(receipts) {
5650
+ const operations = /* @__PURE__ */ new Map();
5651
+ for (const receipt of receipts) {
5652
+ operations.set(
5653
+ receipt.operation_id,
5654
+ (operations.get(receipt.operation_id) ?? false) || receipt.required
5655
+ );
5291
5656
  }
5292
- };
5657
+ return [...operations].map(([operation_id, required]) => ({ operation_id, required }));
5658
+ }
5659
+ function expectedReceipts(receipts) {
5660
+ return [...receipts].map((receipt) => ({
5661
+ receipt_id: receipt.receipt_id,
5662
+ required: receipt.required
5663
+ }));
5664
+ }
5665
+ function isTerminalReceipt(receipt) {
5666
+ return receipt.receipt_status !== "pending";
5667
+ }
5668
+ function isCompletedReceipt(receipt) {
5669
+ return receipt.receipt_status === "completed";
5670
+ }
5671
+ function isFailedReceipt(receipt) {
5672
+ return receipt.receipt_status === "failed";
5673
+ }
5674
+ function assertSameReceipt(expected, actual) {
5675
+ if (actual.receipt_id !== expected.receipt_id || actual.operation_id !== expected.operation_id || actual.attempt !== expected.attempt || actual.operation_key !== expected.operation_key || actual.normalized_input_hash !== expected.normalized_input_hash) {
5676
+ throw new InputError("Recovered receipt does not match the registered operation attempt");
5677
+ }
5678
+ }
5293
5679
 
5294
5680
  // src/api/trace.ts
5295
5681
  var SEARCH = "/api/agent-observability/v1/traces/_search";
5296
- var EVIDENCE_EVENTS = "/api/agent-observability/v1/evidence/events";
5297
- var EVIDENCE_ARTIFACTS = "/api/agent-observability/v1/evidence/artifacts";
5298
- var REQUESTS = "/api/agent-observability/v1/requests";
5299
- var INTERACTIONS = "/api/agent-observability/v1/interactions";
5682
+ var BUSINESS_PROVENANCE = "/api/agent-observability/v1/business-provenance";
5683
+ var REQUESTS = `${BUSINESS_PROVENANCE}/requests`;
5684
+ var INTERACTIONS = `${BUSINESS_PROVENANCE}/interactions`;
5300
5685
  var TRACES = "/api/agent-observability/v1/traces";
5301
5686
  function isoToNanos(iso) {
5302
5687
  const ms = Date.parse(iso);
@@ -5339,67 +5724,9 @@ async function getRawSpansByConversation(ctx, conversationId, opts = {}) {
5339
5724
  function traceSearch(ctx, body) {
5340
5725
  return request(ctx, SEARCH, { method: "POST", body });
5341
5726
  }
5342
- function emitEvidenceEvents(ctx, body) {
5343
- return request(ctx, EVIDENCE_EVENTS, {
5344
- method: "POST",
5345
- body,
5346
- headers: evidenceWriteHeaders(ctx),
5347
- redirect: "manual"
5348
- });
5349
- }
5350
- function emitEvidenceArtifact(ctx, body) {
5351
- return request(ctx, EVIDENCE_ARTIFACTS, {
5352
- method: "POST",
5353
- body,
5354
- headers: evidenceWriteHeaders(ctx),
5355
- redirect: "manual"
5356
- });
5357
- }
5358
- function evidenceWriteHeaders(ctx) {
5359
- return ctx.evidenceIngestToken ? { "x-bkn-trace-ingest-token": ctx.evidenceIngestToken } : void 0;
5360
- }
5361
- function getEvidenceArtifact(ctx, artifactId) {
5362
- return request(ctx, `${EVIDENCE_ARTIFACTS}/${encodeURIComponent(artifactId)}`);
5363
- }
5364
- function listRequestSummaries(ctx, query = {}) {
5365
- return request(ctx, REQUESTS, {
5366
- query: summaryQuery(query)
5367
- });
5368
- }
5369
- function getRequestSummary(ctx, requestId) {
5370
- return request(ctx, `${REQUESTS}/${encodeURIComponent(requestId)}`);
5371
- }
5372
- function getInteractionSummary(ctx, interactionId) {
5373
- return request(ctx, `${INTERACTIONS}/${encodeURIComponent(interactionId)}`);
5374
- }
5375
- function getRequestTraces(ctx, requestId, query = {}) {
5376
- return request(
5377
- ctx,
5378
- `${REQUESTS}/${encodeURIComponent(requestId)}/traces`,
5379
- { query: summaryQuery(query) }
5380
- );
5381
- }
5382
5727
  function getTraceGraph(ctx, traceId) {
5383
5728
  return request(ctx, `${TRACES}/${encodeURIComponent(traceId)}/trace-graph`);
5384
5729
  }
5385
- function getEvidenceChain(ctx, scope, opts = {}) {
5386
- const target = traceTarget(scope, "evidence-chain");
5387
- return request(ctx, target.path, {
5388
- query: queryWithLimit(target.query, opts)
5389
- });
5390
- }
5391
- function getBusinessGraph(ctx, scope, opts = {}) {
5392
- const target = traceTarget(scope, "business-graph");
5393
- return request(ctx, target.path, {
5394
- query: queryWithLimit(target.query, opts)
5395
- });
5396
- }
5397
- function getSnapshotPreview(ctx, scope, opts = {}) {
5398
- const target = traceTarget(scope, "snapshot-preview");
5399
- return request(ctx, target.path, {
5400
- query: queryWithLimit(target.query, opts)
5401
- });
5402
- }
5403
5730
  async function getSpansByConversation(ctx, conversationId, opts = {}) {
5404
5731
  const agg = await request(ctx, SEARCH, {
5405
5732
  method: "POST",
@@ -5424,38 +5751,6 @@ async function getSpansByConversation(ctx, conversationId, opts = {}) {
5424
5751
  }) ?? {};
5425
5752
  return (spans.hits?.hits ?? []).map((h) => h._source ?? {});
5426
5753
  }
5427
- function traceTarget(scope, subresource) {
5428
- if (typeof scope === "string") {
5429
- return { path: `${TRACES}/${encodeURIComponent(scope)}/${subresource}` };
5430
- }
5431
- if ("traceId" in scope) {
5432
- return { path: `${TRACES}/${encodeURIComponent(scope.traceId)}/${subresource}` };
5433
- }
5434
- const requestPath = subresource === "evidence-chain" ? `${TRACES}/by-request` : `${TRACES}/by-request/${subresource}`;
5435
- return { path: requestPath, query: { request_id: scope.requestId } };
5436
- }
5437
- function queryWithLimit(query, opts) {
5438
- if (opts.limit === void 0 || !Number.isFinite(opts.limit)) return query;
5439
- return { ...query ?? {}, limit: opts.limit };
5440
- }
5441
- function summaryQuery(query) {
5442
- const result = {};
5443
- if (query.limit !== void 0 && Number.isFinite(query.limit)) result.limit = query.limit;
5444
- if (query.cursor) result.cursor = query.cursor;
5445
- if (query.from) result.from = query.from;
5446
- if (query.to) result.to = query.to;
5447
- if (query.status) result.status = query.status;
5448
- if (query.agentOrApp) result.agent_or_app = query.agentOrApp;
5449
- if (query.businessDomain) result.business_domain = query.businessDomain;
5450
- if (query.conversationId) result.conversation_id = query.conversationId;
5451
- if (query.interactionId) result.interaction_id = query.interactionId;
5452
- if (query.knowledgeNetwork) result.knowledge_network = query.knowledgeNetwork;
5453
- if (query.evidenceCompleteness) {
5454
- result.evidence_completeness = query.evidenceCompleteness;
5455
- }
5456
- if (query.keyword) result.keyword = query.keyword;
5457
- return Object.keys(result).length ? result : void 0;
5458
- }
5459
5754
 
5460
5755
  // src/bkn-trace/claude-judge.ts
5461
5756
  import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
@@ -6838,6 +7133,8 @@ async function semanticJudge(question, answer, reference) {
6838
7133
  };
6839
7134
  }
6840
7135
  function trace(ctx) {
7136
+ const lifecycle = traceLifecycleApi(ctx);
7137
+ const managed = new ManagedTrace(lifecycle);
6841
7138
  const diagnoseOne = async (conversationId, opts = {}) => {
6842
7139
  const { spans, traceIds } = await getRawSpansByConversation(ctx, conversationId);
6843
7140
  if (spans.length === 0) throw new Error(`No spans found for conversation: ${conversationId}`);
@@ -6866,34 +7163,14 @@ function trace(ctx) {
6866
7163
  };
6867
7164
  };
6868
7165
  return {
7166
+ /** Low-level BKN Trace 3.0 lifecycle and durable receipt API. */
7167
+ lifecycle,
7168
+ /** Own one complete interaction lifecycle around an application callback. */
7169
+ withInteraction: managed.withInteraction.bind(managed),
6869
7170
  /** Raw trace search (OpenSearch-style body). */
6870
7171
  search: (body) => traceSearch(ctx, body),
6871
- /** Submit BKN Trace phase-two claim/evidence/business events. */
6872
- emitEvidenceEvents: (body) => emitEvidenceEvents(ctx, body),
6873
- /** Store one authorized BKN Trace 2.2 business-content artifact. */
6874
- emitArtifact: (body) => emitEvidenceArtifact(ctx, body),
6875
- /** Read one authorized BKN Trace 2.2 business-content artifact. */
6876
- artifact: (artifactId) => getEvidenceArtifact(ctx, artifactId),
6877
- /** Product-facing business request list and request-to-trace drilldown. */
6878
- requests: {
6879
- get: (requestId) => getRequestSummary(ctx, requestId),
6880
- list: (query) => listRequestSummaries(ctx, query),
6881
- traces: (requestId, query) => getRequestTraces(ctx, requestId, query)
6882
- },
6883
- /** Aggregate all OpenBKN requests and traces for one caller-owned interaction. */
6884
- interactions: {
6885
- get: (interactionId) => getInteractionSummary(ctx, interactionId)
6886
- },
6887
- /** Create a typed BKN Trace 2.1 session for an Agent or AI application. */
6888
- createSession: (options) => new TraceSession({ ...options, emit: (body) => emitEvidenceEvents(ctx, body) }),
6889
7172
  /** Normalized trace tree/status graph by trace id. */
6890
7173
  graph: (traceId) => getTraceGraph(ctx, traceId),
6891
- /** Claim -> evidence/business refs graph by trace id or BKN request id. */
6892
- evidenceChain: (scope, opts) => getEvidenceChain(ctx, scope, opts),
6893
- /** Business semantic graph by trace id or BKN request id. */
6894
- businessGraph: (scope, opts) => getBusinessGraph(ctx, scope, opts),
6895
- /** Metadata-only evidence snapshot preview by trace id or BKN request id. */
6896
- snapshotPreview: (scope, opts) => getSnapshotPreview(ctx, scope, opts),
6897
7174
  /** All span source docs for a conversation. */
6898
7175
  spans: (conversationId, opts) => getSpansByConversation(ctx, conversationId, opts),
6899
7176
  diagnose: diagnoseOne,
@@ -6965,15 +7242,18 @@ function vega(ctx) {
6965
7242
  return {
6966
7243
  catalogs: (opts) => listCatalogs(ctx, opts),
6967
7244
  getCatalog: (id) => getCatalog(ctx, id),
6968
- createCatalog: (req) => createCatalog(ctx, req),
6969
- updateCatalog: (id, req) => updateCatalog(ctx, id, req),
7245
+ createCatalog: (req, opts) => createCatalog(ctx, req, opts),
7246
+ updateCatalog: (id, req, opts) => updateCatalog(ctx, id, req, opts),
6970
7247
  enableCatalog: (id) => enableCatalog(ctx, id),
6971
7248
  disableCatalog: (id) => disableCatalog(ctx, id),
6972
7249
  deleteCatalog: (id) => deleteCatalog(ctx, id),
7250
+ testCatalogConnectionConfig: (req) => testCatalogConnectionConfig(ctx, req),
6973
7251
  testCatalogConnection: (id) => testCatalogConnection(ctx, id),
7252
+ catalogHealthCheckSchedule: (id) => getCatalogHealthCheckSchedule(ctx, id),
7253
+ updateCatalogHealthCheckSchedule: (id, req) => updateCatalogHealthCheckSchedule(ctx, id, req),
6974
7254
  discoverCatalog: (id, wait = false) => discoverCatalog(ctx, id, wait),
6975
7255
  catalogResources: (id, category, limit, offset) => listCatalogResources(ctx, id, category, limit, offset),
6976
- catalogHealth: (ids) => catalogHealthStatus(ctx, ids),
7256
+ catalogHealth: (id) => catalogHealthStatus(ctx, id),
6977
7257
  connectorTypes: () => listConnectorTypes(ctx),
6978
7258
  connectorType: (type) => getConnectorType(ctx, type),
6979
7259
  /** Run SQL / OpenSearch DSL directly against a data source. */
@@ -7120,7 +7400,7 @@ function createClient(opts = {}) {
7120
7400
  kn: kn(ctx),
7121
7401
  resource: resources(ctx),
7122
7402
  dataflows: dataflows(ctx),
7123
- agents: agents(ctx),
7403
+ agents: agents2(ctx),
7124
7404
  context: context(ctx),
7125
7405
  models: models(ctx),
7126
7406
  skills: skills(ctx),
@@ -7158,7 +7438,7 @@ function hostOf(baseUrl) {
7158
7438
  return baseUrl;
7159
7439
  }
7160
7440
  }
7161
- function normalize(baseUrl) {
7441
+ function normalize2(baseUrl) {
7162
7442
  return baseUrl.replace(/\/+$/, "");
7163
7443
  }
7164
7444
  function usernameOf(token) {
@@ -7167,7 +7447,7 @@ function usernameOf(token) {
7167
7447
  return token.username ?? token.displayName ?? claims?.preferred_username ?? claims?.name ?? claims?.sub;
7168
7448
  }
7169
7449
  function attachToken(baseUrl, accessToken, opts = {}) {
7170
- const url = normalize(baseUrl);
7450
+ const url = normalize2(baseUrl);
7171
7451
  const token = {
7172
7452
  baseUrl: url,
7173
7453
  accessToken,
@@ -7270,7 +7550,7 @@ function listPlatforms2() {
7270
7550
  );
7271
7551
  }
7272
7552
  function use(baseUrl) {
7273
- const url = normalize(baseUrl);
7553
+ const url = normalize2(baseUrl);
7274
7554
  if (!readToken(url)) {
7275
7555
  throw new InputError(`No saved credentials for ${url}. Run \`openbkn auth login\` first.`);
7276
7556
  }
@@ -7281,10 +7561,10 @@ function logout() {
7281
7561
  return baseUrl ? deleteToken(baseUrl) : false;
7282
7562
  }
7283
7563
  function deletePlatform(baseUrl, userId) {
7284
- return deleteToken(normalize(baseUrl), userId);
7564
+ return deleteToken(normalize2(baseUrl), userId);
7285
7565
  }
7286
7566
  function switchUser(baseUrl, userOrName) {
7287
- const url = normalize(baseUrl);
7567
+ const url = normalize2(baseUrl);
7288
7568
  const users = usersOf(url);
7289
7569
  const match = users.find((u) => u.userId === userOrName) ?? users.find((u) => (u.username ?? u.displayName) === userOrName);
7290
7570
  if (!match) {
@@ -7295,7 +7575,7 @@ function switchUser(baseUrl, userOrName) {
7295
7575
  return { baseUrl: url, userId: match.userId, username: match.username ?? match.displayName };
7296
7576
  }
7297
7577
  function usersOf(baseUrl) {
7298
- const url = normalize(baseUrl);
7578
+ const url = normalize2(baseUrl);
7299
7579
  return listPlatforms().find((p) => p.baseUrl === url)?.users ?? [];
7300
7580
  }
7301
7581
  function exportCreds() {
@@ -7313,6 +7593,7 @@ function exportCreds() {
7313
7593
 
7314
7594
  export {
7315
7595
  HttpError,
7596
+ ToolError,
7316
7597
  InputError,
7317
7598
  toExitCode,
7318
7599
  formatError,
@@ -7321,6 +7602,7 @@ export {
7321
7602
  deviceLogin,
7322
7603
  credentialDeviceLogin,
7323
7604
  request,
7605
+ lifecycleHint,
7324
7606
  rawCall,
7325
7607
  DEFAULT_BUSINESS_DOMAIN,
7326
7608
  DEFAULT_LIST_LIMIT,
@@ -7334,7 +7616,8 @@ export {
7334
7616
  getUserSafe,
7335
7617
  changePasswordSafe,
7336
7618
  admin,
7337
- agents,
7619
+ agents2 as agents,
7620
+ releaseLifecycleSessions,
7338
7621
  context,
7339
7622
  dataflows,
7340
7623
  parsePkMap,
@@ -7342,11 +7625,15 @@ export {
7342
7625
  kn,
7343
7626
  models,
7344
7627
  resources,
7628
+ classifyPath,
7629
+ filesUnder,
7630
+ renderTree,
7345
7631
  skills,
7346
7632
  toolboxes,
7633
+ traceLifecycleApi,
7347
7634
  renderReportMarkdown,
7348
7635
  validateFixturePath,
7349
- TraceSession,
7636
+ ManagedTrace,
7350
7637
  trace,
7351
7638
  vega,
7352
7639
  createClient,
@@ -7363,4 +7650,4 @@ export {
7363
7650
  exportCreds,
7364
7651
  auth_exports
7365
7652
  };
7366
- //# sourceMappingURL=chunk-LH3ONZGQ.js.map
7653
+ //# sourceMappingURL=chunk-PC2F54XD.js.map