@deepagents/context 0.39.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +44 -11
  2. package/dist/browser.js +171 -15
  3. package/dist/browser.js.map +3 -3
  4. package/dist/index.d.ts +6 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2309 -1233
  7. package/dist/index.js.map +4 -4
  8. package/dist/lib/agent.d.ts +6 -0
  9. package/dist/lib/agent.d.ts.map +1 -1
  10. package/dist/lib/chain-summary.d.ts +18 -0
  11. package/dist/lib/chain-summary.d.ts.map +1 -0
  12. package/dist/lib/engine.d.ts +25 -6
  13. package/dist/lib/engine.d.ts.map +1 -1
  14. package/dist/lib/fragments/message/user.d.ts +21 -2
  15. package/dist/lib/fragments/message/user.d.ts.map +1 -1
  16. package/dist/lib/fragments.d.ts +8 -3
  17. package/dist/lib/fragments.d.ts.map +1 -1
  18. package/dist/lib/resolvers/async-resolver.d.ts +8 -0
  19. package/dist/lib/resolvers/async-resolver.d.ts.map +1 -0
  20. package/dist/lib/resolvers/function-resolver.d.ts +8 -0
  21. package/dist/lib/resolvers/function-resolver.d.ts.map +1 -0
  22. package/dist/lib/resolvers/generator-resolver.d.ts +13 -0
  23. package/dist/lib/resolvers/generator-resolver.d.ts.map +1 -0
  24. package/dist/lib/resolvers/index.d.ts +10 -0
  25. package/dist/lib/resolvers/index.d.ts.map +1 -0
  26. package/dist/lib/resolvers/iterable-resolver.d.ts +12 -0
  27. package/dist/lib/resolvers/iterable-resolver.d.ts.map +1 -0
  28. package/dist/lib/resolvers/loader-resolver.d.ts +13 -0
  29. package/dist/lib/resolvers/loader-resolver.d.ts.map +1 -0
  30. package/dist/lib/resolvers/promise-resolver.d.ts +7 -0
  31. package/dist/lib/resolvers/promise-resolver.d.ts.map +1 -0
  32. package/dist/lib/resolvers/types.d.ts +45 -0
  33. package/dist/lib/resolvers/types.d.ts.map +1 -0
  34. package/dist/lib/sandbox/ast-utils.d.ts +5 -2
  35. package/dist/lib/sandbox/ast-utils.d.ts.map +1 -1
  36. package/dist/lib/sandbox/bash-tool.d.ts.map +1 -1
  37. package/dist/lib/sandbox/container-tool.d.ts +32 -8
  38. package/dist/lib/sandbox/container-tool.d.ts.map +1 -1
  39. package/dist/lib/sandbox/docker-sandbox-errors.d.ts +58 -0
  40. package/dist/lib/sandbox/docker-sandbox-errors.d.ts.map +1 -0
  41. package/dist/lib/sandbox/docker-sandbox.d.ts +69 -357
  42. package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -1
  43. package/dist/lib/sandbox/index.d.ts +2 -7
  44. package/dist/lib/sandbox/index.d.ts.map +1 -1
  45. package/dist/lib/sandbox/installers/github-release.d.ts +46 -0
  46. package/dist/lib/sandbox/installers/github-release.d.ts.map +1 -0
  47. package/dist/lib/sandbox/installers/index.d.ts +7 -0
  48. package/dist/lib/sandbox/installers/index.d.ts.map +1 -0
  49. package/dist/lib/sandbox/installers/installer.d.ts +44 -0
  50. package/dist/lib/sandbox/installers/installer.d.ts.map +1 -0
  51. package/dist/lib/sandbox/installers/npm.d.ts +32 -0
  52. package/dist/lib/sandbox/installers/npm.d.ts.map +1 -0
  53. package/dist/lib/sandbox/installers/package-manager.d.ts +19 -0
  54. package/dist/lib/sandbox/installers/package-manager.d.ts.map +1 -0
  55. package/dist/lib/sandbox/installers/pip.d.ts +37 -0
  56. package/dist/lib/sandbox/installers/pip.d.ts.map +1 -0
  57. package/dist/lib/sandbox/installers/url-binary.d.ts +41 -0
  58. package/dist/lib/sandbox/installers/url-binary.d.ts.map +1 -0
  59. package/dist/lib/sandbox/subcommand.d.ts +11 -11
  60. package/dist/lib/sandbox/subcommand.d.ts.map +1 -1
  61. package/dist/lib/sandbox/upload-skills.d.ts +6 -0
  62. package/dist/lib/sandbox/upload-skills.d.ts.map +1 -1
  63. package/dist/lib/sandbox/virtual-sandbox.d.ts +10 -0
  64. package/dist/lib/sandbox/virtual-sandbox.d.ts.map +1 -0
  65. package/dist/lib/save/reminder-target-handler.d.ts +36 -0
  66. package/dist/lib/save/reminder-target-handler.d.ts.map +1 -0
  67. package/dist/lib/save/save-pipeline.d.ts +28 -0
  68. package/dist/lib/save/save-pipeline.d.ts.map +1 -0
  69. package/dist/lib/save/tool-output-target-handler.d.ts +7 -0
  70. package/dist/lib/save/tool-output-target-handler.d.ts.map +1 -0
  71. package/dist/lib/save/user-target-handler.d.ts +7 -0
  72. package/dist/lib/save/user-target-handler.d.ts.map +1 -0
  73. package/dist/lib/stream/change-source.d.ts +11 -0
  74. package/dist/lib/stream/change-source.d.ts.map +1 -0
  75. package/dist/lib/stream/ddl.stream.postgres-notify.d.ts +3 -0
  76. package/dist/lib/stream/ddl.stream.postgres-notify.d.ts.map +1 -0
  77. package/dist/lib/stream/ddl.stream.postgres.d.ts +2 -0
  78. package/dist/lib/stream/ddl.stream.postgres.d.ts.map +1 -0
  79. package/dist/lib/stream/polling-change-source.d.ts +27 -0
  80. package/dist/lib/stream/polling-change-source.d.ts.map +1 -0
  81. package/dist/lib/stream/polling-policy.d.ts +0 -1
  82. package/dist/lib/stream/polling-policy.d.ts.map +1 -1
  83. package/dist/lib/stream/postgres-notify-change-source.d.ts +15 -0
  84. package/dist/lib/stream/postgres-notify-change-source.d.ts.map +1 -0
  85. package/dist/lib/stream/postgres.stream-store.d.ts +29 -0
  86. package/dist/lib/stream/postgres.stream-store.d.ts.map +1 -0
  87. package/dist/lib/stream/stream-manager.d.ts +8 -27
  88. package/dist/lib/stream/stream-manager.d.ts.map +1 -1
  89. package/dist/lib/ui-message-guards.d.ts +10 -0
  90. package/dist/lib/ui-message-guards.d.ts.map +1 -0
  91. package/package.json +2 -7
  92. package/dist/lib/sandbox/extension.d.ts +0 -39
  93. package/dist/lib/sandbox/extension.d.ts.map +0 -1
  94. package/dist/lib/sandbox/openapi/demo.d.ts +0 -2
  95. package/dist/lib/sandbox/openapi/demo.d.ts.map +0 -1
  96. package/dist/lib/sandbox/openapi/extension.d.ts +0 -19
  97. package/dist/lib/sandbox/openapi/extension.d.ts.map +0 -1
  98. package/dist/lib/sandbox/openapi/format.d.ts +0 -11
  99. package/dist/lib/sandbox/openapi/format.d.ts.map +0 -1
  100. package/dist/lib/sandbox/openapi/schema.d.ts +0 -9
  101. package/dist/lib/sandbox/openapi/schema.d.ts.map +0 -1
  102. package/dist/lib/sandbox/openapi/skill/index.d.ts +0 -6
  103. package/dist/lib/sandbox/openapi/skill/index.d.ts.map +0 -1
  104. package/dist/lib/sandbox/openapi/skill/index.js +0 -14
  105. package/dist/lib/sandbox/openapi/skill/index.js.map +0 -7
  106. package/dist/lib/sandbox/openapi/skill/openapi-cli/SKILL.md +0 -109
  107. package/dist/lib/sandbox/openapi/validate.d.ts +0 -7
  108. package/dist/lib/sandbox/openapi/validate.d.ts.map +0 -1
  109. package/dist/lib/sandbox/routing-sandbox.d.ts +0 -28
  110. package/dist/lib/sandbox/routing-sandbox.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -163,7 +163,12 @@ function isFragment(data) {
163
163
  return typeof data === "object" && data !== null && "name" in data && ("data" in data || "codec" in data) && typeof data.name === "string";
164
164
  }
165
165
  function isFragmentObject(data) {
166
- return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
166
+ if (typeof data !== "object" || data === null) return false;
167
+ if (Array.isArray(data)) return false;
168
+ if (data instanceof Promise) return false;
169
+ if (Symbol.asyncIterator in data || Symbol.iterator in data) return false;
170
+ if (isFragment(data)) return false;
171
+ return true;
167
172
  }
