@deepagents/context 3.0.1 → 4.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 (61) hide show
  1. package/README.md +94 -4
  2. package/dist/browser.js +88 -146
  3. package/dist/browser.js.map +3 -3
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1085 -687
  7. package/dist/index.js.map +4 -4
  8. package/dist/lib/agent.d.ts.map +1 -1
  9. package/dist/lib/chain-summary.d.ts +1 -0
  10. package/dist/lib/chain-summary.d.ts.map +1 -1
  11. package/dist/lib/chat.d.ts.map +1 -1
  12. package/dist/lib/engine.d.ts +51 -1
  13. package/dist/lib/engine.d.ts.map +1 -1
  14. package/dist/lib/fragments/message/user.d.ts +89 -95
  15. package/dist/lib/fragments/message/user.d.ts.map +1 -1
  16. package/dist/lib/fragments/reminders/once.d.ts +22 -0
  17. package/dist/lib/fragments/reminders/once.d.ts.map +1 -0
  18. package/dist/lib/fragments/reminders/turn-predicates.d.ts +1 -1
  19. package/dist/lib/fragments/reminders/turn-predicates.d.ts.map +1 -1
  20. package/dist/lib/sandbox/agent-os-sandbox.d.ts.map +1 -1
  21. package/dist/lib/sandbox/bash-tool.d.ts +9 -4
  22. package/dist/lib/sandbox/bash-tool.d.ts.map +1 -1
  23. package/dist/lib/sandbox/daytona-sandbox.d.ts.map +1 -1
  24. package/dist/lib/sandbox/docker-sandbox.d.ts +43 -0
  25. package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -1
  26. package/dist/lib/sandbox/index.d.ts +2 -1
  27. package/dist/lib/sandbox/index.d.ts.map +1 -1
  28. package/dist/lib/sandbox/installers/bin.d.ts.map +1 -1
  29. package/dist/lib/sandbox/installers/index.d.ts +7 -7
  30. package/dist/lib/sandbox/installers/index.d.ts.map +1 -1
  31. package/dist/lib/sandbox/installers/installer.d.ts +0 -5
  32. package/dist/lib/sandbox/installers/installer.d.ts.map +1 -1
  33. package/dist/lib/sandbox/installers/url-binary.d.ts.map +1 -1
  34. package/dist/lib/sandbox/shell-quote.d.ts +10 -0
  35. package/dist/lib/sandbox/shell-quote.d.ts.map +1 -0
  36. package/dist/lib/sandbox/strace/file-change.d.ts +21 -0
  37. package/dist/lib/sandbox/strace/file-change.d.ts.map +1 -0
  38. package/dist/lib/sandbox/strace/file-changes.d.ts +66 -0
  39. package/dist/lib/sandbox/strace/file-changes.d.ts.map +1 -0
  40. package/dist/lib/sandbox/strace/index.d.ts +89 -0
  41. package/dist/lib/sandbox/strace/index.d.ts.map +1 -0
  42. package/dist/lib/sandbox/strace/index.js +285 -0
  43. package/dist/lib/sandbox/strace/index.js.map +7 -0
  44. package/dist/lib/sandbox/types.d.ts +6 -9
  45. package/dist/lib/sandbox/types.d.ts.map +1 -1
  46. package/dist/lib/sandbox/virtual-sandbox.d.ts.map +1 -1
  47. package/dist/lib/save/reminder-eval.d.ts +19 -0
  48. package/dist/lib/save/reminder-eval.d.ts.map +1 -0
  49. package/dist/lib/save/save-pipeline.d.ts +1 -7
  50. package/dist/lib/save/save-pipeline.d.ts.map +1 -1
  51. package/dist/lib/skills/skill-reminder.d.ts +2 -2
  52. package/dist/lib/skills/skill-reminder.d.ts.map +1 -1
  53. package/package.json +7 -2
  54. package/dist/lib/sandbox/file-events.d.ts +0 -42
  55. package/dist/lib/sandbox/file-events.d.ts.map +0 -1
  56. package/dist/lib/save/reminder-target-handler.d.ts +0 -36
  57. package/dist/lib/save/reminder-target-handler.d.ts.map +0 -1
  58. package/dist/lib/save/tool-output-target-handler.d.ts +0 -7
  59. package/dist/lib/save/tool-output-target-handler.d.ts.map +0 -1
  60. package/dist/lib/save/user-target-handler.d.ts +0 -7
  61. package/dist/lib/save/user-target-handler.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1038,10 +1038,10 @@ ${rendered.split("\n").slice(1).join("\n")}`;
1038
1038
  return `${this.#pad(depth)}-`;
1039
1039
  }
1040
1040
  const lines = entries.split("\n");
1041
- const first = lines[0].trimStart();
1041
+ const first2 = lines[0].trimStart();
1042
1042
  const rest = lines.slice(1).join("\n");
1043
- return rest ? `${this.#pad(depth)}- ${first}
1044
- ${rest}` : `${this.#pad(depth)}- ${first}`;
1043
+ return rest ? `${this.#pad(depth)}- ${first2}
1044
+ ${rest}` : `${this.#pad(depth)}- ${first2}`;
1045
1045
  }
1046
1046
  return `${this.#pad(depth)}- ${this.#formatValue(item)}`;
1047
1047
  }
@@ -1161,6 +1161,11 @@ function getReminderRanges(metadata) {
1161
1161
  end: record.end
1162
1162
  }));
1163
1163
  }
1164
+ function getReminderOnceIds(message2) {
1165
+ const meta = message2.metadata;
1166
+ if (!isRecord(meta) || !Array.isArray(meta.onceIds)) return [];
1167
+ return meta.onceIds.filter((id) => typeof id === "string");
1168
+ }
1164
1169
  function getReminderMetadataRecords(metadata) {
1165
1170
  const reminders = metadata?.reminders;
1166
1171
  if (!Array.isArray(reminders)) return [];
@@ -1168,35 +1173,12 @@ function getReminderMetadataRecords(metadata) {
1168
1173
  (item) => isRecord(item) && typeof item.partIndex === "number" && typeof item.start === "number" && typeof item.end === "number"
1169
1174
  );
1170
1175
  }
1171
- function reminderTargetOf(record) {
1172
- return record.target === "tool-output" ? "tool-output" : "user";
1173
- }
1174
1176
  function normalizeReminderTarget(target) {
1175
1177
  if (target === void 0 || target === "user") return "user";
1176
1178
  if (target === "tool-output") return "tool-output";
1179
+ if (target === "steer") return "steer";
1177
1180
  throw new Error(`Unsupported reminder target: ${String(target)}`);
1178
1181
  }
1179
- function isConditionalReminderOptions(options) {
1180
- return options !== void 0 && "when" in options;
1181
- }
1182
- function isPromiseLike(value) {
1183
- return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";
1184
- }
1185
- function normalizeImmediateReminderText(textOrFragment) {
1186
- if (isFragment(textOrFragment)) {
1187
- return new XmlRenderer().render([textOrFragment]);
1188
- }
1189
- if (typeof textOrFragment === "string") {
1190
- return textOrFragment;
1191
- }
1192
- return (ctx) => {
1193
- const resolved = textOrFragment(ctx);
1194
- if (isPromiseLike(resolved)) {
1195
- throw new Error("Async reminder text requires a when predicate");
1196
- }
1197
- return resolved;
1198
- };
1199
- }
1200
1182
  function normalizeConditionalReminderText(textOrFragment) {
1201
1183
  return isFragment(textOrFragment) ? new XmlRenderer().render([textOrFragment]) : textOrFragment;
1202
1184
  }
@@ -1204,7 +1186,7 @@ function isOutputAvailableToolPart(part) {
1204
1186
  return isStaticToolUIPart(part) && part.state === "output-available";
1205
1187
  }
1206
1188
  function isToolOutputReminderEnvelope(value) {
1207
- return isRecord(value) && typeof value.systemReminder === "string";
1189
+ return isRecord(value) && "result" in value && typeof value.systemReminder === "string" && value.systemReminder.startsWith(SYSTEM_REMINDER_OPEN_TAG);
1208
1190
  }
1209
1191
  function stripTextByRanges(text, ranges) {
1210
1192
  if (ranges.length === 0) {
@@ -1233,43 +1215,22 @@ function stripTextByRanges(text, ranges) {
1233
1215
  return output.trimEnd();
1234
1216
  }
1235
1217
  function stripReminders(message2) {
1218
+ if (isSyntheticSteerMessage(message2)) {
1219
+ return stripSyntheticSteerMessage(message2);
1220
+ }
1236
1221
  const reminderRecords = getReminderMetadataRecords(
1237
1222
  isRecord(message2.metadata) ? message2.metadata : void 0
1238
1223
  );
1239
1224
  const rangesByPartIndex = /* @__PURE__ */ new Map();
1240
- const toolRemindersByPartIndex = /* @__PURE__ */ new Map();
1241
1225
  for (const range of reminderRecords) {
1242
- if (reminderTargetOf(range) === "tool-output") {
1243
- const records = toolRemindersByPartIndex.get(range.partIndex) ?? [];
1244
- records.push(range);
1245
- toolRemindersByPartIndex.set(range.partIndex, records);
1246
- continue;
1247
- }
1248
1226
  const partRanges = rangesByPartIndex.get(range.partIndex) ?? [];
1249
1227
  partRanges.push({ start: range.start, end: range.end });
1250
1228
  rangesByPartIndex.set(range.partIndex, partRanges);
1251
1229
  }
1252
1230
  const strippedParts = message2.parts.flatMap((part, partIndex) => {
1253
1231
  const clonedPart = { ...part };
1254
- const toolReminderRecords = toolRemindersByPartIndex.get(partIndex);
1255
- if (toolReminderRecords !== void 0 && isOutputAvailableToolPart(clonedPart)) {
1256
- if (typeof clonedPart.output === "string") {
1257
- return [
1258
- {
1259
- ...clonedPart,
1260
- output: stripTextByRanges(
1261
- clonedPart.output,
1262
- toolReminderRecords.map((record) => ({
1263
- start: record.start,
1264
- end: record.end
1265
- }))
1266
- )
1267
- }
1268
- ];
1269
- }
1270
- if (isToolOutputReminderEnvelope(clonedPart.output)) {
1271
- return [{ ...clonedPart, output: clonedPart.output.result }];
1272
- }
1232
+ if (isOutputAvailableToolPart(clonedPart) && isToolOutputReminderEnvelope(clonedPart.output)) {
1233
+ return [{ ...clonedPart, output: clonedPart.output.result }];
1273
1234
  }
1274
1235
  const ranges = rangesByPartIndex.get(partIndex);
1275
1236
  if (clonedPart.type !== "text" || ranges === void 0) {
@@ -1409,64 +1370,12 @@ function applyReminderToMessage(message2, item, ctx) {
1409
1370
  }
1410
1371
  return item.asPart ? applyPartReminder(message2, resolved.text) : applyInlineReminder(message2, resolved.text);
1411
1372
  }
1412
- function findSingleOutputAvailableToolPart(message2) {
1413
- let match = null;
1414
- for (let partIndex = 0; partIndex < message2.parts.length; partIndex++) {
1415
- const part = message2.parts[partIndex];
1416
- if (!isOutputAvailableToolPart(part)) continue;
1417
- if (match) return null;
1418
- match = { partIndex, part };
1419
- }
1420
- return match;
1421
- }
1422
- function applyToolOutputRemindersToMessage(message2, reminders) {
1423
- if (reminders.length === 0) return [];
1424
- const target = findSingleOutputAvailableToolPart(message2);
1425
- if (!target) return [];
1426
- for (const reminder2 of reminders) {
1427
- if (reminder2.metadata) {
1428
- mergeMessageMetadata(message2, reminder2.metadata);
1429
- }
1430
- }
1431
- const added = [];
1432
- if (typeof target.part.output === "string") {
1433
- let output = target.part.output;
1434
- for (const reminder2 of reminders) {
1435
- const reminderText = formatTaggedReminder(reminder2.text);
1436
- const start = output.length;
1437
- output = `${output}${reminderText}`;
1438
- added.push({
1439
- id: generateId2(),
1440
- text: reminder2.text,
1441
- target: "tool-output",
1442
- partIndex: target.partIndex,
1443
- start,
1444
- end: start + reminderText.length,
1445
- mode: "tool-output"
1446
- });
1447
- }
1448
- message2.parts[target.partIndex] = {
1449
- ...target.part,
1450
- output
1451
- };
1452
- return added;
1453
- }
1454
- message2.parts[target.partIndex] = {
1455
- ...target.part,
1456
- output: {
1457
- result: target.part.output,
1458
- systemReminder: reminders.map((reminder2) => reminder2.text).join("\n")
1459
- }
1373
+ function applyRemindersToToolOutput(output, texts) {
1374
+ if (texts.length === 0) return output;
1375
+ return {
1376
+ result: output === void 0 ? null : output,
1377
+ systemReminder: formatTaggedReminder(texts.join("\n"))
1460
1378
  };
1461
- return reminders.map((reminder2) => ({
1462
- id: generateId2(),
1463
- text: reminder2.text,
1464
- target: "tool-output",
1465
- partIndex: target.partIndex,
1466
- start: 0,
1467
- end: 0,
1468
- mode: "tool-output"
1469
- }));
1470
1379
  }
1471
1380
  function mergeReminderMetadata(message2, addedReminders) {
1472
1381
  if (addedReminders.length === 0) return;
@@ -1478,54 +1387,32 @@ function mergeReminderMetadata(message2, addedReminders) {
1478
1387
  function reminder(textOrFragment, options) {
1479
1388
  const target = normalizeReminderTarget(options?.target);
1480
1389
  const asPart = target === "user" ? options?.asPart ?? false : false;
1481
- if (isConditionalReminderOptions(options)) {
1482
- const text2 = normalizeConditionalReminderText(textOrFragment);
1483
- if (typeof text2 === "string") {
1484
- assertReminderText(text2);
1485
- }
1486
- return {
1487
- name: "reminder",
1488
- data: null,
1489
- metadata: {
1490
- reminder: {
1491
- text: text2,
1492
- when: options.when,
1493
- asPart,
1494
- target
1495
- }
1496
- }
1497
- };
1390
+ if (options?.when === void 0 && target !== "user") {
1391
+ throw new Error(`Reminder target "${target}" requires a when predicate`);
1498
1392
  }
1499
- if (target !== "user") {
1500
- throw new Error('Reminder target "tool-output" requires a when predicate');
1501
- }
1502
- const text = normalizeImmediateReminderText(textOrFragment);
1393
+ const text = normalizeConditionalReminderText(textOrFragment);
1503
1394
  if (typeof text === "string") {
1504
1395
  assertReminderText(text);
1505
1396
  }
1506
1397
  return {
1507
- text,
1508
- asPart,
1509
- target
1398
+ name: "reminder",
1399
+ data: null,
1400
+ metadata: {
1401
+ reminder: {
1402
+ text,
1403
+ when: options?.when ?? (() => true),
1404
+ asPart,
1405
+ target
1406
+ }
1407
+ }
1510
1408
  };
1511
1409
  }
1512
- function user(content, ...reminders) {
1410
+ function user(content) {
1513
1411
  const message2 = typeof content === "string" ? {
1514
1412
  id: generateId2(),
1515
1413
  role: "user",
1516
1414
  parts: [{ type: "text", text: content }]
1517
1415
  } : { ...content, role: "user", parts: [...content.parts] };
1518
- if (reminders.length > 0) {
1519
- const plainText = extractPlainText(message2);
1520
- const added = [];
1521
- for (const item of reminders) {
1522
- const meta = applyReminderToMessage(message2, item, {
1523
- content: plainText
1524
- });
1525
- if (meta) added.push(meta);
1526
- }
1527
- mergeReminderMetadata(message2, added);
1528
- }
1529
1416
  return {
1530
1417
  id: message2.id,
1531
1418
  name: "user",
@@ -1541,6 +1428,58 @@ function user(content, ...reminders) {
1541
1428
  }
1542
1429
  };
1543
1430
  }
1431
+ function applyUserRemindersToMessage(message2, reminders) {
1432
+ if (reminders.length === 0) return;
1433
+ const plainText = extractPlainText(message2);
1434
+ const added = [];
1435
+ for (const item of reminders) {
1436
+ const meta = applyReminderToMessage(message2, item, { content: plainText });
1437
+ if (meta) added.push(meta);
1438
+ }
1439
+ mergeReminderMetadata(message2, added);
1440
+ }
1441
+ function synthesizeSteerUserMessage(text, firedAt, onceIds = []) {
1442
+ const texts = Array.isArray(text) ? text : [text];
1443
+ for (const value of texts) assertReminderText(value);
1444
+ return {
1445
+ id: generateId2(),
1446
+ role: "user",
1447
+ parts: texts.map((value) => ({
1448
+ type: "text",
1449
+ text: formatTaggedReminder(value)
1450
+ })),
1451
+ metadata: {
1452
+ synthetic: {
1453
+ source: "steer-reminder",
1454
+ firedAt,
1455
+ ...onceIds.length > 0 ? { onceIds } : {}
1456
+ }
1457
+ }
1458
+ };
1459
+ }
1460
+ function isSyntheticSteerMessage(message2) {
1461
+ const meta = message2.metadata;
1462
+ if (!isRecord(meta)) return false;
1463
+ const synthetic = meta.synthetic;
1464
+ if (!isRecord(synthetic)) return false;
1465
+ return synthetic.source === "steer-reminder";
1466
+ }
1467
+ function stripSyntheticSteerMessage(message2) {
1468
+ const next = {
1469
+ ...message2,
1470
+ parts: message2.parts.filter((part) => part.type !== "text")
1471
+ };
1472
+ if (isRecord(message2.metadata)) {
1473
+ const metadata = { ...message2.metadata };
1474
+ delete metadata.synthetic;
1475
+ if (Object.keys(metadata).length > 0) {
1476
+ next.metadata = metadata;
1477
+ } else {
1478
+ delete next.metadata;
1479
+ }
1480
+ }
1481
+ return next;
1482
+ }
1544
1483
 
1545
1484
  // packages/context/src/lib/guardrail.ts
1546
1485
  function pass(part) {
@@ -1607,7 +1546,10 @@ var Agent = class _Agent {
1607
1546
  sandbox;
1608
1547
  constructor(options) {
1609
1548
  this.#options = options;
1610
- this.tools = { ...options.sandbox.tools, ...options.tools || {} };
1549
+ this.tools = wrapToolsWithOutputReminders(
1550
+ { ...options.sandbox.tools, ...options.tools || {} },
1551
+ options.context
1552
+ );
1611
1553
  this.context = options.context;
1612
1554
  this.model = options.model;
1613
1555
  this.sandbox = options.sandbox;
@@ -1714,6 +1656,7 @@ var Agent = class _Agent {
1714
1656
  config?.abortSignal
1715
1657
  ),
1716
1658
  stopWhen: stepCountIs(200),
1659
+ prepareStep: context.createSteerPrepareStep(),
1717
1660
  experimental_transform: config?.transform ?? smoothStream(),
1718
1661
  tools: this.tools,
1719
1662
  experimental_context: contextVariables,
@@ -1750,9 +1693,14 @@ var Agent = class _Agent {
1750
1693
  generateId: assistantMsgId ? () => assistantMsgId : generateId3,
1751
1694
  onStepFinish: async ({ responseMessage }) => {
1752
1695
  if (!stepSaved) return;
1753
- const normalizedMessage = assistantMsgId ? { ...responseMessage, id: assistantMsgId } : responseMessage;
1754
- context.set(assistant(normalizedMessage));
1755
- await context.save({ branch: false });
1696
+ const head = await context.headMessage();
1697
+ if (head?.name === "assistant") {
1698
+ await context.writeAssistantSegment(responseMessage);
1699
+ } else {
1700
+ const message2 = assistantMsgId ? { ...responseMessage, id: assistantMsgId } : responseMessage;
1701
+ context.set(assistant(message2));
1702
+ await context.save({ branch: false });
1703
+ }
1756
1704
  stepSaved.resolve();
1757
1705
  stepSaved = null;
1758
1706
  },
@@ -1959,6 +1907,29 @@ ${details}
1959
1907
  });
1960
1908
  }
1961
1909
  };
1910
+ function wrapToolsWithOutputReminders(tools, context) {
1911
+ if (!context) return tools;
1912
+ const wrapped = {};
1913
+ for (const [name, toolDef] of Object.entries(tools)) {
1914
+ const execute = toolDef.execute;
1915
+ if (typeof execute !== "function") {
1916
+ wrapped[name] = toolDef;
1917
+ continue;
1918
+ }
1919
+ wrapped[name] = {
1920
+ ...toolDef,
1921
+ execute: async (input, options) => {
1922
+ const result = await execute.call(toolDef, input, options);
1923
+ if (isAsyncIterable(result)) return result;
1924
+ return context.applyToolOutputReminders(result);
1925
+ }
1926
+ };
1927
+ }
1928
+ return wrapped;
1929
+ }
1930
+ function isAsyncIterable(value) {
1931
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
1932
+ }
1962
1933
  function agent(options) {
1963
1934
  return new Agent(options);
1964
1935
  }
@@ -2062,6 +2033,7 @@ import { z as z2 } from "zod";
2062
2033
 
2063
2034
  // packages/context/src/lib/engine.ts
2064
2035
  import {
2036
+ convertToModelMessages as convertToModelMessages2,
2065
2037
  generateId as generateId4,
2066
2038
  validateUIMessages
2067
2039
  } from "ai";
@@ -2097,30 +2069,42 @@ var ChainSummaryBuilder = class {
2097
2069
  #lastMessage;
2098
2070
  #lastAssistantMessage;
2099
2071
  #lastAssistantMessages = [];
2072
+ #firedOnceIds = /* @__PURE__ */ new Set();
2100
2073
  ingestStored(msg) {
2101
2074
  this.#messageCount++;
2102
2075
  if (msg.name === "assistant") {
2103
- const message2 = requireUIMessage(
2076
+ const message3 = requireUIMessage(
2104
2077
  msg.data,
2105
2078
  `Stored assistant message "${msg.id}"`
2106
2079
  );
2107
- this.#lastAssistantMessage = message2;
2108
- this.#lastAssistantMessages.push(message2);
2080
+ this.#lastAssistantMessage = message3;
2081
+ this.#lastAssistantMessages.push(message3);
2109
2082
  return;
2110
2083
  }
2111
2084
  if (msg.name !== "user") {
2112
2085
  return;
2113
2086
  }
2114
- this.#turn++;
2115
- this.#lastMessageAt = msg.createdAt;
2116
- this.#lastMessage = requireUIMessage(
2087
+ const message2 = requireUIMessage(
2117
2088
  msg.data,
2118
2089
  `Stored user message "${msg.id}"`
2119
2090
  );
2091
+ if (isSyntheticSteerMessage(message2)) {
2092
+ for (const id of message2.metadata.synthetic.onceIds ?? []) {
2093
+ this.#firedOnceIds.add(id);
2094
+ }
2095
+ return;
2096
+ }
2097
+ for (const id of getReminderOnceIds(message2)) this.#firedOnceIds.add(id);
2098
+ this.#turn++;
2099
+ this.#lastMessageAt = msg.createdAt;
2100
+ this.#lastMessage = message2;
2120
2101
  }
2121
2102
  ingestPending(fragment2) {
2122
2103
  this.#messageCount++;
2123
- if (fragment2.name === "user") this.#turn++;
2104
+ if (fragment2.name !== "user") return;
2105
+ const encoded = fragment2.codec?.encode();
2106
+ if (encoded && isSyntheticSteerMessage(encoded)) return;
2107
+ this.#turn++;
2124
2108
  }
2125
2109
  build() {
2126
2110
  return {
@@ -2129,7 +2113,8 @@ var ChainSummaryBuilder = class {
2129
2113
  lastMessageAt: this.#lastMessageAt,
2130
2114
  lastMessage: this.#lastMessage,
2131
2115
  lastAssistantMessage: this.#lastAssistantMessage,
2132
- lastAssistantMessages: this.#lastAssistantMessages
2116
+ lastAssistantMessages: this.#lastAssistantMessages,
2117
+ firedOnceIds: this.#firedOnceIds
2133
2118
  };
2134
2119
  }
2135
2120
  };
@@ -2338,7 +2323,7 @@ var GeneratorResolver = class {
2338
2323
  async resolve(value, ctx) {
2339
2324
  const iterable = value(ctx);
2340
2325
  const collected = [];
2341
- if (isAsyncIterable(iterable)) {
2326
+ if (isAsyncIterable2(iterable)) {
2342
2327
  for await (const chunk of iterable) {
2343
2328
  pushLimited(collected, chunk, this.#maxItems, this.name);
2344
2329
  }
@@ -2350,7 +2335,7 @@ var GeneratorResolver = class {
2350
2335
  return collected;
2351
2336
  }
2352
2337
  };
2353
- function isAsyncIterable(value) {
2338
+ function isAsyncIterable2(value) {
2354
2339
  return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
2355
2340
  }
2356
2341
  function pushLimited(collected, chunk, maxItems, resolverName) {
@@ -2388,7 +2373,7 @@ var IterableResolver = class {
2388
2373
  async resolve(value, _ctx) {
2389
2374
  const iterable = value;
2390
2375
  const collected = [];
2391
- if (isAsyncIterable2(iterable)) {
2376
+ if (isAsyncIterable3(iterable)) {
2392
2377
  for await (const chunk of iterable) {
2393
2378
  pushLimited2(collected, chunk, this.#maxItems, this.name);
2394
2379
  }
@@ -2400,7 +2385,7 @@ var IterableResolver = class {
2400
2385
  return collected;
2401
2386
  }
2402
2387
  };
2403
- function isAsyncIterable2(value) {
2388
+ function isAsyncIterable3(value) {
2404
2389
  return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
2405
2390
  }
2406
2391
  function pushLimited2(collected, chunk, maxItems, resolverName) {
@@ -2556,16 +2541,62 @@ function defaultResolvers() {
2556
2541
  ];
2557
2542
  }
2558
2543
 
2544
+ // packages/context/src/lib/save/reminder-eval.ts
2545
+ async function evaluateFiredReminders(configs, whenCtx) {
2546
+ if (configs.length === 0) return [];
2547
+ const collectors = configs.map(() => /* @__PURE__ */ new Set());
2548
+ const whenResults = await Promise.allSettled(
2549
+ // Wrap in an async thunk so a synchronously-throwing predicate becomes a
2550
+ // rejected promise (allSettled cannot catch a throw inside the map itself).
2551
+ configs.map(
2552
+ async (config, i) => config.when({ ...whenCtx, onceCollector: collectors[i] })
2553
+ )
2554
+ );
2555
+ const fired = [];
2556
+ for (let i = 0; i < configs.length; i++) {
2557
+ const result = whenResults[i];
2558
+ if (result.status === "rejected") {
2559
+ console.warn("reminder when() predicate threw; treating as not fired", {
2560
+ reason: result.reason
2561
+ });
2562
+ continue;
2563
+ }
2564
+ if (result.value === true) {
2565
+ fired.push({ config: configs[i], onceIds: [...collectors[i]] });
2566
+ }
2567
+ }
2568
+ if (fired.length === 0) return [];
2569
+ const resolutions = await Promise.allSettled(
2570
+ fired.map(({ config }) => resolveReminderAsync(config, whenCtx))
2571
+ );
2572
+ const matched = [];
2573
+ for (let i = 0; i < fired.length; i++) {
2574
+ const result = resolutions[i];
2575
+ if (result.status === "rejected") {
2576
+ console.warn("reminder text resolver threw; skipping reminder", {
2577
+ reason: result.reason
2578
+ });
2579
+ continue;
2580
+ }
2581
+ if (result.value) {
2582
+ matched.push({
2583
+ config: fired[i].config,
2584
+ resolved: result.value,
2585
+ onceIds: fired[i].onceIds
2586
+ });
2587
+ }
2588
+ }
2589
+ return matched;
2590
+ }
2591
+
2559
2592
  // packages/context/src/lib/save/save-pipeline.ts
2560
2593
  var SavePipeline = class {
2561
2594
  #engine;
2562
2595
  #pending;
2563
- #fragments;
2564
2596
  #shouldBranch = true;
2565
- constructor(engine, pending, fragments) {
2597
+ constructor(engine, pending) {
2566
2598
  this.#engine = engine;
2567
2599
  this.#pending = pending;
2568
- this.#fragments = fragments;
2569
2600
  }
2570
2601
  async applyUpdateBranching(shouldBranch) {
2571
2602
  this.#shouldBranch = shouldBranch;
@@ -2581,66 +2612,6 @@ var SavePipeline = class {
2581
2612
  }
2582
2613
  return this;
2583
2614
  }
2584
- async evaluateReminders(handlers) {
2585
- const conditional = this.#fragments.filter(isConditionalReminder);
2586
- if (conditional.length === 0) return this;
2587
- const configsByTarget = /* @__PURE__ */ new Map();
2588
- for (const fragment2 of conditional) {
2589
- const config = fragment2.metadata.reminder;
2590
- const target = config.target;
2591
- const list = configsByTarget.get(target) ?? [];
2592
- list.push(config);
2593
- configsByTarget.set(target, list);
2594
- }
2595
- const chain = await this.#engine.getChainSummary();
2596
- const base = this.#engine.buildBaseWhenCtx(chain);
2597
- const sharedUserMessage = this.#encodePendingUserMessage();
2598
- for (const handler of handlers) {
2599
- const configs = configsByTarget.get(handler.target);
2600
- if (!configs || configs.length === 0) continue;
2601
- const prepared = handler.prepare({
2602
- pending: this.#pending,
2603
- base,
2604
- chain,
2605
- sharedUserMessage
2606
- });
2607
- if (!prepared) continue;
2608
- const whenResults = await Promise.all(
2609
- configs.map((config) => config.when(prepared.whenCtx))
2610
- );
2611
- const fired = configs.filter((_, i) => whenResults[i]);
2612
- if (fired.length === 0) continue;
2613
- const resolvedOrNull = await Promise.all(
2614
- fired.map((config) => resolveReminderAsync(config, prepared.whenCtx))
2615
- );
2616
- const matched = [];
2617
- for (let i = 0; i < resolvedOrNull.length; i++) {
2618
- const resolution = resolvedOrNull[i];
2619
- if (resolution)
2620
- matched.push({ config: fired[i], resolved: resolution });
2621
- }
2622
- if (matched.length === 0) continue;
2623
- handler.apply({
2624
- pending: this.#pending,
2625
- carrier: prepared.carrier,
2626
- fired: matched.map((m) => m.config),
2627
- resolved: matched.map((m) => m.resolved)
2628
- });
2629
- }
2630
- return this;
2631
- }
2632
- #encodePendingUserMessage() {
2633
- const fragmentIndex = this.#pending.findLastIndex(
2634
- (fragment3) => fragment3.name === "user"
2635
- );
2636
- if (fragmentIndex < 0) return void 0;
2637
- const fragment2 = this.#pending[fragmentIndex];
2638
- if (!fragment2.codec) return void 0;
2639
- return requireUserUIMessage(
2640
- fragment2.codec.encode(),
2641
- `Pending user fragment "${fragment2.name}"`
2642
- );
2643
- }
2644
2615
  async persist() {
2645
2616
  let parentId = this.#engine.getActiveBranch().headMessageId;
2646
2617
  const now = Date.now();
@@ -2680,108 +2651,6 @@ var SavePipeline = class {
2680
2651
  }
2681
2652
  };
2682
2653
 
2683
- // packages/context/src/lib/save/tool-output-target-handler.ts
2684
- var ToolOutputTargetHandler = class {
2685
- target = "tool-output";
2686
- prepare({
2687
- pending,
2688
- base,
2689
- chain,
2690
- sharedUserMessage
2691
- }) {
2692
- const fragmentIndex = pending.findLastIndex(
2693
- (fragment3) => fragment3.name === "assistant"
2694
- );
2695
- if (fragmentIndex < 0) return null;
2696
- const fragment2 = pending[fragmentIndex];
2697
- if (!fragment2.codec) return null;
2698
- const currentMessage = sharedUserMessage ?? chain.lastMessage;
2699
- if (!currentMessage) return null;
2700
- const message2 = requireUIMessage(
2701
- fragment2.codec.encode(),
2702
- `Pending assistant fragment "${fragment2.name}"`
2703
- );
2704
- if (!findSingleOutputAvailableToolPart(message2)) return null;
2705
- return {
2706
- whenCtx: {
2707
- ...base,
2708
- content: extractPlainText(currentMessage),
2709
- currentMessage,
2710
- lastAssistantMessage: message2,
2711
- lastAssistantMessages: [
2712
- ...chain.lastAssistantMessages ?? [],
2713
- message2
2714
- ]
2715
- },
2716
- carrier: { message: message2, fragmentIndex }
2717
- };
2718
- }
2719
- apply({ pending, carrier, resolved }) {
2720
- const reminders = resolved.map((resolution) => ({
2721
- text: resolution.text,
2722
- metadata: resolution.metadata
2723
- }));
2724
- const metadata = applyToolOutputRemindersToMessage(
2725
- carrier.message,
2726
- reminders
2727
- );
2728
- mergeReminderMetadata(carrier.message, metadata);
2729
- const originalId = pending[carrier.fragmentIndex].id;
2730
- const message2 = originalId ? { ...carrier.message, id: originalId } : carrier.message;
2731
- const updated = assistant(message2);
2732
- if (originalId) updated.id = originalId;
2733
- pending[carrier.fragmentIndex] = updated;
2734
- }
2735
- };
2736
-
2737
- // packages/context/src/lib/save/user-target-handler.ts
2738
- var UserTargetHandler = class {
2739
- target = "user";
2740
- prepare({
2741
- pending,
2742
- base,
2743
- chain
2744
- }) {
2745
- const fragmentIndex = pending.findLastIndex(
2746
- (fragment3) => fragment3.name === "user"
2747
- );
2748
- if (fragmentIndex < 0) return null;
2749
- const fragment2 = pending[fragmentIndex];
2750
- if (!fragment2.codec) return null;
2751
- const message2 = requireUserUIMessage(
2752
- fragment2.codec.encode(),
2753
- `Pending user fragment "${fragment2.name}"`
2754
- );
2755
- return {
2756
- whenCtx: {
2757
- ...base,
2758
- content: extractPlainText(message2),
2759
- currentMessage: message2,
2760
- lastAssistantMessage: chain.lastAssistantMessage,
2761
- lastAssistantMessages: chain.lastAssistantMessages
2762
- },
2763
- carrier: { message: message2, fragmentIndex },
2764
- sharedUserMessage: message2
2765
- };
2766
- }
2767
- apply({ pending, carrier, fired, resolved }) {
2768
- const reminders = resolved.map((resolution, i) => ({
2769
- text: resolution.text,
2770
- asPart: fired[i].asPart,
2771
- target: "user",
2772
- metadata: resolution.metadata
2773
- }));
2774
- const originalId = pending[carrier.fragmentIndex].id;
2775
- const message2 = requireUserUIMessage(
2776
- originalId ? { ...carrier.message, id: originalId } : carrier.message,
2777
- "Pending user reminder carrier"
2778
- );
2779
- const recreated = user(message2, ...reminders);
2780
- if (originalId) recreated.id = originalId;
2781
- pending[carrier.fragmentIndex] = recreated;
2782
- }
2783
- };
2784
-
2785
2654
  // packages/context/src/lib/store/sqlite.store.ts
2786
2655
  import { DatabaseSync } from "node:sqlite";
2787
2656
 
@@ -3402,6 +3271,23 @@ function isSkillPathMapping(value) {
3402
3271
  function isEmptyAssistantPlaceholder(message2) {
3403
3272
  return message2.role === "assistant" && message2.parts.length === 0;
3404
3273
  }
3274
+ function stepStartPartIndices(parts) {
3275
+ const indices = [];
3276
+ for (let i = 0; i < parts.length; i++) {
3277
+ if (parts[i].type === "step-start") indices.push(i);
3278
+ }
3279
+ return indices;
3280
+ }
3281
+ function spliceSteerMessages(messages, fired) {
3282
+ const ordered = [...fired].sort((a, b) => a.spliceIndex - b.spliceIndex);
3283
+ const out = [...messages];
3284
+ let offset = 0;
3285
+ for (const fire of ordered) {
3286
+ out.splice(fire.spliceIndex + offset, 0, ...fire.synthModel);
3287
+ offset += fire.synthModel.length;
3288
+ }
3289
+ return out;
3290
+ }
3405
3291
  var ContextEngine = class _ContextEngine {
3406
3292
  /** Non-message fragments (role, hints, etc.) - not persisted in graph */
3407
3293
  #fragments = [];
@@ -3759,26 +3645,286 @@ var ContextEngine = class _ContextEngine {
3759
3645
  if (this.#pendingMessages.length === 0) {
3760
3646
  return { headMessageId: this.#branch?.headMessageId ?? void 0 };
3761
3647
  }
3762
- const pipeline = new SavePipeline(
3763
- this.#asSavePipelineEngine(),
3764
- this.#pendingMessages,
3765
- this.#fragments
3766
- );
3648
+ const pending = this.#pendingMessages;
3649
+ const pipeline = new SavePipeline(this.#asSavePipelineEngine(), pending);
3767
3650
  await pipeline.applyUpdateBranching(options?.branch ?? true);
3768
- await pipeline.evaluateReminders(this.#reminderHandlers);
3651
+ await this.#foldUserReminders(pending);
3769
3652
  const result = await pipeline.persist();
3770
3653
  this.#pendingMessages = [];
3771
3654
  return result;
3772
3655
  }
3773
- #reminderHandlers = [
3774
- new UserTargetHandler(),
3775
- new ToolOutputTargetHandler()
3776
- ];
3656
+ #currentSteerSession;
3657
+ /**
3658
+ * Build the `prepareStep` hook that injects steer reminders mid-loop.
3659
+ *
3660
+ * Semantics are "inject once, persist": when a steer reminder's predicate
3661
+ * fires (only after the model has produced ≥1 step with content — the mid-loop
3662
+ * gate), its `<system-reminder>` user message is spliced into the model prompt
3663
+ * at the step boundary where it fired AND re-spliced on every subsequent step,
3664
+ * so the model keeps seeing it for the rest of the loop.
3665
+ *
3666
+ * Firing is edge-triggered with a post-fire re-sample: each fire resets the
3667
+ * elapsed reference (`lastSyntheticAt`), then the config is immediately
3668
+ * re-evaluated against the reset context — self-resetting predicates like
3669
+ * `elapsedExceeds` read false and re-arm (so they recur every N within one
3670
+ * stream), while constant predicates like `everyNTurns` read true and disarm
3671
+ * (so they fire once per stream). Any later false sample re-arms a config.
3672
+ * All state is closure-local (a fresh SteerSession per call), so overlapping
3673
+ * streams / guardrail retries never share a cursor.
3674
+ *
3675
+ * The session is also consumed by writeAssistantSegment, which carves the
3676
+ * streamed assistant message into the matching `[assistant, steer, assistant]`
3677
+ * split — so the stored chain reproduces exactly the prompt the model saw
3678
+ * (store/prompt parity).
3679
+ */
3680
+ createSteerPrepareStep() {
3681
+ const session = {
3682
+ firedOnceIds: /* @__PURE__ */ new Set(),
3683
+ fired: [],
3684
+ currentSegStart: 0,
3685
+ materialized: 0
3686
+ };
3687
+ this.#currentSteerSession = session;
3688
+ return async ({ steps, stepNumber, messages }) => {
3689
+ const priorStep = stepNumber >= 1 ? steps[stepNumber - 1] : void 0;
3690
+ const canFire = (priorStep?.content?.length ?? 0) > 0;
3691
+ if (canFire) {
3692
+ const configs = this.#remindersFor("steer");
3693
+ if (configs.length > 0) {
3694
+ const whenCtx = await this.#steerWhenCtx(session);
3695
+ const matched = await evaluateFiredReminders(configs, whenCtx);
3696
+ if (matched.length > 0) {
3697
+ const onceIds = [...new Set(matched.flatMap((m) => m.onceIds))];
3698
+ for (const id of onceIds) session.firedOnceIds.add(id);
3699
+ const synth = synthesizeSteerUserMessage(
3700
+ matched.map((m) => m.resolved.text),
3701
+ Date.now(),
3702
+ onceIds
3703
+ );
3704
+ const synthModel = await convertToModelMessages2([synth], {
3705
+ ignoreIncompleteToolCalls: true
3706
+ });
3707
+ session.fired.push({
3708
+ afterStep: stepNumber - 1,
3709
+ spliceIndex: messages.length,
3710
+ synth,
3711
+ synthModel
3712
+ });
3713
+ }
3714
+ }
3715
+ }
3716
+ if (session.fired.length === 0) return void 0;
3717
+ return {
3718
+ messages: spliceSteerMessages(
3719
+ messages,
3720
+ session.fired
3721
+ )
3722
+ };
3723
+ };
3724
+ }
3725
+ /**
3726
+ * Persist the streamed assistant message, carving it into the steer split when
3727
+ * steer reminders fired this turn.
3728
+ *
3729
+ * Called from chat()'s onStepFinish/onFinish (and the guardrail path) with the
3730
+ * cumulative response message. Segment boundaries come from the `step-start`
3731
+ * markers in the message itself — no cross-track store read — so the carve is
3732
+ * race-free. Idempotent: finalized segments keep stable ids; the open segment
3733
+ * is updated in place. With no active steer it degrades to a plain in-place
3734
+ * write of the whole message to the reserved head.
3735
+ */
3736
+ async writeAssistantSegment(message2) {
3737
+ const head = await this.headMessage();
3738
+ if (head?.name !== "assistant") {
3739
+ throw new Error(
3740
+ "writeAssistantSegment: expected an assistant message at chain head."
3741
+ );
3742
+ }
3743
+ const session = this.#currentSteerSession;
3744
+ if (!session || session.fired.length === 0) {
3745
+ this.set(assistant({ ...message2, id: head.id }));
3746
+ await this.save({ branch: false });
3747
+ return;
3748
+ }
3749
+ if (session.currentSegId === void 0) {
3750
+ session.currentSegId = head.id;
3751
+ session.currentSegStart = 0;
3752
+ }
3753
+ const stepStarts = stepStartPartIndices(message2.parts);
3754
+ while (session.materialized < session.fired.length) {
3755
+ const fire = session.fired[session.materialized];
3756
+ const boundary = stepStarts[fire.afterStep + 1];
3757
+ if (boundary === void 0) break;
3758
+ this.set(
3759
+ assistant({
3760
+ id: session.currentSegId,
3761
+ role: "assistant",
3762
+ parts: message2.parts.slice(session.currentSegStart, boundary)
3763
+ })
3764
+ );
3765
+ this.set(user(fire.synth));
3766
+ session.currentSegId = generateId4();
3767
+ session.currentSegStart = boundary;
3768
+ session.materialized++;
3769
+ }
3770
+ this.set(
3771
+ assistant({
3772
+ ...message2,
3773
+ id: session.currentSegId,
3774
+ parts: message2.parts.slice(session.currentSegStart)
3775
+ })
3776
+ );
3777
+ await this.save({ branch: false });
3778
+ }
3779
+ #remindersFor(target) {
3780
+ return this.#fragments.filter(isConditionalReminder).map((fragment2) => fragment2.metadata.reminder).filter((config) => config.target === target);
3781
+ }
3782
+ #buildBaseWhenCtx(chain) {
3783
+ const rawUsage = this.#chatData?.metadata?.usage;
3784
+ const usage = isLanguageModelUsage(rawUsage) ? rawUsage : void 0;
3785
+ const elapsed = chain.lastMessageAt !== void 0 ? Date.now() - chain.lastMessageAt : void 0;
3786
+ const chatData = this.#chatData;
3787
+ if (!chatData) {
3788
+ throw new Error("ContextEngine must be initialized before reminders run");
3789
+ }
3790
+ return {
3791
+ turn: chain.turn,
3792
+ messageCount: chain.messageCount,
3793
+ lastMessageAt: chain.lastMessageAt,
3794
+ lastMessage: chain.lastMessage,
3795
+ chat: chatData,
3796
+ usage,
3797
+ branch: this.#branchName,
3798
+ elapsed
3799
+ };
3800
+ }
3801
+ #buildWhenCtx(chain, currentMessage) {
3802
+ return {
3803
+ ...this.#buildBaseWhenCtx(chain),
3804
+ content: extractPlainText(currentMessage),
3805
+ currentMessage,
3806
+ lastAssistantMessage: chain.lastAssistantMessage,
3807
+ lastAssistantMessages: chain.lastAssistantMessages
3808
+ };
3809
+ }
3810
+ /**
3811
+ * `steer` reminders fire mid-loop via prepareStep and `tool-output` reminders
3812
+ * wrap at tool-execution time, so by the time save() runs only user-target
3813
+ * reminders remain to fold into the last pending user message.
3814
+ */
3815
+ async #foldUserReminders(pending) {
3816
+ const configs = this.#remindersFor("user");
3817
+ if (configs.length === 0) return;
3818
+ const fragmentIndex = pending.findLastIndex(
3819
+ (fragment3) => fragment3.name === "user"
3820
+ );
3821
+ if (fragmentIndex < 0) return;
3822
+ const fragment2 = pending[fragmentIndex];
3823
+ if (!fragment2.codec) return;
3824
+ const message2 = requireUserUIMessage(
3825
+ fragment2.codec.encode(),
3826
+ `Pending user fragment "${fragment2.name}"`
3827
+ );
3828
+ const chain = await this.#getChainContext();
3829
+ const matched = await evaluateFiredReminders(configs, {
3830
+ ...this.#buildWhenCtx(chain, message2),
3831
+ firedOnceIds: chain.firedOnceIds
3832
+ });
3833
+ if (matched.length === 0) return;
3834
+ const onceIds = [...new Set(matched.flatMap((m) => m.onceIds))];
3835
+ const reminders = matched.map((m) => ({
3836
+ text: m.resolved.text,
3837
+ asPart: m.config.asPart,
3838
+ target: "user",
3839
+ metadata: m.resolved.metadata
3840
+ }));
3841
+ const carrier = {
3842
+ ...message2,
3843
+ id: fragment2.id ?? message2.id,
3844
+ parts: [...message2.parts]
3845
+ };
3846
+ if (onceIds.length > 0) {
3847
+ carrier.metadata = {
3848
+ ...carrier.metadata,
3849
+ onceIds
3850
+ };
3851
+ }
3852
+ applyUserRemindersToMessage(carrier, reminders);
3853
+ pending[fragmentIndex] = user(carrier);
3854
+ }
3855
+ async #steerWhenCtx(session) {
3856
+ await this.#ensureInitialized();
3857
+ if (!session.whenBase) {
3858
+ const chain = await this.#getChainContext();
3859
+ const base2 = this.#buildBaseWhenCtx(chain);
3860
+ const currentMessage2 = chain.lastMessage;
3861
+ if (!currentMessage2) {
3862
+ throw new Error(
3863
+ "steer reminders require a user message earlier in the turn"
3864
+ );
3865
+ }
3866
+ session.whenBase = {
3867
+ base: base2,
3868
+ content: extractPlainText(currentMessage2),
3869
+ currentMessage: currentMessage2,
3870
+ lastAssistantMessage: chain.lastAssistantMessage,
3871
+ lastAssistantMessages: chain.lastAssistantMessages,
3872
+ chainFiredOnceIds: chain.firedOnceIds
3873
+ };
3874
+ }
3875
+ const {
3876
+ base,
3877
+ content,
3878
+ currentMessage,
3879
+ lastAssistantMessage,
3880
+ lastAssistantMessages,
3881
+ chainFiredOnceIds
3882
+ } = session.whenBase;
3883
+ const elapsed = base.lastMessageAt !== void 0 ? Date.now() - base.lastMessageAt : void 0;
3884
+ return {
3885
+ ...base,
3886
+ elapsed,
3887
+ content,
3888
+ currentMessage,
3889
+ lastAssistantMessage,
3890
+ lastAssistantMessages,
3891
+ firedOnceIds: /* @__PURE__ */ new Set([...chainFiredOnceIds, ...session.firedOnceIds])
3892
+ };
3893
+ }
3894
+ /**
3895
+ * Evaluate `target: 'tool-output'` reminders against a tool's raw result and
3896
+ * return the (possibly wrapped) output.
3897
+ *
3898
+ * Called by the agent's tool wrapper right after each `execute()` resolves —
3899
+ * upstream of both the model and the store, so the next model step and the
3900
+ * persisted chain see the exact same wrapped value (store/prompt parity).
3901
+ *
3902
+ * Returns the output unchanged when no tool-output reminder fires. Without a
3903
+ * persisted user message there is no turn context to evaluate against
3904
+ * (e.g. asTool forks that set a pending user without saving), so the output
3905
+ * passes through untouched.
3906
+ */
3907
+ async applyToolOutputReminders(output) {
3908
+ const configs = this.#remindersFor("tool-output");
3909
+ if (configs.length === 0) return output;
3910
+ await this.#ensureInitialized();
3911
+ const chain = await this.#getChainContext();
3912
+ const currentMessage = chain.lastMessage;
3913
+ if (!currentMessage) return output;
3914
+ const matched = await evaluateFiredReminders(
3915
+ configs,
3916
+ this.#buildWhenCtx(chain, currentMessage)
3917
+ );
3918
+ if (matched.length === 0) return output;
3919
+ return applyRemindersToToolOutput(
3920
+ output,
3921
+ matched.map((m) => m.resolved.text)
3922
+ );
3923
+ }
3777
3924
  #asSavePipelineEngine() {
3778
3925
  return {
3779
3926
  store: this.#store,
3780
3927
  chatId: this.#chatId,
3781
- branchName: this.#branchName,
3782
3928
  getActiveBranch: () => ({
3783
3929
  id: this.#activeBranch.id,
3784
3930
  headMessageId: this.#activeBranch.headMessageId
@@ -3790,29 +3936,7 @@ var ContextEngine = class _ContextEngine {
3790
3936
  );
3791
3937
  this.#activeBranch.headMessageId = headMessageId;
3792
3938
  },
3793
- rewindForUpdate: (parentId) => this.#rewindForUpdate(parentId),
3794
- getChainSummary: () => this.#getChainContext(),
3795
- buildBaseWhenCtx: (chain) => {
3796
- const rawUsage = this.#chatData?.metadata?.usage;
3797
- const usage = isLanguageModelUsage(rawUsage) ? rawUsage : void 0;
3798
- const elapsed = chain.lastMessageAt !== void 0 ? Date.now() - chain.lastMessageAt : void 0;
3799
- const chatData = this.#chatData;
3800
- if (!chatData) {
3801
- throw new Error(
3802
- "ContextEngine must be initialized before reminders run"
3803
- );
3804
- }
3805
- return {
3806
- turn: chain.turn,
3807
- messageCount: chain.messageCount,
3808
- lastMessageAt: chain.lastMessageAt,
3809
- lastMessage: chain.lastMessage,
3810
- chat: chatData,
3811
- usage,
3812
- branch: this.#branchName,
3813
- elapsed
3814
- };
3815
- }
3939
+ rewindForUpdate: (parentId) => this.#rewindForUpdate(parentId)
3816
3940
  };
3817
3941
  }
3818
3942
  /**
@@ -4689,7 +4813,6 @@ var defaultChatMessageMetadata = ({
4689
4813
  };
4690
4814
  async function chat(agent2, options = {}) {
4691
4815
  const context = agent2.context;
4692
- const sandbox = agent2.sandbox;
4693
4816
  if (!context) {
4694
4817
  throw new Error(
4695
4818
  "Agent is missing a context. Provide context when creating the agent."
@@ -4701,7 +4824,7 @@ async function chat(agent2, options = {}) {
4701
4824
  "chat: expected an assistant message at head. Call context.continue(input) before chat()."
4702
4825
  );
4703
4826
  }
4704
- const assistantMsgId = head.id;
4827
+ const initialAssistantMsgId = head.id;
4705
4828
  const uiMessages = await context.getMessages();
4706
4829
  const streamContextVariables = options.contextVariables === void 0 ? {} : options.contextVariables;
4707
4830
  const [title, result] = await Promise.all([
@@ -4723,42 +4846,30 @@ async function chat(agent2, options = {}) {
4723
4846
  sendReasoning: true,
4724
4847
  sendSources: true,
4725
4848
  originalMessages: uiMessages,
4726
- generateMessageId: () => assistantMsgId,
4849
+ generateMessageId: () => initialAssistantMsgId,
4727
4850
  messageMetadata: options.messageMetadata ?? defaultChatMessageMetadata
4728
4851
  });
4729
4852
  return createUIMessageStream2({
4730
4853
  originalMessages: uiMessages,
4731
- generateId: () => assistantMsgId,
4854
+ generateId: () => initialAssistantMsgId,
4732
4855
  onStepFinish: async ({ responseMessage }) => {
4733
- const normalizedMessage = {
4734
- ...responseMessage,
4735
- id: assistantMsgId
4736
- };
4737
- context.set(assistant(normalizedMessage));
4738
- await context.save({ branch: false });
4856
+ await context.writeAssistantSegment(responseMessage);
4739
4857
  },
4740
4858
  onFinish: async ({ responseMessage, isAborted }) => {
4741
- const normalizedMessage = {
4742
- ...responseMessage,
4743
- id: assistantMsgId
4744
- };
4859
+ let message2 = responseMessage;
4745
4860
  if (isAborted) {
4746
- normalizedMessage.parts = sanitizeAbortedParts(normalizedMessage.parts);
4861
+ message2 = { ...message2, parts: sanitizeAbortedParts(message2.parts) };
4747
4862
  }
4748
- const drained = sandbox.drainFileEvents();
4749
- const fileEvents = isAborted ? [] : drained;
4750
- const finalMetadata = await options.finalAssistantMetadata?.(normalizedMessage);
4863
+ const finalMetadata = await options.finalAssistantMetadata?.(message2);
4751
4864
  const mergedMetadata = {
4752
- ...normalizedMessage.metadata ?? {},
4753
- ...fileEvents.length > 0 ? { fileEvents } : {},
4865
+ ...message2.metadata ?? {},
4754
4866
  ...finalMetadata ?? {}
4755
4867
  };
4756
- const hasMetadata = Object.keys(mergedMetadata).length > 0;
4757
- const finalMessage = hasMetadata ? { ...normalizedMessage, metadata: mergedMetadata } : normalizedMessage;
4758
- context.set(assistant(finalMessage));
4759
- await context.save({ branch: false });
4760
- const totalUsage = await result.totalUsage;
4761
- await context.trackUsage(totalUsage);
4868
+ if (Object.keys(mergedMetadata).length > 0) {
4869
+ message2 = { ...message2, metadata: mergedMetadata };
4870
+ }
4871
+ await context.writeAssistantSegment(message2);
4872
+ await context.trackUsage(await result.totalUsage);
4762
4873
  },
4763
4874
  execute: async ({ writer }) => {
4764
4875
  writer.merge(uiStream);
@@ -5345,6 +5456,23 @@ function lastAssistantLength(spec) {
5345
5456
  };
5346
5457
  }
5347
5458
 
5459
+ // packages/context/src/lib/fragments/reminders/once.ts
5460
+ function once(id) {
5461
+ if (id.trim().length === 0) {
5462
+ throw new Error("once(id) requires a non-empty id");
5463
+ }
5464
+ return (ctx) => {
5465
+ if (ctx.firedOnceIds === void 0) {
5466
+ throw new Error(
5467
+ `once('${id}') is not supported on target:'tool-output' reminders`
5468
+ );
5469
+ }
5470
+ if (ctx.firedOnceIds.has(id)) return false;
5471
+ ctx.onceCollector?.add(id);
5472
+ return true;
5473
+ };
5474
+ }
5475
+
5348
5476
  // packages/context/src/lib/fragments/reminders/temporal/elapsed.ts
5349
5477
  function elapsedExceeds(ms) {
5350
5478
  return (ctx) => (ctx.elapsed ?? 0) >= ms;
@@ -5502,15 +5630,15 @@ function dateReminder(options) {
5502
5630
  const now = /* @__PURE__ */ new Date();
5503
5631
  const currentDate = formatDateKey(now, tz);
5504
5632
  const currentDay = formatDayOfWeek(now, tz);
5505
- let diff2 = "";
5633
+ let diff = "";
5506
5634
  if (ctx.lastMessageAt !== void 0) {
5507
5635
  const prev = new Date(ctx.lastMessageAt);
5508
- diff2 = formatDiff([
5636
+ diff = formatDiff([
5509
5637
  diffLine("date", formatDateKey(prev, tz), currentDate),
5510
5638
  diffLine("day of week", formatDayOfWeek(prev, tz), currentDay)
5511
5639
  ]);
5512
5640
  }
5513
- return `${diff2}Date: ${currentDate}
5641
+ return `${diff}Date: ${currentDate}
5514
5642
  Day of Week: ${currentDay}`;
5515
5643
  },
5516
5644
  { when: dayChanged(options), asPart: false }
@@ -5522,14 +5650,14 @@ function timeReminder(options) {
5522
5650
  const tz = resolveTz(options, ctx);
5523
5651
  const now = /* @__PURE__ */ new Date();
5524
5652
  const currentTime = formatTime(now, tz);
5525
- let diff2 = "";
5653
+ let diff = "";
5526
5654
  if (ctx.lastMessageAt !== void 0) {
5527
5655
  const prev = new Date(ctx.lastMessageAt);
5528
- diff2 = formatDiff([
5656
+ diff = formatDiff([
5529
5657
  diffLine("hour", formatHour(prev, tz), formatHour(now, tz))
5530
5658
  ]);
5531
5659
  }
5532
- return `${diff2}Time: ${currentTime}`;
5660
+ return `${diff}Time: ${currentTime}`;
5533
5661
  },
5534
5662
  { when: hourChanged(options), asPart: false }
5535
5663
  );
@@ -5540,14 +5668,14 @@ function monthReminder(options) {
5540
5668
  const tz = resolveTz(options, ctx);
5541
5669
  const now = /* @__PURE__ */ new Date();
5542
5670
  const currentMonth = formatMonthName(now, tz);
5543
- let diff2 = "";
5671
+ let diff = "";
5544
5672
  if (ctx.lastMessageAt !== void 0) {
5545
5673
  const prev = new Date(ctx.lastMessageAt);
5546
- diff2 = formatDiff([
5674
+ diff = formatDiff([
5547
5675
  diffLine("month", formatMonthName(prev, tz), currentMonth)
5548
5676
  ]);
5549
5677
  }
5550
- return `${diff2}Month: ${currentMonth}`;
5678
+ return `${diff}Month: ${currentMonth}`;
5551
5679
  },
5552
5680
  { when: monthChanged(options), asPart: false }
5553
5681
  );
@@ -5558,14 +5686,14 @@ function yearReminder(options) {
5558
5686
  const tz = resolveTz(options, ctx);
5559
5687
  const now = /* @__PURE__ */ new Date();
5560
5688
  const currentYear = formatYear(now, tz);
5561
- let diff2 = "";
5689
+ let diff = "";
5562
5690
  if (ctx.lastMessageAt !== void 0) {
5563
5691
  const prev = new Date(ctx.lastMessageAt);
5564
- diff2 = formatDiff([
5692
+ diff = formatDiff([
5565
5693
  diffLine("year", formatYear(prev, tz), currentYear)
5566
5694
  ]);
5567
5695
  }
5568
- return `${diff2}Year: ${currentYear}`;
5696
+ return `${diff}Year: ${currentYear}`;
5569
5697
  },
5570
5698
  { when: yearChanged(options), asPart: false }
5571
5699
  );
@@ -5576,14 +5704,14 @@ function seasonReminder(options) {
5576
5704
  const tz = resolveTz(options, ctx);
5577
5705
  const now = /* @__PURE__ */ new Date();
5578
5706
  const currentSeason = getSeason(getMonthIndex(now, tz));
5579
- let diff2 = "";
5707
+ let diff = "";
5580
5708
  if (ctx.lastMessageAt !== void 0) {
5581
5709
  const prev = new Date(ctx.lastMessageAt);
5582
- diff2 = formatDiff([
5710
+ diff = formatDiff([
5583
5711
  diffLine("season", getSeason(getMonthIndex(prev, tz)), currentSeason)
5584
5712
  ]);
5585
5713
  }
5586
- return `${diff2}Season: ${currentSeason}`;
5714
+ return `${diff}Season: ${currentSeason}`;
5587
5715
  },
5588
5716
  { when: seasonChanged(options), asPart: false }
5589
5717
  );
@@ -5601,11 +5729,11 @@ function localeReminder() {
5601
5729
  const current = getLocaleFromMessage(ctx.currentMessage);
5602
5730
  if (!current) return "";
5603
5731
  const last = getLocaleFromMessage(ctx.lastMessage);
5604
- const diff2 = last ? formatDiff([
5732
+ const diff = last ? formatDiff([
5605
5733
  diffLine("language", last.language, current.language),
5606
5734
  diffLine("timezone", last.timeZone, current.timeZone)
5607
5735
  ]) : "";
5608
- return `${diff2}Language: ${current.language}
5736
+ return `${diff}Language: ${current.language}
5609
5737
  Timezone: ${current.timeZone}`;
5610
5738
  },
5611
5739
  { when: whenFn, asPart: false }
@@ -5692,7 +5820,7 @@ function toolCallCount(name, spec) {
5692
5820
  function everyNTurns(n) {
5693
5821
  return ({ turn }) => turn % n === 0;
5694
5822
  }
5695
- function once() {
5823
+ function first() {
5696
5824
  return ({ turn }) => turn === 1;
5697
5825
  }
5698
5826
  function firstN(n) {
@@ -6066,6 +6194,9 @@ async function createAgentOsSandbox(options = {}) {
6066
6194
  await os.dispose();
6067
6195
  } catch {
6068
6196
  }
6197
+ },
6198
+ [Symbol.asyncDispose]() {
6199
+ return this.dispose();
6069
6200
  }
6070
6201
  };
6071
6202
  }
@@ -6114,160 +6245,6 @@ import {
6114
6245
  } from "bash-tool";
6115
6246
  import z3 from "zod";
6116
6247
 
6117
- // packages/context/src/lib/sandbox/file-events.ts
6118
- var SnapshotFailedError = class extends Error {
6119
- stderr;
6120
- constructor(message2, stderr) {
6121
- super(message2);
6122
- this.name = "SnapshotFailedError";
6123
- this.stderr = stderr;
6124
- }
6125
- };
6126
- function shellQuote(value) {
6127
- return `'${value.replace(/'/g, `'\\''`)}'`;
6128
- }
6129
- async function snapshot(execute, destination) {
6130
- const probe = await execute(`[ -d ${shellQuote(destination)} ]`);
6131
- if (probe.exitCode !== 0) return /* @__PURE__ */ new Map();
6132
- const list = await execute(
6133
- `find ${shellQuote(destination)} -type f -exec sha256sum {} +`
6134
- );
6135
- if (list.exitCode !== 0) {
6136
- throw new SnapshotFailedError(
6137
- `snapshot failed for ${destination}`,
6138
- list.stderr
6139
- );
6140
- }
6141
- const snap = /* @__PURE__ */ new Map();
6142
- if (!list.stdout) return snap;
6143
- for (const line of list.stdout.split("\n")) {
6144
- if (line.length < 66) continue;
6145
- snap.set(line.slice(66), line.slice(0, 64));
6146
- }
6147
- return snap;
6148
- }
6149
- function lazyReadable(innerPromise) {
6150
- let reader = null;
6151
- return new ReadableStream({
6152
- async pull(controller) {
6153
- if (!reader) {
6154
- const inner = await innerPromise;
6155
- reader = inner.getReader();
6156
- }
6157
- const { done, value } = await reader.read();
6158
- if (done) controller.close();
6159
- else controller.enqueue(value);
6160
- },
6161
- async cancel(reason) {
6162
- if (reader) {
6163
- await reader.cancel(reason);
6164
- } else {
6165
- const inner = await innerPromise;
6166
- await inner.cancel(reason);
6167
- }
6168
- }
6169
- });
6170
- }
6171
- function diff(before, after) {
6172
- const events = [];
6173
- const ts = Date.now();
6174
- for (const [path5, hash] of after) {
6175
- const prior = before.get(path5);
6176
- if (prior === void 0) {
6177
- events.push({ path: path5, op: "write", timestamp: ts });
6178
- } else if (prior !== hash) {
6179
- events.push({ path: path5, op: "modify", timestamp: ts });
6180
- }
6181
- }
6182
- for (const path5 of before.keys()) {
6183
- if (!after.has(path5)) {
6184
- events.push({ path: path5, op: "delete", timestamp: ts });
6185
- }
6186
- }
6187
- return events;
6188
- }
6189
- function observeSandboxFileEvents(sandbox, options) {
6190
- const { destination } = options;
6191
- if (!destination) {
6192
- throw new Error("observeSandboxFileEvents: destination is required");
6193
- }
6194
- const innerExecute = sandbox.executeCommand.bind(sandbox);
6195
- const innerReadFile = sandbox.readFile.bind(sandbox);
6196
- const innerWriteFiles = sandbox.writeFiles.bind(sandbox);
6197
- const buffer = [];
6198
- const observe = async (fn) => {
6199
- const before = await snapshot(innerExecute, destination);
6200
- const takeAfter = async () => {
6201
- const after = await snapshot(innerExecute, destination);
6202
- buffer.push(...diff(before, after));
6203
- };
6204
- try {
6205
- const result = await fn();
6206
- await takeAfter();
6207
- return result;
6208
- } catch (err) {
6209
- await takeAfter().catch(() => {
6210
- });
6211
- throw err;
6212
- }
6213
- };
6214
- const decorated = {
6215
- async executeCommand(command, options2) {
6216
- return observe(() => innerExecute(command, options2));
6217
- },
6218
- async readFile(path5) {
6219
- const content = await innerReadFile(path5);
6220
- buffer.push({ path: path5, op: "read", timestamp: Date.now() });
6221
- return content;
6222
- },
6223
- async writeFiles(files) {
6224
- await observe(() => innerWriteFiles(files));
6225
- },
6226
- dispose: sandbox.dispose.bind(sandbox)
6227
- };
6228
- if (sandbox.spawn) {
6229
- const innerSpawn = sandbox.spawn.bind(sandbox);
6230
- decorated.spawn = (command, options2) => {
6231
- const started = (async () => {
6232
- const before = await snapshot(innerExecute, destination).catch(
6233
- () => /* @__PURE__ */ new Map()
6234
- );
6235
- const child = innerSpawn(command, options2);
6236
- return { before, child };
6237
- })();
6238
- const exit = (async () => {
6239
- const { before, child } = await started;
6240
- try {
6241
- return await child.exit;
6242
- } finally {
6243
- try {
6244
- const after = await snapshot(innerExecute, destination);
6245
- buffer.push(...diff(before, after));
6246
- } catch {
6247
- }
6248
- }
6249
- })();
6250
- const stdoutPromise = started.then((s) => s.child.stdout);
6251
- const stderrPromise = started.then((s) => s.child.stderr);
6252
- stdoutPromise.catch(() => {
6253
- });
6254
- stderrPromise.catch(() => {
6255
- });
6256
- return {
6257
- stdout: lazyReadable(stdoutPromise),
6258
- stderr: lazyReadable(stderrPromise),
6259
- exit
6260
- };
6261
- };
6262
- }
6263
- return {
6264
- sandbox: decorated,
6265
- drain() {
6266
- return buffer.splice(0, buffer.length);
6267
- }
6268
- };
6269
- }
6270
-
6271
6248
  // packages/context/src/lib/sandbox/upload-skills.ts
6272
6249
  import * as path3 from "node:path";
6273
6250
 
@@ -6434,10 +6411,7 @@ async function createBashTool(options) {
6434
6411
  destination,
6435
6412
  ...rest
6436
6413
  } = options;
6437
- const observer = observeSandboxFileEvents(backend, {
6438
- destination: destination ?? "/workspace"
6439
- });
6440
- const sandbox = withAbortSignal(withBashExceptionCatch(observer.sandbox));
6414
+ const sandbox = withAbortSignal(withBashExceptionCatch(backend));
6441
6415
  const combinedInstructions = [extraInstructions, REASONING_INSTRUCTION].filter(Boolean).join("\n\n");
6442
6416
  const toolkit = await externalCreateBashTool({
6443
6417
  ...rest,
@@ -6485,14 +6459,30 @@ async function createBashTool(options) {
6485
6459
  return { type: "json", value: visible };
6486
6460
  }
6487
6461
  });
6488
- const skills2 = await uploadSkills(sandbox, skillInputs);
6462
+ const upstreamWriteFile = toolkit.tools.writeFile;
6463
+ const originalWriteExecute = upstreamWriteFile.execute;
6464
+ const writeFileBuilder = tool2;
6465
+ const writeFile = writeFileBuilder({
6466
+ ...upstreamWriteFile,
6467
+ execute: async (input, options2) => {
6468
+ if (!originalWriteExecute) {
6469
+ throw new Error("writeFile tool execution is not available");
6470
+ }
6471
+ try {
6472
+ return await originalWriteExecute(input, options2);
6473
+ } catch (err) {
6474
+ if (err instanceof BashException) return err.format();
6475
+ throw err;
6476
+ }
6477
+ }
6478
+ });
6479
+ const skills2 = await uploadSkills(backend, skillInputs);
6489
6480
  return {
6490
6481
  ...toolkit,
6491
6482
  sandbox,
6492
6483
  bash,
6493
- tools: { ...toolkit.tools, bash },
6494
- skills: skills2,
6495
- drainFileEvents: () => observer.drain()
6484
+ tools: { ...toolkit.tools, bash, writeFile },
6485
+ skills: skills2
6496
6486
  };
6497
6487
  }
6498
6488
 
@@ -6595,6 +6585,13 @@ function resolveRealCwd(ctx) {
6595
6585
  // packages/context/src/lib/sandbox/daytona-sandbox.ts
6596
6586
  import "bash-tool";
6597
6587
  import { randomUUID } from "node:crypto";
6588
+
6589
+ // packages/context/src/lib/sandbox/shell-quote.ts
6590
+ function shellQuote(s) {
6591
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6592
+ }
6593
+
6594
+ // packages/context/src/lib/sandbox/daytona-sandbox.ts
6598
6595
  var DAYTONA_DEFAULT_DESTINATION = "/home/daytona";
6599
6596
  var DAYTONA_EXIT_POLL_INTERVAL_MS = 250;
6600
6597
  var DAYTONA_EXIT_POLL_TIMEOUT_MS = 3e4;
@@ -6817,7 +6814,7 @@ function createDaytonaSandboxMethods(args) {
6817
6814
  async writeFiles(files) {
6818
6815
  try {
6819
6816
  for (const dir of uniqueParentDirectories(files.map((f) => f.path))) {
6820
- const result = await executeCommand(`mkdir -p ${shellQuote2(dir)}`);
6817
+ const result = await executeCommand(`mkdir -p ${shellQuote(dir)}`);
6821
6818
  if (result.exitCode !== 0) {
6822
6819
  throw new DaytonaCommandError(
6823
6820
  `Failed to create directory "${dir}": ${result.stderr}`
@@ -6840,6 +6837,9 @@ function createDaytonaSandboxMethods(args) {
6840
6837
  }
6841
6838
  },
6842
6839
  async dispose() {
6840
+ },
6841
+ [Symbol.asyncDispose]() {
6842
+ return this.dispose();
6843
6843
  }
6844
6844
  };
6845
6845
  }
@@ -7007,17 +7007,17 @@ function createTextReadable() {
7007
7007
  };
7008
7008
  }
7009
7009
  function buildSessionCommand(command, options) {
7010
- const cwdPrefix = options.cwd ? `cd ${shellQuote2(options.cwd)} && ` : "";
7010
+ const cwdPrefix = options.cwd ? `cd ${shellQuote(options.cwd)} && ` : "";
7011
7011
  const env = options.env ?? {};
7012
7012
  const entries = Object.entries(env);
7013
7013
  if (entries.length === 0) {
7014
- return `sh -lc ${shellQuote2(`${cwdPrefix}${command}`)}`;
7014
+ return `sh -lc ${shellQuote(`${cwdPrefix}${command}`)}`;
7015
7015
  }
7016
7016
  for (const [key] of entries) {
7017
7017
  validateEnvKey(key);
7018
7018
  }
7019
- const exports = entries.map(([key, value]) => `export ${key}=${shellQuote2(value)}`).join("; ");
7020
- return `sh -lc ${shellQuote2(`${exports}; ${cwdPrefix}${command}`)}`;
7019
+ const exports = entries.map(([key, value]) => `export ${key}=${shellQuote(value)}`).join("; ");
7020
+ return `sh -lc ${shellQuote(`${exports}; ${cwdPrefix}${command}`)}`;
7021
7021
  }
7022
7022
  function validateEnvKey(key) {
7023
7023
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
@@ -7043,9 +7043,6 @@ function abortedExitInfo() {
7043
7043
  success: false
7044
7044
  };
7045
7045
  }
7046
- function shellQuote2(value) {
7047
- return `'${value.replace(/'/g, `'\\''`)}'`;
7048
- }
7049
7046
  function uniqueParentDirectories(paths) {
7050
7047
  const dirs = /* @__PURE__ */ new Set();
7051
7048
  for (const path5 of paths) {
@@ -7222,9 +7219,6 @@ function isDebianBased(image) {
7222
7219
  const debianPatterns = ["debian", "ubuntu", "node", "python"];
7223
7220
  return debianPatterns.some((pattern) => lower.includes(pattern));
7224
7221
  }
7225
- function shellQuote3(s) {
7226
- return `'${s.replace(/'/g, `'\\''`)}'`;
7227
- }
7228
7222
  function createInstallerContext(containerId, image) {
7229
7223
  const packageManager = isDebianBased(image) ? "apt-get" : "apk";
7230
7224
  let archPromise = null;
@@ -7270,7 +7264,7 @@ function createInstallerContext(containerId, image) {
7270
7264
  };
7271
7265
  const installPackages = async (packages) => {
7272
7266
  if (packages.length === 0) return;
7273
- const quoted = packages.map(shellQuote3).join(" ");
7267
+ const quoted = packages.map(shellQuote).join(" ");
7274
7268
  let cmd;
7275
7269
  if (packageManager === "apt-get") {
7276
7270
  cmd = aptUpdated ? `apt-get install -y ${quoted}` : `apt-get update && apt-get install -y ${quoted}`;
@@ -7292,7 +7286,7 @@ function createInstallerContext(containerId, image) {
7292
7286
  const ensureTool = async (checkName, installName) => {
7293
7287
  const cacheKey = installName ?? checkName;
7294
7288
  if (ensuredTools.has(cacheKey)) return;
7295
- const check = await exec(`which ${shellQuote3(checkName)}`);
7289
+ const check = await exec(`which ${shellQuote(checkName)}`);
7296
7290
  if (check.exitCode === 0) {
7297
7291
  ensuredTools.add(cacheKey);
7298
7292
  return;
@@ -7326,9 +7320,19 @@ var DockerSandboxStrategy = class {
7326
7320
  env;
7327
7321
  name;
7328
7322
  command;
7323
+ securityOpt;
7324
+ platform;
7329
7325
  createdVolumes = /* @__PURE__ */ new Set();
7330
7326
  constructor(args = {}) {
7331
- const { volumes = [], resources = {}, env = {}, name, command } = args;
7327
+ const {
7328
+ volumes = [],
7329
+ resources = {},
7330
+ env = {},
7331
+ name,
7332
+ command,
7333
+ securityOpt = [],
7334
+ platform
7335
+ } = args;
7332
7336
  for (const key of Object.keys(env)) {
7333
7337
  validateEnvKey2(key);
7334
7338
  }
@@ -7342,6 +7346,8 @@ var DockerSandboxStrategy = class {
7342
7346
  this.env = env;
7343
7347
  this.name = name;
7344
7348
  this.command = command;
7349
+ this.securityOpt = securityOpt;
7350
+ this.platform = platform;
7345
7351
  }
7346
7352
  async create() {
7347
7353
  const image = await this.getImage();
@@ -7543,6 +7549,12 @@ var DockerSandboxStrategy = class {
7543
7549
  "-w",
7544
7550
  "/workspace"
7545
7551
  ];
7552
+ if (this.platform) {
7553
+ args.push("--platform", this.platform);
7554
+ }
7555
+ for (const opt of this.securityOpt) {
7556
+ args.push("--security-opt", opt);
7557
+ }
7546
7558
  for (const [key, value] of Object.entries(this.env)) {
7547
7559
  args.push("-e", `${key}=${value}`);
7548
7560
  }
@@ -7736,6 +7748,9 @@ var DockerSandboxStrategy = class {
7736
7748
  dispose: async () => {
7737
7749
  await this.stopContainer(containerId);
7738
7750
  await this.cleanupCreatedVolumes();
7751
+ },
7752
+ [Symbol.asyncDispose]() {
7753
+ return this.dispose();
7739
7754
  }
7740
7755
  };
7741
7756
  return sandbox;
@@ -7802,7 +7817,9 @@ var RuntimeStrategy = class extends DockerSandboxStrategy {
7802
7817
  resources: args.resources,
7803
7818
  env: args.env,
7804
7819
  name: args.name,
7805
- command: args.command
7820
+ command: args.command,
7821
+ securityOpt: args.securityOpt,
7822
+ platform: args.platform
7806
7823
  });
7807
7824
  this.image = args.image ?? "alpine:latest";
7808
7825
  this.installers = args.installers ?? [];
@@ -7821,21 +7838,25 @@ var DockerfileStrategy = class extends DockerSandboxStrategy {
7821
7838
  imageTag;
7822
7839
  dockerfile;
7823
7840
  dockerContext;
7841
+ showBuildLogs;
7824
7842
  constructor(args) {
7825
7843
  super({
7826
7844
  volumes: args.volumes,
7827
7845
  resources: args.resources,
7828
7846
  env: args.env,
7829
7847
  name: args.name,
7830
- command: args.command
7848
+ command: args.command,
7849
+ securityOpt: args.securityOpt,
7850
+ platform: args.platform
7831
7851
  });
7832
7852
  this.dockerfile = args.dockerfile;
7833
7853
  this.dockerContext = args.context ?? ".";
7854
+ this.showBuildLogs = args.showBuildLogs ?? false;
7834
7855
  this.imageTag = this.computeImageTag();
7835
7856
  }
7836
7857
  computeImageTag() {
7837
7858
  const content = this.isInlineDockerfile() ? this.dockerfile : readFileSync2(this.dockerfile, "utf-8");
7838
- const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
7859
+ const hash = createHash("sha256").update(content).update(this.platform ?? "").digest("hex").slice(0, 12);
7839
7860
  return `sandbox-${hash}`;
7840
7861
  }
7841
7862
  isInlineDockerfile() {
@@ -7860,24 +7881,54 @@ var DockerfileStrategy = class extends DockerSandboxStrategy {
7860
7881
  }
7861
7882
  async buildImage() {
7862
7883
  try {
7884
+ const platformFlag = this.platform ? `--platform ${this.platform} ` : "";
7863
7885
  if (this.isInlineDockerfile()) {
7864
- const buildCmd = `echo '${this.dockerfile.replace(/'/g, "'\\''")}' | docker build -t ${this.imageTag} -f - ${this.dockerContext}`;
7865
- await spawn3("sh", ["-c", buildCmd]);
7886
+ const buildCmd = `echo '${this.dockerfile.replace(/'/g, "'\\''")}' | docker build ${platformFlag}-t ${this.imageTag} -f - ${this.dockerContext}`;
7887
+ if (this.showBuildLogs) {
7888
+ await this.runStreamed("sh", ["-c", buildCmd]);
7889
+ } else {
7890
+ await spawn3("sh", ["-c", buildCmd]);
7891
+ }
7866
7892
  } else {
7867
- await spawn3("docker", [
7893
+ const args = [
7868
7894
  "build",
7895
+ ...this.platform ? ["--platform", this.platform] : [],
7869
7896
  "-t",
7870
7897
  this.imageTag,
7871
7898
  "-f",
7872
7899
  this.dockerfile,
7873
7900
  this.dockerContext
7874
- ]);
7901
+ ];
7902
+ if (this.showBuildLogs) {
7903
+ await this.runStreamed("docker", args);
7904
+ } else {
7905
+ await spawn3("docker", args);
7906
+ }
7875
7907
  }
7876
7908
  } catch (error) {
7877
7909
  const err = error;
7878
7910
  throw new DockerfileBuildError(err.stderr || err.message);
7879
7911
  }
7880
7912
  }
7913
+ /**
7914
+ * Run a build command with stdio inherited so its output streams live to the
7915
+ * parent terminal. On failure the build error is already on screen; the
7916
+ * rejection just carries the exit code.
7917
+ */
7918
+ runStreamed(command, args) {
7919
+ return new Promise((resolve4, reject) => {
7920
+ const child = childSpawn(command, args, { stdio: "inherit" });
7921
+ child.once("error", reject);
7922
+ child.once(
7923
+ "exit",
7924
+ (code) => code === 0 ? resolve4() : reject(
7925
+ new Error(
7926
+ `docker build exited with code ${code} (see build output above)`
7927
+ )
7928
+ )
7929
+ );
7930
+ });
7931
+ }
7881
7932
  };
7882
7933
  var ComposeStrategy = class extends DockerSandboxStrategy {
7883
7934
  projectName;
@@ -7993,11 +8044,14 @@ async function createDockerSandbox(options = {}) {
7993
8044
  strategy = new DockerfileStrategy({
7994
8045
  dockerfile: options.dockerfile,
7995
8046
  context: options.context,
8047
+ showBuildLogs: options.showBuildLogs,
7996
8048
  volumes: options.volumes,
7997
8049
  resources: options.resources,
7998
8050
  env: options.env,
7999
8051
  name: options.name,
8000
- command: options.command
8052
+ command: options.command,
8053
+ securityOpt: options.securityOpt,
8054
+ platform: options.platform
8001
8055
  });
8002
8056
  } else {
8003
8057
  strategy = new RuntimeStrategy({
@@ -8007,7 +8061,9 @@ async function createDockerSandbox(options = {}) {
8007
8061
  resources: options.resources,
8008
8062
  env: options.env,
8009
8063
  name: options.name,
8010
- command: options.command
8064
+ command: options.command,
8065
+ securityOpt: options.securityOpt,
8066
+ platform: options.platform
8011
8067
  });
8012
8068
  }
8013
8069
  return strategy.create();
@@ -8021,6 +8077,338 @@ async function useSandbox(options, fn) {
8021
8077
  }
8022
8078
  }
8023
8079
 
8080
+ // packages/context/src/lib/sandbox/strace/file-changes.ts
8081
+ import { randomUUID as randomUUID2 } from "node:crypto";
8082
+ import { posix as posix2 } from "node:path";
8083
+
8084
+ // packages/context/src/lib/sandbox/strace/index.ts
8085
+ import { posix } from "node:path";
8086
+ var STRACE_FLAGS = "-f -y -qq -e trace=%file,write,pwrite64,writev";
8087
+ function buildStraceCommand(command, traceFile, traceDir) {
8088
+ return `mkdir -p ${shellQuote(traceDir)} 2>/dev/null; strace ${STRACE_FLAGS} -o ${shellQuote(traceFile)} -- sh -c ${shellQuote(command)}`;
8089
+ }
8090
+ function buildTracedCommand(command, traceFile, traceDir, sentinel) {
8091
+ return `mkdir -p ${shellQuote(traceDir)} 2>/dev/null; strace ${STRACE_FLAGS} -o ${shellQuote(traceFile)} -- sh -c ${shellQuote(command)}; __dat_rc=$?; printf '\\n%s\\n' ${shellQuote(sentinel)}; base64 ${shellQuote(traceFile)} 2>/dev/null; rm -f ${shellQuote(traceFile)} 2>/dev/null; exit $__dat_rc`;
8092
+ }
8093
+ function splitTracedOutput(stdout, sentinel) {
8094
+ const marker = `
8095
+ ${sentinel}
8096
+ `;
8097
+ const at = stdout.indexOf(marker);
8098
+ if (at === -1) return { stdout, trace: "" };
8099
+ const base64 = stdout.slice(at + marker.length);
8100
+ return {
8101
+ stdout: stdout.slice(0, at),
8102
+ trace: base64 ? Buffer.from(base64, "base64").toString("utf8") : ""
8103
+ };
8104
+ }
8105
+ var CREATE_OR_TRUNC_FLAGS = /O_(CREAT|TRUNC)/;
8106
+ var RENAME_SYSCALLS = /* @__PURE__ */ new Set(["rename", "renameat", "renameat2"]);
8107
+ var DELETE_SYSCALLS = /* @__PURE__ */ new Set(["unlink", "unlinkat", "rmdir"]);
8108
+ var MKDIR_SYSCALLS = /* @__PURE__ */ new Set(["mkdir", "mkdirat"]);
8109
+ var LINK_SYSCALLS = /* @__PURE__ */ new Set(["link", "linkat", "symlink", "symlinkat"]);
8110
+ var OPEN_SYSCALLS = /* @__PURE__ */ new Set(["open", "openat", "openat2", "creat"]);
8111
+ var WRITE_SYSCALLS = /* @__PURE__ */ new Set([
8112
+ "write",
8113
+ "pwrite64",
8114
+ "pwrite",
8115
+ "writev",
8116
+ "pwritev2"
8117
+ ]);
8118
+ var TRUNCATE_SYSCALLS = /* @__PURE__ */ new Set(["truncate", "ftruncate"]);
8119
+ function stitchLines(raw) {
8120
+ const pending = /* @__PURE__ */ new Map();
8121
+ const out = [];
8122
+ for (const original of raw.split("\n")) {
8123
+ const line = original.trimEnd();
8124
+ if (!line) continue;
8125
+ const pid = line.match(/^\s*(\d+)\s+/)?.[1] ?? "0";
8126
+ if (/<unfinished \.\.\.>\s*$/.test(line)) {
8127
+ pending.set(pid, line.replace(/<unfinished \.\.\.>\s*$/, ""));
8128
+ continue;
8129
+ }
8130
+ const resumed = line.match(/<\.\.\.\s+\S+\s+resumed>(.*)$/);
8131
+ if (resumed) {
8132
+ const head = pending.get(pid);
8133
+ pending.delete(pid);
8134
+ if (head !== void 0) out.push(head + resumed[1]);
8135
+ continue;
8136
+ }
8137
+ out.push(line);
8138
+ }
8139
+ return out;
8140
+ }
8141
+ function parseCall(line) {
8142
+ const m = line.match(
8143
+ /^\s*(?:\d+\s+)?([a-z_][a-z0-9_]*)\((.*)\)\s*=\s*(.+?)\s*$/
8144
+ );
8145
+ if (!m) return null;
8146
+ const [, syscall, args, rest] = m;
8147
+ const errnoMatch = rest.match(/^(-?\d+)\s+([A-Z][A-Z0-9_]*)/);
8148
+ if (errnoMatch) {
8149
+ return { syscall, args, retval: errnoMatch[1], errno: errnoMatch[2] };
8150
+ }
8151
+ return { syscall, args, retval: rest.split(/\s+/)[0] };
8152
+ }
8153
+ var STRACE_ESCAPE = {
8154
+ n: 10,
8155
+ t: 9,
8156
+ r: 13,
8157
+ v: 11,
8158
+ f: 12,
8159
+ '"': 34,
8160
+ "\\": 92
8161
+ };
8162
+ function decodeStraceString(s) {
8163
+ const bytes = [];
8164
+ for (let i = 0; i < s.length; ) {
8165
+ if (s[i] === "\\" && i + 1 < s.length) {
8166
+ const octal = s.slice(i + 1, i + 4).match(/^[0-7]{1,3}/)?.[0];
8167
+ if (octal) {
8168
+ bytes.push(parseInt(octal, 8) & 255);
8169
+ i += 1 + octal.length;
8170
+ continue;
8171
+ }
8172
+ const mapped = STRACE_ESCAPE[s[i + 1]];
8173
+ bytes.push(mapped ?? s.charCodeAt(i + 1));
8174
+ i += 2;
8175
+ continue;
8176
+ }
8177
+ const code = s.charCodeAt(i);
8178
+ if (code < 128) bytes.push(code);
8179
+ else for (const b of Buffer.from(s[i], "utf8")) bytes.push(b);
8180
+ i += 1;
8181
+ }
8182
+ return Buffer.from(bytes).toString("utf8");
8183
+ }
8184
+ function quotedStrings(args) {
8185
+ const out = [];
8186
+ const re = /"((?:[^"\\]|\\.)*)"/g;
8187
+ let m;
8188
+ while ((m = re.exec(args)) !== null) {
8189
+ out.push(decodeStraceString(m[1]));
8190
+ }
8191
+ return out;
8192
+ }
8193
+ function dirfdPath(args) {
8194
+ return args.match(/^\s*(?:AT_FDCWD|-?\d+)<([^>]*)>/)?.[1] ?? "/";
8195
+ }
8196
+ function fdPath(args) {
8197
+ return args.match(/^\s*-?\d+<([^>]*)>/)?.[1] ?? null;
8198
+ }
8199
+ function openFlags(args) {
8200
+ return args.match(/\bO_[A-Z_]+(?:\|O_[A-Z_]+)*/)?.[0] ?? "";
8201
+ }
8202
+ function matchesGlobs(path5, include, exclude) {
8203
+ return include.some((glob) => posix.matchesGlob(path5, glob)) && !exclude?.some((glob) => posix.matchesGlob(path5, glob));
8204
+ }
8205
+ function parseStraceTrace(raw, options) {
8206
+ const { include, exclude, traceDir, traceFile } = options;
8207
+ const now = Date.now();
8208
+ const state = /* @__PURE__ */ new Map();
8209
+ const renames = [];
8210
+ const resolve4 = (dir, p) => posix.normalize(p.startsWith("/") ? p : posix.join(dir, p));
8211
+ const write = (path5) => {
8212
+ state.set(path5, "write");
8213
+ };
8214
+ const remove = (path5) => {
8215
+ if (state.get(path5) === "write") state.delete(path5);
8216
+ else state.set(path5, "delete");
8217
+ };
8218
+ for (const line of stitchLines(raw)) {
8219
+ const call = parseCall(line);
8220
+ if (!call || call.errno || call.retval === "-1") continue;
8221
+ const { syscall, args } = call;
8222
+ const dir = dirfdPath(args);
8223
+ if (RENAME_SYSCALLS.has(syscall)) {
8224
+ const strings = quotedStrings(args);
8225
+ if (strings.length < 2) continue;
8226
+ const from = resolve4(dir, strings[0]);
8227
+ const to = resolve4(dir, strings[strings.length - 1]);
8228
+ state.delete(from);
8229
+ renames.push({ from, to });
8230
+ continue;
8231
+ }
8232
+ if (DELETE_SYSCALLS.has(syscall)) {
8233
+ const strings = quotedStrings(args);
8234
+ if (strings.length) remove(resolve4(dir, strings[0]));
8235
+ continue;
8236
+ }
8237
+ if (MKDIR_SYSCALLS.has(syscall)) {
8238
+ const strings = quotedStrings(args);
8239
+ if (strings.length) write(resolve4(dir, strings[0]));
8240
+ continue;
8241
+ }
8242
+ if (LINK_SYSCALLS.has(syscall)) {
8243
+ const strings = quotedStrings(args);
8244
+ if (strings.length) write(resolve4(dir, strings[strings.length - 1]));
8245
+ continue;
8246
+ }
8247
+ if (OPEN_SYSCALLS.has(syscall)) {
8248
+ if (!CREATE_OR_TRUNC_FLAGS.test(openFlags(args))) continue;
8249
+ const strings = quotedStrings(args);
8250
+ if (strings.length) write(resolve4(dir, strings[0]));
8251
+ continue;
8252
+ }
8253
+ if (WRITE_SYSCALLS.has(syscall)) {
8254
+ const p = fdPath(args);
8255
+ if (p && Number(call.retval) > 0) write(posix.normalize(p));
8256
+ continue;
8257
+ }
8258
+ if (TRUNCATE_SYSCALLS.has(syscall)) {
8259
+ const p = fdPath(args) ?? quotedStrings(args)[0];
8260
+ if (p) write(resolve4(dir, p));
8261
+ }
8262
+ }
8263
+ const keep = (path5) => {
8264
+ if (traceFile && path5 === traceFile) return false;
8265
+ if (traceDir && (path5 === traceDir || path5.startsWith(`${traceDir}/`))) {
8266
+ return false;
8267
+ }
8268
+ if (/^\/(proc|sys|dev)(\/|$)/.test(path5)) return false;
8269
+ return matchesGlobs(path5, include, exclude);
8270
+ };
8271
+ const changes = [];
8272
+ const emittedRenameTargets = /* @__PURE__ */ new Set();
8273
+ for (const { from, to } of renames) {
8274
+ if (!keep(to)) continue;
8275
+ if (state.get(to) === "delete") continue;
8276
+ changes.push({ op: "rename", path: to, from, timestamp: now });
8277
+ emittedRenameTargets.add(to);
8278
+ }
8279
+ const tail = [];
8280
+ for (const [path5, op] of state) {
8281
+ if (emittedRenameTargets.has(path5)) continue;
8282
+ if (keep(path5)) tail.push({ op, path: path5, timestamp: now });
8283
+ }
8284
+ tail.sort((a, b) => {
8285
+ if (a.path < b.path) return -1;
8286
+ if (a.path > b.path) return 1;
8287
+ return 0;
8288
+ });
8289
+ changes.push(...tail);
8290
+ return changes;
8291
+ }
8292
+
8293
+ // packages/context/src/lib/sandbox/strace/file-changes.ts
8294
+ var DEFAULT_TRACE_DIR = "/tmp/dat-trace";
8295
+ function stripStraceDiagnostics(stderr) {
8296
+ if (!stderr.includes("strace: ")) return stderr;
8297
+ return stderr.split("\n").filter((line) => !line.startsWith("strace: ")).join("\n");
8298
+ }
8299
+ function warnOnFileChangesError(error) {
8300
+ console.warn(
8301
+ "[withStraceFileChanges] onFileChanges threw on spawn; isolated",
8302
+ error
8303
+ );
8304
+ }
8305
+ async function withStraceFileChanges(sandbox, options) {
8306
+ const { include, exclude, onFileChanges } = options;
8307
+ const onError = options.onError ?? warnOnFileChangesError;
8308
+ const traceDir = options.traceDir ?? DEFAULT_TRACE_DIR;
8309
+ const innerExecute = sandbox.executeCommand.bind(sandbox);
8310
+ const innerReadFile = sandbox.readFile.bind(sandbox);
8311
+ const innerWriteFiles = sandbox.writeFiles.bind(sandbox);
8312
+ const removeTrace = (traceFile) => void innerExecute(`rm -f ${shellQuote(traceFile)}`).catch(() => {
8313
+ });
8314
+ const readChanges = async (traceFile) => {
8315
+ try {
8316
+ return parseStraceTrace(await innerReadFile(traceFile), {
8317
+ include,
8318
+ exclude,
8319
+ traceFile,
8320
+ traceDir
8321
+ });
8322
+ } catch {
8323
+ return [];
8324
+ } finally {
8325
+ removeTrace(traceFile);
8326
+ }
8327
+ };
8328
+ const collectSpawn = async (traceFile) => {
8329
+ const changes = await readChanges(traceFile);
8330
+ if (!changes.length || !onFileChanges) return;
8331
+ try {
8332
+ await onFileChanges(changes);
8333
+ } catch (error) {
8334
+ onError(error);
8335
+ }
8336
+ };
8337
+ const decorated = {
8338
+ async executeCommand(command, execOptions) {
8339
+ const traceFile = `${traceDir}/${randomUUID2()}.strace`;
8340
+ const sentinel = `__dat_trace_${randomUUID2()}__`;
8341
+ const wrapped = buildTracedCommand(
8342
+ command,
8343
+ traceFile,
8344
+ traceDir,
8345
+ sentinel
8346
+ );
8347
+ let raw;
8348
+ try {
8349
+ raw = await innerExecute(wrapped, execOptions);
8350
+ } catch (error) {
8351
+ removeTrace(traceFile);
8352
+ throw error;
8353
+ }
8354
+ const { stdout, trace } = splitTracedOutput(raw.stdout, sentinel);
8355
+ const result = {
8356
+ ...raw,
8357
+ stdout,
8358
+ stderr: stripStraceDiagnostics(raw.stderr)
8359
+ };
8360
+ if (execOptions?.signal?.aborted) return result;
8361
+ const changes = trace ? parseStraceTrace(trace, { include, exclude, traceFile, traceDir }) : [];
8362
+ if (changes.length) {
8363
+ useBashMeta()?.setHidden({ fileChanges: changes });
8364
+ await onFileChanges?.(changes);
8365
+ }
8366
+ return result;
8367
+ },
8368
+ readFile: innerReadFile,
8369
+ // The writeFile tool mutates the filesystem outside strace's view, so
8370
+ // observe it directly: synthesize a `write` change per file (under the
8371
+ // observation root) and run it through onFileChanges. A throw propagates to
8372
+ // the writeFile tool's execute (which rejects), the same gate as the bash
8373
+ // path — except there's no CommandResult here, so throw a plain Error to
8374
+ // reject; BashException.format() is meaningful only for bash commands.
8375
+ writeFiles: async (files) => {
8376
+ await innerWriteFiles(files);
8377
+ const now = Date.now();
8378
+ const changes = files.map((f) => posix2.normalize(f.path)).filter((path5) => matchesGlobs(path5, include, exclude)).map((path5) => ({ op: "write", path: path5, timestamp: now }));
8379
+ if (changes.length) await onFileChanges?.(changes);
8380
+ },
8381
+ dispose: async () => {
8382
+ await innerExecute(`rm -rf ${shellQuote(traceDir)}`).catch(() => {
8383
+ });
8384
+ await sandbox.dispose();
8385
+ },
8386
+ [Symbol.asyncDispose]() {
8387
+ return this.dispose();
8388
+ }
8389
+ };
8390
+ if (sandbox.spawn) {
8391
+ const innerSpawn = sandbox.spawn.bind(sandbox);
8392
+ decorated.spawn = (command, spawnOptions) => {
8393
+ const traceFile = `${traceDir}/${randomUUID2()}.strace`;
8394
+ const child = innerSpawn(
8395
+ buildStraceCommand(command, traceFile, traceDir),
8396
+ spawnOptions
8397
+ );
8398
+ const exit = (async () => {
8399
+ try {
8400
+ return await child.exit;
8401
+ } finally {
8402
+ await collectSpawn(traceFile).catch(() => {
8403
+ });
8404
+ }
8405
+ })();
8406
+ return { stdout: child.stdout, stderr: child.stderr, exit };
8407
+ };
8408
+ }
8409
+ return decorated;
8410
+ }
8411
+
8024
8412
  // packages/context/src/lib/sandbox/gcs.ts
8025
8413
  function gcs(options) {
8026
8414
  return {
@@ -8031,22 +8419,57 @@ function gcs(options) {
8031
8419
  };
8032
8420
  }
8033
8421
 
8034
- // packages/context/src/lib/sandbox/installers/package-manager.ts
8035
- var PackageInstaller = class extends Installer {
8422
+ // packages/context/src/lib/sandbox/installers/bin.ts
8423
+ import { basename, posix as posix3 } from "node:path";
8424
+ var NOT_FOUND_EXIT = 11;
8425
+ var CHMOD_FAILED_EXIT = 12;
8426
+ var BinInstaller = class extends Installer {
8036
8427
  kind;
8037
- packages;
8038
- constructor(packages) {
8428
+ binary;
8429
+ options;
8430
+ #name;
8431
+ #target;
8432
+ constructor(binary, options = {}) {
8039
8433
  super();
8040
- this.packages = packages;
8041
- this.kind = packages.length === 0 ? "pkg:<empty>" : `pkg:${packages.join(",")}`;
8434
+ this.binary = binary;
8435
+ this.options = options;
8436
+ this.#name = resolveName(binary, options);
8437
+ this.#target = options.target ?? posix3.join("/usr/local/bin", this.#name);
8438
+ this.kind = `bin:${this.#name}`;
8042
8439
  }
8043
8440
  async install(ctx) {
8044
- if (this.packages.length === 0) return;
8045
- await ctx.installPackages([...this.packages]);
8441
+ const b = shellQuote(this.binary);
8442
+ const t = shellQuote(this.#target);
8443
+ const dir = shellQuote(posix3.dirname(this.#target));
8444
+ const result = await ctx.exec(
8445
+ `test -f ${b} || { echo "binary not found at ${this.binary}" >&2; exit ${NOT_FOUND_EXIT}; }; if ! test -x ${b}; then chmod +x ${b} || exit ${CHMOD_FAILED_EXIT}; fi; mkdir -p ${dir} && ln -sf ${b} ${t}`
8446
+ );
8447
+ if (result.exitCode === 0) return;
8448
+ throw new InstallError({
8449
+ target: this.#name,
8450
+ source: "bin",
8451
+ reason: explainFailure(result.exitCode, result.stderr, this.binary),
8452
+ containerId: ctx.containerId
8453
+ });
8046
8454
  }
8047
8455
  };
8048
- function pkg(packages) {
8049
- return new PackageInstaller(packages);
8456
+ function bin(binary, options) {
8457
+ return new BinInstaller(binary, options);
8458
+ }
8459
+ function explainFailure(exitCode, stderr, binary) {
8460
+ if (exitCode === NOT_FOUND_EXIT) {
8461
+ return `binary not found at ${binary}`;
8462
+ }
8463
+ if (exitCode === CHMOD_FAILED_EXIT && /read-only file system/i.test(stderr)) {
8464
+ return `${binary} is not executable and the bind mount is read-only \u2014 run \`chmod +x\` on the host, or mount with \`readOnly: false\`. (${stderr.trim()})`;
8465
+ }
8466
+ return stderr || `bin installer failed with exit code ${exitCode}`;
8467
+ }
8468
+ function resolveName(binary, options) {
8469
+ if (options.name) return options.name;
8470
+ const base = basename(binary);
8471
+ const dot = base.lastIndexOf(".");
8472
+ return dot > 0 ? base.slice(0, dot) : base;
8050
8473
  }
8051
8474
 
8052
8475
  // packages/context/src/lib/sandbox/installers/url-binary.ts
@@ -8103,9 +8526,9 @@ async function downloadAndInstall(ctx, name, url, binaryPath, source = "url") {
8103
8526
  function buildTarGzInstallCmd(name, url, binaryPathInArchive) {
8104
8527
  return `
8105
8528
  set -e
8106
- NAME=${shellQuote3(name)}
8107
- URL=${shellQuote3(url)}
8108
- BIN_IN_ARCHIVE=${shellQuote3(binaryPathInArchive)}
8529
+ NAME=${shellQuote(name)}
8530
+ URL=${shellQuote(url)}
8531
+ BIN_IN_ARCHIVE=${shellQuote(binaryPathInArchive)}
8109
8532
  TMPDIR=$(mktemp -d)
8110
8533
  cd "$TMPDIR"
8111
8534
  curl -fsSL "$URL" -o archive.tar.gz
@@ -8124,13 +8547,39 @@ function buildTarGzInstallCmd(name, url, binaryPathInArchive) {
8124
8547
  }
8125
8548
  function buildRawInstallCmd(name, url) {
8126
8549
  return `
8127
- NAME=${shellQuote3(name)}
8128
- URL=${shellQuote3(url)}
8550
+ NAME=${shellQuote(name)}
8551
+ URL=${shellQuote(url)}
8129
8552
  curl -fsSL "$URL" -o "/usr/local/bin/$NAME"
8130
8553
  chmod +x "/usr/local/bin/$NAME"
8131
8554
  `;
8132
8555
  }
8133
8556
 
8557
+ // packages/context/src/lib/sandbox/installers/github-release.ts
8558
+ var GithubReleaseInstaller = class extends Installer {
8559
+ kind;
8560
+ options;
8561
+ constructor(options) {
8562
+ super();
8563
+ this.options = options;
8564
+ this.kind = `github-release:${options.owner}/${options.repo}@${options.version}`;
8565
+ }
8566
+ async install(ctx) {
8567
+ const arch = await ctx.arch();
8568
+ const assetName = this.options.asset(arch);
8569
+ const url = `https://github.com/${this.options.owner}/${this.options.repo}/releases/download/${this.options.version}/${assetName}`;
8570
+ await downloadAndInstall(
8571
+ ctx,
8572
+ this.options.name,
8573
+ url,
8574
+ this.options.binaryPath,
8575
+ "github-release"
8576
+ );
8577
+ }
8578
+ };
8579
+ function githubRelease(options) {
8580
+ return new GithubReleaseInstaller(options);
8581
+ }
8582
+
8134
8583
  // packages/context/src/lib/sandbox/installers/npm.ts
8135
8584
  var NODE_BINARIES = ["node", "npm"];
8136
8585
  var ENSURE_RUNTIME_HINT = "Pass `{ ensureRuntime: true }` to auto-install, or add it via `pkg([...])`.";
@@ -8178,57 +8627,22 @@ async function ensureNodeRuntime(ctx, ensure) {
8178
8627
  }
8179
8628
  }
8180
8629
 
8181
- // packages/context/src/lib/sandbox/installers/bin.ts
8182
- import { basename, posix } from "node:path";
8183
- var NOT_FOUND_EXIT = 11;
8184
- var CHMOD_FAILED_EXIT = 12;
8185
- var BinInstaller = class extends Installer {
8630
+ // packages/context/src/lib/sandbox/installers/package-manager.ts
8631
+ var PackageInstaller = class extends Installer {
8186
8632
  kind;
8187
- binary;
8188
- options;
8189
- #name;
8190
- #target;
8191
- constructor(binary, options = {}) {
8633
+ packages;
8634
+ constructor(packages) {
8192
8635
  super();
8193
- this.binary = binary;
8194
- this.options = options;
8195
- this.#name = resolveName(binary, options);
8196
- this.#target = options.target ?? posix.join("/usr/local/bin", this.#name);
8197
- this.kind = `bin:${this.#name}`;
8636
+ this.packages = packages;
8637
+ this.kind = packages.length === 0 ? "pkg:<empty>" : `pkg:${packages.join(",")}`;
8198
8638
  }
8199
8639
  async install(ctx) {
8200
- const b = shellQuote3(this.binary);
8201
- const t = shellQuote3(this.#target);
8202
- const dir = shellQuote3(posix.dirname(this.#target));
8203
- const result = await ctx.exec(
8204
- `test -f ${b} || { echo "binary not found at ${this.binary}" >&2; exit ${NOT_FOUND_EXIT}; }; if ! test -x ${b}; then chmod +x ${b} || exit ${CHMOD_FAILED_EXIT}; fi; mkdir -p ${dir} && ln -sf ${b} ${t}`
8205
- );
8206
- if (result.exitCode === 0) return;
8207
- throw new InstallError({
8208
- target: this.#name,
8209
- source: "bin",
8210
- reason: explainFailure(result.exitCode, result.stderr, this.binary),
8211
- containerId: ctx.containerId
8212
- });
8640
+ if (this.packages.length === 0) return;
8641
+ await ctx.installPackages([...this.packages]);
8213
8642
  }
8214
8643
  };
8215
- function bin(binary, options) {
8216
- return new BinInstaller(binary, options);
8217
- }
8218
- function explainFailure(exitCode, stderr, binary) {
8219
- if (exitCode === NOT_FOUND_EXIT) {
8220
- return `binary not found at ${binary}`;
8221
- }
8222
- if (exitCode === CHMOD_FAILED_EXIT && /read-only file system/i.test(stderr)) {
8223
- return `${binary} is not executable and the bind mount is read-only \u2014 run \`chmod +x\` on the host, or mount with \`readOnly: false\`. (${stderr.trim()})`;
8224
- }
8225
- return stderr || `bin installer failed with exit code ${exitCode}`;
8226
- }
8227
- function resolveName(binary, options) {
8228
- if (options.name) return options.name;
8229
- const base = basename(binary);
8230
- const dot = base.lastIndexOf(".");
8231
- return dot > 0 ? base.slice(0, dot) : base;
8644
+ function pkg(packages) {
8645
+ return new PackageInstaller(packages);
8232
8646
  }
8233
8647
 
8234
8648
  // packages/context/src/lib/sandbox/installers/pip.ts
@@ -8282,32 +8696,6 @@ function formatVersion(version) {
8282
8696
  return /^[<>=!~]/.test(version) ? version : `==${version}`;
8283
8697
  }
8284
8698
 
8285
- // packages/context/src/lib/sandbox/installers/github-release.ts
8286
- var GithubReleaseInstaller = class extends Installer {
8287
- kind;
8288
- options;
8289
- constructor(options) {
8290
- super();
8291
- this.options = options;
8292
- this.kind = `github-release:${options.owner}/${options.repo}@${options.version}`;
8293
- }
8294
- async install(ctx) {
8295
- const arch = await ctx.arch();
8296
- const assetName = this.options.asset(arch);
8297
- const url = `https://github.com/${this.options.owner}/${this.options.repo}/releases/download/${this.options.version}/${assetName}`;
8298
- await downloadAndInstall(
8299
- ctx,
8300
- this.options.name,
8301
- url,
8302
- this.options.binaryPath,
8303
- "github-release"
8304
- );
8305
- }
8306
- };
8307
- function githubRelease(options) {
8308
- return new GithubReleaseInstaller(options);
8309
- }
8310
-
8311
8699
  // packages/context/src/lib/sandbox/subcommand.ts
8312
8700
  import {
8313
8701
  defineCommand as defineCommand2,
@@ -8413,6 +8801,9 @@ async function createVirtualSandbox(options) {
8413
8801
  }
8414
8802
  },
8415
8803
  async dispose() {
8804
+ },
8805
+ [Symbol.asyncDispose]() {
8806
+ return this.dispose();
8416
8807
  }
8417
8808
  };
8418
8809
  }
@@ -8477,11 +8868,14 @@ ${lines.join("\n")}`;
8477
8868
  }
8478
8869
  function skillsReminder(skillsOrClassifier, options) {
8479
8870
  const classifier = Array.isArray(skillsOrClassifier) ? new BM25Classifier(skillsOrClassifier) : skillsOrClassifier;
8480
- return reminder((ctx) => {
8481
- const matches = classifier.match(ctx.content, options);
8482
- if (matches.length === 0) return "";
8483
- return formatSkillReminder(matches);
8484
- });
8871
+ return reminder(
8872
+ (ctx) => {
8873
+ const matches = classifier.match(ctx.content, options);
8874
+ if (matches.length === 0) return "";
8875
+ return formatSkillReminder(matches);
8876
+ },
8877
+ { target: "user" }
8878
+ );
8485
8879
  }
8486
8880
 
8487
8881
  // packages/context/src/lib/soul/protocol.md
@@ -11654,7 +12048,6 @@ export {
11654
12048
  PostgresStreamStore,
11655
12049
  PromiseResolver,
11656
12050
  RuntimeStrategy,
11657
- SnapshotFailedError,
11658
12051
  SqlServerContextStore,
11659
12052
  SqliteContextStore,
11660
12053
  SqliteStreamStore,
@@ -11682,7 +12075,8 @@ export {
11682
12075
  applyInlineReminder,
11683
12076
  applyPartReminder,
11684
12077
  applyReminderToMessage,
11685
- applyToolOutputRemindersToMessage,
12078
+ applyRemindersToToolOutput,
12079
+ applyUserRemindersToMessage,
11686
12080
  asStaticWordPartText,
11687
12081
  asStaticWordText,
11688
12082
  assertCountSpec,
@@ -11716,6 +12110,7 @@ export {
11716
12110
  defineSubcommandGroup,
11717
12111
  discoverSkillMappings,
11718
12112
  discoverSkillsInDirectory,
12113
+ downloadAndInstall,
11719
12114
  elapsedExceeds,
11720
12115
  encodeSerializedValue,
11721
12116
  errorRecoveryGuardrail,
@@ -11726,7 +12121,7 @@ export {
11726
12121
  executorContext,
11727
12122
  explain,
11728
12123
  fail,
11729
- findSingleOutputAvailableToolPart,
12124
+ first,
11730
12125
  firstN,
11731
12126
  fragment,
11732
12127
  fromFragment,
@@ -11734,6 +12129,7 @@ export {
11734
12129
  getFragmentData,
11735
12130
  getLocaleFromMessage,
11736
12131
  getModelsRegistry,
12132
+ getReminderOnceIds,
11737
12133
  getReminderRanges,
11738
12134
  getSeason,
11739
12135
  githubRelease,
@@ -11750,6 +12146,7 @@ export {
11750
12146
  isFragmentObject,
11751
12147
  isMessageFragment,
11752
12148
  isRecord,
12149
+ isSyntheticSteerMessage,
11753
12150
  lastAssistantLength,
11754
12151
  loadSkillMetadata,
11755
12152
  localeReminder,
@@ -11765,7 +12162,6 @@ export {
11765
12162
  not,
11766
12163
  npm,
11767
12164
  nullUsage,
11768
- observeSandboxFileEvents,
11769
12165
  once,
11770
12166
  or,
11771
12167
  parseFrontmatter,
@@ -11795,7 +12191,7 @@ export {
11795
12191
  seasonChanged,
11796
12192
  seasonReminder,
11797
12193
  selfCritique,
11798
- shellQuote3 as shellQuote,
12194
+ shellQuote,
11799
12195
  skills,
11800
12196
  skillsReminder,
11801
12197
  socraticPrompting,
@@ -11806,6 +12202,7 @@ export {
11806
12202
  stripTextByRanges,
11807
12203
  structuredOutput,
11808
12204
  styleGuide,
12205
+ synthesizeSteerUserMessage,
11809
12206
  temporalReminder,
11810
12207
  term,
11811
12208
  timeReminder,
@@ -11825,6 +12222,7 @@ export {
11825
12222
  visualizeGraph,
11826
12223
  weekChanged,
11827
12224
  withAbortSignal,
12225
+ withStraceFileChanges,
11828
12226
  withinLastN,
11829
12227
  workflow,
11830
12228
  yearChanged,