168
173
  function isMessageFragment(fragment2) {
169
174
  return fragment2.type === "message";
@@ -236,7 +241,10 @@ function assistantText(content, options) {
236
241
  }
237
242
 
238
243
  // packages/context/src/lib/fragments/message/user.ts
239
- import { generateId as generateId2 } from "ai";
244
+ import {
245
+ generateId as generateId2,
246
+ isStaticToolUIPart
247
+ } from "ai";
240
248
 
241
249
  // packages/context/src/lib/renderers/abstract.renderer.ts
242
250
  import pluralize from "pluralize";
@@ -1130,7 +1138,56 @@ function isConditionalReminder(fragment2) {
1130
1138
  var SYSTEM_REMINDER_OPEN_TAG = "<system-reminder>";
1131
1139
  var SYSTEM_REMINDER_CLOSE_TAG = "</system-reminder>";
1132
1140
  function getReminderRanges(metadata) {
1133
- return metadata?.reminders ?? [];
1141
+ return getReminderMetadataRecords(metadata).map((record) => ({
1142
+ partIndex: record.partIndex,
1143
+ start: record.start,
1144
+ end: record.end
1145
+ }));
1146
+ }
1147
+ function getReminderMetadataRecords(metadata) {
1148
+ const reminders = metadata?.reminders;
1149
+ if (!Array.isArray(reminders)) return [];
1150
+ return reminders.filter(
1151
+ (item) => isRecord(item) && typeof item.partIndex === "number" && typeof item.start === "number" && typeof item.end === "number"
1152
+ );
1153
+ }
1154
+ function reminderTargetOf(record) {
1155
+ return record.target === "tool-output" ? "tool-output" : "user";
1156
+ }
1157
+ function normalizeReminderTarget(target) {
1158
+ if (target === void 0 || target === "user") return "user";
1159
+ if (target === "tool-output") return "tool-output";
1160
+ throw new Error(`Unsupported reminder target: ${String(target)}`);
1161
+ }
1162
+ function isConditionalReminderOptions(options) {
1163
+ return options !== void 0 && "when" in options;
1164
+ }
1165
+ function isPromiseLike(value) {
1166
+ return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";
1167
+ }
1168
+ function normalizeImmediateReminderText(textOrFragment) {
1169
+ if (isFragment(textOrFragment)) {
1170
+ return new XmlRenderer().render([textOrFragment]);
1171
+ }
1172
+ if (typeof textOrFragment === "string") {
1173
+ return textOrFragment;
1174
+ }
1175
+ return (ctx) => {
1176
+ const resolved = textOrFragment(ctx);
1177
+ if (isPromiseLike(resolved)) {
1178
+ throw new Error("Async reminder text requires a when predicate");
1179
+ }
1180
+ return resolved;
1181
+ };
1182
+ }
1183
+ function normalizeConditionalReminderText(textOrFragment) {
1184
+ return isFragment(textOrFragment) ? new XmlRenderer().render([textOrFragment]) : textOrFragment;
1185
+ }
1186
+ function isOutputAvailableToolPart(part) {
1187
+ return isStaticToolUIPart(part) && part.state === "output-available";
1188
+ }
1189
+ function isToolOutputReminderEnvelope(value) {
1190
+ return isRecord(value) && typeof value.systemReminder === "string";
1134
1191
  }
1135
1192
  function stripTextByRanges(text, ranges) {
1136
1193
  if (ranges.length === 0) {
@@ -1159,17 +1216,44 @@ function stripTextByRanges(text, ranges) {
1159
1216
  return output.trimEnd();
1160
1217
  }
1161
1218
  function stripReminders(message2) {
1162
- const reminderRanges = getReminderRanges(
1219
+ const reminderRecords = getReminderMetadataRecords(
1163
1220
  isRecord(message2.metadata) ? message2.metadata : void 0
1164
1221
  );
1165
1222
  const rangesByPartIndex = /* @__PURE__ */ new Map();
1166
- for (const range of reminderRanges) {
1223
+ const toolRemindersByPartIndex = /* @__PURE__ */ new Map();
1224
+ for (const range of reminderRecords) {
1225
+ if (reminderTargetOf(range) === "tool-output") {
1226
+ const records = toolRemindersByPartIndex.get(range.partIndex) ?? [];
1227
+ records.push(range);
1228
+ toolRemindersByPartIndex.set(range.partIndex, records);
1229
+ continue;
1230
+ }
1167
1231
  const partRanges = rangesByPartIndex.get(range.partIndex) ?? [];
1168
1232
  partRanges.push({ start: range.start, end: range.end });
1169
1233
  rangesByPartIndex.set(range.partIndex, partRanges);
1170
1234
  }
1171
1235
  const strippedParts = message2.parts.flatMap((part, partIndex) => {
1172
1236
  const clonedPart = { ...part };
1237
+ const toolReminderRecords = toolRemindersByPartIndex.get(partIndex);
1238
+ if (toolReminderRecords !== void 0 && isOutputAvailableToolPart(clonedPart)) {
1239
+ if (typeof clonedPart.output === "string") {
1240
+ return [
1241
+ {
1242
+ ...clonedPart,
1243
+ output: stripTextByRanges(
1244
+ clonedPart.output,
1245
+ toolReminderRecords.map((record) => ({
1246
+ start: record.start,
1247
+ end: record.end
1248
+ }))
1249
+ )
1250
+ }
1251
+ ];
1252
+ }
1253
+ if (isToolOutputReminderEnvelope(clonedPart.output)) {
1254
+ return [{ ...clonedPart, output: clonedPart.output.result }];
1255
+ }
1256
+ }
1173
1257
  const ranges = rangesByPartIndex.get(partIndex);
1174
1258
  if (clonedPart.type !== "text" || ranges === void 0) {
1175
1259
  return [clonedPart];
@@ -1239,6 +1323,7 @@ function applyInlineReminder(message2, value) {
1239
1323
  return {
1240
1324
  id: generateId2(),
1241
1325
  text: value,
1326
+ target: "user",
1242
1327
  partIndex,
1243
1328
  start,
1244
1329
  end: start + reminderText.length,
@@ -1252,6 +1337,7 @@ function applyPartReminder(message2, value) {
1252
1337
  return {
1253
1338
  id: generateId2(),
1254
1339
  text: value,
1340
+ target: "user",
1255
1341
  partIndex,
1256
1342
  start: 0,
1257
1343
  end: value.length,
@@ -1306,6 +1392,65 @@ function applyReminderToMessage(message2, item, ctx) {
1306
1392
  }
1307
1393
  return item.asPart ? applyPartReminder(message2, resolved.text) : applyInlineReminder(message2, resolved.text);
1308
1394
  }
1395
+ function findSingleOutputAvailableToolPart(message2) {
1396
+ let match = null;
1397
+ for (let partIndex = 0; partIndex < message2.parts.length; partIndex++) {
1398
+ const part = message2.parts[partIndex];
1399
+ if (!isOutputAvailableToolPart(part)) continue;
1400
+ if (match) return null;
1401
+ match = { partIndex, part };
1402
+ }
1403
+ return match;
1404
+ }
1405
+ function applyToolOutputRemindersToMessage(message2, reminders) {
1406
+ if (reminders.length === 0) return [];
1407
+ const target = findSingleOutputAvailableToolPart(message2);
1408
+ if (!target) return [];
1409
+ for (const reminder2 of reminders) {
1410
+ if (reminder2.metadata) {
1411
+ mergeMessageMetadata(message2, reminder2.metadata);
1412
+ }
1413
+ }
1414
+ const added = [];
1415
+ if (typeof target.part.output === "string") {
1416
+ let output = target.part.output;
1417
+ for (const reminder2 of reminders) {
1418
+ const reminderText = formatTaggedReminder(reminder2.text);
1419
+ const start = output.length;
1420
+ output = `${output}${reminderText}`;
1421
+ added.push({
1422
+ id: generateId2(),
1423
+ text: reminder2.text,
1424
+ target: "tool-output",
1425
+ partIndex: target.partIndex,
1426
+ start,
1427
+ end: start + reminderText.length,
1428
+ mode: "tool-output"
1429
+ });
1430
+ }
1431
+ message2.parts[target.partIndex] = {
1432
+ ...target.part,
1433
+ output
1434
+ };
1435
+ return added;
1436
+ }
1437
+ message2.parts[target.partIndex] = {
1438
+ ...target.part,
1439
+ output: {
1440
+ result: target.part.output,
1441
+ systemReminder: reminders.map((reminder2) => reminder2.text).join("\n")
1442
+ }
1443
+ };
1444
+ return reminders.map((reminder2) => ({
1445
+ id: generateId2(),
1446
+ text: reminder2.text,
1447
+ target: "tool-output",
1448
+ partIndex: target.partIndex,
1449
+ start: 0,
1450
+ end: 0,
1451
+ mode: "tool-output"
1452
+ }));
1453
+ }
1309
1454
  function mergeReminderMetadata(message2, addedReminders) {
1310
1455
  if (addedReminders.length === 0) return;
1311
1456
  const metadata = isRecord(message2.metadata) ? { ...message2.metadata } : {};
@@ -1314,28 +1459,37 @@ function mergeReminderMetadata(message2, addedReminders) {
1314
1459
  message2.metadata = metadata;
1315
1460
  }
1316
1461
  function reminder(textOrFragment, options) {
1317
- const fromFragment2 = isFragment(textOrFragment);
1318
- const text = fromFragment2 ? new XmlRenderer().render([textOrFragment]) : textOrFragment;
1319
- if (typeof text === "string") {
1320
- assertReminderText(text);
1321
- }
1322
- const asPart = options?.asPart ?? false;
1323
- if (options && "when" in options && options.when) {
1462
+ const target = normalizeReminderTarget(options?.target);
1463
+ const asPart = target === "user" ? options?.asPart ?? false : false;
1464
+ if (isConditionalReminderOptions(options)) {
1465
+ const text2 = normalizeConditionalReminderText(textOrFragment);
1466
+ if (typeof text2 === "string") {
1467
+ assertReminderText(text2);
1468
+ }
1324
1469
  return {
1325
1470
  name: "reminder",
1326
1471
  data: null,
1327
1472
  metadata: {
1328
1473
  reminder: {
1329
- text,
1474
+ text: text2,
1330
1475
  when: options.when,
1331
- asPart
1476
+ asPart,
1477
+ target
1332
1478
  }
1333
1479
  }
1334
1480
  };
1335
1481
  }
1482
+ if (target !== "user") {
1483
+ throw new Error('Reminder target "tool-output" requires a when predicate');
1484
+ }
1485
+ const text = normalizeImmediateReminderText(textOrFragment);
1486
+ if (typeof text === "string") {
1487
+ assertReminderText(text);
1488
+ }
1336
1489
  return {
1337
1490
  text,
1338
- asPart
1491
+ asPart,
1492
+ target
1339
1493
  };
1340
1494
  }
1341
1495
  function user(content, ...reminders) {
@@ -1450,7 +1604,8 @@ var Agent = class _Agent {
1450
1604
  throw new Error(`Agent ${this.#options.name} is missing a model.`);
1451
1605
  }
1452
1606
  const { messages, systemPrompt } = await this.#options.context.resolve({
1453
- renderer: new XmlRenderer()
1607
+ renderer: new XmlRenderer(),
1608
+ sandbox: this.#options.sandbox
1454
1609
  });
1455
1610
  return generateText2({
1456
1611
  abortSignal: config?.abortSignal,
@@ -1522,7 +1677,8 @@ var Agent = class _Agent {
1522
1677
  throw new Error(`Agent ${this.#options.name} is missing a model.`);
1523
1678
  }
1524
1679
  const { messages, systemPrompt } = await context.resolve({
1525
- renderer: new XmlRenderer()
1680
+ renderer: new XmlRenderer(),
1681
+ sandbox: this.#options.sandbox
1526
1682
  });
1527
1683
  const runId = generateId3();
1528
1684
  return streamText({
@@ -1797,7 +1953,8 @@ function structuredOutput(options) {
1797
1953
  throw new Error(`structuredOutput is missing a model.`);
1798
1954
  }
1799
1955
  const { messages, systemPrompt } = await options.context.resolve({
1800
- renderer: new XmlRenderer()
1956
+ renderer: new XmlRenderer(),
1957
+ sandbox: options.sandbox
1801
1958
  });
1802
1959
  const result = await generateText2({
1803
1960
  abortSignal: config?.abortSignal,
@@ -1827,7 +1984,8 @@ function structuredOutput(options) {
1827
1984
  throw new Error(`structuredOutput is missing a model.`);
1828
1985
  }
1829
1986
  const { messages, systemPrompt } = await options.context.resolve({
1830
- renderer: new XmlRenderer()
1987
+ renderer: new XmlRenderer(),
1988
+ sandbox: options.sandbox
1831
1989
  });
1832
1990
  return streamText({
1833
1991
  abortSignal: config?.abortSignal,
@@ -1886,7 +2044,74 @@ import {
1886
2044
  generateId as generateId4,
1887
2045
  validateUIMessages
1888
2046
  } from "ai";
1889
- import { mergeWith } from "lodash-es";
2047
+
2048
+ // packages/context/src/lib/ui-message-guards.ts
2049
+ function isRecord2(value) {
2050
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2051
+ }
2052
+ function isUIMessage(value) {
2053
+ return isRecord2(value) && typeof value.id === "string" && (value.role === "system" || value.role === "user" || value.role === "assistant") && Array.isArray(value.parts);
2054
+ }
2055
+ function isUserUIMessage(value) {
2056
+ return isUIMessage(value) && value.role === "user";
2057
+ }
2058
+ function requireUIMessage(value, source) {
2059
+ if (isUIMessage(value)) {
2060
+ return value;
2061
+ }
2062
+ throw new Error(`${source} is not a UIMessage`);
2063
+ }
2064
+ function requireUserUIMessage(value, source) {
2065
+ if (isUserUIMessage(value)) {
2066
+ return value;
2067
+ }
2068
+ throw new Error(`${source} is not a user UIMessage`);
2069
+ }
2070
+
2071
+ // packages/context/src/lib/chain-summary.ts
2072
+ var ChainSummaryBuilder = class {
2073
+ #turn = 0;
2074
+ #messageCount = 0;
2075
+ #lastMessageAt;
2076
+ #lastMessage;
2077
+ #lastAssistantMessage;
2078
+ #lastAssistantMessages = [];
2079
+ ingestStored(msg) {
2080
+ this.#messageCount++;
2081
+ if (msg.name === "assistant") {
2082
+ const message2 = requireUIMessage(
2083
+ msg.data,
2084
+ `Stored assistant message "${msg.id}"`
2085
+ );
2086
+ this.#lastAssistantMessage = message2;
2087
+ this.#lastAssistantMessages.push(message2);
2088
+ return;
2089
+ }
2090
+ if (msg.name !== "user") {
2091
+ return;
2092
+ }
2093
+ this.#turn++;
2094
+ this.#lastMessageAt = msg.createdAt;
2095
+ this.#lastMessage = requireUIMessage(
2096
+ msg.data,
2097
+ `Stored user message "${msg.id}"`
2098
+ );
2099
+ }
2100
+ ingestPending(fragment2) {
2101
+ this.#messageCount++;
2102
+ if (fragment2.name === "user") this.#turn++;
2103
+ }
2104
+ build() {
2105
+ return {
2106
+ turn: this.#turn,
2107
+ messageCount: this.#messageCount,
2108
+ lastMessageAt: this.#lastMessageAt,
2109
+ lastMessage: this.#lastMessage,
2110
+ lastAssistantMessage: this.#lastAssistantMessage,
2111
+ lastAssistantMessages: this.#lastAssistantMessages
2112
+ };
2113
+ }
2114
+ };
1890
2115
 
1891
2116
  // packages/context/src/lib/estimate.ts
1892
2117
  import { encode } from "gpt-tokenizer";
@@ -2048,6 +2273,490 @@ async function estimate(modelId, renderer, ...fragments) {
2048
2273
  };
2049
2274
  }
2050
2275
 
2276
+ // packages/context/src/lib/resolvers/async-resolver.ts
2277
+ var AsyncResolver = class {
2278
+ name = "AsyncResolver";
2279
+ requiresSandbox = true;
2280
+ canResolve(value) {
2281
+ return typeof value === "function" && value.constructor.name === "AsyncFunction";
2282
+ }
2283
+ resolve(value, ctx) {
2284
+ return value(ctx);
2285
+ }
2286
+ };
2287
+
2288
+ // packages/context/src/lib/resolvers/function-resolver.ts
2289
+ var FunctionResolver = class {
2290
+ name = "FunctionResolver";
2291
+ requiresSandbox = true;
2292
+ canResolve(value) {
2293
+ return typeof value === "function";
2294
+ }
2295
+ async resolve(value, ctx) {
2296
+ const loader = value;
2297
+ return loader(ctx);
2298
+ }
2299
+ };
2300
+
2301
+ // packages/context/src/lib/resolvers/generator-resolver.ts
2302
+ var DEFAULT_MAX_ITEMS = 1e4;
2303
+ var GENERATOR_CTOR_NAMES = /* @__PURE__ */ new Set([
2304
+ "GeneratorFunction",
2305
+ "AsyncGeneratorFunction"
2306
+ ]);
2307
+ var GeneratorResolver = class {
2308
+ name = "GeneratorResolver";
2309
+ requiresSandbox = true;
2310
+ #maxItems;
2311
+ constructor(options = {}) {
2312
+ this.#maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS;
2313
+ }
2314
+ canResolve(value) {
2315
+ return typeof value === "function" && GENERATOR_CTOR_NAMES.has(value.constructor.name);
2316
+ }
2317
+ async resolve(value, ctx) {
2318
+ const iterable = value(ctx);
2319
+ const collected = [];
2320
+ if (isAsyncIterable(iterable)) {
2321
+ for await (const chunk of iterable) {
2322
+ pushLimited(collected, chunk, this.#maxItems, this.name);
2323
+ }
2324
+ } else {
2325
+ for (const chunk of iterable) {
2326
+ pushLimited(collected, chunk, this.#maxItems, this.name);
2327
+ }
2328
+ }
2329
+ return collected;
2330
+ }
2331
+ };
2332
+ function isAsyncIterable(value) {
2333
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
2334
+ }
2335
+ function pushLimited(collected, chunk, maxItems, resolverName) {
2336
+ if (collected.length >= maxItems) {
2337
+ throw new Error(
2338
+ `${resolverName}: generator yielded more than ${maxItems} items`
2339
+ );
2340
+ }
2341
+ collected.push(chunk);
2342
+ }
2343
+
2344
+ // packages/context/src/lib/resolvers/iterable-resolver.ts
2345
+ var DEFAULT_MAX_ITEMS2 = 1e4;
2346
+ function hasIterableProtocol(value) {
2347
+ return Symbol.asyncIterator in value || Symbol.iterator in value;
2348
+ }
2349
+ function isBoxedPrimitive(value) {
2350
+ return value instanceof String || value instanceof Number || value instanceof Boolean;
2351
+ }
2352
+ var IterableResolver = class {
2353
+ name = "IterableResolver";
2354
+ #maxItems;
2355
+ constructor(options = {}) {
2356
+ this.#maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS2;
2357
+ }
2358
+ canResolve(value) {
2359
+ if (value === null || value === void 0) return false;
2360
+ if (typeof value !== "object") return false;
2361
+ if (Array.isArray(value)) return false;
2362
+ if (value instanceof Promise) return false;
2363
+ if (isBoxedPrimitive(value)) return false;
2364
+ if (isFragment(value)) return false;
2365
+ return hasIterableProtocol(value);
2366
+ }
2367
+ async resolve(value, _ctx) {
2368
+ const iterable = value;
2369
+ const collected = [];
2370
+ if (isAsyncIterable2(iterable)) {
2371
+ for await (const chunk of iterable) {
2372
+ pushLimited2(collected, chunk, this.#maxItems, this.name);
2373
+ }
2374
+ } else {
2375
+ for (const chunk of iterable) {
2376
+ pushLimited2(collected, chunk, this.#maxItems, this.name);
2377
+ }
2378
+ }
2379
+ return collected;
2380
+ }
2381
+ };
2382
+ function isAsyncIterable2(value) {
2383
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
2384
+ }
2385
+ function pushLimited2(collected, chunk, maxItems, resolverName) {
2386
+ if (collected.length >= maxItems) {
2387
+ throw new Error(
2388
+ `${resolverName}: iterable yielded more than ${maxItems} items`
2389
+ );
2390
+ }
2391
+ collected.push(chunk);
2392
+ }
2393
+
2394
+ // packages/context/src/lib/resolvers/promise-resolver.ts
2395
+ var PromiseResolver = class {
2396
+ name = "PromiseResolver";
2397
+ canResolve(value) {
2398
+ return value instanceof Promise;
2399
+ }
2400
+ resolve(value, _ctx) {
2401
+ return value;
2402
+ }
2403
+ };
2404
+
2405
+ // packages/context/src/lib/resolvers/loader-resolver.ts
2406
+ var FragmentLoaderResolver = class {
2407
+ #resolvers;
2408
+ #maxDepth;
2409
+ constructor(resolvers, options = {}) {
2410
+ this.#resolvers = resolvers;
2411
+ this.#maxDepth = options.maxDepth ?? 10;
2412
+ }
2413
+ async resolve(fragments, ctx) {
2414
+ await Promise.all(
2415
+ fragments.map((f) => this.#walkFragment(f, ctx, 0, /* @__PURE__ */ new Set(), f.name))
2416
+ );
2417
+ }
2418
+ async #walkFragment(fragment2, ctx, depth, ancestors, path5) {
2419
+ if (!("data" in fragment2)) return;
2420
+ fragment2.data = await this.#walkData(
2421
+ fragment2.data,
2422
+ ctx,
2423
+ depth,
2424
+ ancestors,
2425
+ path5
2426
+ );
2427
+ }
2428
+ async #walkData(value, ctx, depth, ancestors, path5) {
2429
+ if (ctx.signal?.aborted) {
2430
+ throw ctx.signal.reason ?? new Error("Resolver aborted");
2431
+ }
2432
+ if (depth > this.#maxDepth) {
2433
+ throw new Error(
2434
+ `Resolver recursion exceeded maxDepth=${this.#maxDepth} at fragment '${path5}'`
2435
+ );
2436
+ }
2437
+ const handler = this.#resolvers.find((r) => r.canResolve(value));
2438
+ if (handler) {
2439
+ if (handler.requiresSandbox && !ctx.sandbox) {
2440
+ throw new Error(
2441
+ `Fragment '${path5}' is dispatched to ${handler.name}, which requires a sandbox; none was provided`
2442
+ );
2443
+ }
2444
+ let resolved;
2445
+ try {
2446
+ resolved = await this.#raceWithSignal(
2447
+ handler.resolve(value, ctx),
2448
+ ctx.signal
2449
+ );
2450
+ } catch (cause) {
2451
+ throw new Error(
2452
+ `Async fragment '${path5}' failed in ${handler.name}: ${cause instanceof Error ? cause.message : String(cause)}`,
2453
+ { cause }
2454
+ );
2455
+ }
2456
+ return this.#walkData(resolved, ctx, depth + 1, ancestors, path5);
2457
+ }
2458
+ if (value === null || value === void 0) {
2459
+ return value;
2460
+ }
2461
+ if (isFragment(value)) {
2462
+ if (ancestors.has(value)) return void 0;
2463
+ const childAncestors = new Set(ancestors);
2464
+ childAncestors.add(value);
2465
+ const childPath = `${path5}.${value.name}`;
2466
+ await this.#walkFragment(
2467
+ value,
2468
+ ctx,
2469
+ depth + 1,
2470
+ childAncestors,
2471
+ childPath
2472
+ );
2473
+ return value;
2474
+ }
2475
+ if (Array.isArray(value)) {
2476
+ if (ancestors.has(value)) return void 0;
2477
+ const childAncestors = new Set(ancestors);
2478
+ childAncestors.add(value);
2479
+ return Promise.all(
2480
+ value.map(
2481
+ (item) => this.#walkData(item, ctx, depth, childAncestors, path5)
2482
+ )
2483
+ );
2484
+ }
2485
+ if (isFragmentObject(value)) {
2486
+ if (ancestors.has(value)) return void 0;
2487
+ const childAncestors = new Set(ancestors);
2488
+ childAncestors.add(value);
2489
+ const out = {};
2490
+ const entries = Object.entries(value);
2491
+ const resolvedValues = await Promise.all(
2492
+ entries.map(
2493
+ ([, v]) => this.#walkData(v, ctx, depth, childAncestors, path5)
2494
+ )
2495
+ );
2496
+ for (let i = 0; i < entries.length; i += 1) {
2497
+ const [k] = entries[i];
2498
+ const v = resolvedValues[i];
2499
+ if (v !== void 0) out[k] = v;
2500
+ }
2501
+ return out;
2502
+ }
2503
+ return value;
2504
+ }
2505
+ #raceWithSignal(promise, signal) {
2506
+ if (!signal) return promise;
2507
+ if (signal.aborted) {
2508
+ return Promise.reject(signal.reason ?? new Error("Resolver aborted"));
2509
+ }
2510
+ return new Promise((resolve4, reject) => {
2511
+ const onAbort = () => reject(signal.reason ?? new Error("Resolver aborted"));
2512
+ signal.addEventListener("abort", onAbort, { once: true });
2513
+ promise.then(
2514
+ (value) => {
2515
+ signal.removeEventListener("abort", onAbort);
2516
+ resolve4(value);
2517
+ },
2518
+ (err) => {
2519
+ signal.removeEventListener("abort", onAbort);
2520
+ reject(err);
2521
+ }
2522
+ );
2523
+ });
2524
+ }
2525
+ };
2526
+
2527
+ // packages/context/src/lib/resolvers/index.ts
2528
+ function defaultResolvers() {
2529
+ return [
2530
+ new AsyncResolver(),
2531
+ new GeneratorResolver(),
2532
+ new FunctionResolver(),
2533
+ new PromiseResolver(),
2534
+ new IterableResolver()
2535
+ ];
2536
+ }
2537
+
2538
+ // packages/context/src/lib/save/save-pipeline.ts
2539
+ var SavePipeline = class {
2540
+ #engine;
2541
+ #pending;
2542
+ #fragments;
2543
+ #shouldBranch = true;
2544
+ constructor(engine, pending, fragments) {
2545
+ this.#engine = engine;
2546
+ this.#pending = pending;
2547
+ this.#fragments = fragments;
2548
+ }
2549
+ async applyUpdateBranching(shouldBranch) {
2550
+ this.#shouldBranch = shouldBranch;
2551
+ if (!shouldBranch) return this;
2552
+ for (const fragment2 of this.#pending) {
2553
+ if (!fragment2.id) continue;
2554
+ const existing = await this.#engine.store.getMessage(fragment2.id);
2555
+ if (existing && existing.parentId) {
2556
+ await this.#engine.rewindForUpdate(existing.parentId);
2557
+ fragment2.id = crypto.randomUUID();
2558
+ return this;
2559
+ }
2560
+ }
2561
+ return this;
2562
+ }
2563
+ async evaluateReminders(handlers) {
2564
+ const conditional = this.#fragments.filter(isConditionalReminder);
2565
+ if (conditional.length === 0) return this;
2566
+ const configsByTarget = /* @__PURE__ */ new Map();
2567
+ for (const fragment2 of conditional) {
2568
+ const config = fragment2.metadata.reminder;
2569
+ const target = config.target;
2570
+ const list = configsByTarget.get(target) ?? [];
2571
+ list.push(config);
2572
+ configsByTarget.set(target, list);
2573
+ }
2574
+ const chain = await this.#engine.getChainSummary();
2575
+ const base = this.#engine.buildBaseWhenCtx(chain);
2576
+ const sharedUserMessage = this.#encodePendingUserMessage();
2577
+ for (const handler of handlers) {
2578
+ const configs = configsByTarget.get(handler.target);
2579
+ if (!configs || configs.length === 0) continue;
2580
+ const prepared = handler.prepare({
2581
+ pending: this.#pending,
2582
+ base,
2583
+ chain,
2584
+ sharedUserMessage
2585
+ });
2586
+ if (!prepared) continue;
2587
+ const whenResults = await Promise.all(
2588
+ configs.map((config) => config.when(prepared.whenCtx))
2589
+ );
2590
+ const fired = configs.filter((_, i) => whenResults[i]);
2591
+ if (fired.length === 0) continue;
2592
+ const resolvedOrNull = await Promise.all(
2593
+ fired.map((config) => resolveReminderAsync(config, prepared.whenCtx))
2594
+ );
2595
+ const matched = [];
2596
+ for (let i = 0; i < resolvedOrNull.length; i++) {
2597
+ const resolution = resolvedOrNull[i];
2598
+ if (resolution)
2599
+ matched.push({ config: fired[i], resolved: resolution });
2600
+ }
2601
+ if (matched.length === 0) continue;
2602
+ handler.apply({
2603
+ pending: this.#pending,
2604
+ carrier: prepared.carrier,
2605
+ fired: matched.map((m) => m.config),
2606
+ resolved: matched.map((m) => m.resolved)
2607
+ });
2608
+ }
2609
+ return this;
2610
+ }
2611
+ #encodePendingUserMessage() {
2612
+ const fragmentIndex = this.#pending.findLastIndex(
2613
+ (fragment3) => fragment3.name === "user"
2614
+ );
2615
+ if (fragmentIndex < 0) return void 0;
2616
+ const fragment2 = this.#pending[fragmentIndex];
2617
+ if (!fragment2.codec) return void 0;
2618
+ return requireUserUIMessage(
2619
+ fragment2.codec.encode(),
2620
+ `Pending user fragment "${fragment2.name}"`
2621
+ );
2622
+ }
2623
+ async persist() {
2624
+ let parentId = this.#engine.getActiveBranch().headMessageId;
2625
+ const now = Date.now();
2626
+ for (const fragment2 of this.#pending) {
2627
+ if (!fragment2.codec) {
2628
+ throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2629
+ }
2630
+ const msgId = fragment2.id ?? crypto.randomUUID();
2631
+ let msgParentId = parentId;
2632
+ if (!this.#shouldBranch && msgId === parentId) {
2633
+ const existing = await this.#engine.store.getMessage(msgId);
2634
+ if (existing) msgParentId = existing.parentId;
2635
+ }
2636
+ const messageData = {
2637
+ id: msgId,
2638
+ chatId: this.#engine.chatId,
2639
+ parentId: msgParentId,
2640
+ name: fragment2.name,
2641
+ type: fragment2.type,
2642
+ data: fragment2.codec.encode(),
2643
+ createdAt: now
2644
+ };
2645
+ await this.#engine.store.addMessage(messageData);
2646
+ parentId = messageData.id;
2647
+ }
2648
+ if (parentId === null) {
2649
+ throw new Error(
2650
+ "Pipeline persisted no messages but pending was not empty"
2651
+ );
2652
+ }
2653
+ await this.#engine.commitHead(parentId);
2654
+ return { headMessageId: parentId };
2655
+ }
2656
+ };
2657
+
2658
+ // packages/context/src/lib/save/tool-output-target-handler.ts
2659
+ var ToolOutputTargetHandler = class {
2660
+ target = "tool-output";
2661
+ prepare({
2662
+ pending,
2663
+ base,
2664
+ chain,
2665
+ sharedUserMessage
2666
+ }) {
2667
+ const fragmentIndex = pending.findLastIndex(
2668
+ (fragment3) => fragment3.name === "assistant"
2669
+ );
2670
+ if (fragmentIndex < 0) return null;
2671
+ const fragment2 = pending[fragmentIndex];
2672
+ if (!fragment2.codec) return null;
2673
+ const currentMessage = sharedUserMessage ?? chain.lastMessage;
2674
+ if (!currentMessage) return null;
2675
+ const message2 = requireUIMessage(
2676
+ fragment2.codec.encode(),
2677
+ `Pending assistant fragment "${fragment2.name}"`
2678
+ );
2679
+ if (!findSingleOutputAvailableToolPart(message2)) return null;
2680
+ return {
2681
+ whenCtx: {
2682
+ ...base,
2683
+ content: extractPlainText(currentMessage),
2684
+ currentMessage,
2685
+ lastAssistantMessage: message2,
2686
+ lastAssistantMessages: [
2687
+ ...chain.lastAssistantMessages ?? [],
2688
+ message2
2689
+ ]
2690
+ },
2691
+ carrier: { message: message2, fragmentIndex }
2692
+ };
2693
+ }
2694
+ apply({ pending, carrier, resolved }) {
2695
+ const reminders = resolved.map((resolution) => ({
2696
+ text: resolution.text,
2697
+ metadata: resolution.metadata
2698
+ }));
2699
+ const metadata = applyToolOutputRemindersToMessage(
2700
+ carrier.message,
2701
+ reminders
2702
+ );
2703
+ mergeReminderMetadata(carrier.message, metadata);
2704
+ const originalId = pending[carrier.fragmentIndex].id;
2705
+ const message2 = originalId ? { ...carrier.message, id: originalId } : carrier.message;
2706
+ const updated = assistant(message2);
2707
+ if (originalId) updated.id = originalId;
2708
+ pending[carrier.fragmentIndex] = updated;
2709
+ }
2710
+ };
2711
+
2712
+ // packages/context/src/lib/save/user-target-handler.ts
2713
+ var UserTargetHandler = class {
2714
+ target = "user";
2715
+ prepare({
2716
+ pending,
2717
+ base,
2718
+ chain
2719
+ }) {
2720
+ const fragmentIndex = pending.findLastIndex(
2721
+ (fragment3) => fragment3.name === "user"
2722
+ );
2723
+ if (fragmentIndex < 0) return null;
2724
+ const fragment2 = pending[fragmentIndex];
2725
+ if (!fragment2.codec) return null;
2726
+ const message2 = requireUserUIMessage(
2727
+ fragment2.codec.encode(),
2728
+ `Pending user fragment "${fragment2.name}"`
2729
+ );
2730
+ return {
2731
+ whenCtx: {
2732
+ ...base,
2733
+ content: extractPlainText(message2),
2734
+ currentMessage: message2,
2735
+ lastAssistantMessage: chain.lastAssistantMessage,
2736
+ lastAssistantMessages: chain.lastAssistantMessages
2737
+ },
2738
+ carrier: { message: message2, fragmentIndex },
2739
+ sharedUserMessage: message2
2740
+ };
2741
+ }
2742
+ apply({ pending, carrier, fired, resolved }) {
2743
+ const reminders = resolved.map((resolution, i) => ({
2744
+ text: resolution.text,
2745
+ asPart: fired[i].asPart,
2746
+ target: "user",
2747
+ metadata: resolution.metadata
2748
+ }));
2749
+ const originalId = pending[carrier.fragmentIndex].id;
2750
+ const message2 = requireUserUIMessage(
2751
+ originalId ? { ...carrier.message, id: originalId } : carrier.message,
2752
+ "Pending user reminder carrier"
2753
+ );
2754
+ const recreated = user(message2, ...reminders);
2755
+ if (originalId) recreated.id = originalId;
2756
+ pending[carrier.fragmentIndex] = recreated;
2757
+ }
2758
+ };
2759
+
2051
2760
  // packages/context/src/lib/store/sqlite.store.ts
2052
2761
  import { DatabaseSync } from "node:sqlite";
2053
2762
 
@@ -2593,6 +3302,55 @@ function estimateMessageContent(data) {
2593
3302
  function isLanguageModelUsage(value) {
2594
3303
  return typeof value === "object" && value !== null && "totalTokens" in value;
2595
3304
  }
3305
+ function addUsageValue(current, next) {
3306
+ if (current === void 0 && next === void 0) {
3307
+ return void 0;
3308
+ }
3309
+ return (current ?? 0) + (next ?? 0);
3310
+ }
3311
+ function mergeLanguageModelUsage(current, next) {
3312
+ return {
3313
+ inputTokens: addUsageValue(current?.inputTokens, next.inputTokens),
3314
+ inputTokenDetails: {
3315
+ noCacheTokens: addUsageValue(
3316
+ current?.inputTokenDetails?.noCacheTokens,
3317
+ next.inputTokenDetails?.noCacheTokens
3318
+ ),
3319
+ cacheReadTokens: addUsageValue(
3320
+ current?.inputTokenDetails?.cacheReadTokens,
3321
+ next.inputTokenDetails?.cacheReadTokens
3322
+ ),
3323
+ cacheWriteTokens: addUsageValue(
3324
+ current?.inputTokenDetails?.cacheWriteTokens,
3325
+ next.inputTokenDetails?.cacheWriteTokens
3326
+ )
3327
+ },
3328
+ outputTokens: addUsageValue(current?.outputTokens, next.outputTokens),
3329
+ outputTokenDetails: {
3330
+ textTokens: addUsageValue(
3331
+ current?.outputTokenDetails?.textTokens,
3332
+ next.outputTokenDetails?.textTokens
3333
+ ),
3334
+ reasoningTokens: addUsageValue(
3335
+ current?.outputTokenDetails?.reasoningTokens,
3336
+ next.outputTokenDetails?.reasoningTokens
3337
+ )
3338
+ },
3339
+ totalTokens: addUsageValue(current?.totalTokens, next.totalTokens),
3340
+ reasoningTokens: addUsageValue(
3341
+ current?.reasoningTokens,
3342
+ next.reasoningTokens
3343
+ ),
3344
+ cachedInputTokens: addUsageValue(
3345
+ current?.cachedInputTokens,
3346
+ next.cachedInputTokens
3347
+ ),
3348
+ raw: next.raw ?? current?.raw
3349
+ };
3350
+ }
3351
+ function isSkillPathMapping(value) {
3352
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.name === "string" && typeof value.description === "string" && typeof value.host === "string" && typeof value.sandbox === "string";
3353
+ }
2596
3354
  function isEmptyAssistantPlaceholder(message2) {
2597
3355
  return message2.role === "assistant" && message2.parts.length === 0;
2598
3356
  }
@@ -2610,6 +3368,7 @@ var ContextEngine = class _ContextEngine {
2610
3368
  #initialized = false;
2611
3369
  /** Initial metadata to merge on first initialization */
2612
3370
  #initialMetadata;
3371
+ #loaderResolver;
2613
3372
  get #activeBranch() {
2614
3373
  if (!this.#branch) {
2615
3374
  throw new Error(
@@ -2633,6 +3392,9 @@ var ContextEngine = class _ContextEngine {
2633
3392
  this.#userId = options.userId;
2634
3393
  this.#branchName = "main";
2635
3394
  this.#initialMetadata = options.metadata;
3395
+ this.#loaderResolver = new FragmentLoaderResolver(
3396
+ options.resolvers ?? defaultResolvers()
3397
+ );
2636
3398
  }
2637
3399
  /**
2638
3400
  * Initialize the chat and branch if they don't exist.
@@ -2739,43 +3501,16 @@ var ContextEngine = class _ContextEngine {
2739
3501
  */
2740
3502
  async #getChainContext() {
2741
3503
  await this.#ensureInitialized();
2742
- let turn = 0;
2743
- let messageCount = 0;
2744
- let lastMessageAt;
2745
- let lastMessage;
2746
- let lastAssistantMessage;
2747
- const lastAssistantMessages = [];
3504
+ const builder = new ChainSummaryBuilder();
2748
3505
  if (this.#branch?.headMessageId) {
2749
3506
  const chain = await this.#store.getMessageChain(
2750
3507
  this.#branch.headMessageId
2751
3508
  );
2752
- for (const msg of chain) {
2753
- messageCount++;
2754
- if (msg.name === "assistant") {
2755
- const assistantMsg = msg.data;
2756
- lastAssistantMessage = assistantMsg;
2757
- lastAssistantMessages.push(assistantMsg);
2758
- }
2759
- if (msg.name !== "user") {
2760
- continue;
2761
- }
2762
- turn++;
2763
- lastMessageAt = msg.createdAt;
2764
- lastMessage = msg.data;
2765
- }
2766
- }
2767
- for (const fragment2 of this.#pendingMessages) {
2768
- messageCount++;
2769
- if (fragment2.name === "user") turn++;
3509
+ for (const msg of chain) builder.ingestStored(msg);
2770
3510
  }
2771
- return {
2772
- turn,
2773
- messageCount,
2774
- lastMessageAt,
2775
- lastMessage,
2776
- lastAssistantMessage,
2777
- lastAssistantMessages
2778
- };
3511
+ for (const fragment2 of this.#pendingMessages)
3512
+ builder.ingestPending(fragment2);
3513
+ return builder.build();
2779
3514
  }
2780
3515
  async getTurnCount() {
2781
3516
  const { turn } = await this.#getChainContext();
@@ -2788,7 +3523,9 @@ var ContextEngine = class _ContextEngine {
2788
3523
  this.#branch.headMessageId
2789
3524
  );
2790
3525
  for (const msg of chain) {
2791
- if (msg.name === "user") return msg.data;
3526
+ if (msg.name === "user") {
3527
+ return requireUIMessage(msg.data, `Stored user message "${msg.id}"`);
3528
+ }
2792
3529
  }
2793
3530
  }
2794
3531
  for (const fragment2 of this.#pendingMessages) {
@@ -2796,7 +3533,10 @@ var ContextEngine = class _ContextEngine {
2796
3533
  if (!fragment2.codec) {
2797
3534
  throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2798
3535
  }
2799
- return fragment2.codec.encode();
3536
+ return requireUIMessage(
3537
+ fragment2.codec.encode(),
3538
+ `Pending fragment "${fragment2.name}"`
3539
+ );
2800
3540
  }
2801
3541
  return void 0;
2802
3542
  }
@@ -2839,7 +3579,7 @@ var ContextEngine = class _ContextEngine {
2839
3579
  this.#branch.headMessageId
2840
3580
  );
2841
3581
  for (const msg of chain) {
2842
- const data = msg.data;
3582
+ const data = requireUIMessage(msg.data, `Stored message "${msg.id}"`);
2843
3583
  if (isEmptyAssistantPlaceholder(data)) continue;
2844
3584
  messages.push(data);
2845
3585
  }
@@ -2848,7 +3588,10 @@ var ContextEngine = class _ContextEngine {
2848
3588
  if (!fragment2.codec) {
2849
3589
  throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2850
3590
  }
2851
- const encoded = fragment2.codec.encode();
3591
+ const encoded = requireUIMessage(
3592
+ fragment2.codec.encode(),
3593
+ `Pending fragment "${fragment2.name}"`
3594
+ );
2852
3595
  if (isEmptyAssistantPlaceholder(encoded)) continue;
2853
3596
  messages.push(encoded);
2854
3597
  }
@@ -2940,6 +3683,11 @@ var ContextEngine = class _ContextEngine {
2940
3683
  */
2941
3684
  async resolve(options) {
2942
3685
  await this.#ensureInitialized();
3686
+ await this.#loaderResolver.resolve(this.#fragments, {
3687
+ sandbox: options.sandbox,
3688
+ context: this,
3689
+ signal: options.signal
3690
+ });
2943
3691
  const systemPrompt = options.renderer.render(this.#renderableFragments);
2944
3692
  const messages = await this.getMessages();
2945
3693
  return { systemPrompt, messages };
@@ -2963,116 +3711,61 @@ var ContextEngine = class _ContextEngine {
2963
3711
  if (this.#pendingMessages.length === 0) {
2964
3712
  return { headMessageId: this.#branch?.headMessageId ?? void 0 };
2965
3713
  }
2966
- const shouldBranch = options?.branch ?? true;
2967
- if (shouldBranch) {
2968
- for (const fragment2 of this.#pendingMessages) {
2969
- if (fragment2.id) {
2970
- const existing = await this.#store.getMessage(fragment2.id);
2971
- if (existing && existing.parentId) {
2972
- await this.#rewindForUpdate(existing.parentId);
2973
- fragment2.id = crypto.randomUUID();
2974
- break;
2975
- }
3714
+ const pipeline = new SavePipeline(
3715
+ this.#asSavePipelineEngine(),
3716
+ this.#pendingMessages,
3717
+ this.#fragments
3718
+ );
3719
+ await pipeline.applyUpdateBranching(options?.branch ?? true);
3720
+ await pipeline.evaluateReminders(this.#reminderHandlers);
3721
+ const result = await pipeline.persist();
3722
+ this.#pendingMessages = [];
3723
+ return result;
3724
+ }
3725
+ #reminderHandlers = [
3726
+ new UserTargetHandler(),
3727
+ new ToolOutputTargetHandler()
3728
+ ];
3729
+ #asSavePipelineEngine() {
3730
+ return {
3731
+ store: this.#store,
3732
+ chatId: this.#chatId,
3733
+ branchName: this.#branchName,
3734
+ getActiveBranch: () => ({
3735
+ id: this.#activeBranch.id,
3736
+ headMessageId: this.#activeBranch.headMessageId
3737
+ }),
3738
+ commitHead: async (headMessageId) => {
3739
+ await this.#store.updateBranchHead(
3740
+ this.#activeBranch.id,
3741
+ headMessageId
3742
+ );
3743
+ this.#activeBranch.headMessageId = headMessageId;
3744
+ },
3745
+ rewindForUpdate: (parentId) => this.#rewindForUpdate(parentId),
3746
+ getChainSummary: () => this.#getChainContext(),
3747
+ buildBaseWhenCtx: (chain) => {
3748
+ const rawUsage = this.#chatData?.metadata?.usage;
3749
+ const usage = isLanguageModelUsage(rawUsage) ? rawUsage : void 0;
3750
+ const elapsed = chain.lastMessageAt !== void 0 ? Date.now() - chain.lastMessageAt : void 0;
3751
+ const chatData = this.#chatData;
3752
+ if (!chatData) {
3753
+ throw new Error(
3754
+ "ContextEngine must be initialized before reminders run"
3755
+ );
2976
3756
  }
3757
+ return {
3758
+ turn: chain.turn,
3759
+ messageCount: chain.messageCount,
3760
+ lastMessageAt: chain.lastMessageAt,
3761
+ lastMessage: chain.lastMessage,
3762
+ chat: chatData,
3763
+ usage,
3764
+ branch: this.#branchName,
3765
+ elapsed
3766
+ };
2977
3767
  }
2978
- }
2979
- const conditionalReminders = this.#fragments.filter(isConditionalReminder);
2980
- if (conditionalReminders.length > 0) {
2981
- let lastUserIndex = -1;
2982
- for (let i = this.#pendingMessages.length - 1; i >= 0; i--) {
2983
- if (this.#pendingMessages[i].name === "user") {
2984
- lastUserIndex = i;
2985
- break;
2986
- }
2987
- }
2988
- if (lastUserIndex >= 0) {
2989
- const lastUserFragment = this.#pendingMessages[lastUserIndex];
2990
- if (lastUserFragment.codec) {
2991
- const {
2992
- turn,
2993
- messageCount,
2994
- lastMessageAt,
2995
- lastMessage,
2996
- lastAssistantMessage,
2997
- lastAssistantMessages
2998
- } = await this.#getChainContext();
2999
- const original = lastUserFragment.codec.encode();
3000
- const plainText = extractPlainText(original);
3001
- const rawUsage = this.#chatData?.metadata?.usage;
3002
- const usage = isLanguageModelUsage(rawUsage) ? rawUsage : void 0;
3003
- const elapsed = lastMessageAt !== void 0 ? Date.now() - lastMessageAt : void 0;
3004
- const whenCtx = {
3005
- turn,
3006
- content: plainText,
3007
- lastMessageAt,
3008
- lastMessage,
3009
- currentMessage: original,
3010
- chat: this.#chatData,
3011
- usage,
3012
- branch: this.#branchName,
3013
- elapsed,
3014
- messageCount,
3015
- lastAssistantMessage,
3016
- lastAssistantMessages
3017
- };
3018
- const configs = conditionalReminders.map(
3019
- (it) => it.metadata.reminder
3020
- );
3021
- const whenResults = await Promise.all(
3022
- configs.map((it) => it.when(whenCtx))
3023
- );
3024
- const firedReminders = configs.filter((_, i) => whenResults[i]);
3025
- if (firedReminders.length > 0) {
3026
- const resolvedOrNull = await Promise.all(
3027
- firedReminders.map((it) => resolveReminderAsync(it, whenCtx))
3028
- );
3029
- const reminders = resolvedOrNull.flatMap((resolved, i) => {
3030
- if (!resolved) return [];
3031
- return [
3032
- {
3033
- text: resolved.text,
3034
- asPart: firedReminders[i].asPart,
3035
- metadata: resolved.metadata
3036
- }
3037
- ];
3038
- });
3039
- const recreated = user(original, ...reminders);
3040
- recreated.id = lastUserFragment.id;
3041
- this.#pendingMessages[lastUserIndex] = recreated;
3042
- }
3043
- }
3044
- }
3045
- }
3046
- let parentId = this.#activeBranch.headMessageId;
3047
- const now = Date.now();
3048
- for (const fragment2 of this.#pendingMessages) {
3049
- if (!fragment2.codec) {
3050
- throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
3051
- }
3052
- const msgId = fragment2.id ?? crypto.randomUUID();
3053
- let msgParentId = parentId;
3054
- if (!shouldBranch && msgId === parentId) {
3055
- const existing = await this.#store.getMessage(msgId);
3056
- if (existing) {
3057
- msgParentId = existing.parentId;
3058
- }
3059
- }
3060
- const messageData = {
3061
- id: msgId,
3062
- chatId: this.#chatId,
3063
- parentId: msgParentId,
3064
- name: fragment2.name,
3065
- type: fragment2.type,
3066
- data: fragment2.codec.encode(),
3067
- createdAt: now
3068
- };
3069
- await this.#store.addMessage(messageData);
3070
- parentId = messageData.id;
3071
- }
3072
- await this.#store.updateBranchHead(this.#activeBranch.id, parentId);
3073
- this.#activeBranch.headMessageId = parentId;
3074
- this.#pendingMessages = [];
3075
- return { headMessageId: this.#activeBranch.headMessageId ?? void 0 };
3768
+ };
3076
3769
  }
3077
3770
  /**
3078
3771
  * Estimate token count and cost for the full context.
@@ -3088,6 +3781,11 @@ var ContextEngine = class _ContextEngine {
3088
3781
  */
3089
3782
  async estimate(modelId, options = {}) {
3090
3783
  await this.#ensureInitialized();
3784
+ await this.#loaderResolver.resolve(this.#fragments, {
3785
+ sandbox: options.sandbox,
3786
+ context: this,
3787
+ signal: options.signal
3788
+ });
3091
3789
  const renderer = options.renderer ?? new XmlRenderer();
3092
3790
  const registry = getModelsRegistry();
3093
3791
  await registry.load();
@@ -3360,13 +4058,9 @@ var ContextEngine = class _ContextEngine {
3360
4058
  async trackUsage(usage) {
3361
4059
  await this.#ensureInitialized();
3362
4060
  const freshChatData = await this.#store.getChat(this.#chatId);
3363
- const currentUsage = freshChatData?.metadata?.usage ?? {};
3364
- const updatedUsage = mergeWith(
3365
- {},
3366
- currentUsage,
3367
- usage,
3368
- (a, b) => typeof a === "number" || typeof b === "number" ? (a ?? 0) + (b ?? 0) : void 0
3369
- );
4061
+ const storedUsage = freshChatData?.metadata?.usage;
4062
+ const currentUsage = isLanguageModelUsage(storedUsage) ? storedUsage : void 0;
4063
+ const updatedUsage = mergeLanguageModelUsage(currentUsage, usage);
3370
4064
  this.#chatData = await this.#store.updateChat(this.#chatId, {
3371
4065
  metadata: {
3372
4066
  ...freshChatData?.metadata,
@@ -3421,8 +4115,9 @@ var ContextEngine = class _ContextEngine {
3421
4115
  */
3422
4116
  getSkillMounts() {
3423
4117
  for (const fragment2 of this.#fragments) {
3424
- if (fragment2.name === "available_skills" && fragment2.metadata?.mounts) {
3425
- return { mounts: fragment2.metadata.mounts };
4118
+ const mounts = fragment2.metadata?.mounts;
4119
+ if (fragment2.name === "available_skills" && Array.isArray(mounts) && mounts.every(isSkillPathMapping)) {
4120
+ return { mounts };
3426
4121
  }
3427
4122
  }
3428
4123
  return { mounts: [] };
@@ -3449,7 +4144,11 @@ var ContextEngine = class _ContextEngine {
3449
4144
  async inspect(options) {
3450
4145
  await this.#ensureInitialized();
3451
4146
  const { renderer } = options;
3452
- const estimateResult = await this.estimate(options.modelId, { renderer });
4147
+ const estimateResult = await this.estimate(options.modelId, {
4148
+ renderer,
4149
+ sandbox: options.sandbox,
4150
+ signal: options.signal
4151
+ });
3453
4152
  const rendered = renderer.render(this.#renderableFragments);
3454
4153
  const persistedMessages = [];
3455
4154
  if (this.#branch?.headMessageId) {
@@ -4875,7 +5574,7 @@ function temporalReminder(options) {
4875
5574
  }
4876
5575
 
4877
5576
  // packages/context/src/lib/fragments/reminders/tool-predicates.ts
4878
- import { isStaticToolUIPart } from "ai";
5577
+ import { isStaticToolUIPart as isStaticToolUIPart2 } from "ai";
4879
5578
  var COMPLETED_STATES = /* @__PURE__ */ new Set([
4880
5579
  "input-available",
4881
5580
  "output-available",
@@ -4889,7 +5588,7 @@ function toolNameOf(part) {
4889
5588
  }
4890
5589
  function toolPartsOf(message2) {
4891
5590
  if (!message2) return [];
4892
- return message2.parts.filter(isStaticToolUIPart);
5591
+ return message2.parts.filter(isStaticToolUIPart2);
4893
5592
  }
4894
5593
  function toolCall(options) {
4895
5594
  return (ctx) => {
@@ -5231,44 +5930,6 @@ async function useAgentOsSandbox(options, fn) {
5231
5930
  }
5232
5931
  }
5233
5932
 
5234
- // packages/context/src/lib/sandbox/ast-utils.ts
5235
- function asStaticWordText(word) {
5236
- if (!word) {
5237
- return null;
5238
- }
5239
- return asStaticWordPartText(
5240
- word.parts
5241
- );
5242
- }
5243
- function asStaticWordPartText(parts) {
5244
- let text = "";
5245
- for (const part of parts) {
5246
- const type = part.type;
5247
- if (type === "Literal" || type === "SingleQuoted" || type === "Escaped") {
5248
- if (typeof part.value !== "string") {
5249
- return null;
5250
- }
5251
- text += part.value;
5252
- continue;
5253
- }
5254
- if (type === "DoubleQuoted") {
5255
- if (!Array.isArray(part.parts)) {
5256
- return null;
5257
- }
5258
- const inner = asStaticWordPartText(
5259
- part.parts
5260
- );
5261
- if (inner == null) {
5262
- return null;
5263
- }
5264
- text += inner;
5265
- continue;
5266
- }
5267
- return null;
5268
- }
5269
- return text;
5270
- }
5271
-
5272
5933
  // packages/context/src/lib/sandbox/bash-exception.ts
5273
5934
  var BashException = class extends Error {
5274
5935
  };
@@ -5411,10 +6072,9 @@ function joinSandbox(base, relative3) {
5411
6072
  const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
5412
6073
  return `${normalizedBase}/${relative3}`;
5413
6074
  }
5414
- async function uploadSkills(sandbox, inputs) {
6075
+ function discoverSkillMappings(inputs) {
5415
6076
  if (inputs.length === 0) return [];
5416
6077
  const discoveredByName = /* @__PURE__ */ new Map();
5417
- const filesToUpload = [];
5418
6078
  for (const { host, sandbox: sandboxBase } of inputs) {
5419
6079
  const absoluteHost = path3.resolve(expandHome2(host));
5420
6080
  for (const skill of discoverSkillsInDirectory(host)) {
@@ -5427,6 +6087,14 @@ async function uploadSkills(sandbox, inputs) {
5427
6087
  sandbox: joinSandbox(sandboxBase, relative3)
5428
6088
  });
5429
6089
  }
6090
+ }
6091
+ return Array.from(discoveredByName.values());
6092
+ }
6093
+ async function uploadSkills(sandbox, inputs) {
6094
+ if (inputs.length === 0) return [];
6095
+ const skills2 = discoverSkillMappings(inputs);
6096
+ const filesToUpload = [];
6097
+ for (const { host, sandbox: sandboxBase } of inputs) {
5430
6098
  const walked = await walkDirectory(host);
5431
6099
  for (const file of walked) {
5432
6100
  filesToUpload.push({
@@ -5438,7 +6106,7 @@ async function uploadSkills(sandbox, inputs) {
5438
6106
  if (filesToUpload.length > 0) {
5439
6107
  await sandbox.writeFiles(filesToUpload);
5440
6108
  }
5441
- return Array.from(discoveredByName.values());
6109
+ return skills2;
5442
6110
  }
5443
6111
 
5444
6112
  // packages/context/src/lib/sandbox/bash-tool.ts
@@ -5474,7 +6142,16 @@ async function createBashTool(options = {}) {
5474
6142
  throw new Error("bash tool execution is not available");
5475
6143
  }
5476
6144
  return runWithBashMeta(async () => {
5477
- const result = await originalExecute({ command }, execOptions);
6145
+ let result;
6146
+ try {
6147
+ result = await originalExecute({ command }, execOptions);
6148
+ } catch (err) {
6149
+ if (err instanceof BashException) {
6150
+ result = err.format();
6151
+ } else {
6152
+ throw err;
6153
+ }
6154
+ }
5478
6155
  const state = readBashMeta();
5479
6156
  if (!state) return result;
5480
6157
  const hasHidden = Object.keys(state.hidden).length > 0;
@@ -5508,43 +6185,6 @@ async function createBashTool(options = {}) {
5508
6185
  };
5509
6186
  }
5510
6187
 
5511
- // packages/context/src/lib/sandbox/extension.ts
5512
- var DuplicateCommandError = class extends Error {
5513
- commandName;
5514
- constructor(commandName) {
5515
- super(`Duplicate extension command name: "${commandName}"`);
5516
- this.name = "DuplicateCommandError";
5517
- this.commandName = commandName;
5518
- }
5519
- };
5520
- function chainHooks(...hooks) {
5521
- return async (value) => {
5522
- let current = value;
5523
- for (const hook of hooks) {
5524
- current = await hook(current);
5525
- }
5526
- return current;
5527
- };
5528
- }
5529
- function mergeExtensions(...extensions) {
5530
- const commands = [];
5531
- const seen = /* @__PURE__ */ new Set();
5532
- for (const ext of extensions) {
5533
- for (const cmd of ext.commands ?? []) {
5534
- if (seen.has(cmd.name)) throw new DuplicateCommandError(cmd.name);
5535
- seen.add(cmd.name);
5536
- commands.push(cmd);
5537
- }
5538
- }
5539
- const hooks = extensions.map((e) => e.onBeforeBashCall).filter((h) => !!h);
5540
- return {
5541
- commands,
5542
- plugins: extensions.flatMap((e) => e.plugins ?? []),
5543
- env: Object.assign({}, ...extensions.map((e) => e.env ?? {})),
5544
- onBeforeBashCall: hooks.length > 0 ? chainHooks(...hooks) : void 0
5545
- };
5546
- }
5547
-
5548
6188
  // packages/context/src/lib/sandbox/file-events.ts
5549
6189
  var ObservedFs = class {
5550
6190
  #base;
@@ -5790,9 +6430,11 @@ function resolveRealCwd(ctx) {
5790
6430
 
5791
6431
  // packages/context/src/lib/sandbox/docker-sandbox.ts
5792
6432
  import "bash-tool";
5793
- import spawn2 from "nano-spawn";
6433
+ import spawn3 from "nano-spawn";
5794
6434
  import { createHash } from "node:crypto";
5795
6435
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
6436
+
6437
+ // packages/context/src/lib/sandbox/docker-sandbox-errors.ts
5796
6438
  var DockerSandboxError = class extends Error {
5797
6439
  containerId;
5798
6440
  constructor(message2, containerId) {
@@ -5834,19 +6476,33 @@ var PackageInstallError = class extends DockerSandboxError {
5834
6476
  this.stderr = stderr;
5835
6477
  }
5836
6478
  };
5837
- var BinaryInstallError = class extends DockerSandboxError {
5838
- binaryName;
5839
- url;
6479
+ var InstallError = class extends DockerSandboxError {
6480
+ target;
6481
+ source;
5840
6482
  reason;
5841
- constructor(binaryName, url, reason, containerId) {
6483
+ url;
6484
+ constructor(opts) {
6485
+ const where = opts.url ? `${opts.source} (${opts.url})` : opts.source;
5842
6486
  super(
5843
- `Failed to install binary "${binaryName}" from ${url}: ${reason}`,
5844
- containerId
6487
+ `Failed to install "${opts.target}" via ${where}: ${opts.reason}`,
6488
+ opts.containerId
5845
6489
  );
5846
- this.name = "BinaryInstallError";
5847
- this.binaryName = binaryName;
5848
- this.url = url;
5849
- this.reason = reason;
6490
+ this.name = "InstallError";
6491
+ this.target = opts.target;
6492
+ this.source = opts.source;
6493
+ this.reason = opts.reason;
6494
+ this.url = opts.url;
6495
+ }
6496
+ };
6497
+ var MissingRuntimeError = class extends DockerSandboxError {
6498
+ runtime;
6499
+ required;
6500
+ constructor(runtime, required, details, containerId) {
6501
+ const base = `Required runtime "${runtime}" is not installed (needs: ${required.join(", ")}).`;
6502
+ super(details ? `${base} ${details}` : base, containerId);
6503
+ this.name = "MissingRuntimeError";
6504
+ this.runtime = runtime;
6505
+ this.required = required;
5850
6506
  }
5851
6507
  };
5852
6508
  var MountPathError = class extends DockerSandboxError {
@@ -5879,12 +6535,108 @@ var ComposeStartError = class extends DockerSandboxError {
5879
6535
  this.stderr = stderr;
5880
6536
  }
5881
6537
  };
6538
+
6539
+ // packages/context/src/lib/sandbox/installers/installer.ts
6540
+ import "bash-tool";
6541
+ import spawn2 from "nano-spawn";
6542
+ var Installer = class {
6543
+ };
5882
6544
  function isDebianBased(image) {
5883
6545
  const lower = image.toLowerCase();
5884
6546
  if (lower.includes("alpine")) return false;
5885
6547
  const debianPatterns = ["debian", "ubuntu", "node", "python"];
5886
6548
  return debianPatterns.some((pattern) => lower.includes(pattern));
5887
6549
  }
6550
+ function shellQuote(s) {
6551
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6552
+ }
6553
+ function createInstallerContext(containerId, image) {
6554
+ const packageManager = isDebianBased(image) ? "apt-get" : "apk";
6555
+ let archPromise = null;
6556
+ const ensuredTools = /* @__PURE__ */ new Set();
6557
+ let aptUpdated = false;
6558
+ const exec = async (command) => {
6559
+ try {
6560
+ const result = await spawn2("docker", [
6561
+ "exec",
6562
+ containerId,
6563
+ "sh",
6564
+ "-c",
6565
+ command
6566
+ ]);
6567
+ return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
6568
+ } catch (error) {
6569
+ const err = error;
6570
+ return {
6571
+ stdout: err.stdout ?? "",
6572
+ stderr: err.stderr ?? err.message ?? "",
6573
+ exitCode: err.exitCode ?? 1
6574
+ };
6575
+ }
6576
+ };
6577
+ const arch = async () => {
6578
+ if (!archPromise) {
6579
+ const attempt = (async () => {
6580
+ const result = await exec("uname -m");
6581
+ if (result.exitCode !== 0) {
6582
+ throw new DockerSandboxError(
6583
+ `Failed to detect container architecture: ${result.stderr}`,
6584
+ containerId
6585
+ );
6586
+ }
6587
+ return result.stdout.trim();
6588
+ })();
6589
+ archPromise = attempt.catch((err) => {
6590
+ archPromise = null;
6591
+ throw err;
6592
+ });
6593
+ }
6594
+ return archPromise;
6595
+ };
6596
+ const installPackages = async (packages) => {
6597
+ if (packages.length === 0) return;
6598
+ const quoted = packages.map(shellQuote).join(" ");
6599
+ let cmd;
6600
+ if (packageManager === "apt-get") {
6601
+ cmd = aptUpdated ? `apt-get install -y ${quoted}` : `apt-get update && apt-get install -y ${quoted}`;
6602
+ } else {
6603
+ cmd = `apk add --no-cache ${quoted}`;
6604
+ }
6605
+ const result = await exec(cmd);
6606
+ if (result.exitCode !== 0) {
6607
+ throw new PackageInstallError(
6608
+ packages,
6609
+ image,
6610
+ packageManager,
6611
+ result.stderr,
6612
+ containerId
6613
+ );
6614
+ }
6615
+ if (packageManager === "apt-get") aptUpdated = true;
6616
+ };
6617
+ const ensureTool = async (checkName, installName) => {
6618
+ const cacheKey = installName ?? checkName;
6619
+ if (ensuredTools.has(cacheKey)) return;
6620
+ const check = await exec(`which ${shellQuote(checkName)}`);
6621
+ if (check.exitCode === 0) {
6622
+ ensuredTools.add(cacheKey);
6623
+ return;
6624
+ }
6625
+ await installPackages([installName ?? checkName]);
6626
+ ensuredTools.add(cacheKey);
6627
+ };
6628
+ return {
6629
+ containerId,
6630
+ image,
6631
+ packageManager,
6632
+ arch,
6633
+ exec,
6634
+ installPackages,
6635
+ ensureTool
6636
+ };
6637
+ }
6638
+
6639
+ // packages/context/src/lib/sandbox/docker-sandbox.ts
5888
6640
  function isDockerfileOptions(opts) {
5889
6641
  return "dockerfile" in opts;
5890
6642
  }
@@ -5896,7 +6648,8 @@ var DockerSandboxStrategy = class {
5896
6648
  mounts;
5897
6649
  resources;
5898
6650
  env;
5899
- constructor(mounts = [], resources = {}, env = {}) {
6651
+ constructor(args = {}) {
6652
+ const { mounts = [], resources = {}, env = {} } = args;
5900
6653
  for (const key of Object.keys(env)) {
5901
6654
  if (key.length === 0 || key.includes("=")) {
5902
6655
  throw new DockerSandboxError(
@@ -5908,16 +6661,6 @@ var DockerSandboxStrategy = class {
5908
6661
  this.resources = resources;
5909
6662
  this.env = env;
5910
6663
  }
5911
- /**
5912
- * Template method - defines the algorithm skeleton for creating a sandbox.
5913
- *
5914
- * Steps:
5915
- * 1. Validate mount paths exist on host
5916
- * 2. Get/build the Docker image (strategy-specific)
5917
- * 3. Start the container
5918
- * 4. Configure the container (strategy-specific)
5919
- * 5. Create and return sandbox methods
5920
- */
5921
6664
  async create() {
5922
6665
  this.validateMounts();
5923
6666
  const image = await this.getImage();
@@ -5931,12 +6674,6 @@ var DockerSandboxStrategy = class {
5931
6674
  }
5932
6675
  return this.createSandboxMethods();
5933
6676
  }
5934
- // ─────────────────────────────────────────────────────────────────────────
5935
- // Common implementations (shared by all strategies)
5936
- // ─────────────────────────────────────────────────────────────────────────
5937
- /**
5938
- * Validates that all mount paths exist on the host filesystem.
5939
- */
5940
6677
  validateMounts() {
5941
6678
  for (const mount of this.mounts) {
5942
6679
  if (!existsSync3(mount.hostPath)) {
@@ -5944,24 +6681,18 @@ var DockerSandboxStrategy = class {
5944
6681
  }
5945
6682
  }
5946
6683
  }
5947
- /**
5948
- * Builds the docker run command arguments.
5949
- */
5950
6684
  buildDockerArgs(image, containerId) {
5951
6685
  const { memory = "1g", cpus = 2 } = this.resources;
5952
6686
  const args = [
5953
6687
  "run",
5954
6688
  "-d",
5955
- // Detached mode
5956
6689
  "--rm",
5957
- // Remove container when stopped
5958
6690
  "--name",
5959
6691
  containerId,
5960
6692
  `--memory=${memory}`,
5961
6693
  `--cpus=${cpus}`,
5962
6694
  "-w",
5963
6695
  "/workspace"
5964
- // Set working directory
5965
6696
  ];
5966
6697
  for (const [key, value] of Object.entries(this.env)) {
5967
6698
  args.push("-e", `${key}=${value}`);
@@ -5973,14 +6704,11 @@ var DockerSandboxStrategy = class {
5973
6704
  args.push(image, "tail", "-f", "/dev/null");
5974
6705
  return args;
5975
6706
  }
5976
- /**
5977
- * Starts a Docker container with the given image.
5978
- */
5979
6707
  async startContainer(image) {
5980
6708
  const containerId = `sandbox-${crypto.randomUUID().slice(0, 8)}`;
5981
6709
  const args = this.buildDockerArgs(image, containerId);
5982
6710
  try {
5983
- await spawn2("docker", args);
6711
+ await spawn3("docker", args);
5984
6712
  } catch (error) {
5985
6713
  const err = error;
5986
6714
  if (err.message?.includes("Cannot connect") || err.message?.includes("docker daemon") || err.stderr?.includes("Cannot connect")) {
@@ -5990,21 +6718,15 @@ var DockerSandboxStrategy = class {
5990
6718
  }
5991
6719
  return containerId;
5992
6720
  }
5993
- /**
5994
- * Stops a Docker container.
5995
- */
5996
6721
  async stopContainer(containerId) {
5997
6722
  try {
5998
- await spawn2("docker", ["stop", containerId]);
6723
+ await spawn3("docker", ["stop", containerId]);
5999
6724
  } catch {
6000
6725
  }
6001
6726
  }
6002
- /**
6003
- * Executes a command in the container.
6004
- */
6005
6727
  async exec(command) {
6006
6728
  try {
6007
- const result = await spawn2("docker", [
6729
+ const result = await spawn3("docker", [
6008
6730
  "exec",
6009
6731
  this.context.containerId,
6010
6732
  "sh",
@@ -6025,9 +6747,6 @@ var DockerSandboxStrategy = class {
6025
6747
  };
6026
6748
  }
6027
6749
  }
6028
- /**
6029
- * Creates the DockerSandbox interface with all methods.
6030
- */
6031
6750
  createSandboxMethods() {
6032
6751
  const { containerId } = this.context;
6033
6752
  const sandbox = {
@@ -6067,171 +6786,19 @@ var DockerSandboxStrategy = class {
6067
6786
  };
6068
6787
  var RuntimeStrategy = class extends DockerSandboxStrategy {
6069
6788
  image;
6070
- packages;
6071
- binaries;
6072
- constructor(image = "alpine:latest", packages = [], binaries = [], mounts, resources, env) {
6073
- super(mounts, resources, env);
6074
- this.image = image;
6075
- this.packages = packages;
6076
- this.binaries = binaries;
6789
+ installers;
6790
+ constructor(args = {}) {
6791
+ super({ mounts: args.mounts, resources: args.resources, env: args.env });
6792
+ this.image = args.image ?? "alpine:latest";
6793
+ this.installers = args.installers ?? [];
6077
6794
  }
6078
6795
  async getImage() {
6079
6796
  return this.image;
6080
6797
  }
6081
6798
  async configure() {
6082
- await this.installPackages();
6083
- await this.installBinaries();
6084
- }
6085
- /**
6086
- * Installs packages using the appropriate package manager (apk/apt-get).
6087
- */
6088
- async installPackages() {
6089
- if (this.packages.length === 0) return;
6090
- const useApt = isDebianBased(this.image);
6091
- const installCmd = useApt ? `apt-get update && apt-get install -y ${this.packages.join(" ")}` : `apk add --no-cache ${this.packages.join(" ")}`;
6092
- try {
6093
- await spawn2("docker", [
6094
- "exec",
6095
- this.context.containerId,
6096
- "sh",
6097
- "-c",
6098
- installCmd
6099
- ]);
6100
- } catch (error) {
6101
- const err = error;
6102
- throw new PackageInstallError(
6103
- this.packages,
6104
- this.image,
6105
- useApt ? "apt-get" : "apk",
6106
- err.stderr || err.message,
6107
- this.context.containerId
6108
- );
6109
- }
6110
- }
6111
- /**
6112
- * Installs binaries from URLs.
6113
- */
6114
- async installBinaries() {
6115
- if (this.binaries.length === 0) return;
6116
- await this.ensureCurl();
6117
- const arch = await this.detectArchitecture();
6118
- for (const binary of this.binaries) {
6119
- await this.installBinary(binary, arch);
6120
- }
6121
- }
6122
- /**
6123
- * Ensures curl is installed in the container.
6124
- */
6125
- async ensureCurl() {
6126
- const checkResult = await spawn2("docker", [
6127
- "exec",
6128
- this.context.containerId,
6129
- "which",
6130
- "curl"
6131
- ]).catch(() => null);
6132
- if (checkResult) return;
6133
- const useApt = isDebianBased(this.image);
6134
- const curlInstallCmd = useApt ? "apt-get update && apt-get install -y curl" : "apk add --no-cache curl";
6135
- try {
6136
- await spawn2("docker", [
6137
- "exec",
6138
- this.context.containerId,
6139
- "sh",
6140
- "-c",
6141
- curlInstallCmd
6142
- ]);
6143
- } catch (error) {
6144
- const err = error;
6145
- throw new BinaryInstallError(
6146
- "curl",
6147
- "package-manager",
6148
- `Required for binary downloads: ${err.stderr || err.message}`,
6149
- this.context.containerId
6150
- );
6151
- }
6152
- }
6153
- /**
6154
- * Detects the container's CPU architecture.
6155
- */
6156
- async detectArchitecture() {
6157
- try {
6158
- const result = await spawn2("docker", [
6159
- "exec",
6160
- this.context.containerId,
6161
- "uname",
6162
- "-m"
6163
- ]);
6164
- return result.stdout.trim();
6165
- } catch (error) {
6166
- const err = error;
6167
- throw new DockerSandboxError(
6168
- `Failed to detect container architecture: ${err.stderr || err.message}`,
6169
- this.context.containerId
6170
- );
6171
- }
6172
- }
6173
- /**
6174
- * Installs a single binary from URL.
6175
- */
6176
- async installBinary(binary, arch) {
6177
- let url;
6178
- if (typeof binary.url === "string") {
6179
- url = binary.url;
6180
- } else {
6181
- const archUrl = binary.url[arch];
6182
- if (!archUrl) {
6183
- throw new BinaryInstallError(
6184
- binary.name,
6185
- `arch:${arch}`,
6186
- `No URL provided for architecture "${arch}". Available: ${Object.keys(binary.url).join(", ")}`,
6187
- this.context.containerId
6188
- );
6189
- }
6190
- url = archUrl;
6191
- }
6192
- const isTarGz = url.endsWith(".tar.gz") || url.endsWith(".tgz");
6193
- let installCmd;
6194
- if (isTarGz) {
6195
- const binaryPathInArchive = binary.binaryPath || binary.name;
6196
- installCmd = `
6197
- set -e
6198
- TMPDIR=$(mktemp -d)
6199
- cd "$TMPDIR"
6200
- curl -fsSL "${url}" -o archive.tar.gz
6201
- tar -xzf archive.tar.gz
6202
- BINARY_FILE=$(find . -name "${binaryPathInArchive}" -o -name "${binary.name}" | head -1)
6203
- if [ -z "$BINARY_FILE" ]; then
6204
- echo "Binary not found in archive. Contents:" >&2
6205
- find . -type f >&2
6206
- exit 1
6207
- fi
6208
- chmod +x "$BINARY_FILE"
6209
- mv "$BINARY_FILE" /usr/local/bin/${binary.name}
6210
- cd /
6211
- rm -rf "$TMPDIR"
6212
- `;
6213
- } else {
6214
- installCmd = `
6215
- curl -fsSL "${url}" -o /usr/local/bin/${binary.name}
6216
- chmod +x /usr/local/bin/${binary.name}
6217
- `;
6218
- }
6219
- try {
6220
- await spawn2("docker", [
6221
- "exec",
6222
- this.context.containerId,
6223
- "sh",
6224
- "-c",
6225
- installCmd
6226
- ]);
6227
- } catch (error) {
6228
- const err = error;
6229
- throw new BinaryInstallError(
6230
- binary.name,
6231
- url,
6232
- err.stderr || err.message,
6233
- this.context.containerId
6234
- );
6799
+ const ctx = createInstallerContext(this.context.containerId, this.image);
6800
+ for (const installer of this.installers) {
6801
+ await installer.install(ctx);
6235
6802
  }
6236
6803
  }
6237
6804
  };
@@ -6239,24 +6806,17 @@ var DockerfileStrategy = class extends DockerSandboxStrategy {
6239
6806
  imageTag;
6240
6807
  dockerfile;
6241
6808
  dockerContext;
6242
- constructor(dockerfile, dockerContext = ".", mounts, resources, env) {
6243
- super(mounts, resources, env);
6244
- this.dockerfile = dockerfile;
6245
- this.dockerContext = dockerContext;
6809
+ constructor(args) {
6810
+ super({ mounts: args.mounts, resources: args.resources, env: args.env });
6811
+ this.dockerfile = args.dockerfile;
6812
+ this.dockerContext = args.context ?? ".";
6246
6813
  this.imageTag = this.computeImageTag();
6247
6814
  }
6248
- /**
6249
- * Computes a deterministic image tag based on Dockerfile content.
6250
- * Same Dockerfile → same tag → Docker skips rebuild if image exists.
6251
- */
6252
6815
  computeImageTag() {
6253
6816
  const content = this.isInlineDockerfile() ? this.dockerfile : readFileSync2(this.dockerfile, "utf-8");
6254
6817
  const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
6255
6818
  return `sandbox-${hash}`;
6256
6819
  }
6257
- /**
6258
- * Checks if the dockerfile property is inline content or a file path.
6259
- */
6260
6820
  isInlineDockerfile() {
6261
6821
  return this.dockerfile.includes("\n");
6262
6822
  }
@@ -6269,27 +6829,21 @@ var DockerfileStrategy = class extends DockerSandboxStrategy {
6269
6829
  }
6270
6830
  async configure() {
6271
6831
  }
6272
- /**
6273
- * Checks if the image already exists locally.
6274
- */
6275
6832
  async imageExists() {
6276
6833
  try {
6277
- await spawn2("docker", ["image", "inspect", this.imageTag]);
6834
+ await spawn3("docker", ["image", "inspect", this.imageTag]);
6278
6835
  return true;
6279
6836
  } catch {
6280
6837
  return false;
6281
6838
  }
6282
6839
  }
6283
- /**
6284
- * Builds the Docker image from the Dockerfile.
6285
- */
6286
6840
  async buildImage() {
6287
6841
  try {
6288
6842
  if (this.isInlineDockerfile()) {
6289
6843
  const buildCmd = `echo '${this.dockerfile.replace(/'/g, "'\\''")}' | docker build -t ${this.imageTag} -f - ${this.dockerContext}`;
6290
- await spawn2("sh", ["-c", buildCmd]);
6844
+ await spawn3("sh", ["-c", buildCmd]);
6291
6845
  } else {
6292
- await spawn2("docker", [
6846
+ await spawn3("docker", [
6293
6847
  "build",
6294
6848
  "-t",
6295
6849
  this.imageTag,
@@ -6308,33 +6862,23 @@ var ComposeStrategy = class extends DockerSandboxStrategy {
6308
6862
  projectName;
6309
6863
  composeFile;
6310
6864
  service;
6311
- constructor(composeFile, service, resources) {
6312
- super([], resources);
6313
- this.composeFile = composeFile;
6314
- this.service = service;
6865
+ constructor(args) {
6866
+ super({ resources: args.resources });
6867
+ this.composeFile = args.compose;
6868
+ this.service = args.service;
6315
6869
  this.projectName = this.computeProjectName();
6316
6870
  }
6317
- /**
6318
- * Deterministic project name based on compose file content for caching.
6319
- * Same compose file → same project name → faster subsequent startups.
6320
- */
6321
6871
  computeProjectName() {
6322
6872
  const content = readFileSync2(this.composeFile, "utf-8");
6323
6873
  const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
6324
6874
  return `sandbox-${hash}`;
6325
6875
  }
6326
- /**
6327
- * Override: No image to get - compose manages its own images.
6328
- */
6329
6876
  async getImage() {
6330
6877
  return "";
6331
6878
  }
6332
- /**
6333
- * Override: Start all services with docker compose up.
6334
- */
6335
6879
  async startContainer(_image) {
6336
6880
  try {
6337
- await spawn2("docker", [
6881
+ await spawn3("docker", [
6338
6882
  "compose",
6339
6883
  "-f",
6340
6884
  this.composeFile,
@@ -6354,12 +6898,9 @@ var ComposeStrategy = class extends DockerSandboxStrategy {
6354
6898
  }
6355
6899
  async configure() {
6356
6900
  }
6357
- /**
6358
- * Override: Execute commands in the target service.
6359
- */
6360
6901
  async exec(command) {
6361
6902
  try {
6362
- const result = await spawn2("docker", [
6903
+ const result = await spawn3("docker", [
6363
6904
  "compose",
6364
6905
  "-f",
6365
6906
  this.composeFile,
@@ -6367,7 +6908,6 @@ var ComposeStrategy = class extends DockerSandboxStrategy {
6367
6908
  this.projectName,
6368
6909
  "exec",
6369
6910
  "-T",
6370
- // -T disables pseudo-TTY
6371
6911
  this.service,
6372
6912
  "sh",
6373
6913
  "-c",
@@ -6383,12 +6923,9 @@ var ComposeStrategy = class extends DockerSandboxStrategy {
6383
6923
  };
6384
6924
  }
6385
6925
  }
6386
- /**
6387
- * Override: Stop all services with docker compose down.
6388
- */
6389
6926
  async stopContainer(_containerId) {
6390
6927
  try {
6391
- await spawn2("docker", [
6928
+ await spawn3("docker", [
6392
6929
  "compose",
6393
6930
  "-f",
6394
6931
  this.composeFile,
@@ -6403,28 +6940,27 @@ var ComposeStrategy = class extends DockerSandboxStrategy {
6403
6940
  async function createDockerSandbox(options = {}) {
6404
6941
  let strategy;
6405
6942
  if (isComposeOptions(options)) {
6406
- strategy = new ComposeStrategy(
6407
- options.compose,
6408
- options.service,
6409
- options.resources
6410
- );
6943
+ strategy = new ComposeStrategy({
6944
+ compose: options.compose,
6945
+ service: options.service,
6946
+ resources: options.resources
6947
+ });
6411
6948
  } else if (isDockerfileOptions(options)) {
6412
- strategy = new DockerfileStrategy(
6413
- options.dockerfile,
6414
- options.context,
6415
- options.mounts,
6416
- options.resources,
6417
- options.env
6418
- );
6949
+ strategy = new DockerfileStrategy({
6950
+ dockerfile: options.dockerfile,
6951
+ context: options.context,
6952
+ mounts: options.mounts,
6953
+ resources: options.resources,
6954
+ env: options.env
6955
+ });
6419
6956
  } else {
6420
- strategy = new RuntimeStrategy(
6421
- options.image,
6422
- options.packages,
6423
- options.binaries,
6424
- options.mounts,
6425
- options.resources,
6426
- options.env
6427
- );
6957
+ strategy = new RuntimeStrategy({
6958
+ image: options.image,
6959
+ installers: options.installers,
6960
+ mounts: options.mounts,
6961
+ resources: options.resources,
6962
+ env: options.env
6963
+ });
6428
6964
  }
6429
6965
  return strategy.create();
6430
6966
  }
@@ -6463,15 +6999,14 @@ async function createContainerTool(options = {}) {
6463
6999
  } else {
6464
7000
  const {
6465
7001
  image,
6466
- packages,
6467
- binaries,
7002
+ installers,
6468
7003
  mounts,
6469
7004
  resources,
6470
7005
  env,
6471
7006
  skills: skills2 = [],
6472
7007
  ...rest
6473
7008
  } = options;
6474
- sandboxOptions = { image, packages, binaries, mounts, resources, env };
7009
+ sandboxOptions = { image, installers, mounts, resources, env };
6475
7010
  bashOptions = rest;
6476
7011
  skillInputs = skills2;
6477
7012
  }
@@ -6489,215 +7024,252 @@ async function createContainerTool(options = {}) {
6489
7024
  };
6490
7025
  }
6491
7026
 
6492
- // packages/context/src/lib/sandbox/routing-sandbox.ts
6493
- import {
6494
- Bash,
6495
- defineCommand as defineCommand2,
6496
- parse
6497
- } from "just-bash";
6498
- function isInstallable(s) {
6499
- return typeof s.install === "function";
6500
- }
6501
- async function createVirtualSandbox(options) {
6502
- let bash = null;
6503
- const ensureInstalled = (op) => {
6504
- if (!bash) {
6505
- throw new Error(
6506
- `createVirtualSandbox: ${op} called before install(). Wrap this sandbox with createRoutingSandbox before use.`
6507
- );
6508
- }
6509
- return bash;
6510
- };
6511
- const sandbox = {
6512
- async install(ext) {
6513
- if (bash) {
6514
- throw new Error(
6515
- "createVirtualSandbox: install() called twice (extensions are install-once)"
6516
- );
6517
- }
6518
- const adapted = ext.commands.map(
6519
- (cmd) => adaptExtensionCommandForBash(cmd, () => sandbox)
6520
- );
6521
- bash = new Bash({
6522
- fs: options.fs,
6523
- cwd: options.cwd,
6524
- env: { ...options.env, ...ext.env },
6525
- customCommands: adapted
6526
- });
6527
- for (const plugin of ext.plugins) {
6528
- bash.registerTransformPlugin(plugin);
6529
- }
6530
- },
6531
- async executeCommand(command) {
6532
- const instance = ensureInstalled("executeCommand");
6533
- const result = await instance.exec(command);
6534
- return {
6535
- stdout: result.stdout,
6536
- stderr: result.stderr,
6537
- exitCode: result.exitCode
6538
- };
6539
- },
6540
- async readFile(path5) {
6541
- return ensureInstalled("readFile").readFile(path5);
6542
- },
6543
- async writeFiles(files) {
6544
- const instance = ensureInstalled("writeFiles");
6545
- for (const f of files) {
6546
- await instance.writeFile(
6547
- f.path,
6548
- typeof f.content === "string" ? f.content : Buffer.from(f.content).toString("utf-8")
6549
- );
6550
- }
6551
- }
6552
- };
6553
- return sandbox;
7027
+ // packages/context/src/lib/sandbox/installers/package-manager.ts
7028
+ var PackageInstaller = class extends Installer {
7029
+ kind;
7030
+ packages;
7031
+ constructor(packages) {
7032
+ super();
7033
+ this.packages = packages;
7034
+ this.kind = packages.length === 0 ? "pkg:<empty>" : `pkg:${packages.join(",")}`;
7035
+ }
7036
+ async install(ctx) {
7037
+ if (this.packages.length === 0) return;
7038
+ await ctx.installPackages([...this.packages]);
7039
+ }
7040
+ };
7041
+ function pkg(packages) {
7042
+ return new PackageInstaller(packages);
6554
7043
  }
6555
- async function createRoutingSandbox(opts) {
6556
- const merged = mergeExtensions(...opts.hostExtensions);
6557
- if (isInstallable(opts.backend)) {
6558
- await opts.backend.install(merged);
6559
- return wrapPreCallHook(opts.backend, merged.onBeforeBashCall);
7044
+
7045
+ // packages/context/src/lib/sandbox/installers/url-binary.ts
7046
+ var UrlBinaryInstaller = class extends Installer {
7047
+ kind;
7048
+ options;
7049
+ constructor(options) {
7050
+ super();
7051
+ this.options = options;
7052
+ this.kind = `url-binary:${options.name}`;
7053
+ }
7054
+ async install(ctx) {
7055
+ const url = await resolveUrl(ctx, this.options);
7056
+ await downloadAndInstall(
7057
+ ctx,
7058
+ this.options.name,
7059
+ url,
7060
+ this.options.binaryPath
7061
+ );
7062
+ }
7063
+ };
7064
+ function urlBinary(options) {
7065
+ return new UrlBinaryInstaller(options);
7066
+ }
7067
+ async function resolveUrl(ctx, options) {
7068
+ if (typeof options.url === "string") return options.url;
7069
+ const arch = await ctx.arch();
7070
+ const archUrl = options.url[arch];
7071
+ if (!archUrl) {
7072
+ throw new InstallError({
7073
+ target: options.name,
7074
+ source: "url",
7075
+ reason: `No URL provided for architecture "${arch}". Available: ${Object.keys(options.url).join(", ")}`,
7076
+ containerId: ctx.containerId
7077
+ });
7078
+ }
7079
+ return archUrl;
7080
+ }
7081
+ async function downloadAndInstall(ctx, name, url, binaryPath, source = "url") {
7082
+ await ctx.ensureTool("curl");
7083
+ const isTarGz = url.endsWith(".tar.gz") || url.endsWith(".tgz");
7084
+ const installCmd = isTarGz ? buildTarGzInstallCmd(name, url, binaryPath ?? name) : buildRawInstallCmd(name, url);
7085
+ const result = await ctx.exec(installCmd);
7086
+ if (result.exitCode !== 0) {
7087
+ throw new InstallError({
7088
+ target: name,
7089
+ source,
7090
+ url,
7091
+ reason: result.stderr,
7092
+ containerId: ctx.containerId
7093
+ });
6560
7094
  }
6561
- return createShallowRouter(opts.backend, merged, opts.cwd ?? "/");
6562
7095
  }
6563
- function wrapPreCallHook(backend, hook) {
6564
- if (!hook) return backend;
6565
- return {
6566
- executeCommand: async (raw) => {
6567
- const { command } = await hook({ command: raw });
6568
- return backend.executeCommand(command);
6569
- },
6570
- readFile: (path5) => backend.readFile(path5),
6571
- writeFiles: (files) => backend.writeFiles(files)
6572
- };
7096
+ function buildTarGzInstallCmd(name, url, binaryPathInArchive) {
7097
+ return `
7098
+ set -e
7099
+ NAME=${shellQuote(name)}
7100
+ URL=${shellQuote(url)}
7101
+ BIN_IN_ARCHIVE=${shellQuote(binaryPathInArchive)}
7102
+ TMPDIR=$(mktemp -d)
7103
+ cd "$TMPDIR"
7104
+ curl -fsSL "$URL" -o archive.tar.gz
7105
+ tar -xzf archive.tar.gz
7106
+ BINARY_FILE=$(find . -name "$BIN_IN_ARCHIVE" -o -name "$NAME" | head -1)
7107
+ if [ -z "$BINARY_FILE" ]; then
7108
+ echo "Binary not found in archive. Contents:" >&2
7109
+ find . -type f >&2
7110
+ exit 1
7111
+ fi
7112
+ chmod +x "$BINARY_FILE"
7113
+ mv "$BINARY_FILE" "/usr/local/bin/$NAME"
7114
+ cd /
7115
+ rm -rf "$TMPDIR"
7116
+ `;
7117
+ }
7118
+ function buildRawInstallCmd(name, url) {
7119
+ return `
7120
+ NAME=${shellQuote(name)}
7121
+ URL=${shellQuote(url)}
7122
+ curl -fsSL "$URL" -o "/usr/local/bin/$NAME"
7123
+ chmod +x "/usr/local/bin/$NAME"
7124
+ `;
6573
7125
  }
6574
- function createShallowRouter(backend, ext, cwd) {
6575
- const byName = new Map(ext.commands.map((c) => [c.name, c]));
6576
- const transformer = new Bash({});
6577
- for (const plugin of ext.plugins) {
6578
- transformer.registerTransformPlugin(plugin);
6579
- }
6580
- return {
6581
- readFile: (path5) => backend.readFile(path5),
6582
- writeFiles: (files) => backend.writeFiles(files),
6583
- executeCommand: async (raw) => {
6584
- const preHook = ext.onBeforeBashCall ? (await ext.onBeforeBashCall({ command: raw })).command : raw;
6585
- let transformed;
6586
- let dispatch;
6587
- try {
6588
- transformed = ext.plugins.length > 0 ? transformer.transform(preHook).script : preHook;
6589
- dispatch = extractDispatchTarget(parse(transformed), cwd);
6590
- } catch (err) {
6591
- return {
6592
- stdout: "",
6593
- stderr: `parse error: ${err.message}
6594
- `,
6595
- exitCode: 2
6596
- };
6597
- }
6598
- const [name, ...args] = dispatch?.tokens ?? [];
6599
- const cmd = name ? byName.get(name) : void 0;
6600
- if (cmd) {
6601
- return cmd.handler(args, {
6602
- sandbox: backend,
6603
- cwd: dispatch?.cwd ?? cwd,
6604
- env: ext.env,
6605
- stdin: ""
6606
- });
6607
- }
6608
- return backend.executeCommand(transformed);
7126
+
7127
+ // packages/context/src/lib/sandbox/installers/npm.ts
7128
+ var NODE_BINARIES = ["node", "npm"];
7129
+ var ENSURE_RUNTIME_HINT = "Pass `{ ensureRuntime: true }` to auto-install, or add it via `pkg([...])`.";
7130
+ var NpmInstaller = class extends Installer {
7131
+ kind;
7132
+ packageName;
7133
+ options;
7134
+ constructor(packageName, options = {}) {
7135
+ super();
7136
+ this.packageName = packageName;
7137
+ this.options = options;
7138
+ this.kind = `npm:${packageName}`;
7139
+ }
7140
+ async install(ctx) {
7141
+ await ensureNodeRuntime(ctx, this.options.ensureRuntime ?? false);
7142
+ const spec = this.options.version ? `${this.packageName}@${this.options.version}` : this.packageName;
7143
+ const result = await ctx.exec(`npm install -g ${spec}`);
7144
+ if (result.exitCode !== 0) {
7145
+ throw new InstallError({
7146
+ target: this.packageName,
7147
+ source: "npm",
7148
+ reason: result.stderr,
7149
+ containerId: ctx.containerId
7150
+ });
6609
7151
  }
6610
- };
6611
- }
6612
- function extractDispatchTarget(ast, defaultCwd) {
6613
- if (ast.statements.length !== 1) return null;
6614
- const statement = ast.statements[0];
6615
- if (statement.background) return null;
6616
- if (statement.operators.length === 0 && statement.pipelines.length === 1) {
6617
- const tokens = tokenizeSimplePipeline(statement.pipelines[0]);
6618
- return tokens ? { tokens, cwd: defaultCwd } : null;
6619
- }
6620
- if (statement.operators.length === 1 && statement.operators[0] === "&&" && statement.pipelines.length === 2) {
6621
- const cd = simpleCommandFromPipeline(statement.pipelines[0]);
6622
- const cdCwd = readCdTarget(cd);
6623
- if (!cdCwd) return null;
6624
- const tokens = tokenizeSimplePipeline(statement.pipelines[1]);
6625
- return tokens ? { tokens, cwd: cdCwd } : null;
6626
7152
  }
6627
- return null;
7153
+ };
7154
+ function npm(packageName, options) {
7155
+ return new NpmInstaller(packageName, options);
6628
7156
  }
6629
- function simpleCommandFromPipeline(pipeline) {
6630
- if (!pipeline) return null;
6631
- if (pipeline.negated || pipeline.timed || pipeline.commands.length !== 1) {
6632
- return null;
7157
+ async function ensureNodeRuntime(ctx, ensure) {
7158
+ if (ensure) {
7159
+ await ctx.ensureTool("node", "nodejs");
7160
+ await ctx.ensureTool("npm");
7161
+ return;
7162
+ }
7163
+ const check = await ctx.exec("which node && which npm");
7164
+ if (check.exitCode !== 0) {
7165
+ throw new MissingRuntimeError(
7166
+ "npm",
7167
+ NODE_BINARIES,
7168
+ ENSURE_RUNTIME_HINT,
7169
+ ctx.containerId
7170
+ );
6633
7171
  }
6634
- const command = pipeline.commands[0];
6635
- return command?.type === "SimpleCommand" ? command : null;
6636
7172
  }
6637
- function readCdTarget(command) {
6638
- if (!command) return null;
6639
- if (command.redirections.length > 0 || command.args.length !== 1) {
6640
- return null;
7173
+
7174
+ // packages/context/src/lib/sandbox/installers/pip.ts
7175
+ var PYTHON_BINARIES = ["python3", "pip3"];
7176
+ var PipInstaller = class extends Installer {
7177
+ kind;
7178
+ packageName;
7179
+ options;
7180
+ constructor(packageName, options = {}) {
7181
+ super();
7182
+ this.packageName = packageName;
7183
+ this.options = options;
7184
+ this.kind = `pip:${packageName}`;
7185
+ }
7186
+ async install(ctx) {
7187
+ await ensurePythonRuntime(ctx, this.options.ensureRuntime ?? false);
7188
+ const spec = this.options.version ? `${this.packageName}${formatVersion(this.options.version)}` : this.packageName;
7189
+ const flags = this.options.breakSystemPackages ?? true ? "--break-system-packages" : "";
7190
+ const result = await ctx.exec(`pip3 install ${flags} ${spec}`.trim());
7191
+ if (result.exitCode !== 0) {
7192
+ throw new InstallError({
7193
+ target: this.packageName,
7194
+ source: "pypi",
7195
+ reason: result.stderr,
7196
+ containerId: ctx.containerId
7197
+ });
7198
+ }
7199
+ }
7200
+ };
7201
+ function pip(packageName, options) {
7202
+ return new PipInstaller(packageName, options);
7203
+ }
7204
+ async function ensurePythonRuntime(ctx, ensure) {
7205
+ if (ensure) {
7206
+ const pipPackage = ctx.packageManager === "apk" ? "py3-pip" : "python3-pip";
7207
+ await ctx.ensureTool("python3");
7208
+ await ctx.ensureTool("pip3", pipPackage);
7209
+ return;
7210
+ }
7211
+ const check = await ctx.exec("which python3 && which pip3");
7212
+ if (check.exitCode !== 0) {
7213
+ throw new MissingRuntimeError(
7214
+ "pip",
7215
+ PYTHON_BINARIES,
7216
+ "Pass `{ ensureRuntime: true }` to auto-install, or add via `pkg([...])` (Alpine: `python3 py3-pip`; Debian: `python3 python3-pip`).",
7217
+ ctx.containerId
7218
+ );
6641
7219
  }
6642
- const name = asStaticWordText(command.name);
6643
- if (name !== "cd") return null;
6644
- return asStaticWordText(command.args[0]);
6645
- }
6646
- function tokenizeSimplePipeline(pipeline) {
6647
- const command = simpleCommandFromPipeline(pipeline);
6648
- if (!command || command.redirections.length > 0) return null;
6649
- const name = asStaticWordText(command.name);
6650
- if (!name) return null;
6651
- const args = [];
6652
- for (const arg of command.args) {
6653
- const text = asStaticWordText(arg);
6654
- if (text == null) return null;
6655
- args.push(text);
6656
- }
6657
- return [name, ...args];
6658
- }
6659
- function adaptExtensionCommandForBash(ext, getSandbox) {
6660
- return defineCommand2(ext.name, async (args, bashCtx) => {
6661
- return ext.handler(args, {
6662
- sandbox: getSandbox(),
6663
- cwd: bashCtx.cwd,
6664
- env: Object.fromEntries(bashCtx.env),
6665
- stdin: bashCtx.stdin,
6666
- signal: bashCtx.signal
6667
- });
6668
- });
7220
+ }
7221
+ function formatVersion(version) {
7222
+ return /^[<>=!~]/.test(version) ? version : `==${version}`;
6669
7223
  }
6670
7224
 
6671
- // packages/context/src/lib/sandbox/openapi/extension.ts
6672
- import { rpc } from "@sdk-it/rpc";
6673
- import {
6674
- forEachOperation,
6675
- loadSpec,
6676
- toIR
6677
- } from "@sdk-it/spec";
7225
+ // packages/context/src/lib/sandbox/installers/github-release.ts
7226
+ var GithubReleaseInstaller = class extends Installer {
7227
+ kind;
7228
+ options;
7229
+ constructor(options) {
7230
+ super();
7231
+ this.options = options;
7232
+ this.kind = `github-release:${options.owner}/${options.repo}@${options.version}`;
7233
+ }
7234
+ async install(ctx) {
7235
+ const arch = await ctx.arch();
7236
+ const assetName = this.options.asset(arch);
7237
+ const url = `https://github.com/${this.options.owner}/${this.options.repo}/releases/download/${this.options.version}/${assetName}`;
7238
+ await downloadAndInstall(
7239
+ ctx,
7240
+ this.options.name,
7241
+ url,
7242
+ this.options.binaryPath,
7243
+ "github-release"
7244
+ );
7245
+ }
7246
+ };
7247
+ function githubRelease(options) {
7248
+ return new GithubReleaseInstaller(options);
7249
+ }
6678
7250
 
6679
7251
  // packages/context/src/lib/sandbox/subcommand.ts
6680
- import { parse as parse2 } from "just-bash";
7252
+ import {
7253
+ defineCommand as defineCommand2,
7254
+ parse
7255
+ } from "just-bash";
6681
7256
  function defineSubcommandGroup(name, subcommands) {
6682
7257
  const usageLines = Object.entries(subcommands).map(([, def]) => ` ${name} ${def.usage.padEnd(30)} ${def.description}`).join("\n");
6683
- return {
6684
- name,
6685
- handler: async (args, ctx) => {
6686
- const subcommand = args[0];
6687
- const restArgs = args.slice(1);
6688
- if (subcommand && subcommand in subcommands) {
6689
- return subcommands[subcommand].handler(restArgs, ctx);
6690
- }
6691
- return {
6692
- stdout: "",
6693
- stderr: `${name}: ${subcommand ? `unknown subcommand '${subcommand}'` : "missing subcommand"}
7258
+ return defineCommand2(name, async (args, ctx) => {
7259
+ const subcommand = args[0];
7260
+ const restArgs = args.slice(1);
7261
+ if (subcommand && subcommand in subcommands) {
7262
+ return subcommands[subcommand].handler(restArgs, ctx);
7263
+ }
7264
+ return {
7265
+ stdout: "",
7266
+ stderr: `${name}: ${subcommand ? `unknown subcommand '${subcommand}'` : "missing subcommand"}
6694
7267
 
6695
7268
  Usage:
6696
7269
  ${usageLines}`,
6697
- exitCode: 1
6698
- };
6699
- }
6700
- };
7270
+ exitCode: 1
7271
+ };
7272
+ });
6701
7273
  }
6702
7274
  function escapeRegExp(s) {
6703
7275
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -6728,7 +7300,7 @@ function buildSubcommandRepair(name, subcommands) {
6728
7300
  const match = raw.match(pattern);
6729
7301
  if (!match) return raw;
6730
7302
  try {
6731
- parse2(raw);
7303
+ parse(raw);
6732
7304
  return raw;
6733
7305
  } catch {
6734
7306
  }
@@ -6740,7 +7312,7 @@ function buildSubcommandRepair(name, subcommands) {
6740
7312
  if (repairedArgs == null) return raw;
6741
7313
  const repaired = `${prefix} ${repairedArgs}`;
6742
7314
  try {
6743
- parse2(repaired);
7315
+ parse(repaired);
6744
7316
  return repaired;
6745
7317
  } catch {
6746
7318
  return raw;
@@ -6748,278 +7320,35 @@ function buildSubcommandRepair(name, subcommands) {
6748
7320
  };
6749
7321
  }
6750
7322
 
6751
- // packages/context/src/lib/sandbox/openapi/format.ts
6752
- function formatResponse(value) {
6753
- const payload = unwrapApiResponse(value);
6754
- if (Array.isArray(payload)) {
6755
- const stdout = payload.length === 0 ? "" : payload.map((item) => JSON.stringify(item)).join("\n") + "\n";
6756
- return { stdout, stderr: "", exitCode: 0 };
6757
- }
6758
- return { stdout: JSON.stringify(payload) + "\n", stderr: "", exitCode: 0 };
6759
- }
6760
- function unwrapApiResponse(value) {
6761
- if (value !== null && typeof value === "object" && "status" in value && "data" in value) {
6762
- return value.data;
6763
- }
6764
- return value;
6765
- }
6766
- function formatError(details) {
6767
- const payload = { ...details.extra ?? {} };
6768
- payload.ok = false;
6769
- payload.group = details.group;
6770
- payload.code = details.code;
6771
- payload.message = details.message;
6772
- if (details.operation) payload.operation = details.operation;
6773
- return {
6774
- stdout: "",
6775
- stderr: JSON.stringify(payload) + "\n",
6776
- exitCode: 1
6777
- };
6778
- }
6779
-
6780
- // packages/context/src/lib/sandbox/openapi/schema.ts
6781
- import { zodToJsonSchema } from "zod-to-json-schema";
6782
- function buildSchemaSubcommand(group, client, entries) {
7323
+ // packages/context/src/lib/sandbox/virtual-sandbox.ts
7324
+ import { Bash } from "just-bash";
7325
+ async function createVirtualSandbox(options) {
7326
+ const bash = new Bash({
7327
+ fs: options.fs,
7328
+ cwd: options.cwd,
7329
+ env: options.env,
7330
+ customCommands: options.customCommands
7331
+ });
6783
7332
  return {
6784
- usage: "schema",
6785
- description: "Dump JSON Schema for every operation in this group.",
6786
- handler: async () => {
6787
- try {
6788
- const operations = entries.map((entry) => {
6789
- const [method, path5] = entry.endpoint.split(" ");
6790
- const zodSchema = client.schemas[entry.endpoint]?.schema;
6791
- const input = zodSchema ? zodToJsonSchema(zodSchema, {
6792
- target: "jsonSchema7",
6793
- $refStrategy: "none"
6794
- }) : {};
6795
- return entry.summary ? {
6796
- operationId: entry.operationId,
6797
- method,
6798
- path: path5,
6799
- summary: entry.summary,
6800
- input
6801
- } : { operationId: entry.operationId, method, path: path5, input };
6802
- });
6803
- return formatResponse({ group, operations });
6804
- } catch (error) {
6805
- return formatError({
6806
- group,
6807
- operation: "schema",
6808
- code: "schema_dump_failed",
6809
- message: error instanceof Error ? error.message : String(error)
6810
- });
6811
- }
6812
- }
6813
- };
6814
- }
6815
-
6816
- // packages/context/src/lib/sandbox/openapi/validate.ts
6817
- function extractPathParams(path5) {
6818
- const names = [];
6819
- const re = /{([^}]+)}/g;
6820
- let match;
6821
- while ((match = re.exec(path5)) !== null) {
6822
- names.push(match[1]);
6823
- }
6824
- return names;
6825
- }
6826
- var UNSAFE_CHARS = /[/\\?#&]/;
6827
- var CONTROL = /[\x00-\x1f\x7f]/;
6828
- var PRE_ENCODED = /%[0-9a-fA-F]{2}/;
6829
- function findUnsafePathParam(input, names) {
6830
- if (input === null || typeof input !== "object" || Array.isArray(input)) {
6831
- return null;
6832
- }
6833
- const record = input;
6834
- for (const name of names) {
6835
- const value = record[name];
6836
- if (typeof value !== "string") continue;
6837
- if (value.length === 0) {
6838
- return { field: name, reason: "is empty" };
6839
- }
6840
- if (value === "." || value === "..") {
6841
- return {
6842
- field: name,
6843
- reason: `resolves to '${value}' path segment`
6844
- };
6845
- }
6846
- const unsafeChar = value.match(UNSAFE_CHARS);
6847
- if (unsafeChar) {
6848
- return {
6849
- field: name,
6850
- reason: `contains disallowed character '${unsafeChar[0]}'`
6851
- };
6852
- }
6853
- if (CONTROL.test(value)) {
6854
- return { field: name, reason: "contains a control character" };
6855
- }
6856
- const encoded = value.match(PRE_ENCODED);
6857
- if (encoded) {
7333
+ async executeCommand(command) {
7334
+ const result = await bash.exec(command);
6858
7335
  return {
6859
- field: name,
6860
- reason: `contains pre-encoded sequence '${encoded[0]}'`
7336
+ stdout: result.stdout,
7337
+ stderr: result.stderr,
7338
+ exitCode: result.exitCode
6861
7339
  };
6862
- }
6863
- }
6864
- return null;
6865
- }
6866
-
6867
- // packages/context/src/lib/sandbox/openapi/extension.ts
6868
- var RESERVED_SUBCOMMAND = "schema";
6869
- function serializeError(err) {
6870
- if (err === null || err === void 0) return err;
6871
- if (typeof err !== "object") return String(err);
6872
- if (err instanceof Error) {
6873
- const out = {
6874
- name: err.name,
6875
- message: err.message
6876
- };
6877
- for (const key of Object.getOwnPropertyNames(err)) {
6878
- if (key === "stack") continue;
6879
- const value = err[key];
6880
- out[key] = key === "cause" ? serializeError(value) : value;
6881
- }
6882
- return out;
6883
- }
6884
- return err;
6885
- }
6886
- async function createOpenAPIExtension(options) {
6887
- const { name, openapi, includeOperation, ...clientOptions } = options;
6888
- const ir = toIR({
6889
- spec: await loadSpec(openapi),
6890
- responses: { flattenErrorResponses: true }
6891
- });
6892
- if ((ir.servers?.length ?? 0) === 0 && !clientOptions.baseUrl) {
6893
- throw new Error(
6894
- `createOpenAPIExtension("${name}"): openapi spec has no servers and no baseUrl was provided`
6895
- );
6896
- }
6897
- const planned = [];
6898
- const registered = /* @__PURE__ */ new Map();
6899
- forEachOperation(ir, (entry, operation) => {
6900
- if (includeOperation && !includeOperation(entry, operation)) return;
6901
- const subName = operation["x-fn-name"] || operation.operationId;
6902
- const endpoint = `${entry.method.toUpperCase()} ${entry.path}`;
6903
- if (!subName) {
6904
- throw new Error(
6905
- `createOpenAPIExtension("${name}"): operation ${endpoint} has no x-fn-name or operationId`
6906
- );
6907
- }
6908
- if (subName === RESERVED_SUBCOMMAND) {
6909
- throw new Error(
6910
- `createOpenAPIExtension("${name}"): operation name "${RESERVED_SUBCOMMAND}" is reserved for introspection`
6911
- );
6912
- }
6913
- const existing = registered.get(subName);
6914
- if (existing) {
6915
- throw new Error(
6916
- `createOpenAPIExtension("${name}"): duplicate subcommand "${subName}" (from ${existing} and ${endpoint})`
6917
- );
6918
- }
6919
- registered.set(subName, endpoint);
6920
- planned.push({
6921
- subName,
6922
- endpoint,
6923
- description: operation.summary || operation.description || endpoint,
6924
- summary: operation.summary,
6925
- pathParamNames: extractPathParams(entry.path)
6926
- });
6927
- });
6928
- if (planned.length === 0) {
6929
- throw new Error(
6930
- `createOpenAPIExtension("${name}"): no operations matched; spec has no operations or includeOperation filtered all of them out`
6931
- );
6932
- }
6933
- const client = await rpc(openapi, clientOptions);
6934
- const subcommands = {};
6935
- const schemaEntries = [];
6936
- for (const {
6937
- subName,
6938
- endpoint,
6939
- description,
6940
- summary,
6941
- pathParamNames
6942
- } of planned) {
6943
- const schema = client.schemas[endpoint]?.schema;
6944
- if (!schema) {
6945
- throw new Error(
6946
- `createOpenAPIExtension("${name}"): no input schema for ${endpoint}`
6947
- );
6948
- }
6949
- schemaEntries.push({ endpoint, operationId: subName, summary });
6950
- subcommands[subName] = {
6951
- usage: `${subName} '<json>'`,
6952
- description,
6953
- repair: repairQuotedArg,
6954
- handler: async (args, ctx) => {
6955
- const raw = args.join(" ").trim();
6956
- if (!raw) {
6957
- return formatError({
6958
- group: name,
6959
- operation: subName,
6960
- code: "missing_input",
6961
- message: "no input provided"
6962
- });
6963
- }
6964
- let input;
6965
- try {
6966
- input = JSON.parse(raw);
6967
- } catch (error) {
6968
- return formatError({
6969
- group: name,
6970
- operation: subName,
6971
- code: "invalid_json",
6972
- message: error instanceof Error ? error.message : String(error)
6973
- });
6974
- }
6975
- const parsed = schema.safeParse(input);
6976
- if (!parsed.success) {
6977
- return formatError({
6978
- group: name,
6979
- operation: subName,
6980
- code: "schema_validation",
6981
- message: parsed.error.message,
6982
- extra: { issues: parsed.error.issues }
6983
- });
6984
- }
6985
- const unsafe = findUnsafePathParam(parsed.data, pathParamNames);
6986
- if (unsafe) {
6987
- return formatError({
6988
- group: name,
6989
- operation: subName,
6990
- code: "path_param_unsafe",
6991
- message: `path parameter '${unsafe.field}' ${unsafe.reason}`,
6992
- extra: { field: unsafe.field }
6993
- });
6994
- }
6995
- try {
6996
- const response = await client.request(endpoint, parsed.data, {
6997
- signal: ctx.signal
6998
- });
6999
- return formatResponse(response);
7000
- } catch (error) {
7001
- return formatError({
7002
- group: name,
7003
- operation: subName,
7004
- code: "request_failed",
7005
- message: error instanceof Error ? error.message : String(error),
7006
- extra: { error: serializeError(error) }
7007
- });
7008
- }
7340
+ },
7341
+ async readFile(path5) {
7342
+ return bash.readFile(path5);
7343
+ },
7344
+ async writeFiles(files) {
7345
+ for (const file of files) {
7346
+ await bash.writeFile(
7347
+ file.path,
7348
+ typeof file.content === "string" ? file.content : Buffer.from(file.content).toString("utf-8")
7349
+ );
7009
7350
  }
7010
- };
7011
- }
7012
- subcommands[RESERVED_SUBCOMMAND] = buildSchemaSubcommand(
7013
- name,
7014
- client,
7015
- schemaEntries
7016
- );
7017
- const command = defineSubcommandGroup(name, subcommands);
7018
- const repair = buildSubcommandRepair(name, subcommands);
7019
- return {
7020
- commands: [command],
7021
- onBeforeBashCall: ({ command: raw }) => ({ command: repair(raw) }),
7022
- client
7351
+ }
7023
7352
  };
7024
7353
  }
7025
7354
 
@@ -8718,14 +9047,16 @@ async function persistedWriter(options) {
8718
9047
  };
8719
9048
  }
8720
9049
 
9050
+ // packages/context/src/lib/stream/polling-change-source.ts
9051
+ import { setTimeout as delay } from "node:timers/promises";
9052
+
8721
9053
  // packages/context/src/lib/stream/polling-policy.ts
8722
9054
  var DEFAULT_WATCH_POLLING = {
8723
9055
  minMs: 25,
8724
9056
  maxMs: 500,
8725
9057
  multiplier: 2,
8726
9058
  jitterRatio: 0.15,
8727
- statusCheckEvery: 3,
8728
- chunkPageSize: 128
9059
+ statusCheckEvery: 3
8729
9060
  };
8730
9061
  var DEFAULT_CANCEL_POLLING = {
8731
9062
  minMs: 50,
@@ -8741,8 +9072,7 @@ function normalizeWatchPolling(polling, fallback = DEFAULT_WATCH_POLLING) {
8741
9072
  const normalizedBase = normalizeAdaptivePolling(merged, fallback);
8742
9073
  return {
8743
9074
  ...normalizedBase,
8744
- statusCheckEvery: clampInt(merged.statusCheckEvery, 1, 1e4),
8745
- chunkPageSize: clampInt(merged.chunkPageSize, 1, 1e4)
9075
+ statusCheckEvery: clampInt(merged.statusCheckEvery, 1, 1e4)
8746
9076
  };
8747
9077
  }
8748
9078
  function normalizeCancelPolling(polling, fallback = DEFAULT_CANCEL_POLLING) {
@@ -8763,7 +9093,7 @@ function nextAdaptivePollingDelay(state) {
8763
9093
  state.config.minMs,
8764
9094
  state.config.maxMs
8765
9095
  );
8766
- const delay = applyJitter(
9096
+ const delay2 = applyJitter(
8767
9097
  current,
8768
9098
  state.config.jitterRatio,
8769
9099
  state.config.minMs,
@@ -8774,7 +9104,7 @@ function nextAdaptivePollingDelay(state) {
8774
9104
  state.config.minMs,
8775
9105
  state.config.maxMs
8776
9106
  );
8777
- return delay;
9107
+ return delay2;
8778
9108
  }
8779
9109
  function normalizeAdaptivePolling(polling, fallback) {
8780
9110
  const merged = {
@@ -8809,42 +9139,836 @@ function clampFloat(value, min, max) {
8809
9139
  return Math.min(max, Math.max(min, value));
8810
9140
  }
8811
9141
 
8812
- // packages/context/src/lib/stream/sqlite.stream-store.ts
8813
- import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
8814
-
8815
- // packages/context/src/lib/stream/ddl.stream.sqlite.sql
8816
- var ddl_stream_sqlite_default = "PRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS streams (\n id TEXT PRIMARY KEY,\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','running','completed','failed','cancelled')),\n createdAt INTEGER NOT NULL,\n startedAt INTEGER,\n finishedAt INTEGER,\n cancelRequestedAt INTEGER,\n error TEXT\n);\n\nCREATE TABLE IF NOT EXISTS stream_chunks (\n streamId TEXT NOT NULL,\n seq INTEGER NOT NULL,\n data TEXT NOT NULL,\n createdAt INTEGER NOT NULL,\n PRIMARY KEY (streamId, seq),\n FOREIGN KEY (streamId) REFERENCES streams(id) ON DELETE CASCADE\n);\n\n-- Supports ordered listing across all streams.\nCREATE INDEX IF NOT EXISTS idx_streams_created_at_id\n ON streams(createdAt, id);\n\n-- Supports status-filtered ordered listing (e.g. running streams).\nCREATE INDEX IF NOT EXISTS idx_streams_status_created_at_id\n ON streams(status, createdAt, id);\n";
8817
-
8818
- // packages/context/src/lib/stream/stream-store.ts
8819
- var StreamStore = class {
8820
- async listRunningStreamIds() {
8821
- return this.listStreamIds({ status: "running" });
8822
- }
8823
- };
8824
-
8825
- // packages/context/src/lib/stream/sqlite.stream-store.ts
8826
- var SqliteStreamStore = class extends StreamStore {
8827
- #db;
8828
- #statements = /* @__PURE__ */ new Map();
8829
- #closed = false;
8830
- #stmt(sql) {
8831
- let stmt = this.#statements.get(sql);
8832
- if (!stmt) {
8833
- stmt = this.#db.prepare(sql);
8834
- this.#statements.set(sql, stmt);
9142
+ // packages/context/src/lib/stream/polling-change-source.ts
9143
+ var PollingChangeSource = class {
9144
+ #reads;
9145
+ #config;
9146
+ #onPoll;
9147
+ constructor(options) {
9148
+ this.#reads = options.reads;
9149
+ this.#config = normalizeWatchPolling(options.config, DEFAULT_WATCH_POLLING);
9150
+ this.#onPoll = options.onPoll;
9151
+ }
9152
+ async *subscribe(streamId, signal) {
9153
+ const initialStatus = await this.#reads.getStreamStatus(streamId);
9154
+ if (initialStatus === void 0) {
9155
+ throw new Error(`Stream "${streamId}" not found`);
9156
+ }
9157
+ yield { kind: "tick" };
9158
+ if (isTerminal(initialStatus)) return;
9159
+ const delayState = createAdaptivePollingState(this.#config);
9160
+ let pollsSinceStatus = 0;
9161
+ let lastStatus = initialStatus;
9162
+ while (!signal.aborted) {
9163
+ const delayMs = nextAdaptivePollingDelay(delayState);
9164
+ this.#emit({ type: "idle", streamId, delayMs });
9165
+ if (!await waitForDelay(delayMs, signal)) return;
9166
+ pollsSinceStatus += 1;
9167
+ const shouldCheckStatus = pollsSinceStatus >= this.#config.statusCheckEvery;
9168
+ if (!shouldCheckStatus) {
9169
+ yield { kind: "chunks" };
9170
+ continue;
9171
+ }
9172
+ pollsSinceStatus = 0;
9173
+ const status = await this.#reads.getStreamStatus(streamId);
9174
+ this.#emit({
9175
+ type: "poll",
9176
+ streamId,
9177
+ delayMs,
9178
+ status: status ?? "missing"
9179
+ });
9180
+ if (status === void 0) {
9181
+ yield { kind: "status" };
9182
+ return;
9183
+ }
9184
+ if (status === "running" && lastStatus !== "running") {
9185
+ resetAdaptivePolling(delayState);
9186
+ }
9187
+ lastStatus = status;
9188
+ if (isTerminal(status)) {
9189
+ yield { kind: "status" };
9190
+ return;
9191
+ }
9192
+ yield { kind: "tick" };
8835
9193
  }
8836
- return stmt;
8837
9194
  }
8838
- close() {
8839
- if (this.#closed) return;
8840
- this.#closed = true;
8841
- this.#statements.clear();
8842
- this.#db.close();
9195
+ #emit(event) {
9196
+ if (!this.#onPoll) return;
9197
+ try {
9198
+ this.#onPoll(event);
9199
+ } catch {
9200
+ }
8843
9201
  }
8844
- constructor(pathOrDb) {
8845
- super();
8846
- this.#db = typeof pathOrDb === "string" ? new DatabaseSync2(pathOrDb) : pathOrDb;
8847
- this.#db.exec(ddl_stream_sqlite_default);
9202
+ };
9203
+ function isTerminal(status) {
9204
+ return status !== "queued" && status !== "running";
9205
+ }
9206
+ function waitForDelay(ms, signal) {
9207
+ return delay(ms, true, { signal }).catch((err) => {
9208
+ if (signal.aborted) return false;
9209
+ throw err;
9210
+ });
9211
+ }
9212
+
9213
+ // packages/context/src/lib/sandbox/ast-utils.ts
9214
+ import { serialize } from "just-bash";
9215
+ function asStaticWordText(word, options = {}) {
9216
+ if (!word) {
9217
+ return null;
9218
+ }
9219
+ return asStaticWordPartText(
9220
+ word.parts,
9221
+ options
9222
+ );
9223
+ }
9224
+ function asStaticWordPartText(parts, options = {}) {
9225
+ let text = "";
9226
+ for (const part of parts) {
9227
+ const type = part.type;
9228
+ if (type === "Literal" || type === "SingleQuoted" || type === "Escaped") {
9229
+ if (typeof part.value !== "string") {
9230
+ return null;
9231
+ }
9232
+ text += part.value;
9233
+ continue;
9234
+ }
9235
+ if (type === "DoubleQuoted") {
9236
+ if (!Array.isArray(part.parts)) {
9237
+ return null;
9238
+ }
9239
+ const inner = asStaticWordPartText(
9240
+ part.parts,
9241
+ options
9242
+ );
9243
+ if (inner == null) {
9244
+ return null;
9245
+ }
9246
+ text += inner;
9247
+ continue;
9248
+ }
9249
+ if (options.preserveLegacyBackticks && type === "CommandSubstitution" && part.legacy === true) {
9250
+ text += "`" + serialize(part.body).trim() + "`";
9251
+ continue;
9252
+ }
9253
+ return null;
9254
+ }
9255
+ return text;
9256
+ }
9257
+
9258
+ // packages/context/src/lib/stream/postgres-notify-change-source.ts
9259
+ import { createRequire as createRequire3 } from "node:module";
9260
+
9261
+ // packages/context/src/lib/stream/ddl.stream.postgres-notify.ts
9262
+ var DEFAULT_POSTGRES_STREAM_CHANGES_CHANNEL = "deepagents_stream_changes";
9263
+ function postgresStreamNotifyDDL(schema, channel = DEFAULT_POSTGRES_STREAM_CHANGES_CHANNEL) {
9264
+ return `
9265
+ CREATE SCHEMA IF NOT EXISTS "${schema}";
9266
+
9267
+ CREATE OR REPLACE FUNCTION "${schema}"."notify_stream_chunks_insert"()
9268
+ RETURNS TRIGGER AS $$
9269
+ DECLARE
9270
+ changed_stream_id TEXT;
9271
+ BEGIN
9272
+ FOR changed_stream_id IN
9273
+ SELECT DISTINCT stream_id FROM new_rows
9274
+ LOOP
9275
+ PERFORM pg_notify(
9276
+ '${channel}',
9277
+ json_build_object(
9278
+ 'schema', TG_TABLE_SCHEMA,
9279
+ 'streamId', changed_stream_id,
9280
+ 'kind', 'chunks'
9281
+ )::text
9282
+ );
9283
+ END LOOP;
9284
+ RETURN NULL;
9285
+ END;
9286
+ $$ LANGUAGE plpgsql;
9287
+
9288
+ CREATE OR REPLACE TRIGGER "stream_chunks_notify_insert"
9289
+ AFTER INSERT ON "${schema}"."stream_chunks"
9290
+ REFERENCING NEW TABLE AS new_rows
9291
+ FOR EACH STATEMENT
9292
+ EXECUTE FUNCTION "${schema}"."notify_stream_chunks_insert"();
9293
+
9294
+ CREATE OR REPLACE FUNCTION "${schema}"."notify_stream_status_update"()
9295
+ RETURNS TRIGGER AS $$
9296
+ BEGIN
9297
+ IF OLD.status IS DISTINCT FROM NEW.status THEN
9298
+ PERFORM pg_notify(
9299
+ '${channel}',
9300
+ json_build_object(
9301
+ 'schema', TG_TABLE_SCHEMA,
9302
+ 'streamId', NEW.id,
9303
+ 'kind', 'status'
9304
+ )::text
9305
+ );
9306
+ END IF;
9307
+ RETURN NEW;
9308
+ END;
9309
+ $$ LANGUAGE plpgsql;
9310
+
9311
+ CREATE OR REPLACE TRIGGER "streams_notify_status_update"
9312
+ AFTER UPDATE OF status ON "${schema}"."streams"
9313
+ FOR EACH ROW
9314
+ EXECUTE FUNCTION "${schema}"."notify_stream_status_update"();
9315
+ `;
9316
+ }
9317
+
9318
+ // packages/context/src/lib/stream/postgres-notify-change-source.ts
9319
+ var PostgresNotifyChangeSource = class _PostgresNotifyChangeSource {
9320
+ #pool;
9321
+ #schema;
9322
+ #channel;
9323
+ #ownsPool;
9324
+ #listener;
9325
+ #listenPromise;
9326
+ #isInitialized = false;
9327
+ #isClosing = false;
9328
+ #isClosed = false;
9329
+ #subscribers = /* @__PURE__ */ new Map();
9330
+ #onNotification = (message2) => {
9331
+ this.#handleNotification(message2);
9332
+ };
9333
+ #onListenerError = (error) => {
9334
+ this.#failListener(error);
9335
+ };
9336
+ #onListenerEnd = () => {
9337
+ if (!this.#isClosing) {
9338
+ this.#failListener(
9339
+ new Error("PostgreSQL stream notification listener ended")
9340
+ );
9341
+ }
9342
+ };
9343
+ constructor(options) {
9344
+ const schema = options.schema ?? "public";
9345
+ const channel = options.channel ?? DEFAULT_POSTGRES_STREAM_CHANGES_CHANNEL;
9346
+ assertIdentifier(schema, "schema");
9347
+ assertIdentifier(channel, "channel");
9348
+ this.#schema = schema;
9349
+ this.#channel = channel;
9350
+ const pg = _PostgresNotifyChangeSource.#requirePg();
9351
+ if (options.pool instanceof pg.Pool) {
9352
+ this.#pool = options.pool;
9353
+ this.#ownsPool = false;
9354
+ } else {
9355
+ this.#pool = typeof options.pool === "string" ? new pg.Pool({ connectionString: options.pool }) : new pg.Pool(options.pool);
9356
+ this.#ownsPool = true;
9357
+ }
9358
+ }
9359
+ static #requirePg() {
9360
+ try {
9361
+ const require2 = createRequire3(import.meta.url);
9362
+ return require2("pg");
9363
+ } catch {
9364
+ throw new Error(
9365
+ 'PostgresNotifyChangeSource requires the "pg" package. Install it with: npm install pg'
9366
+ );
9367
+ }
9368
+ }
9369
+ async initialize() {
9370
+ await this.#pool.query(
9371
+ postgresStreamNotifyDDL(this.#schema, this.#channel)
9372
+ );
9373
+ this.#isInitialized = true;
9374
+ }
9375
+ async *subscribe(streamId, signal) {
9376
+ this.#ensureInitialized();
9377
+ this.#ensureOpen();
9378
+ const queue = new ChangeQueue();
9379
+ this.#addSubscriber(streamId, queue);
9380
+ const cleanup = () => {
9381
+ queue.close();
9382
+ this.#removeSubscriber(streamId, queue);
9383
+ };
9384
+ signal.addEventListener("abort", cleanup, { once: true });
9385
+ try {
9386
+ await this.#ensureListening();
9387
+ if (signal.aborted || this.#isClosed) return;
9388
+ yield { kind: "tick" };
9389
+ while (!signal.aborted && !this.#isClosed) {
9390
+ const change = await queue.next(signal);
9391
+ if (!change) return;
9392
+ yield change;
9393
+ }
9394
+ } finally {
9395
+ signal.removeEventListener("abort", cleanup);
9396
+ cleanup();
9397
+ await this.#releaseListenerIfIdle();
9398
+ }
9399
+ }
9400
+ async close() {
9401
+ if (this.#isClosed) return;
9402
+ this.#isClosed = true;
9403
+ this.#isClosing = true;
9404
+ for (const subscribers of this.#subscribers.values()) {
9405
+ for (const subscriber of subscribers) {
9406
+ subscriber.close();
9407
+ }
9408
+ }
9409
+ this.#subscribers.clear();
9410
+ if (this.#listenPromise) {
9411
+ try {
9412
+ await this.#listenPromise;
9413
+ } catch {
9414
+ }
9415
+ }
9416
+ await this.#releaseListener();
9417
+ if (this.#ownsPool) {
9418
+ await this.#pool.end();
9419
+ }
9420
+ }
9421
+ #ensureInitialized() {
9422
+ if (!this.#isInitialized) {
9423
+ throw new Error(
9424
+ "PostgresNotifyChangeSource not initialized. Call await source.initialize() after construction."
9425
+ );
9426
+ }
9427
+ }
9428
+ #ensureOpen() {
9429
+ if (this.#isClosed) {
9430
+ throw new Error("PostgresNotifyChangeSource is closed.");
9431
+ }
9432
+ }
9433
+ async #ensureListening() {
9434
+ this.#ensureOpen();
9435
+ if (this.#listener) return;
9436
+ if (this.#listenPromise) return this.#listenPromise;
9437
+ this.#listenPromise = (async () => {
9438
+ const listener = await this.#pool.connect();
9439
+ let success = false;
9440
+ try {
9441
+ listener.on("notification", this.#onNotification);
9442
+ listener.on("error", this.#onListenerError);
9443
+ listener.on("end", this.#onListenerEnd);
9444
+ await listener.query(`LISTEN ${quoteIdentifier(this.#channel)}`);
9445
+ if (this.#isClosed) {
9446
+ return;
9447
+ }
9448
+ this.#listener = listener;
9449
+ success = true;
9450
+ } finally {
9451
+ this.#listenPromise = void 0;
9452
+ if (!success) {
9453
+ listener.off("notification", this.#onNotification);
9454
+ listener.off("error", this.#onListenerError);
9455
+ listener.off("end", this.#onListenerEnd);
9456
+ listener.release(true);
9457
+ }
9458
+ }
9459
+ })();
9460
+ return this.#listenPromise;
9461
+ }
9462
+ #handleNotification(message2) {
9463
+ if (message2.channel !== this.#channel || !message2.payload) return;
9464
+ const payload = parsePayload(message2.payload);
9465
+ if (!payload) return;
9466
+ if (payload.schema !== this.#schema) return;
9467
+ if (payload.kind !== "chunks" && payload.kind !== "status") return;
9468
+ const subscribers = this.#subscribers.get(payload.streamId);
9469
+ if (!subscribers) return;
9470
+ const change = payload.kind === "chunks" ? { kind: "chunks" } : { kind: "status" };
9471
+ for (const subscriber of subscribers) {
9472
+ subscriber.push(change);
9473
+ }
9474
+ }
9475
+ #failListener(error) {
9476
+ const listener = this.#listener;
9477
+ this.#listener = void 0;
9478
+ this.#listenPromise = void 0;
9479
+ if (listener) {
9480
+ listener.off("notification", this.#onNotification);
9481
+ listener.off("error", this.#onListenerError);
9482
+ listener.off("end", this.#onListenerEnd);
9483
+ listener.release(error);
9484
+ }
9485
+ for (const subscribers of this.#subscribers.values()) {
9486
+ for (const subscriber of subscribers) {
9487
+ subscriber.fail(error);
9488
+ }
9489
+ }
9490
+ this.#subscribers.clear();
9491
+ }
9492
+ async #releaseListenerIfIdle() {
9493
+ if (this.#subscribers.size > 0) return;
9494
+ await this.#releaseListener();
9495
+ }
9496
+ async #releaseListener() {
9497
+ const listener = this.#listener;
9498
+ if (!listener) return;
9499
+ this.#listener = void 0;
9500
+ listener.off("notification", this.#onNotification);
9501
+ listener.off("error", this.#onListenerError);
9502
+ listener.off("end", this.#onListenerEnd);
9503
+ try {
9504
+ await listener.query(`UNLISTEN ${quoteIdentifier(this.#channel)}`);
9505
+ } finally {
9506
+ listener.release();
9507
+ }
9508
+ }
9509
+ #addSubscriber(streamId, subscriber) {
9510
+ let subscribers = this.#subscribers.get(streamId);
9511
+ if (!subscribers) {
9512
+ subscribers = /* @__PURE__ */ new Set();
9513
+ this.#subscribers.set(streamId, subscribers);
9514
+ }
9515
+ subscribers.add(subscriber);
9516
+ }
9517
+ #removeSubscriber(streamId, subscriber) {
9518
+ const subscribers = this.#subscribers.get(streamId);
9519
+ if (!subscribers) return;
9520
+ subscribers.delete(subscriber);
9521
+ if (subscribers.size === 0) {
9522
+ this.#subscribers.delete(streamId);
9523
+ }
9524
+ }
9525
+ };
9526
+ var ChangeQueue = class {
9527
+ #values = [];
9528
+ #waiters = /* @__PURE__ */ new Set();
9529
+ #closed = false;
9530
+ #error;
9531
+ push(value) {
9532
+ if (this.#closed) return;
9533
+ const waiter = this.#waiters.values().next().value;
9534
+ if (waiter) {
9535
+ this.#waiters.delete(waiter);
9536
+ waiter.resolve(value);
9537
+ return;
9538
+ }
9539
+ this.#values.push(value);
9540
+ }
9541
+ fail(error) {
9542
+ if (this.#closed) return;
9543
+ this.#closed = true;
9544
+ this.#error = error;
9545
+ this.#values = [];
9546
+ for (const waiter of this.#waiters) {
9547
+ waiter.reject(error);
9548
+ }
9549
+ this.#waiters.clear();
9550
+ }
9551
+ close() {
9552
+ if (this.#closed) return;
9553
+ this.#closed = true;
9554
+ this.#values = [];
9555
+ for (const waiter of this.#waiters) {
9556
+ waiter.resolve(void 0);
9557
+ }
9558
+ this.#waiters.clear();
9559
+ }
9560
+ async next(signal) {
9561
+ if (this.#error) throw this.#error;
9562
+ if (this.#closed || signal.aborted) return void 0;
9563
+ if (this.#values.length > 0) {
9564
+ return this.#values.shift();
9565
+ }
9566
+ return new Promise((resolve4, reject) => {
9567
+ const waiter = { resolve: resolve4, reject };
9568
+ const cleanup = () => {
9569
+ this.#waiters.delete(waiter);
9570
+ signal.removeEventListener("abort", onAbort);
9571
+ };
9572
+ const onAbort = () => {
9573
+ cleanup();
9574
+ resolve4(void 0);
9575
+ };
9576
+ waiter.resolve = (value) => {
9577
+ cleanup();
9578
+ resolve4(value);
9579
+ };
9580
+ waiter.reject = (error) => {
9581
+ cleanup();
9582
+ reject(error);
9583
+ };
9584
+ this.#waiters.add(waiter);
9585
+ signal.addEventListener("abort", onAbort, { once: true });
9586
+ });
9587
+ }
9588
+ };
9589
+ function parsePayload(payload) {
9590
+ try {
9591
+ const parsed = JSON.parse(payload);
9592
+ if (!parsed || typeof parsed !== "object") return void 0;
9593
+ const candidate = parsed;
9594
+ if (typeof candidate.schema !== "string" || typeof candidate.streamId !== "string" || typeof candidate.kind !== "string") {
9595
+ return void 0;
9596
+ }
9597
+ return {
9598
+ schema: candidate.schema,
9599
+ streamId: candidate.streamId,
9600
+ kind: candidate.kind
9601
+ };
9602
+ } catch {
9603
+ return void 0;
9604
+ }
9605
+ }
9606
+ function quoteIdentifier(value) {
9607
+ return `"${value.replaceAll('"', '""')}"`;
9608
+ }
9609
+ function assertIdentifier(value, label) {
9610
+ if (!/^[a-zA-Z_]\w*$/.test(value)) {
9611
+ throw new Error(`Invalid ${label} name: "${value}"`);
9612
+ }
9613
+ }
9614
+
9615
+ // packages/context/src/lib/stream/postgres.stream-store.ts
9616
+ import { createRequire as createRequire4 } from "node:module";
9617
+
9618
+ // packages/context/src/lib/stream/ddl.stream.postgres.ts
9619
+ function postgresStreamDDL(schema) {
9620
+ return `
9621
+ CREATE SCHEMA IF NOT EXISTS "${schema}";
9622
+
9623
+ CREATE TABLE IF NOT EXISTS "${schema}"."streams" (
9624
+ id TEXT PRIMARY KEY,
9625
+ status TEXT NOT NULL DEFAULT 'queued'
9626
+ CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),
9627
+ created_at BIGINT NOT NULL,
9628
+ started_at BIGINT,
9629
+ finished_at BIGINT,
9630
+ cancel_requested_at BIGINT,
9631
+ error TEXT
9632
+ );
9633
+
9634
+ CREATE TABLE IF NOT EXISTS "${schema}"."stream_chunks" (
9635
+ stream_id TEXT NOT NULL,
9636
+ seq INTEGER NOT NULL,
9637
+ data JSONB NOT NULL,
9638
+ created_at BIGINT NOT NULL,
9639
+ PRIMARY KEY (stream_id, seq),
9640
+ FOREIGN KEY (stream_id) REFERENCES "${schema}"."streams"(id) ON DELETE CASCADE
9641
+ );
9642
+
9643
+ CREATE INDEX IF NOT EXISTS "idx_${schema}_streams_created_at_id"
9644
+ ON "${schema}"."streams"(created_at, id);
9645
+
9646
+ CREATE INDEX IF NOT EXISTS "idx_${schema}_streams_status_created_at_id"
9647
+ ON "${schema}"."streams"(status, created_at, id);
9648
+ `;
9649
+ }
9650
+
9651
+ // packages/context/src/lib/stream/stream-store.ts
9652
+ var StreamStore = class {
9653
+ async listRunningStreamIds() {
9654
+ return this.listStreamIds({ status: "running" });
9655
+ }
9656
+ };
9657
+
9658
+ // packages/context/src/lib/stream/postgres.stream-store.ts
9659
+ var PostgresStreamStore = class _PostgresStreamStore extends StreamStore {
9660
+ #pool;
9661
+ #schema;
9662
+ #ownsPool;
9663
+ #isInitialized = false;
9664
+ #isClosed = false;
9665
+ constructor(options) {
9666
+ super();
9667
+ const schema = options.schema ?? "public";
9668
+ assertIdentifier2(schema, "schema");
9669
+ this.#schema = schema;
9670
+ const pg = _PostgresStreamStore.#requirePg();
9671
+ if (options.pool instanceof pg.Pool) {
9672
+ this.#pool = options.pool;
9673
+ this.#ownsPool = false;
9674
+ } else {
9675
+ this.#pool = typeof options.pool === "string" ? new pg.Pool({ connectionString: options.pool }) : new pg.Pool(options.pool);
9676
+ this.#ownsPool = true;
9677
+ }
9678
+ }
9679
+ static #requirePg() {
9680
+ try {
9681
+ const require2 = createRequire4(import.meta.url);
9682
+ return require2("pg");
9683
+ } catch {
9684
+ throw new Error(
9685
+ 'PostgresStreamStore requires the "pg" package. Install it with: npm install pg'
9686
+ );
9687
+ }
9688
+ }
9689
+ #t(name) {
9690
+ return `"${this.#schema}"."${name}"`;
9691
+ }
9692
+ async initialize() {
9693
+ await this.#pool.query(postgresStreamDDL(this.#schema));
9694
+ this.#isInitialized = true;
9695
+ }
9696
+ async close() {
9697
+ if (this.#isClosed) return;
9698
+ this.#isClosed = true;
9699
+ if (this.#ownsPool) {
9700
+ await this.#pool.end();
9701
+ }
9702
+ }
9703
+ #ensureInitialized() {
9704
+ if (!this.#isInitialized) {
9705
+ throw new Error(
9706
+ "PostgresStreamStore not initialized. Call await store.initialize() after construction."
9707
+ );
9708
+ }
9709
+ }
9710
+ async #query(sql, params) {
9711
+ this.#ensureInitialized();
9712
+ const result = await this.#pool.query(sql, params);
9713
+ return result.rows;
9714
+ }
9715
+ async #useTransaction(fn) {
9716
+ this.#ensureInitialized();
9717
+ const client = await this.#pool.connect();
9718
+ try {
9719
+ await client.query("BEGIN");
9720
+ const result = await fn(client);
9721
+ await client.query("COMMIT");
9722
+ return result;
9723
+ } catch (error) {
9724
+ await client.query("ROLLBACK");
9725
+ throw error;
9726
+ } finally {
9727
+ client.release();
9728
+ }
9729
+ }
9730
+ async createStream(stream) {
9731
+ await this.#query(
9732
+ `INSERT INTO ${this.#t("streams")}
9733
+ (id, status, created_at, started_at, finished_at, cancel_requested_at, error)
9734
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
9735
+ streamParams(stream)
9736
+ );
9737
+ }
9738
+ async upsertStream(stream) {
9739
+ const rows = await this.#query(
9740
+ `INSERT INTO ${this.#t("streams")}
9741
+ (id, status, created_at, started_at, finished_at, cancel_requested_at, error)
9742
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
9743
+ ON CONFLICT(id) DO NOTHING
9744
+ RETURNING *`,
9745
+ streamParams(stream)
9746
+ );
9747
+ if (rows[0]) {
9748
+ return { stream: rowToStream(rows[0]), created: true };
9749
+ }
9750
+ const existing = await this.getStream(stream.id);
9751
+ if (!existing) {
9752
+ throw new Error(
9753
+ `Stream "${stream.id}" disappeared between upsert and fetch`
9754
+ );
9755
+ }
9756
+ return { stream: existing, created: false };
9757
+ }
9758
+ async getStream(streamId) {
9759
+ const rows = await this.#query(
9760
+ `SELECT * FROM ${this.#t("streams")} WHERE id = $1`,
9761
+ [streamId]
9762
+ );
9763
+ return rows[0] ? rowToStream(rows[0]) : void 0;
9764
+ }
9765
+ async getStreamStatus(streamId) {
9766
+ const rows = await this.#query(
9767
+ `SELECT status FROM ${this.#t("streams")} WHERE id = $1`,
9768
+ [streamId]
9769
+ );
9770
+ return rows[0]?.status;
9771
+ }
9772
+ async listStreamIds(options) {
9773
+ const params = [];
9774
+ let sql = `SELECT id FROM ${this.#t("streams")}`;
9775
+ if (options?.status) {
9776
+ params.push(options.status);
9777
+ sql += ` WHERE status = $${params.length}`;
9778
+ }
9779
+ sql += " ORDER BY created_at ASC, id ASC";
9780
+ const rows = await this.#query(sql, params);
9781
+ return rows.map((row) => row.id);
9782
+ }
9783
+ async updateStreamStatus(streamId, status, options) {
9784
+ const now = Date.now();
9785
+ switch (status) {
9786
+ case "running":
9787
+ await this.#query(
9788
+ `UPDATE ${this.#t("streams")}
9789
+ SET status = $1, started_at = $2
9790
+ WHERE id = $3`,
9791
+ [status, now, streamId]
9792
+ );
9793
+ break;
9794
+ case "completed":
9795
+ await this.#query(
9796
+ `UPDATE ${this.#t("streams")}
9797
+ SET status = $1, finished_at = $2
9798
+ WHERE id = $3`,
9799
+ [status, now, streamId]
9800
+ );
9801
+ break;
9802
+ case "failed":
9803
+ await this.#query(
9804
+ `UPDATE ${this.#t("streams")}
9805
+ SET status = $1, finished_at = $2, error = $3
9806
+ WHERE id = $4`,
9807
+ [status, now, options?.error ?? null, streamId]
9808
+ );
9809
+ break;
9810
+ case "cancelled":
9811
+ await this.#query(
9812
+ `UPDATE ${this.#t("streams")}
9813
+ SET status = $1, cancel_requested_at = $2, finished_at = $3
9814
+ WHERE id = $4`,
9815
+ [status, now, now, streamId]
9816
+ );
9817
+ break;
9818
+ default:
9819
+ await this.#query(
9820
+ `UPDATE ${this.#t("streams")}
9821
+ SET status = $1
9822
+ WHERE id = $2`,
9823
+ [status, streamId]
9824
+ );
9825
+ }
9826
+ }
9827
+ async appendChunks(chunks) {
9828
+ if (chunks.length === 0) return;
9829
+ const rows = chunks.map((chunk) => ({
9830
+ stream_id: chunk.streamId,
9831
+ seq: chunk.seq,
9832
+ data: chunk.data,
9833
+ created_at: chunk.createdAt
9834
+ }));
9835
+ await this.#query(
9836
+ `INSERT INTO ${this.#t("stream_chunks")} (stream_id, seq, data, created_at)
9837
+ SELECT stream_id, seq, data, created_at
9838
+ FROM jsonb_to_recordset($1::jsonb)
9839
+ AS rows(stream_id TEXT, seq INTEGER, data JSONB, created_at BIGINT)`,
9840
+ [JSON.stringify(rows)]
9841
+ );
9842
+ }
9843
+ async getChunks(streamId, fromSeq, limit) {
9844
+ const params = [streamId];
9845
+ let sql = `SELECT * FROM ${this.#t("stream_chunks")} WHERE stream_id = $1`;
9846
+ if (fromSeq !== void 0) {
9847
+ params.push(fromSeq);
9848
+ sql += ` AND seq >= $${params.length}`;
9849
+ }
9850
+ sql += " ORDER BY seq ASC";
9851
+ if (limit !== void 0) {
9852
+ params.push(limit);
9853
+ sql += ` LIMIT $${params.length}`;
9854
+ }
9855
+ const rows = await this.#query(sql, params);
9856
+ return rows.map((row) => ({
9857
+ streamId: row.stream_id,
9858
+ seq: row.seq,
9859
+ data: row.data,
9860
+ createdAt: toNumber(row.created_at)
9861
+ }));
9862
+ }
9863
+ async deleteStream(streamId) {
9864
+ await this.#query(`DELETE FROM ${this.#t("streams")} WHERE id = $1`, [
9865
+ streamId
9866
+ ]);
9867
+ }
9868
+ async reopenStream(streamId) {
9869
+ return this.#useTransaction(async (client) => {
9870
+ const result = await client.query(
9871
+ `SELECT * FROM ${this.#t("streams")} WHERE id = $1 FOR UPDATE`,
9872
+ [streamId]
9873
+ );
9874
+ const row = result.rows[0];
9875
+ if (!row) {
9876
+ throw new Error(`Stream "${streamId}" not found`);
9877
+ }
9878
+ if (!isTerminal2(row.status)) {
9879
+ throw new Error(
9880
+ `Cannot reopen stream "${streamId}" with status "${row.status}". Only terminal streams can be reopened.`
9881
+ );
9882
+ }
9883
+ await client.query(`DELETE FROM ${this.#t("streams")} WHERE id = $1`, [
9884
+ streamId
9885
+ ]);
9886
+ const now = Date.now();
9887
+ const stream = {
9888
+ id: streamId,
9889
+ status: "queued",
9890
+ createdAt: now,
9891
+ startedAt: null,
9892
+ finishedAt: null,
9893
+ cancelRequestedAt: null,
9894
+ error: null
9895
+ };
9896
+ await client.query(
9897
+ `INSERT INTO ${this.#t("streams")}
9898
+ (id, status, created_at, started_at, finished_at, cancel_requested_at, error)
9899
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
9900
+ streamParams(stream)
9901
+ );
9902
+ return stream;
9903
+ });
9904
+ }
9905
+ };
9906
+ function streamParams(stream) {
9907
+ return [
9908
+ stream.id,
9909
+ stream.status,
9910
+ stream.createdAt,
9911
+ stream.startedAt,
9912
+ stream.finishedAt,
9913
+ stream.cancelRequestedAt,
9914
+ stream.error
9915
+ ];
9916
+ }
9917
+ function rowToStream(row) {
9918
+ return {
9919
+ id: row.id,
9920
+ status: row.status,
9921
+ createdAt: toNumber(row.created_at),
9922
+ startedAt: toNullableNumber(row.started_at),
9923
+ finishedAt: toNullableNumber(row.finished_at),
9924
+ cancelRequestedAt: toNullableNumber(row.cancel_requested_at),
9925
+ error: row.error
9926
+ };
9927
+ }
9928
+ function toNumber(value) {
9929
+ return typeof value === "number" ? value : Number(value);
9930
+ }
9931
+ function toNullableNumber(value) {
9932
+ return value == null ? null : toNumber(value);
9933
+ }
9934
+ function isTerminal2(status) {
9935
+ return status !== "queued" && status !== "running";
9936
+ }
9937
+ function assertIdentifier2(value, label) {
9938
+ if (!/^[a-zA-Z_]\w*$/.test(value)) {
9939
+ throw new Error(`Invalid ${label} name: "${value}"`);
9940
+ }
9941
+ }
9942
+
9943
+ // packages/context/src/lib/stream/sqlite.stream-store.ts
9944
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
9945
+
9946
+ // packages/context/src/lib/stream/ddl.stream.sqlite.sql
9947
+ var ddl_stream_sqlite_default = "PRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS streams (\n id TEXT PRIMARY KEY,\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','running','completed','failed','cancelled')),\n createdAt INTEGER NOT NULL,\n startedAt INTEGER,\n finishedAt INTEGER,\n cancelRequestedAt INTEGER,\n error TEXT\n);\n\nCREATE TABLE IF NOT EXISTS stream_chunks (\n streamId TEXT NOT NULL,\n seq INTEGER NOT NULL,\n data TEXT NOT NULL,\n createdAt INTEGER NOT NULL,\n PRIMARY KEY (streamId, seq),\n FOREIGN KEY (streamId) REFERENCES streams(id) ON DELETE CASCADE\n);\n\n-- Supports ordered listing across all streams.\nCREATE INDEX IF NOT EXISTS idx_streams_created_at_id\n ON streams(createdAt, id);\n\n-- Supports status-filtered ordered listing (e.g. running streams).\nCREATE INDEX IF NOT EXISTS idx_streams_status_created_at_id\n ON streams(status, createdAt, id);\n";
9948
+
9949
+ // packages/context/src/lib/stream/sqlite.stream-store.ts
9950
+ var SqliteStreamStore = class extends StreamStore {
9951
+ #db;
9952
+ #statements = /* @__PURE__ */ new Map();
9953
+ #closed = false;
9954
+ #stmt(sql) {
9955
+ let stmt = this.#statements.get(sql);
9956
+ if (!stmt) {
9957
+ stmt = this.#db.prepare(sql);
9958
+ this.#statements.set(sql, stmt);
9959
+ }
9960
+ return stmt;
9961
+ }
9962
+ close() {
9963
+ if (this.#closed) return;
9964
+ this.#closed = true;
9965
+ this.#statements.clear();
9966
+ this.#db.close();
9967
+ }
9968
+ constructor(pathOrDb) {
9969
+ super();
9970
+ this.#db = typeof pathOrDb === "string" ? new DatabaseSync2(pathOrDb) : pathOrDb;
9971
+ this.#db.exec(ddl_stream_sqlite_default);
8848
9972
  }
8849
9973
  async createStream(stream) {
8850
9974
  this.#stmt(
@@ -9048,26 +10172,20 @@ var SqliteStreamStore = class extends StreamStore {
9048
10172
 
9049
10173
  // packages/context/src/lib/stream/stream-manager.ts
9050
10174
  import { createUIMessageStream as createUIMessageStream3 } from "ai";
9051
- import { setTimeout } from "node:timers/promises";
9052
- function isTerminal(status) {
10175
+ function isTerminal3(status) {
9053
10176
  return status !== "queued" && status !== "running";
9054
10177
  }
10178
+ var DEFAULT_CHUNK_PAGE_SIZE = 128;
9055
10179
  var StreamManager = class {
9056
10180
  #store;
9057
- #watchPollingDefaults;
9058
- #cancelPollingDefaults;
9059
- #onPollingEvent;
10181
+ #changeSource;
10182
+ #chunkPageSize;
10183
+ #onWatchEvent;
9060
10184
  constructor(options) {
9061
10185
  this.#store = options.store;
9062
- this.#watchPollingDefaults = normalizeWatchPolling(
9063
- options.watchPolling,
9064
- DEFAULT_WATCH_POLLING
9065
- );
9066
- this.#cancelPollingDefaults = normalizeCancelPolling(
9067
- options.cancelPolling,
9068
- DEFAULT_CANCEL_POLLING
9069
- );
9070
- this.#onPollingEvent = options.onPollingEvent;
10186
+ this.#changeSource = options.changeSource;
10187
+ this.#chunkPageSize = options.chunkPageSize ?? DEFAULT_CHUNK_PAGE_SIZE;
10188
+ this.#onWatchEvent = options.onWatchEvent;
9071
10189
  }
9072
10190
  get store() {
9073
10191
  return this.#store;
@@ -9091,51 +10209,12 @@ var StreamManager = class {
9091
10209
  }
9092
10210
  async persist(stream, streamId, options) {
9093
10211
  const existing = await this.#store.getStream(streamId);
9094
- if (existing && isTerminal(existing.status)) {
10212
+ if (existing && isTerminal3(existing.status)) {
9095
10213
  return { streamId };
9096
10214
  }
9097
10215
  await this.#store.updateStreamStatus(streamId, "running");
9098
10216
  const ac = new AbortController();
9099
- const cancelPolling = normalizeCancelPolling(
9100
- options?.cancelPolling,
9101
- this.#cancelPollingDefaults
9102
- );
9103
- const pollState = createAdaptivePollingState(cancelPolling);
9104
- const pollCancel = (async () => {
9105
- while (!ac.signal.aborted) {
9106
- const delayMs = nextAdaptivePollingDelay(pollState);
9107
- const continued = await waitForDelay(delayMs, ac.signal);
9108
- if (!continued || ac.signal.aborted) break;
9109
- const status = await this.#store.getStreamStatus(streamId);
9110
- this.#emitPolling({
9111
- type: "persist:cancel-poll",
9112
- streamId,
9113
- delayMs,
9114
- status: status ?? "missing"
9115
- });
9116
- if (status === void 0) {
9117
- ac.abort();
9118
- break;
9119
- }
9120
- if (status === "cancelled") {
9121
- const current = await this.#store.getStream(streamId);
9122
- const latencyMs = current?.cancelRequestedAt != null ? Math.max(0, Date.now() - current.cancelRequestedAt) : null;
9123
- this.#emitPolling({
9124
- type: "persist:cancel-detected",
9125
- streamId,
9126
- latencyMs
9127
- });
9128
- if (options?.onCancelDetected) {
9129
- try {
9130
- await options.onCancelDetected({ streamId, latencyMs });
9131
- } catch {
9132
- }
9133
- }
9134
- ac.abort();
9135
- break;
9136
- }
9137
- }
9138
- })();
10217
+ const cancelWatcher = this.#runCancelWatcher(streamId, ac, options);
9139
10218
  let pw;
9140
10219
  const sink = createUIMessageStream3({
9141
10220
  execute: async ({ writer }) => {
@@ -9176,142 +10255,131 @@ var StreamManager = class {
9176
10255
  }
9177
10256
  } finally {
9178
10257
  if (!ac.signal.aborted) ac.abort();
9179
- await pollCancel;
10258
+ await cancelWatcher;
9180
10259
  }
9181
10260
  return { streamId: pw?.streamId ?? streamId };
9182
10261
  }
9183
- watch(streamId, options) {
10262
+ async #runCancelWatcher(streamId, ac, options) {
10263
+ try {
10264
+ for await (const change of this.#changeSource.subscribe(
10265
+ streamId,
10266
+ ac.signal
10267
+ )) {
10268
+ if (change.kind === "chunks") continue;
10269
+ const status = await this.#store.getStreamStatus(streamId);
10270
+ if (status === void 0) {
10271
+ ac.abort();
10272
+ return;
10273
+ }
10274
+ if (status === "cancelled") {
10275
+ const current = await this.#store.getStream(streamId);
10276
+ const latencyMs = current?.cancelRequestedAt != null ? Math.max(0, Date.now() - current.cancelRequestedAt) : null;
10277
+ this.#emit({
10278
+ type: "persist:cancel-detected",
10279
+ streamId,
10280
+ latencyMs
10281
+ });
10282
+ if (options?.onCancelDetected) {
10283
+ try {
10284
+ await options.onCancelDetected({ streamId, latencyMs });
10285
+ } catch {
10286
+ }
10287
+ }
10288
+ ac.abort();
10289
+ return;
10290
+ }
10291
+ }
10292
+ } catch {
10293
+ }
10294
+ }
10295
+ watch(streamId) {
9184
10296
  const store2 = this.#store;
9185
- const polling = normalizeWatchPolling(options, this.#watchPollingDefaults);
9186
- const delayState = createAdaptivePollingState(polling);
10297
+ const changeSource = this.#changeSource;
10298
+ const pageSize = this.#chunkPageSize;
10299
+ const emit = this.#emit.bind(this);
9187
10300
  const ac = new AbortController();
9188
10301
  const lastSeqRef = { value: -1 };
9189
- let chunkPollsSinceStatus = 0;
9190
- const emitChunks = (controller, chunks) => {
9191
- for (const chunk of chunks) {
9192
- controller.enqueue(chunk.data);
9193
- lastSeqRef.value = chunk.seq;
9194
- }
9195
- return chunks.length;
10302
+ let iterator;
10303
+ const emitPage = (delivered, lastSeq) => {
10304
+ emit({ type: "watch:chunks", streamId, delivered, lastSeq });
9196
10305
  };
9197
- return new ReadableStream({
9198
- async start() {
10306
+ const finalize = async (controller, reason, knownStatus) => {
10307
+ await drainAvailable(
10308
+ controller,
10309
+ store2,
10310
+ streamId,
10311
+ lastSeqRef,
10312
+ pageSize,
10313
+ emitPage
10314
+ );
10315
+ const finalStatus = knownStatus ?? await store2.getStreamStatus(streamId);
10316
+ if (finalStatus === "failed") {
9199
10317
  const stream = await store2.getStream(streamId);
9200
- if (!stream) {
9201
- throw new Error(`Stream "${streamId}" not found`);
10318
+ if (stream) {
10319
+ const errorText = stream.error || "Stream failed";
10320
+ emit({
10321
+ type: "watch:error-emitted",
10322
+ streamId,
10323
+ errorTextLength: errorText.length
10324
+ });
10325
+ controller.enqueue({ type: "error", errorText });
9202
10326
  }
10327
+ }
10328
+ emit({ type: "watch:closed", streamId, reason });
10329
+ controller.close();
10330
+ ac.abort();
10331
+ };
10332
+ return new ReadableStream({
10333
+ start: () => {
10334
+ iterator = changeSource.subscribe(streamId, ac.signal)[Symbol.asyncIterator]();
9203
10335
  },
9204
10336
  pull: async (controller) => {
10337
+ if (!iterator) return;
9205
10338
  while (!ac.signal.aborted) {
9206
- const fromSeq = lastSeqRef.value + 1;
9207
- const chunks = await store2.getChunks(
9208
- streamId,
9209
- fromSeq,
9210
- polling.chunkPageSize
9211
- );
9212
- let statusChecked = false;
9213
- let currentStatus;
9214
- if (chunks.length === 0) {
9215
- chunkPollsSinceStatus = polling.statusCheckEvery;
9216
- } else {
9217
- chunkPollsSinceStatus += 1;
10339
+ let result;
10340
+ try {
10341
+ result = await iterator.next();
10342
+ } catch (error) {
10343
+ iterator.return?.().catch(() => void 0);
10344
+ if (isAbortError(error)) return;
10345
+ throw error;
9218
10346
  }
9219
- if (chunkPollsSinceStatus >= polling.statusCheckEvery) {
9220
- statusChecked = true;
9221
- chunkPollsSinceStatus = 0;
9222
- currentStatus = await store2.getStreamStatus(streamId);
10347
+ if (result.done) {
10348
+ await finalize(controller, "source-ended");
10349
+ return;
9223
10350
  }
9224
- this.#emitPolling({
9225
- type: "watch:poll",
9226
- streamId,
9227
- fromSeq,
9228
- chunkCount: chunks.length,
9229
- statusChecked
9230
- });
9231
- if (chunks.length > 0) {
9232
- const delivered = emitChunks(controller, chunks);
9233
- this.#emitPolling({
9234
- type: "watch:chunks",
10351
+ const change = result.value;
10352
+ let delivered = 0;
10353
+ if (change.kind !== "status") {
10354
+ delivered = await drainAvailable(
10355
+ controller,
10356
+ store2,
9235
10357
  streamId,
9236
- delivered,
9237
- lastSeq: lastSeqRef.value
9238
- });
9239
- resetAdaptivePolling(delayState);
9240
- if (chunks.length >= polling.chunkPageSize) {
9241
- continue;
9242
- }
9243
- return;
10358
+ lastSeqRef,
10359
+ pageSize,
10360
+ emitPage
10361
+ );
9244
10362
  }
9245
- if (statusChecked) {
9246
- if (currentStatus === void 0) {
9247
- this.#emitPolling({
9248
- type: "watch:closed",
9249
- streamId,
9250
- reason: "missing"
9251
- });
9252
- controller.close();
9253
- ac.abort();
10363
+ if (change.kind !== "chunks") {
10364
+ const status = await store2.getStreamStatus(streamId);
10365
+ if (status === void 0) {
10366
+ await finalize(controller, "missing");
9254
10367
  return;
9255
10368
  }
9256
- if (isTerminal(currentStatus)) {
9257
- const drained = await drainRemainingChunks({
9258
- controller,
9259
- store: store2,
9260
- streamId,
9261
- fromSeq: lastSeqRef.value + 1,
9262
- chunkPageSize: polling.chunkPageSize,
9263
- onChunk: (seq) => {
9264
- lastSeqRef.value = seq;
9265
- }
9266
- });
9267
- if (drained > 0) {
9268
- this.#emitPolling({
9269
- type: "watch:chunks",
9270
- streamId,
9271
- delivered: drained,
9272
- lastSeq: lastSeqRef.value
9273
- });
9274
- }
9275
- if (currentStatus === "failed") {
9276
- const stream = await store2.getStream(streamId);
9277
- if (stream) {
9278
- const errorText = stream.error || "Stream failed";
9279
- this.#emitPolling({
9280
- type: "watch:error-emitted",
9281
- streamId,
9282
- errorTextLength: errorText.length
9283
- });
9284
- controller.enqueue({
9285
- type: "error",
9286
- errorText
9287
- });
9288
- }
9289
- }
9290
- this.#emitPolling({
9291
- type: "watch:closed",
9292
- streamId,
9293
- reason: "terminal"
9294
- });
9295
- controller.close();
9296
- ac.abort();
10369
+ if (isTerminal3(status)) {
10370
+ await finalize(controller, "terminal", status);
9297
10371
  return;
9298
10372
  }
9299
10373
  }
9300
- const delayMs = nextAdaptivePollingDelay(delayState);
9301
- this.#emitPolling({
9302
- type: "watch:empty",
9303
- streamId,
9304
- fromSeq: lastSeqRef.value + 1,
9305
- delayMs
9306
- });
9307
- const continued = await waitForDelay(delayMs, ac.signal);
9308
- if (!continued) {
9309
- return;
9310
- }
10374
+ if (delivered > 0) return;
9311
10375
  }
9312
10376
  },
9313
- cancel() {
10377
+ cancel: () => {
9314
10378
  ac.abort();
10379
+ return iterator?.return?.().then(
10380
+ () => void 0,
10381
+ () => void 0
10382
+ );
9315
10383
  }
9316
10384
  });
9317
10385
  }
@@ -9322,41 +10390,32 @@ var StreamManager = class {
9322
10390
  async cleanup(streamId) {
9323
10391
  await this.#store.deleteStream(streamId);
9324
10392
  }
9325
- #emitPolling(event) {
9326
- if (!this.#onPollingEvent) return;
10393
+ #emit(event) {
10394
+ if (!this.#onWatchEvent) return;
9327
10395
  try {
9328
- this.#onPollingEvent(event);
10396
+ this.#onWatchEvent(event);
9329
10397
  } catch {
9330
10398
  }
9331
10399
  }
9332
10400
  };
9333
- async function drainRemainingChunks(options) {
9334
- const { controller, store: store2, streamId, chunkPageSize, onChunk } = options;
9335
- let fromSeq = options.fromSeq;
9336
- let drained = 0;
10401
+ async function drainAvailable(controller, store2, streamId, lastSeqRef, pageSize, onPage) {
10402
+ let total = 0;
9337
10403
  while (true) {
9338
- const chunks = await store2.getChunks(streamId, fromSeq, chunkPageSize);
10404
+ const chunks = await store2.getChunks(
10405
+ streamId,
10406
+ lastSeqRef.value + 1,
10407
+ pageSize
10408
+ );
9339
10409
  if (chunks.length === 0) break;
9340
10410
  for (const chunk of chunks) {
9341
10411
  controller.enqueue(chunk.data);
9342
- onChunk(chunk.seq);
9343
- drained++;
9344
- fromSeq = chunk.seq + 1;
9345
- }
9346
- if (chunks.length < chunkPageSize) {
9347
- break;
10412
+ lastSeqRef.value = chunk.seq;
9348
10413
  }
10414
+ total += chunks.length;
10415
+ onPage?.(chunks.length, lastSeqRef.value);
10416
+ if (chunks.length < pageSize) break;
9349
10417
  }
9350
- return drained;
9351
- }
9352
- async function waitForDelay(ms, signal) {
9353
- try {
9354
- await setTimeout(ms, void 0, signal ? { signal } : void 0);
9355
- return true;
9356
- } catch (error) {
9357
- if (isAbortError(error)) return false;
9358
- throw error;
9359
- }
10418
+ return total;
9360
10419
  }
9361
10420
  function isAbortError(error) {
9362
10421
  return error instanceof Error && (error.name === "AbortError" || /aborted/i.test(error.message));
@@ -9437,9 +10496,9 @@ export {
9437
10496
  AgentOsCreationError,
9438
10497
  AgentOsNotAvailableError,
9439
10498
  AgentOsSandboxError,
10499
+ AsyncResolver,
9440
10500
  BM25Classifier,
9441
10501
  BashException,
9442
- BinaryInstallError,
9443
10502
  ComposeStartError,
9444
10503
  ComposeStrategy,
9445
10504
  ContainerCreationError,
@@ -9453,15 +10512,29 @@ export {
9453
10512
  DockerSandboxStrategy,
9454
10513
  DockerfileBuildError,
9455
10514
  DockerfileStrategy,
9456
- DuplicateCommandError,
10515
+ FragmentLoaderResolver,
10516
+ FunctionResolver,
10517
+ GeneratorResolver,
10518
+ GithubReleaseInstaller,
9457
10519
  InMemoryContextStore,
10520
+ InstallError,
10521
+ Installer,
10522
+ IterableResolver,
9458
10523
  LOCALE_METADATA_KEY,
9459
10524
  MarkdownRenderer,
10525
+ MissingRuntimeError,
9460
10526
  ModelsRegistry,
9461
10527
  MountPathError,
10528
+ NpmInstaller,
9462
10529
  ObservedFs,
9463
10530
  PackageInstallError,
10531
+ PackageInstaller,
10532
+ PipInstaller,
10533
+ PollingChangeSource,
9464
10534
  PostgresContextStore,
10535
+ PostgresNotifyChangeSource,
10536
+ PostgresStreamStore,
10537
+ PromiseResolver,
9465
10538
  RuntimeStrategy,
9466
10539
  SqlServerContextStore,
9467
10540
  SqliteContextStore,
@@ -9471,6 +10544,7 @@ export {
9471
10544
  TitleGenerator,
9472
10545
  TomlRenderer,
9473
10546
  ToonRenderer,
10547
+ UrlBinaryInstaller,
9474
10548
  XmlRenderer,
9475
10549
  addUsage,
9476
10550
  advisorPreamble,
@@ -9485,14 +10559,13 @@ export {
9485
10559
  applyInlineReminder,
9486
10560
  applyPartReminder,
9487
10561
  applyReminderToMessage,
10562
+ applyToolOutputRemindersToMessage,
9488
10563
  asStaticWordPartText,
9489
10564
  asStaticWordText,
9490
10565
  assertCountSpec,
9491
10566
  assistant,
9492
10567
  assistantText,
9493
- buildSchemaSubcommand,
9494
10568
  buildSubcommandRepair,
9495
- chainHooks,
9496
10569
  chat,
9497
10570
  checkCount,
9498
10571
  clarification,
@@ -9507,15 +10580,16 @@ export {
9507
10580
  createBinaryBridges,
9508
10581
  createContainerTool,
9509
10582
  createDockerSandbox,
9510
- createOpenAPIExtension,
10583
+ createInstallerContext,
9511
10584
  createRepairToolCall,
9512
- createRoutingSandbox,
9513
10585
  createVirtualSandbox,
9514
10586
  dateReminder,
9515
10587
  dayChanged,
9516
10588
  defaultChatMessageMetadata,
10589
+ defaultResolvers,
9517
10590
  defaultTokenizer,
9518
10591
  defineSubcommandGroup,
10592
+ discoverSkillMappings,
9519
10593
  discoverSkillsInDirectory,
9520
10594
  elapsedExceeds,
9521
10595
  encodeSerializedValue,
@@ -9526,12 +10600,9 @@ export {
9526
10600
  example,
9527
10601
  executorContext,
9528
10602
  explain,
9529
- extractPathParams,
9530
10603
  fail,
9531
- findUnsafePathParam,
10604
+ findSingleOutputAvailableToolPart,
9532
10605
  firstN,
9533
- formatError,
9534
- formatResponse,
9535
10606
  fragment,
9536
10607
  fromFragment,
9537
10608
  getFragmentData,
@@ -9539,6 +10610,7 @@ export {
9539
10610
  getModelsRegistry,
9540
10611
  getReminderRanges,
9541
10612
  getSeason,
10613
+ githubRelease,
9542
10614
  glossary,
9543
10615
  guardrail,
9544
10616
  hint,
@@ -9546,17 +10618,16 @@ export {
9546
10618
  identity,
9547
10619
  isComposeOptions,
9548
10620
  isConditionalReminder,
10621
+ isDebianBased,
9549
10622
  isDockerfileOptions,
9550
10623
  isFragment,
9551
10624
  isFragmentObject,
9552
- isInstallable,
9553
10625
  isMessageFragment,
9554
10626
  isRecord,
9555
10627
  lastAssistantLength,
9556
10628
  loadSkillMetadata,
9557
10629
  localeReminder,
9558
10630
  mapGenerateErrorToCode,
9559
- mergeExtensions,
9560
10631
  mergeMessageMetadata,
9561
10632
  mergeReminderMetadata,
9562
10633
  message,
@@ -9566,6 +10637,7 @@ export {
9566
10637
  normalizeCancelPolling,
9567
10638
  normalizeWatchPolling,
9568
10639
  not,
10640
+ npm,
9569
10641
  nullUsage,
9570
10642
  once,
9571
10643
  or,
@@ -9573,6 +10645,8 @@ export {
9573
10645
  pass,
9574
10646
  persistedWriter,
9575
10647
  persona,
10648
+ pip,
10649
+ pkg,
9576
10650
  policy,
9577
10651
  preference,
9578
10652
  principle,
@@ -9593,6 +10667,7 @@ export {
9593
10667
  seasonChanged,
9594
10668
  seasonReminder,
9595
10669
  selfCritique,
10670
+ shellQuote,
9596
10671
  skills,
9597
10672
  skillsReminder,
9598
10673
  socraticPrompting,
@@ -9613,6 +10688,7 @@ export {
9613
10688
  toolCalled,
9614
10689
  toolFailed,
9615
10690
  uploadSkills,
10691
+ urlBinary,
9616
10692
  usageExceeds,
9617
10693
  useAgentOsSandbox,
9618
10694
  useBashMeta,