@deepagents/context 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +61 -4
  2. package/dist/browser.js +58 -90
  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 +1637 -1214
  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 +53 -16
  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/bash-tool.d.ts +28 -4
  21. package/dist/lib/sandbox/bash-tool.d.ts.map +1 -1
  22. package/dist/lib/sandbox/docker-sandbox.d.ts +14 -0
  23. package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -1
  24. package/dist/lib/sandbox/file-changes.d.ts +83 -0
  25. package/dist/lib/sandbox/file-changes.d.ts.map +1 -0
  26. package/dist/lib/sandbox/index.d.ts +1 -1
  27. package/dist/lib/sandbox/index.d.ts.map +1 -1
  28. package/dist/lib/sandbox/types.d.ts +0 -8
  29. package/dist/lib/sandbox/types.d.ts.map +1 -1
  30. package/dist/lib/save/reminder-eval.d.ts +19 -0
  31. package/dist/lib/save/reminder-eval.d.ts.map +1 -0
  32. package/dist/lib/save/save-pipeline.d.ts +8 -2
  33. package/dist/lib/save/save-pipeline.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/dist/lib/sandbox/file-events.d.ts +0 -42
  36. package/dist/lib/sandbox/file-events.d.ts.map +0 -1
  37. package/dist/lib/save/reminder-target-handler.d.ts +0 -36
  38. package/dist/lib/save/reminder-target-handler.d.ts.map +0 -1
  39. package/dist/lib/save/tool-output-target-handler.d.ts +0 -7
  40. package/dist/lib/save/tool-output-target-handler.d.ts.map +0 -1
  41. package/dist/lib/save/user-target-handler.d.ts +0 -7
  42. 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
  }
@@ -1168,12 +1168,10 @@ function getReminderMetadataRecords(metadata) {
1168
1168
  (item) => isRecord(item) && typeof item.partIndex === "number" && typeof item.start === "number" && typeof item.end === "number"
1169
1169
  );
1170
1170
  }
1171
- function reminderTargetOf(record) {
1172
- return record.target === "tool-output" ? "tool-output" : "user";
1173
- }
1174
1171
  function normalizeReminderTarget(target) {
1175
1172
  if (target === void 0 || target === "user") return "user";
1176
1173
  if (target === "tool-output") return "tool-output";
1174
+ if (target === "steer") return "steer";
1177
1175
  throw new Error(`Unsupported reminder target: ${String(target)}`);
1178
1176
  }
1179
1177
  function isConditionalReminderOptions(options) {
@@ -1204,7 +1202,7 @@ function isOutputAvailableToolPart(part) {
1204
1202
  return isStaticToolUIPart(part) && part.state === "output-available";
1205
1203
  }
1206
1204
  function isToolOutputReminderEnvelope(value) {
1207
- return isRecord(value) && typeof value.systemReminder === "string";
1205
+ return isRecord(value) && "result" in value && typeof value.systemReminder === "string" && value.systemReminder.startsWith(SYSTEM_REMINDER_OPEN_TAG);
1208
1206
  }
1209
1207
  function stripTextByRanges(text, ranges) {
1210
1208
  if (ranges.length === 0) {
@@ -1233,43 +1231,22 @@ function stripTextByRanges(text, ranges) {
1233
1231
  return output.trimEnd();
1234
1232
  }
1235
1233
  function stripReminders(message2) {
1234
+ if (isSyntheticSteerMessage(message2)) {
1235
+ return stripSyntheticSteerMessage(message2);
1236
+ }
1236
1237
  const reminderRecords = getReminderMetadataRecords(
1237
1238
  isRecord(message2.metadata) ? message2.metadata : void 0
1238
1239
  );
1239
1240
  const rangesByPartIndex = /* @__PURE__ */ new Map();
1240
- const toolRemindersByPartIndex = /* @__PURE__ */ new Map();
1241
1241
  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
1242
  const partRanges = rangesByPartIndex.get(range.partIndex) ?? [];
1249
1243
  partRanges.push({ start: range.start, end: range.end });
1250
1244
  rangesByPartIndex.set(range.partIndex, partRanges);
1251
1245
  }
1252
1246
  const strippedParts = message2.parts.flatMap((part, partIndex) => {
1253
1247
  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
- }
1248
+ if (isOutputAvailableToolPart(clonedPart) && isToolOutputReminderEnvelope(clonedPart.output)) {
1249
+ return [{ ...clonedPart, output: clonedPart.output.result }];
1273
1250
  }
1274
1251
  const ranges = rangesByPartIndex.get(partIndex);
1275
1252
  if (clonedPart.type !== "text" || ranges === void 0) {
@@ -1409,64 +1386,12 @@ function applyReminderToMessage(message2, item, ctx) {
1409
1386
  }
1410
1387
  return item.asPart ? applyPartReminder(message2, resolved.text) : applyInlineReminder(message2, resolved.text);
1411
1388
  }
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
- }
1389
+ function applyRemindersToToolOutput(output, texts) {
1390
+ if (texts.length === 0) return output;
1391
+ return {
1392
+ result: output === void 0 ? null : output,
1393
+ systemReminder: formatTaggedReminder(texts.join("\n"))
1460
1394
  };
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
1395
  }
1471
1396
  function mergeReminderMetadata(message2, addedReminders) {
1472
1397
  if (addedReminders.length === 0) return;
@@ -1497,7 +1422,7 @@ function reminder(textOrFragment, options) {
1497
1422
  };
1498
1423
  }
1499
1424
  if (target !== "user") {
1500
- throw new Error('Reminder target "tool-output" requires a when predicate');
1425
+ throw new Error(`Reminder target "${target}" requires a when predicate`);
1501
1426
  }
1502
1427
  const text = normalizeImmediateReminderText(textOrFragment);
1503
1428
  if (typeof text === "string") {
@@ -1541,6 +1466,48 @@ function user(content, ...reminders) {
1541
1466
  }
1542
1467
  };
1543
1468
  }
1469
+ function synthesizeSteerUserMessage(text, firedAt, onceIds = []) {
1470
+ const texts = Array.isArray(text) ? text : [text];
1471
+ for (const value of texts) assertReminderText(value);
1472
+ return {
1473
+ id: generateId2(),
1474
+ role: "user",
1475
+ parts: texts.map((value) => ({
1476
+ type: "text",
1477
+ text: formatTaggedReminder(value)
1478
+ })),
1479
+ metadata: {
1480
+ synthetic: {
1481
+ source: "steer-reminder",
1482
+ firedAt,
1483
+ ...onceIds.length > 0 ? { onceIds } : {}
1484
+ }
1485
+ }
1486
+ };
1487
+ }
1488
+ function isSyntheticSteerMessage(message2) {
1489
+ const meta = message2.metadata;
1490
+ if (!isRecord(meta)) return false;
1491
+ const synthetic = meta.synthetic;
1492
+ if (!isRecord(synthetic)) return false;
1493
+ return synthetic.source === "steer-reminder";
1494
+ }
1495
+ function stripSyntheticSteerMessage(message2) {
1496
+ const next = {
1497
+ ...message2,
1498
+ parts: message2.parts.filter((part) => part.type !== "text")
1499
+ };
1500
+ if (isRecord(message2.metadata)) {
1501
+ const metadata = { ...message2.metadata };
1502
+ delete metadata.synthetic;
1503
+ if (Object.keys(metadata).length > 0) {
1504
+ next.metadata = metadata;
1505
+ } else {
1506
+ delete next.metadata;
1507
+ }
1508
+ }
1509
+ return next;
1510
+ }
1544
1511
 
1545
1512
  // packages/context/src/lib/guardrail.ts
1546
1513
  function pass(part) {
@@ -1607,7 +1574,10 @@ var Agent = class _Agent {
1607
1574
  sandbox;
1608
1575
  constructor(options) {
1609
1576
  this.#options = options;
1610
- this.tools = { ...options.sandbox.tools, ...options.tools || {} };
1577
+ this.tools = wrapToolsWithOutputReminders(
1578
+ { ...options.sandbox.tools, ...options.tools || {} },
1579
+ options.context
1580
+ );
1611
1581
  this.context = options.context;
1612
1582
  this.model = options.model;
1613
1583
  this.sandbox = options.sandbox;
@@ -1714,6 +1684,7 @@ var Agent = class _Agent {
1714
1684
  config?.abortSignal
1715
1685
  ),
1716
1686
  stopWhen: stepCountIs(200),
1687
+ prepareStep: context.createSteerPrepareStep(),
1717
1688
  experimental_transform: config?.transform ?? smoothStream(),
1718
1689
  tools: this.tools,
1719
1690
  experimental_context: contextVariables,
@@ -1750,9 +1721,14 @@ var Agent = class _Agent {
1750
1721
  generateId: assistantMsgId ? () => assistantMsgId : generateId3,
1751
1722
  onStepFinish: async ({ responseMessage }) => {
1752
1723
  if (!stepSaved) return;
1753
- const normalizedMessage = assistantMsgId ? { ...responseMessage, id: assistantMsgId } : responseMessage;
1754
- context.set(assistant(normalizedMessage));
1755
- await context.save({ branch: false });
1724
+ const head = await context.headMessage();
1725
+ if (head?.name === "assistant") {
1726
+ await context.writeAssistantSegment(responseMessage);
1727
+ } else {
1728
+ const message2 = assistantMsgId ? { ...responseMessage, id: assistantMsgId } : responseMessage;
1729
+ context.set(assistant(message2));
1730
+ await context.save({ branch: false });
1731
+ }
1756
1732
  stepSaved.resolve();
1757
1733
  stepSaved = null;
1758
1734
  },
@@ -1959,6 +1935,29 @@ ${details}
1959
1935
  });
1960
1936
  }
1961
1937
  };
1938
+ function wrapToolsWithOutputReminders(tools, context) {
1939
+ if (!context) return tools;
1940
+ const wrapped = {};
1941
+ for (const [name, toolDef] of Object.entries(tools)) {
1942
+ const execute = toolDef.execute;
1943
+ if (typeof execute !== "function") {
1944
+ wrapped[name] = toolDef;
1945
+ continue;
1946
+ }
1947
+ wrapped[name] = {
1948
+ ...toolDef,
1949
+ execute: async (input, options) => {
1950
+ const result = await execute.call(toolDef, input, options);
1951
+ if (isAsyncIterable(result)) return result;
1952
+ return context.applyToolOutputReminders(result);
1953
+ }
1954
+ };
1955
+ }
1956
+ return wrapped;
1957
+ }
1958
+ function isAsyncIterable(value) {
1959
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
1960
+ }
1962
1961
  function agent(options) {
1963
1962
  return new Agent(options);
1964
1963
  }
@@ -2062,6 +2061,7 @@ import { z as z2 } from "zod";
2062
2061
 
2063
2062
  // packages/context/src/lib/engine.ts
2064
2063
  import {
2064
+ convertToModelMessages as convertToModelMessages2,
2065
2065
  generateId as generateId4,
2066
2066
  validateUIMessages
2067
2067
  } from "ai";
@@ -2097,30 +2097,41 @@ var ChainSummaryBuilder = class {
2097
2097
  #lastMessage;
2098
2098
  #lastAssistantMessage;
2099
2099
  #lastAssistantMessages = [];
2100
+ #firedOnceIds = /* @__PURE__ */ new Set();
2100
2101
  ingestStored(msg) {
2101
2102
  this.#messageCount++;
2102
2103
  if (msg.name === "assistant") {
2103
- const message2 = requireUIMessage(
2104
+ const message3 = requireUIMessage(
2104
2105
  msg.data,
2105
2106
  `Stored assistant message "${msg.id}"`
2106
2107
  );
2107
- this.#lastAssistantMessage = message2;
2108
- this.#lastAssistantMessages.push(message2);
2108
+ this.#lastAssistantMessage = message3;
2109
+ this.#lastAssistantMessages.push(message3);
2109
2110
  return;
2110
2111
  }
2111
2112
  if (msg.name !== "user") {
2112
2113
  return;
2113
2114
  }
2114
- this.#turn++;
2115
- this.#lastMessageAt = msg.createdAt;
2116
- this.#lastMessage = requireUIMessage(
2115
+ const message2 = requireUIMessage(
2117
2116
  msg.data,
2118
2117
  `Stored user message "${msg.id}"`
2119
2118
  );
2119
+ if (isSyntheticSteerMessage(message2)) {
2120
+ for (const id of message2.metadata.synthetic.onceIds ?? []) {
2121
+ this.#firedOnceIds.add(id);
2122
+ }
2123
+ return;
2124
+ }
2125
+ this.#turn++;
2126
+ this.#lastMessageAt = msg.createdAt;
2127
+ this.#lastMessage = message2;
2120
2128
  }
2121
2129
  ingestPending(fragment2) {
2122
2130
  this.#messageCount++;
2123
- if (fragment2.name === "user") this.#turn++;
2131
+ if (fragment2.name !== "user") return;
2132
+ const encoded = fragment2.codec?.encode();
2133
+ if (encoded && isSyntheticSteerMessage(encoded)) return;
2134
+ this.#turn++;
2124
2135
  }
2125
2136
  build() {
2126
2137
  return {
@@ -2129,7 +2140,8 @@ var ChainSummaryBuilder = class {
2129
2140
  lastMessageAt: this.#lastMessageAt,
2130
2141
  lastMessage: this.#lastMessage,
2131
2142
  lastAssistantMessage: this.#lastAssistantMessage,
2132
- lastAssistantMessages: this.#lastAssistantMessages
2143
+ lastAssistantMessages: this.#lastAssistantMessages,
2144
+ firedOnceIds: this.#firedOnceIds
2133
2145
  };
2134
2146
  }
2135
2147
  };
@@ -2338,7 +2350,7 @@ var GeneratorResolver = class {
2338
2350
  async resolve(value, ctx) {
2339
2351
  const iterable = value(ctx);
2340
2352
  const collected = [];
2341
- if (isAsyncIterable(iterable)) {
2353
+ if (isAsyncIterable2(iterable)) {
2342
2354
  for await (const chunk of iterable) {
2343
2355
  pushLimited(collected, chunk, this.#maxItems, this.name);
2344
2356
  }
@@ -2350,7 +2362,7 @@ var GeneratorResolver = class {
2350
2362
  return collected;
2351
2363
  }
2352
2364
  };
2353
- function isAsyncIterable(value) {
2365
+ function isAsyncIterable2(value) {
2354
2366
  return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
2355
2367
  }
2356
2368
  function pushLimited(collected, chunk, maxItems, resolverName) {
@@ -2388,7 +2400,7 @@ var IterableResolver = class {
2388
2400
  async resolve(value, _ctx) {
2389
2401
  const iterable = value;
2390
2402
  const collected = [];
2391
- if (isAsyncIterable2(iterable)) {
2403
+ if (isAsyncIterable3(iterable)) {
2392
2404
  for await (const chunk of iterable) {
2393
2405
  pushLimited2(collected, chunk, this.#maxItems, this.name);
2394
2406
  }
@@ -2400,7 +2412,7 @@ var IterableResolver = class {
2400
2412
  return collected;
2401
2413
  }
2402
2414
  };
2403
- function isAsyncIterable2(value) {
2415
+ function isAsyncIterable3(value) {
2404
2416
  return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
2405
2417
  }
2406
2418
  function pushLimited2(collected, chunk, maxItems, resolverName) {
@@ -2556,6 +2568,54 @@ function defaultResolvers() {
2556
2568
  ];
2557
2569
  }
2558
2570
 
2571
+ // packages/context/src/lib/save/reminder-eval.ts
2572
+ async function evaluateFiredReminders(configs, whenCtx) {
2573
+ if (configs.length === 0) return [];
2574
+ const collectors = configs.map(() => /* @__PURE__ */ new Set());
2575
+ const whenResults = await Promise.allSettled(
2576
+ // Wrap in an async thunk so a synchronously-throwing predicate becomes a
2577
+ // rejected promise (allSettled cannot catch a throw inside the map itself).
2578
+ configs.map(
2579
+ async (config, i) => config.when({ ...whenCtx, onceCollector: collectors[i] })
2580
+ )
2581
+ );
2582
+ const fired = [];
2583
+ for (let i = 0; i < configs.length; i++) {
2584
+ const result = whenResults[i];
2585
+ if (result.status === "rejected") {
2586
+ console.warn("reminder when() predicate threw; treating as not fired", {
2587
+ reason: result.reason
2588
+ });
2589
+ continue;
2590
+ }
2591
+ if (result.value === true) {
2592
+ fired.push({ config: configs[i], onceIds: [...collectors[i]] });
2593
+ }
2594
+ }
2595
+ if (fired.length === 0) return [];
2596
+ const resolutions = await Promise.allSettled(
2597
+ fired.map(({ config }) => resolveReminderAsync(config, whenCtx))
2598
+ );
2599
+ const matched = [];
2600
+ for (let i = 0; i < fired.length; i++) {
2601
+ const result = resolutions[i];
2602
+ if (result.status === "rejected") {
2603
+ console.warn("reminder text resolver threw; skipping reminder", {
2604
+ reason: result.reason
2605
+ });
2606
+ continue;
2607
+ }
2608
+ if (result.value) {
2609
+ matched.push({
2610
+ config: fired[i].config,
2611
+ resolved: result.value,
2612
+ onceIds: fired[i].onceIds
2613
+ });
2614
+ }
2615
+ }
2616
+ return matched;
2617
+ }
2618
+
2559
2619
  // packages/context/src/lib/save/save-pipeline.ts
2560
2620
  var SavePipeline = class {
2561
2621
  #engine;
@@ -2581,65 +2641,49 @@ var SavePipeline = class {
2581
2641
  }
2582
2642
  return this;
2583
2643
  }
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() {
2644
+ /**
2645
+ * Fold any fired `target: 'user'` conditional reminders into the last pending
2646
+ * user message. `steer` reminders fire mid-loop via the engine's prepareStep
2647
+ * hook and `tool-output` reminders wrap at tool-execution time, so neither is
2648
+ * handled here the save pipeline only carries user-message reminders.
2649
+ */
2650
+ async evaluateUserReminders() {
2651
+ const configs = this.#fragments.filter(isConditionalReminder).map((fragment3) => fragment3.metadata.reminder).filter((config) => config.target === "user");
2652
+ if (configs.length === 0) return this;
2633
2653
  const fragmentIndex = this.#pending.findLastIndex(
2634
2654
  (fragment3) => fragment3.name === "user"
2635
2655
  );
2636
- if (fragmentIndex < 0) return void 0;
2656
+ if (fragmentIndex < 0) return this;
2637
2657
  const fragment2 = this.#pending[fragmentIndex];
2638
- if (!fragment2.codec) return void 0;
2639
- return requireUserUIMessage(
2658
+ if (!fragment2.codec) return this;
2659
+ const message2 = requireUserUIMessage(
2640
2660
  fragment2.codec.encode(),
2641
2661
  `Pending user fragment "${fragment2.name}"`
2642
2662
  );
2663
+ const chain = await this.#engine.getChainSummary();
2664
+ const whenCtx = {
2665
+ ...this.#engine.buildBaseWhenCtx(chain),
2666
+ content: extractPlainText(message2),
2667
+ currentMessage: message2,
2668
+ lastAssistantMessage: chain.lastAssistantMessage,
2669
+ lastAssistantMessages: chain.lastAssistantMessages
2670
+ };
2671
+ const matched = await evaluateFiredReminders(configs, whenCtx);
2672
+ if (matched.length === 0) return this;
2673
+ const reminders = matched.map((m) => ({
2674
+ text: m.resolved.text,
2675
+ asPart: m.config.asPart,
2676
+ target: "user",
2677
+ metadata: m.resolved.metadata
2678
+ }));
2679
+ const originalId = fragment2.id;
2680
+ const recreated = user(
2681
+ originalId ? { ...message2, id: originalId } : message2,
2682
+ ...reminders
2683
+ );
2684
+ if (originalId) recreated.id = originalId;
2685
+ this.#pending[fragmentIndex] = recreated;
2686
+ return this;
2643
2687
  }
2644
2688
  async persist() {
2645
2689
  let parentId = this.#engine.getActiveBranch().headMessageId;
@@ -2680,108 +2724,6 @@ var SavePipeline = class {
2680
2724
  }
2681
2725
  };
2682
2726
 
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
2727
  // packages/context/src/lib/store/sqlite.store.ts
2786
2728
  import { DatabaseSync } from "node:sqlite";
2787
2729
 
@@ -3402,6 +3344,23 @@ function isSkillPathMapping(value) {
3402
3344
  function isEmptyAssistantPlaceholder(message2) {
3403
3345
  return message2.role === "assistant" && message2.parts.length === 0;
3404
3346
  }
3347
+ function stepStartPartIndices(parts) {
3348
+ const indices = [];
3349
+ for (let i = 0; i < parts.length; i++) {
3350
+ if (parts[i].type === "step-start") indices.push(i);
3351
+ }
3352
+ return indices;
3353
+ }
3354
+ function spliceSteerMessages(messages, fired) {
3355
+ const ordered = [...fired].sort((a, b) => a.spliceIndex - b.spliceIndex);
3356
+ const out = [...messages];
3357
+ let offset = 0;
3358
+ for (const fire of ordered) {
3359
+ out.splice(fire.spliceIndex + offset, 0, ...fire.synthModel);
3360
+ offset += fire.synthModel.length;
3361
+ }
3362
+ return out;
3363
+ }
3405
3364
  var ContextEngine = class _ContextEngine {
3406
3365
  /** Non-message fragments (role, hints, etc.) - not persisted in graph */
3407
3366
  #fragments = [];
@@ -3765,15 +3724,211 @@ var ContextEngine = class _ContextEngine {
3765
3724
  this.#fragments
3766
3725
  );
3767
3726
  await pipeline.applyUpdateBranching(options?.branch ?? true);
3768
- await pipeline.evaluateReminders(this.#reminderHandlers);
3727
+ await pipeline.evaluateUserReminders();
3769
3728
  const result = await pipeline.persist();
3770
3729
  this.#pendingMessages = [];
3771
3730
  return result;
3772
3731
  }
3773
- #reminderHandlers = [
3774
- new UserTargetHandler(),
3775
- new ToolOutputTargetHandler()
3776
- ];
3732
+ #currentSteerSession;
3733
+ /**
3734
+ * Build the `prepareStep` hook that injects steer reminders mid-loop.
3735
+ *
3736
+ * Semantics are "inject once, persist": when a steer reminder's predicate
3737
+ * fires (only after the model has produced ≥1 step with content — the mid-loop
3738
+ * gate), its `<system-reminder>` user message is spliced into the model prompt
3739
+ * at the step boundary where it fired AND re-spliced on every subsequent step,
3740
+ * so the model keeps seeing it for the rest of the loop.
3741
+ *
3742
+ * Firing is edge-triggered with a post-fire re-sample: each fire resets the
3743
+ * elapsed reference (`lastSyntheticAt`), then the config is immediately
3744
+ * re-evaluated against the reset context — self-resetting predicates like
3745
+ * `elapsedExceeds` read false and re-arm (so they recur every N within one
3746
+ * stream), while constant predicates like `everyNTurns` read true and disarm
3747
+ * (so they fire once per stream). Any later false sample re-arms a config.
3748
+ * All state is closure-local (a fresh SteerSession per call), so overlapping
3749
+ * streams / guardrail retries never share a cursor.
3750
+ *
3751
+ * The session is also consumed by writeAssistantSegment, which carves the
3752
+ * streamed assistant message into the matching `[assistant, steer, assistant]`
3753
+ * split — so the stored chain reproduces exactly the prompt the model saw
3754
+ * (store/prompt parity).
3755
+ */
3756
+ createSteerPrepareStep() {
3757
+ const session = {
3758
+ firedOnceIds: /* @__PURE__ */ new Set(),
3759
+ fired: [],
3760
+ currentSegStart: 0,
3761
+ materialized: 0
3762
+ };
3763
+ this.#currentSteerSession = session;
3764
+ return async ({ steps, stepNumber, messages }) => {
3765
+ const priorStep = stepNumber >= 1 ? steps[stepNumber - 1] : void 0;
3766
+ const canFire = (priorStep?.content?.length ?? 0) > 0;
3767
+ if (canFire) {
3768
+ const configs = this.#steerConfigs();
3769
+ if (configs.length > 0) {
3770
+ const whenCtx = await this.#steerWhenCtx(session);
3771
+ const matched = await evaluateFiredReminders(configs, whenCtx);
3772
+ if (matched.length > 0) {
3773
+ const onceIds = [...new Set(matched.flatMap((m) => m.onceIds))];
3774
+ for (const id of onceIds) session.firedOnceIds.add(id);
3775
+ const synth = synthesizeSteerUserMessage(
3776
+ matched.map((m) => m.resolved.text),
3777
+ Date.now(),
3778
+ onceIds
3779
+ );
3780
+ const synthModel = await convertToModelMessages2([synth], {
3781
+ ignoreIncompleteToolCalls: true
3782
+ });
3783
+ session.fired.push({
3784
+ afterStep: stepNumber - 1,
3785
+ spliceIndex: messages.length,
3786
+ synth,
3787
+ synthModel
3788
+ });
3789
+ }
3790
+ }
3791
+ }
3792
+ if (session.fired.length === 0) return void 0;
3793
+ return {
3794
+ messages: spliceSteerMessages(
3795
+ messages,
3796
+ session.fired
3797
+ )
3798
+ };
3799
+ };
3800
+ }
3801
+ /**
3802
+ * Persist the streamed assistant message, carving it into the steer split when
3803
+ * steer reminders fired this turn.
3804
+ *
3805
+ * Called from chat()'s onStepFinish/onFinish (and the guardrail path) with the
3806
+ * cumulative response message. Segment boundaries come from the `step-start`
3807
+ * markers in the message itself — no cross-track store read — so the carve is
3808
+ * race-free. Idempotent: finalized segments keep stable ids; the open segment
3809
+ * is updated in place. With no active steer it degrades to a plain in-place
3810
+ * write of the whole message to the reserved head.
3811
+ */
3812
+ async writeAssistantSegment(message2) {
3813
+ const head = await this.headMessage();
3814
+ if (head?.name !== "assistant") {
3815
+ throw new Error(
3816
+ "writeAssistantSegment: expected an assistant message at chain head."
3817
+ );
3818
+ }
3819
+ const session = this.#currentSteerSession;
3820
+ if (!session || session.fired.length === 0) {
3821
+ this.set(assistant({ ...message2, id: head.id }));
3822
+ await this.save({ branch: false });
3823
+ return;
3824
+ }
3825
+ if (session.currentSegId === void 0) {
3826
+ session.currentSegId = head.id;
3827
+ session.currentSegStart = 0;
3828
+ }
3829
+ const stepStarts = stepStartPartIndices(message2.parts);
3830
+ while (session.materialized < session.fired.length) {
3831
+ const fire = session.fired[session.materialized];
3832
+ const boundary = stepStarts[fire.afterStep + 1];
3833
+ if (boundary === void 0) break;
3834
+ this.set(
3835
+ assistant({
3836
+ id: session.currentSegId,
3837
+ role: "assistant",
3838
+ parts: message2.parts.slice(session.currentSegStart, boundary)
3839
+ })
3840
+ );
3841
+ this.set(user(fire.synth));
3842
+ session.currentSegId = generateId4();
3843
+ session.currentSegStart = boundary;
3844
+ session.materialized++;
3845
+ }
3846
+ this.set(
3847
+ assistant({
3848
+ ...message2,
3849
+ id: session.currentSegId,
3850
+ parts: message2.parts.slice(session.currentSegStart)
3851
+ })
3852
+ );
3853
+ await this.save({ branch: false });
3854
+ }
3855
+ #steerConfigs() {
3856
+ return this.#fragments.filter(isConditionalReminder).map((fragment2) => fragment2.metadata.reminder).filter((config) => config.target === "steer");
3857
+ }
3858
+ async #steerWhenCtx(session) {
3859
+ await this.#ensureInitialized();
3860
+ if (!session.whenBase) {
3861
+ const chain = await this.#getChainContext();
3862
+ const base2 = this.#asSavePipelineEngine().buildBaseWhenCtx(chain);
3863
+ const currentMessage2 = chain.lastMessage;
3864
+ if (!currentMessage2) {
3865
+ throw new Error(
3866
+ "steer reminders require a user message earlier in the turn"
3867
+ );
3868
+ }
3869
+ session.whenBase = {
3870
+ base: base2,
3871
+ content: extractPlainText(currentMessage2),
3872
+ currentMessage: currentMessage2,
3873
+ lastAssistantMessage: chain.lastAssistantMessage,
3874
+ lastAssistantMessages: chain.lastAssistantMessages,
3875
+ chainFiredOnceIds: chain.firedOnceIds
3876
+ };
3877
+ }
3878
+ const {
3879
+ base,
3880
+ content,
3881
+ currentMessage,
3882
+ lastAssistantMessage,
3883
+ lastAssistantMessages,
3884
+ chainFiredOnceIds
3885
+ } = session.whenBase;
3886
+ const elapsed = base.lastMessageAt !== void 0 ? Date.now() - base.lastMessageAt : void 0;
3887
+ return {
3888
+ ...base,
3889
+ elapsed,
3890
+ content,
3891
+ currentMessage,
3892
+ lastAssistantMessage,
3893
+ lastAssistantMessages,
3894
+ firedOnceIds: /* @__PURE__ */ new Set([...chainFiredOnceIds, ...session.firedOnceIds])
3895
+ };
3896
+ }
3897
+ /**
3898
+ * Evaluate `target: 'tool-output'` reminders against a tool's raw result and
3899
+ * return the (possibly wrapped) output.
3900
+ *
3901
+ * Called by the agent's tool wrapper right after each `execute()` resolves —
3902
+ * upstream of both the model and the store, so the next model step and the
3903
+ * persisted chain see the exact same wrapped value (store/prompt parity).
3904
+ *
3905
+ * Returns the output unchanged when no tool-output reminder fires. Without a
3906
+ * persisted user message there is no turn context to evaluate against
3907
+ * (e.g. asTool forks that set a pending user without saving), so the output
3908
+ * passes through untouched.
3909
+ */
3910
+ async applyToolOutputReminders(output) {
3911
+ const configs = this.#fragments.filter(isConditionalReminder).map((fragment2) => fragment2.metadata.reminder).filter((config) => config.target === "tool-output");
3912
+ if (configs.length === 0) return output;
3913
+ await this.#ensureInitialized();
3914
+ const chain = await this.#getChainContext();
3915
+ const currentMessage = chain.lastMessage;
3916
+ if (!currentMessage) return output;
3917
+ const base = this.#asSavePipelineEngine().buildBaseWhenCtx(chain);
3918
+ const whenCtx = {
3919
+ ...base,
3920
+ content: extractPlainText(currentMessage),
3921
+ currentMessage,
3922
+ lastAssistantMessage: chain.lastAssistantMessage,
3923
+ lastAssistantMessages: chain.lastAssistantMessages
3924
+ };
3925
+ const matched = await evaluateFiredReminders(configs, whenCtx);
3926
+ if (matched.length === 0) return output;
3927
+ return applyRemindersToToolOutput(
3928
+ output,
3929
+ matched.map((m) => m.resolved.text)
3930
+ );
3931
+ }
3777
3932
  #asSavePipelineEngine() {
3778
3933
  return {
3779
3934
  store: this.#store,
@@ -4689,7 +4844,6 @@ var defaultChatMessageMetadata = ({
4689
4844
  };
4690
4845
  async function chat(agent2, options = {}) {
4691
4846
  const context = agent2.context;
4692
- const sandbox = agent2.sandbox;
4693
4847
  if (!context) {
4694
4848
  throw new Error(
4695
4849
  "Agent is missing a context. Provide context when creating the agent."
@@ -4701,7 +4855,7 @@ async function chat(agent2, options = {}) {
4701
4855
  "chat: expected an assistant message at head. Call context.continue(input) before chat()."
4702
4856
  );
4703
4857
  }
4704
- const assistantMsgId = head.id;
4858
+ const initialAssistantMsgId = head.id;
4705
4859
  const uiMessages = await context.getMessages();
4706
4860
  const streamContextVariables = options.contextVariables === void 0 ? {} : options.contextVariables;
4707
4861
  const [title, result] = await Promise.all([
@@ -4723,42 +4877,30 @@ async function chat(agent2, options = {}) {
4723
4877
  sendReasoning: true,
4724
4878
  sendSources: true,
4725
4879
  originalMessages: uiMessages,
4726
- generateMessageId: () => assistantMsgId,
4880
+ generateMessageId: () => initialAssistantMsgId,
4727
4881
  messageMetadata: options.messageMetadata ?? defaultChatMessageMetadata
4728
4882
  });
4729
4883
  return createUIMessageStream2({
4730
4884
  originalMessages: uiMessages,
4731
- generateId: () => assistantMsgId,
4885
+ generateId: () => initialAssistantMsgId,
4732
4886
  onStepFinish: async ({ responseMessage }) => {
4733
- const normalizedMessage = {
4734
- ...responseMessage,
4735
- id: assistantMsgId
4736
- };
4737
- context.set(assistant(normalizedMessage));
4738
- await context.save({ branch: false });
4887
+ await context.writeAssistantSegment(responseMessage);
4739
4888
  },
4740
4889
  onFinish: async ({ responseMessage, isAborted }) => {
4741
- const normalizedMessage = {
4742
- ...responseMessage,
4743
- id: assistantMsgId
4744
- };
4890
+ let message2 = responseMessage;
4745
4891
  if (isAborted) {
4746
- normalizedMessage.parts = sanitizeAbortedParts(normalizedMessage.parts);
4892
+ message2 = { ...message2, parts: sanitizeAbortedParts(message2.parts) };
4747
4893
  }
4748
- const drained = sandbox.drainFileEvents();
4749
- const fileEvents = isAborted ? [] : drained;
4750
- const finalMetadata = await options.finalAssistantMetadata?.(normalizedMessage);
4894
+ const finalMetadata = await options.finalAssistantMetadata?.(message2);
4751
4895
  const mergedMetadata = {
4752
- ...normalizedMessage.metadata ?? {},
4753
- ...fileEvents.length > 0 ? { fileEvents } : {},
4896
+ ...message2.metadata ?? {},
4754
4897
  ...finalMetadata ?? {}
4755
4898
  };
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);
4899
+ if (Object.keys(mergedMetadata).length > 0) {
4900
+ message2 = { ...message2, metadata: mergedMetadata };
4901
+ }
4902
+ await context.writeAssistantSegment(message2);
4903
+ await context.trackUsage(await result.totalUsage);
4762
4904
  },
4763
4905
  execute: async ({ writer }) => {
4764
4906
  writer.merge(uiStream);
@@ -5345,6 +5487,23 @@ function lastAssistantLength(spec) {
5345
5487
  };
5346
5488
  }
5347
5489
 
5490
+ // packages/context/src/lib/fragments/reminders/once.ts
5491
+ function once(id) {
5492
+ if (id.trim().length === 0) {
5493
+ throw new Error("once(id) requires a non-empty id");
5494
+ }
5495
+ return (ctx) => {
5496
+ if (ctx.firedOnceIds === void 0) {
5497
+ throw new Error(
5498
+ `once('${id}') is only supported on target:'steer' reminders`
5499
+ );
5500
+ }
5501
+ if (ctx.firedOnceIds.has(id)) return false;
5502
+ ctx.onceCollector?.add(id);
5503
+ return true;
5504
+ };
5505
+ }
5506
+
5348
5507
  // packages/context/src/lib/fragments/reminders/temporal/elapsed.ts
5349
5508
  function elapsedExceeds(ms) {
5350
5509
  return (ctx) => (ctx.elapsed ?? 0) >= ms;
@@ -5502,15 +5661,15 @@ function dateReminder(options) {
5502
5661
  const now = /* @__PURE__ */ new Date();
5503
5662
  const currentDate = formatDateKey(now, tz);
5504
5663
  const currentDay = formatDayOfWeek(now, tz);
5505
- let diff2 = "";
5664
+ let diff = "";
5506
5665
  if (ctx.lastMessageAt !== void 0) {
5507
5666
  const prev = new Date(ctx.lastMessageAt);
5508
- diff2 = formatDiff([
5667
+ diff = formatDiff([
5509
5668
  diffLine("date", formatDateKey(prev, tz), currentDate),
5510
5669
  diffLine("day of week", formatDayOfWeek(prev, tz), currentDay)
5511
5670
  ]);
5512
5671
  }
5513
- return `${diff2}Date: ${currentDate}
5672
+ return `${diff}Date: ${currentDate}
5514
5673
  Day of Week: ${currentDay}`;
5515
5674
  },
5516
5675
  { when: dayChanged(options), asPart: false }
@@ -5522,14 +5681,14 @@ function timeReminder(options) {
5522
5681
  const tz = resolveTz(options, ctx);
5523
5682
  const now = /* @__PURE__ */ new Date();
5524
5683
  const currentTime = formatTime(now, tz);
5525
- let diff2 = "";
5684
+ let diff = "";
5526
5685
  if (ctx.lastMessageAt !== void 0) {
5527
5686
  const prev = new Date(ctx.lastMessageAt);
5528
- diff2 = formatDiff([
5687
+ diff = formatDiff([
5529
5688
  diffLine("hour", formatHour(prev, tz), formatHour(now, tz))
5530
5689
  ]);
5531
5690
  }
5532
- return `${diff2}Time: ${currentTime}`;
5691
+ return `${diff}Time: ${currentTime}`;
5533
5692
  },
5534
5693
  { when: hourChanged(options), asPart: false }
5535
5694
  );
@@ -5540,14 +5699,14 @@ function monthReminder(options) {
5540
5699
  const tz = resolveTz(options, ctx);
5541
5700
  const now = /* @__PURE__ */ new Date();
5542
5701
  const currentMonth = formatMonthName(now, tz);
5543
- let diff2 = "";
5702
+ let diff = "";
5544
5703
  if (ctx.lastMessageAt !== void 0) {
5545
5704
  const prev = new Date(ctx.lastMessageAt);
5546
- diff2 = formatDiff([
5705
+ diff = formatDiff([
5547
5706
  diffLine("month", formatMonthName(prev, tz), currentMonth)
5548
5707
  ]);
5549
5708
  }
5550
- return `${diff2}Month: ${currentMonth}`;
5709
+ return `${diff}Month: ${currentMonth}`;
5551
5710
  },
5552
5711
  { when: monthChanged(options), asPart: false }
5553
5712
  );
@@ -5558,14 +5717,14 @@ function yearReminder(options) {
5558
5717
  const tz = resolveTz(options, ctx);
5559
5718
  const now = /* @__PURE__ */ new Date();
5560
5719
  const currentYear = formatYear(now, tz);
5561
- let diff2 = "";
5720
+ let diff = "";
5562
5721
  if (ctx.lastMessageAt !== void 0) {
5563
5722
  const prev = new Date(ctx.lastMessageAt);
5564
- diff2 = formatDiff([
5723
+ diff = formatDiff([
5565
5724
  diffLine("year", formatYear(prev, tz), currentYear)
5566
5725
  ]);
5567
5726
  }
5568
- return `${diff2}Year: ${currentYear}`;
5727
+ return `${diff}Year: ${currentYear}`;
5569
5728
  },
5570
5729
  { when: yearChanged(options), asPart: false }
5571
5730
  );
@@ -5576,14 +5735,14 @@ function seasonReminder(options) {
5576
5735
  const tz = resolveTz(options, ctx);
5577
5736
  const now = /* @__PURE__ */ new Date();
5578
5737
  const currentSeason = getSeason(getMonthIndex(now, tz));
5579
- let diff2 = "";
5738
+ let diff = "";
5580
5739
  if (ctx.lastMessageAt !== void 0) {
5581
5740
  const prev = new Date(ctx.lastMessageAt);
5582
- diff2 = formatDiff([
5741
+ diff = formatDiff([
5583
5742
  diffLine("season", getSeason(getMonthIndex(prev, tz)), currentSeason)
5584
5743
  ]);
5585
5744
  }
5586
- return `${diff2}Season: ${currentSeason}`;
5745
+ return `${diff}Season: ${currentSeason}`;
5587
5746
  },
5588
5747
  { when: seasonChanged(options), asPart: false }
5589
5748
  );
@@ -5601,11 +5760,11 @@ function localeReminder() {
5601
5760
  const current = getLocaleFromMessage(ctx.currentMessage);
5602
5761
  if (!current) return "";
5603
5762
  const last = getLocaleFromMessage(ctx.lastMessage);
5604
- const diff2 = last ? formatDiff([
5763
+ const diff = last ? formatDiff([
5605
5764
  diffLine("language", last.language, current.language),
5606
5765
  diffLine("timezone", last.timeZone, current.timeZone)
5607
5766
  ]) : "";
5608
- return `${diff2}Language: ${current.language}
5767
+ return `${diff}Language: ${current.language}
5609
5768
  Timezone: ${current.timeZone}`;
5610
5769
  },
5611
5770
  { when: whenFn, asPart: false }
@@ -5692,7 +5851,7 @@ function toolCallCount(name, spec) {
5692
5851
  function everyNTurns(n) {
5693
5852
  return ({ turn }) => turn % n === 0;
5694
5853
  }
5695
- function once() {
5854
+ function first() {
5696
5855
  return ({ turn }) => turn === 1;
5697
5856
  }
5698
5857
  function firstN(n) {
@@ -6114,158 +6273,878 @@ import {
6114
6273
  } from "bash-tool";
6115
6274
  import z3 from "zod";
6116
6275
 
6117
- // packages/context/src/lib/sandbox/file-events.ts
6118
- var SnapshotFailedError = class extends Error {
6119
- stderr;
6120
- constructor(message2, stderr) {
6276
+ // packages/context/src/lib/sandbox/file-changes.ts
6277
+ import { randomUUID } from "node:crypto";
6278
+ import { posix as posix2 } from "node:path";
6279
+
6280
+ // packages/context/src/lib/sandbox/installers/installer.ts
6281
+ import "bash-tool";
6282
+ import spawn from "nano-spawn";
6283
+
6284
+ // packages/context/src/lib/sandbox/docker-sandbox-errors.ts
6285
+ var DockerSandboxError = class extends Error {
6286
+ containerId;
6287
+ constructor(message2, containerId) {
6121
6288
  super(message2);
6122
- this.name = "SnapshotFailedError";
6123
- this.stderr = stderr;
6289
+ this.name = "DockerSandboxError";
6290
+ this.containerId = containerId;
6124
6291
  }
6125
6292
  };
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
- }
6293
+ var DockerNotAvailableError = class extends DockerSandboxError {
6294
+ constructor() {
6295
+ super("Docker is not available. Ensure Docker daemon is running.");
6296
+ this.name = "DockerNotAvailableError";
6181
6297
  }
6182
- for (const path5 of before.keys()) {
6183
- if (!after.has(path5)) {
6184
- events.push({ path: path5, op: "delete", timestamp: ts });
6185
- }
6298
+ };
6299
+ var ContainerCreationError = class extends DockerSandboxError {
6300
+ image;
6301
+ cause;
6302
+ constructor(message2, image, cause) {
6303
+ super(`Failed to create container from image "${image}": ${message2}`);
6304
+ this.name = "ContainerCreationError";
6305
+ this.image = image;
6306
+ this.cause = cause;
6186
6307
  }
6187
- return events;
6188
- }
6189
- function observeSandboxFileEvents(sandbox, options) {
6190
- const { destination } = options;
6191
- if (!destination) {
6192
- throw new Error("observeSandboxFileEvents: destination is required");
6308
+ };
6309
+ var PackageInstallError = class extends DockerSandboxError {
6310
+ packages;
6311
+ image;
6312
+ packageManager;
6313
+ stderr;
6314
+ constructor(packages, image, packageManager, stderr, containerId) {
6315
+ super(
6316
+ `Package installation failed for [${packages.join(", ")}] using ${packageManager} on ${image}: ${stderr}`,
6317
+ containerId
6318
+ );
6319
+ this.name = "PackageInstallError";
6320
+ this.packages = packages;
6321
+ this.image = image;
6322
+ this.packageManager = packageManager;
6323
+ this.stderr = stderr;
6193
6324
  }
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
- };
6325
+ };
6326
+ var InstallError = class extends DockerSandboxError {
6327
+ target;
6328
+ source;
6329
+ reason;
6330
+ url;
6331
+ constructor(opts) {
6332
+ const where = opts.url ? `${opts.source} (${opts.url})` : opts.source;
6333
+ super(
6334
+ `Failed to install "${opts.target}" via ${where}: ${opts.reason}`,
6335
+ opts.containerId
6336
+ );
6337
+ this.name = "InstallError";
6338
+ this.target = opts.target;
6339
+ this.source = opts.source;
6340
+ this.reason = opts.reason;
6341
+ this.url = opts.url;
6342
+ }
6343
+ };
6344
+ var MissingRuntimeError = class extends DockerSandboxError {
6345
+ runtime;
6346
+ required;
6347
+ constructor(runtime, required, details, containerId) {
6348
+ const base = `Required runtime "${runtime}" is not installed (needs: ${required.join(", ")}).`;
6349
+ super(details ? `${base} ${details}` : base, containerId);
6350
+ this.name = "MissingRuntimeError";
6351
+ this.runtime = runtime;
6352
+ this.required = required;
6353
+ }
6354
+ };
6355
+ var VolumePathError = class extends DockerSandboxError {
6356
+ source;
6357
+ containerPath;
6358
+ reason;
6359
+ constructor(source, containerPath, reason) {
6360
+ super(
6361
+ `Invalid Docker volume path "${source}" -> "${containerPath}": ${reason}`
6362
+ );
6363
+ this.name = "VolumePathError";
6364
+ this.source = source;
6365
+ this.containerPath = containerPath;
6366
+ this.reason = reason;
6367
+ }
6368
+ };
6369
+ var VolumeInspectError = class extends DockerSandboxError {
6370
+ volume;
6371
+ reason;
6372
+ constructor(volume, reason) {
6373
+ super(`Failed to inspect Docker volume "${volume}": ${reason}`);
6374
+ this.name = "VolumeInspectError";
6375
+ this.volume = volume;
6376
+ this.reason = reason;
6377
+ }
6378
+ };
6379
+ var VolumeCreateError = class extends DockerSandboxError {
6380
+ volume;
6381
+ reason;
6382
+ constructor(volume, reason) {
6383
+ super(`Failed to create Docker volume "${volume}": ${reason}`);
6384
+ this.name = "VolumeCreateError";
6385
+ this.volume = volume;
6386
+ this.reason = reason;
6387
+ }
6388
+ };
6389
+ var VolumeRemoveError = class extends DockerSandboxError {
6390
+ volume;
6391
+ reason;
6392
+ constructor(volume, reason) {
6393
+ super(`Failed to remove Docker volume "${volume}": ${reason}`);
6394
+ this.name = "VolumeRemoveError";
6395
+ this.volume = volume;
6396
+ this.reason = reason;
6397
+ }
6398
+ };
6399
+ var DockerfileBuildError = class extends DockerSandboxError {
6400
+ stderr;
6401
+ constructor(stderr) {
6402
+ super(`Dockerfile build failed: ${stderr}`);
6403
+ this.name = "DockerfileBuildError";
6404
+ this.stderr = stderr;
6405
+ }
6406
+ };
6407
+ var ComposeStartError = class extends DockerSandboxError {
6408
+ composeFile;
6409
+ stderr;
6410
+ constructor(composeFile, stderr) {
6411
+ super(`Docker Compose failed to start: ${stderr}`);
6412
+ this.name = "ComposeStartError";
6413
+ this.composeFile = composeFile;
6414
+ this.stderr = stderr;
6415
+ }
6416
+ };
6417
+
6418
+ // packages/context/src/lib/sandbox/installers/installer.ts
6419
+ var Installer = class {
6420
+ };
6421
+ function isDebianBased(image) {
6422
+ const lower = image.toLowerCase();
6423
+ if (lower.includes("alpine")) return false;
6424
+ const debianPatterns = ["debian", "ubuntu", "node", "python"];
6425
+ return debianPatterns.some((pattern) => lower.includes(pattern));
6426
+ }
6427
+ function shellQuote(s) {
6428
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6429
+ }
6430
+ function createInstallerContext(containerId, image) {
6431
+ const packageManager = isDebianBased(image) ? "apt-get" : "apk";
6432
+ let archPromise = null;
6433
+ const ensuredTools = /* @__PURE__ */ new Set();
6434
+ let aptUpdated = false;
6435
+ const exec = async (command) => {
6436
+ try {
6437
+ const result = await spawn("docker", [
6438
+ "exec",
6439
+ containerId,
6440
+ "sh",
6441
+ "-c",
6442
+ command
6443
+ ]);
6444
+ return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
6445
+ } catch (error) {
6446
+ const err = error;
6447
+ return {
6448
+ stdout: err.stdout ?? "",
6449
+ stderr: err.stderr ?? err.message ?? "",
6450
+ exitCode: err.exitCode ?? 1
6451
+ };
6452
+ }
6453
+ };
6454
+ const arch = async () => {
6455
+ if (!archPromise) {
6456
+ const attempt = (async () => {
6457
+ const result = await exec("uname -m");
6458
+ if (result.exitCode !== 0) {
6459
+ throw new DockerSandboxError(
6460
+ `Failed to detect container architecture: ${result.stderr}`,
6461
+ containerId
6462
+ );
6463
+ }
6464
+ return result.stdout.trim();
6465
+ })();
6466
+ archPromise = attempt.catch((err) => {
6467
+ archPromise = null;
6468
+ throw err;
6469
+ });
6470
+ }
6471
+ return archPromise;
6472
+ };
6473
+ const installPackages = async (packages) => {
6474
+ if (packages.length === 0) return;
6475
+ const quoted = packages.map(shellQuote).join(" ");
6476
+ let cmd;
6477
+ if (packageManager === "apt-get") {
6478
+ cmd = aptUpdated ? `apt-get install -y ${quoted}` : `apt-get update && apt-get install -y ${quoted}`;
6479
+ } else {
6480
+ cmd = `apk add --no-cache ${quoted}`;
6481
+ }
6482
+ const result = await exec(cmd);
6483
+ if (result.exitCode !== 0) {
6484
+ throw new PackageInstallError(
6485
+ packages,
6486
+ image,
6487
+ packageManager,
6488
+ result.stderr,
6489
+ containerId
6490
+ );
6491
+ }
6492
+ if (packageManager === "apt-get") aptUpdated = true;
6493
+ };
6494
+ const ensureTool = async (checkName, installName) => {
6495
+ const cacheKey = installName ?? checkName;
6496
+ if (ensuredTools.has(cacheKey)) return;
6497
+ const check = await exec(`which ${shellQuote(checkName)}`);
6498
+ if (check.exitCode === 0) {
6499
+ ensuredTools.add(cacheKey);
6500
+ return;
6501
+ }
6502
+ await installPackages([installName ?? checkName]);
6503
+ ensuredTools.add(cacheKey);
6504
+ };
6505
+ return {
6506
+ containerId,
6507
+ image,
6508
+ packageManager,
6509
+ arch,
6510
+ exec,
6511
+ installPackages,
6512
+ ensureTool
6513
+ };
6514
+ }
6515
+
6516
+ // packages/context/src/lib/sandbox/installers/package-manager.ts
6517
+ var PackageInstaller = class extends Installer {
6518
+ kind;
6519
+ packages;
6520
+ constructor(packages) {
6521
+ super();
6522
+ this.packages = packages;
6523
+ this.kind = packages.length === 0 ? "pkg:<empty>" : `pkg:${packages.join(",")}`;
6524
+ }
6525
+ async install(ctx) {
6526
+ if (this.packages.length === 0) return;
6527
+ await ctx.installPackages([...this.packages]);
6528
+ }
6529
+ };
6530
+ function pkg(packages) {
6531
+ return new PackageInstaller(packages);
6532
+ }
6533
+
6534
+ // packages/context/src/lib/sandbox/installers/url-binary.ts
6535
+ var UrlBinaryInstaller = class extends Installer {
6536
+ kind;
6537
+ options;
6538
+ constructor(options) {
6539
+ super();
6540
+ this.options = options;
6541
+ this.kind = `url-binary:${options.name}`;
6542
+ }
6543
+ async install(ctx) {
6544
+ const url = await resolveUrl(ctx, this.options);
6545
+ await downloadAndInstall(
6546
+ ctx,
6547
+ this.options.name,
6548
+ url,
6549
+ this.options.binaryPath
6550
+ );
6551
+ }
6552
+ };
6553
+ function urlBinary(options) {
6554
+ return new UrlBinaryInstaller(options);
6555
+ }
6556
+ async function resolveUrl(ctx, options) {
6557
+ if (typeof options.url === "string") return options.url;
6558
+ const arch = await ctx.arch();
6559
+ const archUrl = options.url[arch];
6560
+ if (!archUrl) {
6561
+ throw new InstallError({
6562
+ target: options.name,
6563
+ source: "url",
6564
+ reason: `No URL provided for architecture "${arch}". Available: ${Object.keys(options.url).join(", ")}`,
6565
+ containerId: ctx.containerId
6566
+ });
6567
+ }
6568
+ return archUrl;
6569
+ }
6570
+ async function downloadAndInstall(ctx, name, url, binaryPath, source = "url") {
6571
+ await ctx.ensureTool("curl");
6572
+ const isTarGz = url.endsWith(".tar.gz") || url.endsWith(".tgz");
6573
+ const installCmd = isTarGz ? buildTarGzInstallCmd(name, url, binaryPath ?? name) : buildRawInstallCmd(name, url);
6574
+ const result = await ctx.exec(installCmd);
6575
+ if (result.exitCode !== 0) {
6576
+ throw new InstallError({
6577
+ target: name,
6578
+ source,
6579
+ url,
6580
+ reason: result.stderr,
6581
+ containerId: ctx.containerId
6582
+ });
6583
+ }
6584
+ }
6585
+ function buildTarGzInstallCmd(name, url, binaryPathInArchive) {
6586
+ return `
6587
+ set -e
6588
+ NAME=${shellQuote(name)}
6589
+ URL=${shellQuote(url)}
6590
+ BIN_IN_ARCHIVE=${shellQuote(binaryPathInArchive)}
6591
+ TMPDIR=$(mktemp -d)
6592
+ cd "$TMPDIR"
6593
+ curl -fsSL "$URL" -o archive.tar.gz
6594
+ tar -xzf archive.tar.gz
6595
+ BINARY_FILE=$(find . -name "$BIN_IN_ARCHIVE" -o -name "$NAME" | head -1)
6596
+ if [ -z "$BINARY_FILE" ]; then
6597
+ echo "Binary not found in archive. Contents:" >&2
6598
+ find . -type f >&2
6599
+ exit 1
6600
+ fi
6601
+ chmod +x "$BINARY_FILE"
6602
+ mv "$BINARY_FILE" "/usr/local/bin/$NAME"
6603
+ cd /
6604
+ rm -rf "$TMPDIR"
6605
+ `;
6606
+ }
6607
+ function buildRawInstallCmd(name, url) {
6608
+ return `
6609
+ NAME=${shellQuote(name)}
6610
+ URL=${shellQuote(url)}
6611
+ curl -fsSL "$URL" -o "/usr/local/bin/$NAME"
6612
+ chmod +x "/usr/local/bin/$NAME"
6613
+ `;
6614
+ }
6615
+
6616
+ // packages/context/src/lib/sandbox/installers/npm.ts
6617
+ var NODE_BINARIES = ["node", "npm"];
6618
+ var ENSURE_RUNTIME_HINT = "Pass `{ ensureRuntime: true }` to auto-install, or add it via `pkg([...])`.";
6619
+ var NpmInstaller = class extends Installer {
6620
+ kind;
6621
+ packageName;
6622
+ options;
6623
+ constructor(packageName, options = {}) {
6624
+ super();
6625
+ this.packageName = packageName;
6626
+ this.options = options;
6627
+ this.kind = `npm:${packageName}`;
6628
+ }
6629
+ async install(ctx) {
6630
+ await ensureNodeRuntime(ctx, this.options.ensureRuntime ?? false);
6631
+ const spec = this.options.version ? `${this.packageName}@${this.options.version}` : this.packageName;
6632
+ const result = await ctx.exec(`npm install -g ${spec}`);
6633
+ if (result.exitCode !== 0) {
6634
+ throw new InstallError({
6635
+ target: this.packageName,
6636
+ source: "npm",
6637
+ reason: result.stderr,
6638
+ containerId: ctx.containerId
6639
+ });
6640
+ }
6641
+ }
6642
+ };
6643
+ function npm(packageName, options) {
6644
+ return new NpmInstaller(packageName, options);
6645
+ }
6646
+ async function ensureNodeRuntime(ctx, ensure) {
6647
+ if (ensure) {
6648
+ await ctx.ensureTool("node", "nodejs");
6649
+ await ctx.ensureTool("npm");
6650
+ return;
6651
+ }
6652
+ const check = await ctx.exec("which node && which npm");
6653
+ if (check.exitCode !== 0) {
6654
+ throw new MissingRuntimeError(
6655
+ "npm",
6656
+ NODE_BINARIES,
6657
+ ENSURE_RUNTIME_HINT,
6658
+ ctx.containerId
6659
+ );
6660
+ }
6661
+ }
6662
+
6663
+ // packages/context/src/lib/sandbox/installers/bin.ts
6664
+ import { basename, posix } from "node:path";
6665
+ var NOT_FOUND_EXIT = 11;
6666
+ var CHMOD_FAILED_EXIT = 12;
6667
+ var BinInstaller = class extends Installer {
6668
+ kind;
6669
+ binary;
6670
+ options;
6671
+ #name;
6672
+ #target;
6673
+ constructor(binary, options = {}) {
6674
+ super();
6675
+ this.binary = binary;
6676
+ this.options = options;
6677
+ this.#name = resolveName(binary, options);
6678
+ this.#target = options.target ?? posix.join("/usr/local/bin", this.#name);
6679
+ this.kind = `bin:${this.#name}`;
6680
+ }
6681
+ async install(ctx) {
6682
+ const b = shellQuote(this.binary);
6683
+ const t = shellQuote(this.#target);
6684
+ const dir = shellQuote(posix.dirname(this.#target));
6685
+ const result = await ctx.exec(
6686
+ `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}`
6687
+ );
6688
+ if (result.exitCode === 0) return;
6689
+ throw new InstallError({
6690
+ target: this.#name,
6691
+ source: "bin",
6692
+ reason: explainFailure(result.exitCode, result.stderr, this.binary),
6693
+ containerId: ctx.containerId
6694
+ });
6695
+ }
6696
+ };
6697
+ function bin(binary, options) {
6698
+ return new BinInstaller(binary, options);
6699
+ }
6700
+ function explainFailure(exitCode, stderr, binary) {
6701
+ if (exitCode === NOT_FOUND_EXIT) {
6702
+ return `binary not found at ${binary}`;
6703
+ }
6704
+ if (exitCode === CHMOD_FAILED_EXIT && /read-only file system/i.test(stderr)) {
6705
+ 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()})`;
6706
+ }
6707
+ return stderr || `bin installer failed with exit code ${exitCode}`;
6708
+ }
6709
+ function resolveName(binary, options) {
6710
+ if (options.name) return options.name;
6711
+ const base = basename(binary);
6712
+ const dot = base.lastIndexOf(".");
6713
+ return dot > 0 ? base.slice(0, dot) : base;
6714
+ }
6715
+
6716
+ // packages/context/src/lib/sandbox/installers/pip.ts
6717
+ var PYTHON_BINARIES = ["python3", "pip3"];
6718
+ var PipInstaller = class extends Installer {
6719
+ kind;
6720
+ packageName;
6721
+ options;
6722
+ constructor(packageName, options = {}) {
6723
+ super();
6724
+ this.packageName = packageName;
6725
+ this.options = options;
6726
+ this.kind = `pip:${packageName}`;
6727
+ }
6728
+ async install(ctx) {
6729
+ await ensurePythonRuntime(ctx, this.options.ensureRuntime ?? false);
6730
+ const spec = this.options.version ? `${this.packageName}${formatVersion(this.options.version)}` : this.packageName;
6731
+ const flags = this.options.breakSystemPackages ?? true ? "--break-system-packages" : "";
6732
+ const result = await ctx.exec(`pip3 install ${flags} ${spec}`.trim());
6733
+ if (result.exitCode !== 0) {
6734
+ throw new InstallError({
6735
+ target: this.packageName,
6736
+ source: "pypi",
6737
+ reason: result.stderr,
6738
+ containerId: ctx.containerId
6739
+ });
6740
+ }
6741
+ }
6742
+ };
6743
+ function pip(packageName, options) {
6744
+ return new PipInstaller(packageName, options);
6745
+ }
6746
+ async function ensurePythonRuntime(ctx, ensure) {
6747
+ if (ensure) {
6748
+ const pipPackage = ctx.packageManager === "apk" ? "py3-pip" : "python3-pip";
6749
+ await ctx.ensureTool("python3");
6750
+ await ctx.ensureTool("pip3", pipPackage);
6751
+ return;
6752
+ }
6753
+ const check = await ctx.exec("which python3 && which pip3");
6754
+ if (check.exitCode !== 0) {
6755
+ throw new MissingRuntimeError(
6756
+ "pip",
6757
+ PYTHON_BINARIES,
6758
+ "Pass `{ ensureRuntime: true }` to auto-install, or add via `pkg([...])` (Alpine: `python3 py3-pip`; Debian: `python3 python3-pip`).",
6759
+ ctx.containerId
6760
+ );
6761
+ }
6762
+ }
6763
+ function formatVersion(version) {
6764
+ return /^[<>=!~]/.test(version) ? version : `==${version}`;
6765
+ }
6766
+
6767
+ // packages/context/src/lib/sandbox/installers/github-release.ts
6768
+ var GithubReleaseInstaller = class extends Installer {
6769
+ kind;
6770
+ options;
6771
+ constructor(options) {
6772
+ super();
6773
+ this.options = options;
6774
+ this.kind = `github-release:${options.owner}/${options.repo}@${options.version}`;
6775
+ }
6776
+ async install(ctx) {
6777
+ const arch = await ctx.arch();
6778
+ const assetName = this.options.asset(arch);
6779
+ const url = `https://github.com/${this.options.owner}/${this.options.repo}/releases/download/${this.options.version}/${assetName}`;
6780
+ await downloadAndInstall(
6781
+ ctx,
6782
+ this.options.name,
6783
+ url,
6784
+ this.options.binaryPath,
6785
+ "github-release"
6786
+ );
6787
+ }
6788
+ };
6789
+ function githubRelease(options) {
6790
+ return new GithubReleaseInstaller(options);
6791
+ }
6792
+
6793
+ // packages/context/src/lib/sandbox/file-changes.ts
6794
+ var REASON_HINT = {
6795
+ "ptrace-blocked": "ptrace is denied by the sandbox runtime. Modern Docker permits it by default; on a hardened host, relax the runtime seccomp/ptrace policy (allow the ptrace syscall, or add the SYS_PTRACE capability).",
6796
+ "strace-missing": "strace is not installed in the sandbox image. Bake `strace` into the image.",
6797
+ "trace-unparseable": "strace ran but its output is unusable \u2014 the sandbox is likely running under emulation (e.g. amd64 via Rosetta). Use a native-architecture sandbox."
6798
+ };
6799
+ var StraceUnavailableError = class extends Error {
6800
+ reason;
6801
+ diagnostics;
6802
+ constructor(reason, diagnostics) {
6803
+ super(
6804
+ `strace file-change tracking unavailable (${reason}): ${REASON_HINT[reason]}`
6805
+ );
6806
+ this.name = "StraceUnavailableError";
6807
+ this.reason = reason;
6808
+ this.diagnostics = diagnostics;
6809
+ }
6810
+ };
6811
+ var DEFAULT_TRACE_DIR = "/tmp/dat-trace";
6812
+ var STRACE_FLAGS = "-f -y -qq -e trace=%file,write,pwrite64,writev";
6813
+ function buildStraceCommand(command, traceFile, traceDir) {
6814
+ return `mkdir -p ${shellQuote(traceDir)} 2>/dev/null; strace ${STRACE_FLAGS} -o ${shellQuote(traceFile)} -- sh -c ${shellQuote(command)}`;
6815
+ }
6816
+ function stripStraceDiagnostics(stderr) {
6817
+ if (!stderr.includes("strace: ")) return stderr;
6818
+ return stderr.split("\n").filter((line) => !line.startsWith("strace: ")).join("\n");
6819
+ }
6820
+ var CREATE_OR_TRUNC_FLAGS = /O_(CREAT|TRUNC)/;
6821
+ var RENAME_SYSCALLS = /* @__PURE__ */ new Set(["rename", "renameat", "renameat2"]);
6822
+ var DELETE_SYSCALLS = /* @__PURE__ */ new Set(["unlink", "unlinkat", "rmdir"]);
6823
+ var MKDIR_SYSCALLS = /* @__PURE__ */ new Set(["mkdir", "mkdirat"]);
6824
+ var LINK_SYSCALLS = /* @__PURE__ */ new Set(["link", "linkat", "symlink", "symlinkat"]);
6825
+ var OPEN_SYSCALLS = /* @__PURE__ */ new Set(["open", "openat", "openat2", "creat"]);
6826
+ var WRITE_SYSCALLS = /* @__PURE__ */ new Set([
6827
+ "write",
6828
+ "pwrite64",
6829
+ "pwrite",
6830
+ "writev",
6831
+ "pwritev2"
6832
+ ]);
6833
+ var TRUNCATE_SYSCALLS = /* @__PURE__ */ new Set(["truncate", "ftruncate"]);
6834
+ function stitchLines(raw) {
6835
+ const pending = /* @__PURE__ */ new Map();
6836
+ const out = [];
6837
+ for (const original of raw.split("\n")) {
6838
+ const line = original.trimEnd();
6839
+ if (!line) continue;
6840
+ const pid = line.match(/^\s*(\d+)\s+/)?.[1] ?? "0";
6841
+ if (/<unfinished \.\.\.>\s*$/.test(line)) {
6842
+ pending.set(pid, line.replace(/<unfinished \.\.\.>\s*$/, ""));
6843
+ continue;
6844
+ }
6845
+ const resumed = line.match(/<\.\.\.\s+\S+\s+resumed>(.*)$/);
6846
+ if (resumed) {
6847
+ const head = pending.get(pid);
6848
+ pending.delete(pid);
6849
+ if (head !== void 0) out.push(head + resumed[1]);
6850
+ continue;
6851
+ }
6852
+ out.push(line);
6853
+ }
6854
+ return out;
6855
+ }
6856
+ function parseCall(line) {
6857
+ const m = line.match(
6858
+ /^\s*(?:\d+\s+)?([a-z_][a-z0-9_]*)\((.*)\)\s*=\s*(.+?)\s*$/
6859
+ );
6860
+ if (!m) return null;
6861
+ const [, syscall, args, rest] = m;
6862
+ const errnoMatch = rest.match(/^(-?\d+)\s+([A-Z][A-Z0-9_]*)/);
6863
+ if (errnoMatch) {
6864
+ return { syscall, args, retval: errnoMatch[1], errno: errnoMatch[2] };
6865
+ }
6866
+ return { syscall, args, retval: rest.split(/\s+/)[0] };
6867
+ }
6868
+ var STRACE_ESCAPE = {
6869
+ n: 10,
6870
+ t: 9,
6871
+ r: 13,
6872
+ v: 11,
6873
+ f: 12,
6874
+ '"': 34,
6875
+ "\\": 92
6876
+ };
6877
+ function decodeStraceString(s) {
6878
+ const bytes = [];
6879
+ for (let i = 0; i < s.length; ) {
6880
+ if (s[i] === "\\" && i + 1 < s.length) {
6881
+ const octal = s.slice(i + 1, i + 4).match(/^[0-7]{1,3}/)?.[0];
6882
+ if (octal) {
6883
+ bytes.push(parseInt(octal, 8) & 255);
6884
+ i += 1 + octal.length;
6885
+ continue;
6886
+ }
6887
+ const mapped = STRACE_ESCAPE[s[i + 1]];
6888
+ bytes.push(mapped ?? s.charCodeAt(i + 1));
6889
+ i += 2;
6890
+ continue;
6891
+ }
6892
+ const code = s.charCodeAt(i);
6893
+ if (code < 128) bytes.push(code);
6894
+ else for (const b of Buffer.from(s[i], "utf8")) bytes.push(b);
6895
+ i += 1;
6896
+ }
6897
+ return Buffer.from(bytes).toString("utf8");
6898
+ }
6899
+ function quotedStrings(args) {
6900
+ const out = [];
6901
+ const re = /"((?:[^"\\]|\\.)*)"/g;
6902
+ let m;
6903
+ while ((m = re.exec(args)) !== null) {
6904
+ out.push(decodeStraceString(m[1]));
6905
+ }
6906
+ return out;
6907
+ }
6908
+ function dirfdPath(args) {
6909
+ return args.match(/^\s*(?:AT_FDCWD|-?\d+)<([^>]*)>/)?.[1] ?? "/";
6910
+ }
6911
+ function fdPath(args) {
6912
+ return args.match(/^\s*-?\d+<([^>]*)>/)?.[1] ?? null;
6913
+ }
6914
+ function openFlags(args) {
6915
+ return args.match(/\bO_[A-Z_]+(?:\|O_[A-Z_]+)*/)?.[0] ?? "";
6916
+ }
6917
+ function parseStraceTrace(raw, options) {
6918
+ const dest = posix2.normalize(options.destination);
6919
+ const traceDir = options.traceDir;
6920
+ const traceFile = options.traceFile;
6921
+ const now = Date.now();
6922
+ const state = /* @__PURE__ */ new Map();
6923
+ const renames = [];
6924
+ const resolve4 = (dir, p) => posix2.normalize(p.startsWith("/") ? p : posix2.join(dir, p));
6925
+ const write = (path5) => {
6926
+ state.set(path5, "write");
6927
+ };
6928
+ const remove = (path5) => {
6929
+ if (state.get(path5) === "write") state.delete(path5);
6930
+ else state.set(path5, "delete");
6931
+ };
6932
+ for (const line of stitchLines(raw)) {
6933
+ const call = parseCall(line);
6934
+ if (!call || call.errno || call.retval === "-1") continue;
6935
+ const { syscall, args } = call;
6936
+ const dir = dirfdPath(args);
6937
+ if (RENAME_SYSCALLS.has(syscall)) {
6938
+ const strings = quotedStrings(args);
6939
+ if (strings.length < 2) continue;
6940
+ const from = resolve4(dir, strings[0]);
6941
+ const to = resolve4(dir, strings[strings.length - 1]);
6942
+ state.delete(from);
6943
+ renames.push({ from, to });
6944
+ continue;
6945
+ }
6946
+ if (DELETE_SYSCALLS.has(syscall)) {
6947
+ const strings = quotedStrings(args);
6948
+ if (strings.length) remove(resolve4(dir, strings[0]));
6949
+ continue;
6950
+ }
6951
+ if (MKDIR_SYSCALLS.has(syscall)) {
6952
+ const strings = quotedStrings(args);
6953
+ if (strings.length) write(resolve4(dir, strings[0]));
6954
+ continue;
6955
+ }
6956
+ if (LINK_SYSCALLS.has(syscall)) {
6957
+ const strings = quotedStrings(args);
6958
+ if (strings.length) write(resolve4(dir, strings[strings.length - 1]));
6959
+ continue;
6960
+ }
6961
+ if (OPEN_SYSCALLS.has(syscall)) {
6962
+ if (!CREATE_OR_TRUNC_FLAGS.test(openFlags(args))) continue;
6963
+ const strings = quotedStrings(args);
6964
+ if (strings.length) write(resolve4(dir, strings[0]));
6965
+ continue;
6966
+ }
6967
+ if (WRITE_SYSCALLS.has(syscall)) {
6968
+ const p = fdPath(args);
6969
+ if (p && Number(call.retval) > 0) write(posix2.normalize(p));
6970
+ continue;
6971
+ }
6972
+ if (TRUNCATE_SYSCALLS.has(syscall)) {
6973
+ const p = fdPath(args) ?? quotedStrings(args)[0];
6974
+ if (p) write(resolve4(dir, p));
6975
+ }
6976
+ }
6977
+ const keep = (path5) => {
6978
+ if (traceFile && path5 === traceFile) return false;
6979
+ if (traceDir && (path5 === traceDir || path5.startsWith(`${traceDir}/`))) {
6980
+ return false;
6981
+ }
6982
+ if (/^\/(proc|sys|dev)(\/|$)/.test(path5)) return false;
6983
+ return dest === "/" ? true : path5 === dest || path5.startsWith(`${dest}/`);
6984
+ };
6985
+ const changes = [];
6986
+ const emittedRenameTargets = /* @__PURE__ */ new Set();
6987
+ for (const { from, to } of renames) {
6988
+ if (!keep(to)) continue;
6989
+ if (state.get(to) === "delete") continue;
6990
+ changes.push({ op: "rename", path: to, from, timestamp: now });
6991
+ emittedRenameTargets.add(to);
6992
+ }
6993
+ const tail = [];
6994
+ for (const [path5, op] of state) {
6995
+ if (emittedRenameTargets.has(path5)) continue;
6996
+ if (keep(path5)) tail.push({ op, path: path5, timestamp: now });
6997
+ }
6998
+ tail.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
6999
+ changes.push(...tail);
7000
+ return changes;
7001
+ }
7002
+ var warnOnFileChangesError = (error) => {
7003
+ console.warn(
7004
+ "[traceFileChanges] onFileChanges threw on spawn; isolated",
7005
+ error
7006
+ );
7007
+ };
7008
+ function traceFileChanges(sandbox, options) {
7009
+ const { destination, onFileChanges } = options;
7010
+ const onError = options.onError ?? warnOnFileChangesError;
7011
+ const traceDir = options.traceDir ?? DEFAULT_TRACE_DIR;
7012
+ const innerExecute = sandbox.executeCommand.bind(sandbox);
7013
+ const innerReadFile = sandbox.readFile.bind(sandbox);
7014
+ const innerWriteFiles = sandbox.writeFiles.bind(sandbox);
7015
+ const dest = posix2.normalize(destination);
7016
+ const underDestination = (path5) => dest === "/" || path5 === dest || path5.startsWith(`${dest}/`);
7017
+ const removeTrace = (traceFile) => void innerExecute(`rm -f ${shellQuote(traceFile)}`).catch(() => {
7018
+ });
7019
+ const readChanges = async (traceFile) => {
6204
7020
  try {
6205
- const result = await fn();
6206
- await takeAfter();
6207
- return result;
6208
- } catch (err) {
6209
- await takeAfter().catch(() => {
7021
+ return parseStraceTrace(await innerReadFile(traceFile), {
7022
+ destination,
7023
+ traceFile,
7024
+ traceDir
6210
7025
  });
6211
- throw err;
7026
+ } catch {
7027
+ return [];
7028
+ } finally {
7029
+ removeTrace(traceFile);
7030
+ }
7031
+ };
7032
+ const collectCommand = async (traceFile) => {
7033
+ const changes = await readChanges(traceFile);
7034
+ if (!changes.length) return;
7035
+ useBashMeta()?.setHidden({ fileChanges: changes });
7036
+ await onFileChanges?.(changes);
7037
+ };
7038
+ const collectSpawn = async (traceFile) => {
7039
+ const changes = await readChanges(traceFile);
7040
+ if (!changes.length || !onFileChanges) return;
7041
+ try {
7042
+ await onFileChanges(changes);
7043
+ } catch (error) {
7044
+ onError(error);
6212
7045
  }
6213
7046
  };
6214
7047
  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;
7048
+ async executeCommand(command, execOptions) {
7049
+ const traceFile = `${traceDir}/${randomUUID()}.strace`;
7050
+ const wrapped = buildStraceCommand(command, traceFile, traceDir);
7051
+ let result;
7052
+ try {
7053
+ const raw = await innerExecute(wrapped, execOptions);
7054
+ result = { ...raw, stderr: stripStraceDiagnostics(raw.stderr) };
7055
+ } catch (error) {
7056
+ removeTrace(traceFile);
7057
+ throw error;
7058
+ }
7059
+ if (execOptions?.signal?.aborted) {
7060
+ removeTrace(traceFile);
7061
+ return result;
7062
+ }
7063
+ await collectCommand(traceFile);
7064
+ return result;
6222
7065
  },
6223
- async writeFiles(files) {
6224
- await observe(() => innerWriteFiles(files));
7066
+ readFile: innerReadFile,
7067
+ // The writeFile tool mutates the filesystem outside strace's view, so
7068
+ // observe it directly: synthesize a `write` change per file (under the
7069
+ // observation root) and run it through onFileChanges. A throw propagates to
7070
+ // the writeFile tool's execute (which rejects), the same gate as the bash
7071
+ // path — except there's no CommandResult here, so throw a plain Error to
7072
+ // reject; BashException.format() is meaningful only for bash commands.
7073
+ writeFiles: async (files) => {
7074
+ await innerWriteFiles(files);
7075
+ const now = Date.now();
7076
+ const changes = files.map(
7077
+ (f) => posix2.normalize(
7078
+ f.path.startsWith("/") ? f.path : posix2.join(dest, f.path)
7079
+ )
7080
+ ).filter(underDestination).map((path5) => ({ op: "write", path: path5, timestamp: now }));
7081
+ if (changes.length) await onFileChanges?.(changes);
6225
7082
  },
6226
- dispose: sandbox.dispose.bind(sandbox)
7083
+ dispose: async () => {
7084
+ await innerExecute(`rm -rf ${shellQuote(traceDir)}`).catch(() => {
7085
+ });
7086
+ await sandbox.dispose();
7087
+ }
6227
7088
  };
6228
7089
  if (sandbox.spawn) {
6229
7090
  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
- })();
7091
+ decorated.spawn = (command, spawnOptions) => {
7092
+ const traceFile = `${traceDir}/${randomUUID()}.strace`;
7093
+ const child = innerSpawn(
7094
+ buildStraceCommand(command, traceFile, traceDir),
7095
+ spawnOptions
7096
+ );
6238
7097
  const exit = (async () => {
6239
- const { before, child } = await started;
6240
7098
  try {
6241
7099
  return await child.exit;
6242
7100
  } 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
- };
7101
+ await collectSpawn(traceFile).catch(() => {
7102
+ });
7103
+ }
7104
+ })();
7105
+ return { stdout: child.stdout, stderr: child.stderr, exit };
6261
7106
  };
6262
7107
  }
6263
- return {
6264
- sandbox: decorated,
6265
- drain() {
6266
- return buffer.splice(0, buffer.length);
7108
+ return decorated;
7109
+ }
7110
+ var PTRACE_DENIED = /strace:[^\n]*(?:PTRACE_TRACEME|ptrace|Operation not permitted|EPERM)/i;
7111
+ var STRACE_MISSING = /strace:?\s+(?:command\s+)?not found/i;
7112
+ async function selfTestStrace(sandbox) {
7113
+ const probeDir = `/tmp/dat-strace-${randomUUID()}`;
7114
+ const traceFile = `/tmp/dat-strace-${randomUUID()}.trace`;
7115
+ const q = shellQuote;
7116
+ const sequence = `mkdir -p ${q(probeDir)} && echo hi > ${q(`${probeDir}/a.txt`)} && mv ${q(`${probeDir}/a.txt`)} ${q(`${probeDir}/b.txt`)} && echo more > ${q(`${probeDir}/c.txt`)}`;
7117
+ const wrapped = buildStraceCommand(sequence, traceFile, "/tmp");
7118
+ let result;
7119
+ let raw = "";
7120
+ try {
7121
+ result = await sandbox.executeCommand(wrapped);
7122
+ raw = await sandbox.readFile(traceFile).catch(() => "");
7123
+ const diagnostics = `exit=${result.exitCode}
7124
+ stderr=${result.stderr}
7125
+ trace[0:600]=${raw.slice(0, 600)}`;
7126
+ if (PTRACE_DENIED.test(result.stderr)) {
7127
+ throw new StraceUnavailableError("ptrace-blocked", diagnostics);
7128
+ }
7129
+ if (!raw || result.exitCode === 127 || STRACE_MISSING.test(result.stderr)) {
7130
+ throw new StraceUnavailableError("strace-missing", diagnostics);
7131
+ }
7132
+ const changes = parseStraceTrace(raw, { destination: probeDir, traceFile });
7133
+ const hasRealFdPath = raw.includes(`<${probeDir}/`);
7134
+ const hasRename = changes.some(
7135
+ (c) => c.op === "rename" && c.from === `${probeDir}/a.txt` && c.path === `${probeDir}/b.txt`
7136
+ );
7137
+ const hasWrite = changes.some((c) => c.op === "write");
7138
+ const allUnderDir = changes.every(
7139
+ (c) => c.path === probeDir || c.path.startsWith(`${probeDir}/`)
7140
+ );
7141
+ if (!hasRealFdPath || !hasRename || !hasWrite || !allUnderDir) {
7142
+ throw new StraceUnavailableError("trace-unparseable", diagnostics);
6267
7143
  }
6268
- };
7144
+ } finally {
7145
+ void sandbox.executeCommand(`rm -rf ${q(probeDir)} ${q(traceFile)}`).catch(() => {
7146
+ });
7147
+ }
6269
7148
  }
6270
7149
 
6271
7150
  // packages/context/src/lib/sandbox/upload-skills.ts
@@ -6432,12 +7311,18 @@ async function createBashTool(options) {
6432
7311
  extraInstructions,
6433
7312
  sandbox: backend,
6434
7313
  destination,
7314
+ onFileChanges,
7315
+ onError,
6435
7316
  ...rest
6436
7317
  } = options;
6437
- const observer = observeSandboxFileEvents(backend, {
6438
- destination: destination ?? "/workspace"
7318
+ const observationRoot = destination ?? "/workspace";
7319
+ await selfTestStrace(backend);
7320
+ const tracked = traceFileChanges(backend, {
7321
+ destination: observationRoot,
7322
+ onFileChanges,
7323
+ onError
6439
7324
  });
6440
- const sandbox = withAbortSignal(withBashExceptionCatch(observer.sandbox));
7325
+ const sandbox = withAbortSignal(withBashExceptionCatch(tracked));
6441
7326
  const combinedInstructions = [extraInstructions, REASONING_INSTRUCTION].filter(Boolean).join("\n\n");
6442
7327
  const toolkit = await externalCreateBashTool({
6443
7328
  ...rest,
@@ -6485,21 +7370,37 @@ async function createBashTool(options) {
6485
7370
  return { type: "json", value: visible };
6486
7371
  }
6487
7372
  });
6488
- const skills2 = await uploadSkills(sandbox, skillInputs);
7373
+ const upstreamWriteFile = toolkit.tools.writeFile;
7374
+ const originalWriteExecute = upstreamWriteFile.execute;
7375
+ const writeFileBuilder = tool2;
7376
+ const writeFile = writeFileBuilder({
7377
+ ...upstreamWriteFile,
7378
+ execute: async (input, options2) => {
7379
+ if (!originalWriteExecute) {
7380
+ throw new Error("writeFile tool execution is not available");
7381
+ }
7382
+ try {
7383
+ return await originalWriteExecute(input, options2);
7384
+ } catch (err) {
7385
+ if (err instanceof BashException) return err.format();
7386
+ throw err;
7387
+ }
7388
+ }
7389
+ });
7390
+ const skills2 = await uploadSkills(backend, skillInputs);
6489
7391
  return {
6490
7392
  ...toolkit,
6491
7393
  sandbox,
6492
7394
  bash,
6493
- tools: { ...toolkit.tools, bash },
6494
- skills: skills2,
6495
- drainFileEvents: () => observer.drain()
7395
+ tools: { ...toolkit.tools, bash, writeFile },
7396
+ skills: skills2
6496
7397
  };
6497
7398
  }
6498
7399
 
6499
7400
  // packages/context/src/lib/sandbox/binary-bridges.ts
6500
7401
  import { existsSync as existsSync2 } from "fs";
6501
7402
  import { defineCommand } from "just-bash";
6502
- import spawn from "nano-spawn";
7403
+ import spawn2 from "nano-spawn";
6503
7404
  import * as path4 from "path";
6504
7405
  function createBinaryBridges(...binaries) {
6505
7406
  return binaries.map((input) => {
@@ -6537,7 +7438,7 @@ function createBinaryBridges(...binaries) {
6537
7438
  PATH: process.env.PATH
6538
7439
  // Always use host PATH for binary bridges
6539
7440
  };
6540
- const result = await spawn(binaryPath, resolvedArgs, {
7441
+ const result = await spawn2(binaryPath, resolvedArgs, {
6541
7442
  cwd: realCwd,
6542
7443
  env: mergedEnv
6543
7444
  });
@@ -6594,7 +7495,7 @@ function resolveRealCwd(ctx) {
6594
7495
 
6595
7496
  // packages/context/src/lib/sandbox/daytona-sandbox.ts
6596
7497
  import "bash-tool";
6597
- import { randomUUID } from "node:crypto";
7498
+ import { randomUUID as randomUUID2 } from "node:crypto";
6598
7499
  var DAYTONA_DEFAULT_DESTINATION = "/home/daytona";
6599
7500
  var DAYTONA_EXIT_POLL_INTERVAL_MS = 250;
6600
7501
  var DAYTONA_EXIT_POLL_TIMEOUT_MS = 3e4;
@@ -7027,7 +7928,7 @@ function validateEnvKey(key) {
7027
7928
  }
7028
7929
  }
7029
7930
  function createSessionId(kind) {
7030
- return `deepagents-${kind}-${randomUUID()}`;
7931
+ return `deepagents-${kind}-${randomUUID2()}`;
7031
7932
  }
7032
7933
  function abortedCommandResult() {
7033
7934
  return {
@@ -7075,243 +7976,7 @@ import spawn3 from "nano-spawn";
7075
7976
  import { spawn as childSpawn } from "node:child_process";
7076
7977
  import { createHash } from "node:crypto";
7077
7978
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
7078
- import { Readable as Readable2 } from "node:stream";
7079
-
7080
- // packages/context/src/lib/sandbox/docker-sandbox-errors.ts
7081
- var DockerSandboxError = class extends Error {
7082
- containerId;
7083
- constructor(message2, containerId) {
7084
- super(message2);
7085
- this.name = "DockerSandboxError";
7086
- this.containerId = containerId;
7087
- }
7088
- };
7089
- var DockerNotAvailableError = class extends DockerSandboxError {
7090
- constructor() {
7091
- super("Docker is not available. Ensure Docker daemon is running.");
7092
- this.name = "DockerNotAvailableError";
7093
- }
7094
- };
7095
- var ContainerCreationError = class extends DockerSandboxError {
7096
- image;
7097
- cause;
7098
- constructor(message2, image, cause) {
7099
- super(`Failed to create container from image "${image}": ${message2}`);
7100
- this.name = "ContainerCreationError";
7101
- this.image = image;
7102
- this.cause = cause;
7103
- }
7104
- };
7105
- var PackageInstallError = class extends DockerSandboxError {
7106
- packages;
7107
- image;
7108
- packageManager;
7109
- stderr;
7110
- constructor(packages, image, packageManager, stderr, containerId) {
7111
- super(
7112
- `Package installation failed for [${packages.join(", ")}] using ${packageManager} on ${image}: ${stderr}`,
7113
- containerId
7114
- );
7115
- this.name = "PackageInstallError";
7116
- this.packages = packages;
7117
- this.image = image;
7118
- this.packageManager = packageManager;
7119
- this.stderr = stderr;
7120
- }
7121
- };
7122
- var InstallError = class extends DockerSandboxError {
7123
- target;
7124
- source;
7125
- reason;
7126
- url;
7127
- constructor(opts) {
7128
- const where = opts.url ? `${opts.source} (${opts.url})` : opts.source;
7129
- super(
7130
- `Failed to install "${opts.target}" via ${where}: ${opts.reason}`,
7131
- opts.containerId
7132
- );
7133
- this.name = "InstallError";
7134
- this.target = opts.target;
7135
- this.source = opts.source;
7136
- this.reason = opts.reason;
7137
- this.url = opts.url;
7138
- }
7139
- };
7140
- var MissingRuntimeError = class extends DockerSandboxError {
7141
- runtime;
7142
- required;
7143
- constructor(runtime, required, details, containerId) {
7144
- const base = `Required runtime "${runtime}" is not installed (needs: ${required.join(", ")}).`;
7145
- super(details ? `${base} ${details}` : base, containerId);
7146
- this.name = "MissingRuntimeError";
7147
- this.runtime = runtime;
7148
- this.required = required;
7149
- }
7150
- };
7151
- var VolumePathError = class extends DockerSandboxError {
7152
- source;
7153
- containerPath;
7154
- reason;
7155
- constructor(source, containerPath, reason) {
7156
- super(
7157
- `Invalid Docker volume path "${source}" -> "${containerPath}": ${reason}`
7158
- );
7159
- this.name = "VolumePathError";
7160
- this.source = source;
7161
- this.containerPath = containerPath;
7162
- this.reason = reason;
7163
- }
7164
- };
7165
- var VolumeInspectError = class extends DockerSandboxError {
7166
- volume;
7167
- reason;
7168
- constructor(volume, reason) {
7169
- super(`Failed to inspect Docker volume "${volume}": ${reason}`);
7170
- this.name = "VolumeInspectError";
7171
- this.volume = volume;
7172
- this.reason = reason;
7173
- }
7174
- };
7175
- var VolumeCreateError = class extends DockerSandboxError {
7176
- volume;
7177
- reason;
7178
- constructor(volume, reason) {
7179
- super(`Failed to create Docker volume "${volume}": ${reason}`);
7180
- this.name = "VolumeCreateError";
7181
- this.volume = volume;
7182
- this.reason = reason;
7183
- }
7184
- };
7185
- var VolumeRemoveError = class extends DockerSandboxError {
7186
- volume;
7187
- reason;
7188
- constructor(volume, reason) {
7189
- super(`Failed to remove Docker volume "${volume}": ${reason}`);
7190
- this.name = "VolumeRemoveError";
7191
- this.volume = volume;
7192
- this.reason = reason;
7193
- }
7194
- };
7195
- var DockerfileBuildError = class extends DockerSandboxError {
7196
- stderr;
7197
- constructor(stderr) {
7198
- super(`Dockerfile build failed: ${stderr}`);
7199
- this.name = "DockerfileBuildError";
7200
- this.stderr = stderr;
7201
- }
7202
- };
7203
- var ComposeStartError = class extends DockerSandboxError {
7204
- composeFile;
7205
- stderr;
7206
- constructor(composeFile, stderr) {
7207
- super(`Docker Compose failed to start: ${stderr}`);
7208
- this.name = "ComposeStartError";
7209
- this.composeFile = composeFile;
7210
- this.stderr = stderr;
7211
- }
7212
- };
7213
-
7214
- // packages/context/src/lib/sandbox/installers/installer.ts
7215
- import "bash-tool";
7216
- import spawn2 from "nano-spawn";
7217
- var Installer = class {
7218
- };
7219
- function isDebianBased(image) {
7220
- const lower = image.toLowerCase();
7221
- if (lower.includes("alpine")) return false;
7222
- const debianPatterns = ["debian", "ubuntu", "node", "python"];
7223
- return debianPatterns.some((pattern) => lower.includes(pattern));
7224
- }
7225
- function shellQuote3(s) {
7226
- return `'${s.replace(/'/g, `'\\''`)}'`;
7227
- }
7228
- function createInstallerContext(containerId, image) {
7229
- const packageManager = isDebianBased(image) ? "apt-get" : "apk";
7230
- let archPromise = null;
7231
- const ensuredTools = /* @__PURE__ */ new Set();
7232
- let aptUpdated = false;
7233
- const exec = async (command) => {
7234
- try {
7235
- const result = await spawn2("docker", [
7236
- "exec",
7237
- containerId,
7238
- "sh",
7239
- "-c",
7240
- command
7241
- ]);
7242
- return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
7243
- } catch (error) {
7244
- const err = error;
7245
- return {
7246
- stdout: err.stdout ?? "",
7247
- stderr: err.stderr ?? err.message ?? "",
7248
- exitCode: err.exitCode ?? 1
7249
- };
7250
- }
7251
- };
7252
- const arch = async () => {
7253
- if (!archPromise) {
7254
- const attempt = (async () => {
7255
- const result = await exec("uname -m");
7256
- if (result.exitCode !== 0) {
7257
- throw new DockerSandboxError(
7258
- `Failed to detect container architecture: ${result.stderr}`,
7259
- containerId
7260
- );
7261
- }
7262
- return result.stdout.trim();
7263
- })();
7264
- archPromise = attempt.catch((err) => {
7265
- archPromise = null;
7266
- throw err;
7267
- });
7268
- }
7269
- return archPromise;
7270
- };
7271
- const installPackages = async (packages) => {
7272
- if (packages.length === 0) return;
7273
- const quoted = packages.map(shellQuote3).join(" ");
7274
- let cmd;
7275
- if (packageManager === "apt-get") {
7276
- cmd = aptUpdated ? `apt-get install -y ${quoted}` : `apt-get update && apt-get install -y ${quoted}`;
7277
- } else {
7278
- cmd = `apk add --no-cache ${quoted}`;
7279
- }
7280
- const result = await exec(cmd);
7281
- if (result.exitCode !== 0) {
7282
- throw new PackageInstallError(
7283
- packages,
7284
- image,
7285
- packageManager,
7286
- result.stderr,
7287
- containerId
7288
- );
7289
- }
7290
- if (packageManager === "apt-get") aptUpdated = true;
7291
- };
7292
- const ensureTool = async (checkName, installName) => {
7293
- const cacheKey = installName ?? checkName;
7294
- if (ensuredTools.has(cacheKey)) return;
7295
- const check = await exec(`which ${shellQuote3(checkName)}`);
7296
- if (check.exitCode === 0) {
7297
- ensuredTools.add(cacheKey);
7298
- return;
7299
- }
7300
- await installPackages([installName ?? checkName]);
7301
- ensuredTools.add(cacheKey);
7302
- };
7303
- return {
7304
- containerId,
7305
- image,
7306
- packageManager,
7307
- arch,
7308
- exec,
7309
- installPackages,
7310
- ensureTool
7311
- };
7312
- }
7313
-
7314
- // packages/context/src/lib/sandbox/docker-sandbox.ts
7979
+ import { Readable as Readable2 } from "node:stream";
7315
7980
  function isDockerfileOptions(opts) {
7316
7981
  return "dockerfile" in opts;
7317
7982
  }
@@ -7752,560 +8417,314 @@ function buildDockerExecFlags(options) {
7752
8417
  flags.push("-w", options.cwd);
7753
8418
  }
7754
8419
  if (options?.env) {
7755
- for (const [key, value] of Object.entries(options.env)) {
7756
- validateEnvKey2(key);
7757
- flags.push("-e", `${key}=${value}`);
7758
- }
7759
- }
7760
- return flags;
7761
- }
7762
- function toSandboxProcess(child, abortSignal) {
7763
- if (!child.stdout || !child.stderr) {
7764
- child.kill("SIGKILL");
7765
- throw new DockerSandboxError("docker exec child missing stdio streams");
7766
- }
7767
- const onAbort = abortSignal ? () => child.kill("SIGKILL") : void 0;
7768
- if (abortSignal && onAbort) {
7769
- if (abortSignal.aborted) onAbort();
7770
- else abortSignal.addEventListener("abort", onAbort, { once: true });
7771
- }
7772
- return {
7773
- stdout: Readable2.toWeb(child.stdout),
7774
- stderr: Readable2.toWeb(child.stderr),
7775
- exit: new Promise((resolve4, reject) => {
7776
- const settle = () => {
7777
- child.removeListener("exit", onExitEvent);
7778
- child.removeListener("error", onError);
7779
- if (abortSignal && onAbort) {
7780
- abortSignal.removeEventListener("abort", onAbort);
7781
- }
7782
- };
7783
- const onError = (err) => {
7784
- settle();
7785
- reject(err);
7786
- };
7787
- const onExitEvent = (code, exitSignal) => {
7788
- settle();
7789
- resolve4({ code, signal: exitSignal, success: code === 0 });
7790
- };
7791
- child.on("exit", onExitEvent);
7792
- child.on("error", onError);
7793
- })
7794
- };
7795
- }
7796
- var RuntimeStrategy = class extends DockerSandboxStrategy {
7797
- image;
7798
- installers;
7799
- constructor(args = {}) {
7800
- super({
7801
- volumes: args.volumes,
7802
- resources: args.resources,
7803
- env: args.env,
7804
- name: args.name,
7805
- command: args.command
7806
- });
7807
- this.image = args.image ?? "alpine:latest";
7808
- this.installers = args.installers ?? [];
7809
- }
7810
- async getImage() {
7811
- return this.image;
7812
- }
7813
- async configure() {
7814
- const ctx = createInstallerContext(this.context.containerId, this.image);
7815
- for (const installer of this.installers) {
7816
- await installer.install(ctx);
7817
- }
7818
- }
7819
- };
7820
- var DockerfileStrategy = class extends DockerSandboxStrategy {
7821
- imageTag;
7822
- dockerfile;
7823
- dockerContext;
7824
- constructor(args) {
7825
- super({
7826
- volumes: args.volumes,
7827
- resources: args.resources,
7828
- env: args.env,
7829
- name: args.name,
7830
- command: args.command
7831
- });
7832
- this.dockerfile = args.dockerfile;
7833
- this.dockerContext = args.context ?? ".";
7834
- this.imageTag = this.computeImageTag();
7835
- }
7836
- computeImageTag() {
7837
- const content = this.isInlineDockerfile() ? this.dockerfile : readFileSync2(this.dockerfile, "utf-8");
7838
- const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
7839
- return `sandbox-${hash}`;
7840
- }
7841
- isInlineDockerfile() {
7842
- return this.dockerfile.includes("\n");
7843
- }
7844
- async getImage() {
7845
- const exists = await this.imageExists();
7846
- if (!exists) {
7847
- await this.buildImage();
7848
- }
7849
- return this.imageTag;
7850
- }
7851
- async configure() {
7852
- }
7853
- async imageExists() {
7854
- try {
7855
- await spawn3("docker", ["image", "inspect", this.imageTag]);
7856
- return true;
7857
- } catch {
7858
- return false;
7859
- }
7860
- }
7861
- async buildImage() {
7862
- try {
7863
- 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]);
7866
- } else {
7867
- await spawn3("docker", [
7868
- "build",
7869
- "-t",
7870
- this.imageTag,
7871
- "-f",
7872
- this.dockerfile,
7873
- this.dockerContext
7874
- ]);
7875
- }
7876
- } catch (error) {
7877
- const err = error;
7878
- throw new DockerfileBuildError(err.stderr || err.message);
7879
- }
7880
- }
7881
- };
7882
- var ComposeStrategy = class extends DockerSandboxStrategy {
7883
- projectName;
7884
- composeFile;
7885
- service;
7886
- constructor(args) {
7887
- super({ resources: args.resources });
7888
- this.composeFile = args.compose;
7889
- this.service = args.service;
7890
- this.projectName = this.computeProjectName();
7891
- }
7892
- computeProjectName() {
7893
- const content = readFileSync2(this.composeFile, "utf-8");
7894
- const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
7895
- return `sandbox-${hash}`;
7896
- }
7897
- async getImage() {
7898
- return "";
7899
- }
7900
- async startContainer(_image, _containerId) {
7901
- try {
7902
- await spawn3("docker", [
7903
- "compose",
7904
- "-f",
7905
- this.composeFile,
7906
- "-p",
7907
- this.projectName,
7908
- "up",
7909
- "-d"
7910
- ]);
7911
- } catch (error) {
7912
- const err = error;
7913
- if (err.stderr?.includes("Cannot connect")) {
7914
- throw new DockerNotAvailableError();
7915
- }
7916
- throw new ComposeStartError(this.composeFile, err.stderr || err.message);
7917
- }
7918
- }
7919
- defaultContainerId() {
7920
- return this.projectName;
7921
- }
7922
- async configure() {
7923
- }
7924
- async exec(command, options) {
7925
- try {
7926
- const result = await spawn3(
7927
- "docker",
7928
- [
7929
- "compose",
7930
- "-f",
7931
- this.composeFile,
7932
- "-p",
7933
- this.projectName,
7934
- "exec",
7935
- "-T",
7936
- this.service,
7937
- "sh",
7938
- "-c",
7939
- command
7940
- ],
7941
- { signal: options?.signal }
7942
- );
7943
- return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
7944
- } catch (error) {
7945
- const err = error;
7946
- return {
7947
- stdout: err.stdout || "",
7948
- stderr: err.stderr || err.message || "",
7949
- exitCode: err.exitCode ?? 1
7950
- };
7951
- }
7952
- }
7953
- spawnProcess(command, options) {
7954
- const child = childSpawn("docker", [
7955
- "compose",
7956
- "-f",
7957
- this.composeFile,
7958
- "-p",
7959
- this.projectName,
7960
- "exec",
7961
- "-T",
7962
- ...buildDockerExecFlags(options),
7963
- this.service,
7964
- "sh",
7965
- "-c",
7966
- command
7967
- ]);
7968
- return toSandboxProcess(child, options?.signal);
7969
- }
7970
- async stopContainer(_containerId) {
7971
- try {
7972
- await spawn3("docker", [
7973
- "compose",
7974
- "-f",
7975
- this.composeFile,
7976
- "-p",
7977
- this.projectName,
7978
- "down"
7979
- ]);
7980
- } catch {
7981
- }
7982
- }
7983
- };
7984
- async function createDockerSandbox(options = {}) {
7985
- let strategy;
7986
- if (isComposeOptions(options)) {
7987
- strategy = new ComposeStrategy({
7988
- compose: options.compose,
7989
- service: options.service,
7990
- resources: options.resources
7991
- });
7992
- } else if (isDockerfileOptions(options)) {
7993
- strategy = new DockerfileStrategy({
7994
- dockerfile: options.dockerfile,
7995
- context: options.context,
7996
- volumes: options.volumes,
7997
- resources: options.resources,
7998
- env: options.env,
7999
- name: options.name,
8000
- command: options.command
8001
- });
8002
- } else {
8003
- strategy = new RuntimeStrategy({
8004
- image: options.image,
8005
- installers: options.installers,
8006
- volumes: options.volumes,
8007
- resources: options.resources,
8008
- env: options.env,
8009
- name: options.name,
8010
- command: options.command
8011
- });
8420
+ for (const [key, value] of Object.entries(options.env)) {
8421
+ validateEnvKey2(key);
8422
+ flags.push("-e", `${key}=${value}`);
8423
+ }
8012
8424
  }
8013
- return strategy.create();
8425
+ return flags;
8014
8426
  }
8015
- async function useSandbox(options, fn) {
8016
- const sandbox = await createDockerSandbox(options);
8017
- try {
8018
- return await fn(sandbox);
8019
- } finally {
8020
- await sandbox.dispose();
8427
+ function toSandboxProcess(child, abortSignal) {
8428
+ if (!child.stdout || !child.stderr) {
8429
+ child.kill("SIGKILL");
8430
+ throw new DockerSandboxError("docker exec child missing stdio streams");
8431
+ }
8432
+ const onAbort = abortSignal ? () => child.kill("SIGKILL") : void 0;
8433
+ if (abortSignal && onAbort) {
8434
+ if (abortSignal.aborted) onAbort();
8435
+ else abortSignal.addEventListener("abort", onAbort, { once: true });
8021
8436
  }
8022
- }
8023
-
8024
- // packages/context/src/lib/sandbox/gcs.ts
8025
- function gcs(options) {
8026
8437
  return {
8027
- type: "bind",
8028
- hostPath: options.hostPath,
8029
- containerPath: options.mountPath,
8030
- readOnly: options.readOnly ?? false
8438
+ stdout: Readable2.toWeb(child.stdout),
8439
+ stderr: Readable2.toWeb(child.stderr),
8440
+ exit: new Promise((resolve4, reject) => {
8441
+ const settle = () => {
8442
+ child.removeListener("exit", onExitEvent);
8443
+ child.removeListener("error", onError);
8444
+ if (abortSignal && onAbort) {
8445
+ abortSignal.removeEventListener("abort", onAbort);
8446
+ }
8447
+ };
8448
+ const onError = (err) => {
8449
+ settle();
8450
+ reject(err);
8451
+ };
8452
+ const onExitEvent = (code, exitSignal) => {
8453
+ settle();
8454
+ resolve4({ code, signal: exitSignal, success: code === 0 });
8455
+ };
8456
+ child.on("exit", onExitEvent);
8457
+ child.on("error", onError);
8458
+ })
8031
8459
  };
8032
8460
  }
8033
-
8034
- // packages/context/src/lib/sandbox/installers/package-manager.ts
8035
- var PackageInstaller = class extends Installer {
8036
- kind;
8037
- packages;
8038
- constructor(packages) {
8039
- super();
8040
- this.packages = packages;
8041
- this.kind = packages.length === 0 ? "pkg:<empty>" : `pkg:${packages.join(",")}`;
8042
- }
8043
- async install(ctx) {
8044
- if (this.packages.length === 0) return;
8045
- await ctx.installPackages([...this.packages]);
8461
+ var RuntimeStrategy = class extends DockerSandboxStrategy {
8462
+ image;
8463
+ installers;
8464
+ constructor(args = {}) {
8465
+ super({
8466
+ volumes: args.volumes,
8467
+ resources: args.resources,
8468
+ env: args.env,
8469
+ name: args.name,
8470
+ command: args.command
8471
+ });
8472
+ this.image = args.image ?? "alpine:latest";
8473
+ this.installers = args.installers ?? [];
8046
8474
  }
8047
- };
8048
- function pkg(packages) {
8049
- return new PackageInstaller(packages);
8050
- }
8051
-
8052
- // packages/context/src/lib/sandbox/installers/url-binary.ts
8053
- var UrlBinaryInstaller = class extends Installer {
8054
- kind;
8055
- options;
8056
- constructor(options) {
8057
- super();
8058
- this.options = options;
8059
- this.kind = `url-binary:${options.name}`;
8475
+ async getImage() {
8476
+ return this.image;
8060
8477
  }
8061
- async install(ctx) {
8062
- const url = await resolveUrl(ctx, this.options);
8063
- await downloadAndInstall(
8064
- ctx,
8065
- this.options.name,
8066
- url,
8067
- this.options.binaryPath
8068
- );
8478
+ async configure() {
8479
+ const ctx = createInstallerContext(this.context.containerId, this.image);
8480
+ for (const installer of this.installers) {
8481
+ await installer.install(ctx);
8482
+ }
8069
8483
  }
8070
8484
  };
8071
- function urlBinary(options) {
8072
- return new UrlBinaryInstaller(options);
8073
- }
8074
- async function resolveUrl(ctx, options) {
8075
- if (typeof options.url === "string") return options.url;
8076
- const arch = await ctx.arch();
8077
- const archUrl = options.url[arch];
8078
- if (!archUrl) {
8079
- throw new InstallError({
8080
- target: options.name,
8081
- source: "url",
8082
- reason: `No URL provided for architecture "${arch}". Available: ${Object.keys(options.url).join(", ")}`,
8083
- containerId: ctx.containerId
8485
+ var DockerfileStrategy = class extends DockerSandboxStrategy {
8486
+ imageTag;
8487
+ dockerfile;
8488
+ dockerContext;
8489
+ showBuildLogs;
8490
+ constructor(args) {
8491
+ super({
8492
+ volumes: args.volumes,
8493
+ resources: args.resources,
8494
+ env: args.env,
8495
+ name: args.name,
8496
+ command: args.command
8084
8497
  });
8498
+ this.dockerfile = args.dockerfile;
8499
+ this.dockerContext = args.context ?? ".";
8500
+ this.showBuildLogs = args.showBuildLogs ?? false;
8501
+ this.imageTag = this.computeImageTag();
8085
8502
  }
8086
- return archUrl;
8087
- }
8088
- async function downloadAndInstall(ctx, name, url, binaryPath, source = "url") {
8089
- await ctx.ensureTool("curl");
8090
- const isTarGz = url.endsWith(".tar.gz") || url.endsWith(".tgz");
8091
- const installCmd = isTarGz ? buildTarGzInstallCmd(name, url, binaryPath ?? name) : buildRawInstallCmd(name, url);
8092
- const result = await ctx.exec(installCmd);
8093
- if (result.exitCode !== 0) {
8094
- throw new InstallError({
8095
- target: name,
8096
- source,
8097
- url,
8098
- reason: result.stderr,
8099
- containerId: ctx.containerId
8100
- });
8503
+ computeImageTag() {
8504
+ const content = this.isInlineDockerfile() ? this.dockerfile : readFileSync2(this.dockerfile, "utf-8");
8505
+ const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
8506
+ return `sandbox-${hash}`;
8101
8507
  }
8102
- }
8103
- function buildTarGzInstallCmd(name, url, binaryPathInArchive) {
8104
- return `
8105
- set -e
8106
- NAME=${shellQuote3(name)}
8107
- URL=${shellQuote3(url)}
8108
- BIN_IN_ARCHIVE=${shellQuote3(binaryPathInArchive)}
8109
- TMPDIR=$(mktemp -d)
8110
- cd "$TMPDIR"
8111
- curl -fsSL "$URL" -o archive.tar.gz
8112
- tar -xzf archive.tar.gz
8113
- BINARY_FILE=$(find . -name "$BIN_IN_ARCHIVE" -o -name "$NAME" | head -1)
8114
- if [ -z "$BINARY_FILE" ]; then
8115
- echo "Binary not found in archive. Contents:" >&2
8116
- find . -type f >&2
8117
- exit 1
8118
- fi
8119
- chmod +x "$BINARY_FILE"
8120
- mv "$BINARY_FILE" "/usr/local/bin/$NAME"
8121
- cd /
8122
- rm -rf "$TMPDIR"
8123
- `;
8124
- }
8125
- function buildRawInstallCmd(name, url) {
8126
- return `
8127
- NAME=${shellQuote3(name)}
8128
- URL=${shellQuote3(url)}
8129
- curl -fsSL "$URL" -o "/usr/local/bin/$NAME"
8130
- chmod +x "/usr/local/bin/$NAME"
8131
- `;
8132
- }
8133
-
8134
- // packages/context/src/lib/sandbox/installers/npm.ts
8135
- var NODE_BINARIES = ["node", "npm"];
8136
- var ENSURE_RUNTIME_HINT = "Pass `{ ensureRuntime: true }` to auto-install, or add it via `pkg([...])`.";
8137
- var NpmInstaller = class extends Installer {
8138
- kind;
8139
- packageName;
8140
- options;
8141
- constructor(packageName, options = {}) {
8142
- super();
8143
- this.packageName = packageName;
8144
- this.options = options;
8145
- this.kind = `npm:${packageName}`;
8508
+ isInlineDockerfile() {
8509
+ return this.dockerfile.includes("\n");
8146
8510
  }
8147
- async install(ctx) {
8148
- await ensureNodeRuntime(ctx, this.options.ensureRuntime ?? false);
8149
- const spec = this.options.version ? `${this.packageName}@${this.options.version}` : this.packageName;
8150
- const result = await ctx.exec(`npm install -g ${spec}`);
8151
- if (result.exitCode !== 0) {
8152
- throw new InstallError({
8153
- target: this.packageName,
8154
- source: "npm",
8155
- reason: result.stderr,
8156
- containerId: ctx.containerId
8157
- });
8511
+ async getImage() {
8512
+ const exists = await this.imageExists();
8513
+ if (!exists) {
8514
+ await this.buildImage();
8158
8515
  }
8516
+ return this.imageTag;
8159
8517
  }
8160
- };
8161
- function npm(packageName, options) {
8162
- return new NpmInstaller(packageName, options);
8163
- }
8164
- async function ensureNodeRuntime(ctx, ensure) {
8165
- if (ensure) {
8166
- await ctx.ensureTool("node", "nodejs");
8167
- await ctx.ensureTool("npm");
8168
- return;
8518
+ async configure() {
8169
8519
  }
8170
- const check = await ctx.exec("which node && which npm");
8171
- if (check.exitCode !== 0) {
8172
- throw new MissingRuntimeError(
8173
- "npm",
8174
- NODE_BINARIES,
8175
- ENSURE_RUNTIME_HINT,
8176
- ctx.containerId
8177
- );
8520
+ async imageExists() {
8521
+ try {
8522
+ await spawn3("docker", ["image", "inspect", this.imageTag]);
8523
+ return true;
8524
+ } catch {
8525
+ return false;
8526
+ }
8178
8527
  }
8179
- }
8180
-
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 {
8186
- kind;
8187
- binary;
8188
- options;
8189
- #name;
8190
- #target;
8191
- constructor(binary, options = {}) {
8192
- 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}`;
8528
+ async buildImage() {
8529
+ try {
8530
+ if (this.isInlineDockerfile()) {
8531
+ const buildCmd = `echo '${this.dockerfile.replace(/'/g, "'\\''")}' | docker build -t ${this.imageTag} -f - ${this.dockerContext}`;
8532
+ if (this.showBuildLogs) {
8533
+ await this.runStreamed("sh", ["-c", buildCmd]);
8534
+ } else {
8535
+ await spawn3("sh", ["-c", buildCmd]);
8536
+ }
8537
+ } else {
8538
+ const args = [
8539
+ "build",
8540
+ "-t",
8541
+ this.imageTag,
8542
+ "-f",
8543
+ this.dockerfile,
8544
+ this.dockerContext
8545
+ ];
8546
+ if (this.showBuildLogs) {
8547
+ await this.runStreamed("docker", args);
8548
+ } else {
8549
+ await spawn3("docker", args);
8550
+ }
8551
+ }
8552
+ } catch (error) {
8553
+ const err = error;
8554
+ throw new DockerfileBuildError(err.stderr || err.message);
8555
+ }
8198
8556
  }
8199
- 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
8557
+ /**
8558
+ * Run a build command with stdio inherited so its output streams live to the
8559
+ * parent terminal. On failure the build error is already on screen; the
8560
+ * rejection just carries the exit code.
8561
+ */
8562
+ runStreamed(command, args) {
8563
+ return new Promise((resolve4, reject) => {
8564
+ const child = childSpawn(command, args, { stdio: "inherit" });
8565
+ child.once("error", reject);
8566
+ child.once(
8567
+ "exit",
8568
+ (code) => code === 0 ? resolve4() : reject(
8569
+ new Error(
8570
+ `docker build exited with code ${code} (see build output above)`
8571
+ )
8572
+ )
8573
+ );
8212
8574
  });
8213
8575
  }
8214
8576
  };
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}`;
8577
+ var ComposeStrategy = class extends DockerSandboxStrategy {
8578
+ projectName;
8579
+ composeFile;
8580
+ service;
8581
+ constructor(args) {
8582
+ super({ resources: args.resources });
8583
+ this.composeFile = args.compose;
8584
+ this.service = args.service;
8585
+ this.projectName = this.computeProjectName();
8221
8586
  }
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()})`;
8587
+ computeProjectName() {
8588
+ const content = readFileSync2(this.composeFile, "utf-8");
8589
+ const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
8590
+ return `sandbox-${hash}`;
8224
8591
  }
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;
8232
- }
8233
-
8234
- // packages/context/src/lib/sandbox/installers/pip.ts
8235
- var PYTHON_BINARIES = ["python3", "pip3"];
8236
- var PipInstaller = class extends Installer {
8237
- kind;
8238
- packageName;
8239
- options;
8240
- constructor(packageName, options = {}) {
8241
- super();
8242
- this.packageName = packageName;
8243
- this.options = options;
8244
- this.kind = `pip:${packageName}`;
8592
+ async getImage() {
8593
+ return "";
8245
8594
  }
8246
- async install(ctx) {
8247
- await ensurePythonRuntime(ctx, this.options.ensureRuntime ?? false);
8248
- const spec = this.options.version ? `${this.packageName}${formatVersion(this.options.version)}` : this.packageName;
8249
- const flags = this.options.breakSystemPackages ?? true ? "--break-system-packages" : "";
8250
- const result = await ctx.exec(`pip3 install ${flags} ${spec}`.trim());
8251
- if (result.exitCode !== 0) {
8252
- throw new InstallError({
8253
- target: this.packageName,
8254
- source: "pypi",
8255
- reason: result.stderr,
8256
- containerId: ctx.containerId
8257
- });
8595
+ async startContainer(_image, _containerId) {
8596
+ try {
8597
+ await spawn3("docker", [
8598
+ "compose",
8599
+ "-f",
8600
+ this.composeFile,
8601
+ "-p",
8602
+ this.projectName,
8603
+ "up",
8604
+ "-d"
8605
+ ]);
8606
+ } catch (error) {
8607
+ const err = error;
8608
+ if (err.stderr?.includes("Cannot connect")) {
8609
+ throw new DockerNotAvailableError();
8610
+ }
8611
+ throw new ComposeStartError(this.composeFile, err.stderr || err.message);
8258
8612
  }
8259
8613
  }
8260
- };
8261
- function pip(packageName, options) {
8262
- return new PipInstaller(packageName, options);
8263
- }
8264
- async function ensurePythonRuntime(ctx, ensure) {
8265
- if (ensure) {
8266
- const pipPackage = ctx.packageManager === "apk" ? "py3-pip" : "python3-pip";
8267
- await ctx.ensureTool("python3");
8268
- await ctx.ensureTool("pip3", pipPackage);
8269
- return;
8614
+ defaultContainerId() {
8615
+ return this.projectName;
8270
8616
  }
8271
- const check = await ctx.exec("which python3 && which pip3");
8272
- if (check.exitCode !== 0) {
8273
- throw new MissingRuntimeError(
8274
- "pip",
8275
- PYTHON_BINARIES,
8276
- "Pass `{ ensureRuntime: true }` to auto-install, or add via `pkg([...])` (Alpine: `python3 py3-pip`; Debian: `python3 python3-pip`).",
8277
- ctx.containerId
8278
- );
8617
+ async configure() {
8279
8618
  }
8280
- }
8281
- function formatVersion(version) {
8282
- return /^[<>=!~]/.test(version) ? version : `==${version}`;
8283
- }
8284
-
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}`;
8619
+ async exec(command, options) {
8620
+ try {
8621
+ const result = await spawn3(
8622
+ "docker",
8623
+ [
8624
+ "compose",
8625
+ "-f",
8626
+ this.composeFile,
8627
+ "-p",
8628
+ this.projectName,
8629
+ "exec",
8630
+ "-T",
8631
+ this.service,
8632
+ "sh",
8633
+ "-c",
8634
+ command
8635
+ ],
8636
+ { signal: options?.signal }
8637
+ );
8638
+ return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
8639
+ } catch (error) {
8640
+ const err = error;
8641
+ return {
8642
+ stdout: err.stdout || "",
8643
+ stderr: err.stderr || err.message || "",
8644
+ exitCode: err.exitCode ?? 1
8645
+ };
8646
+ }
8293
8647
  }
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
- );
8648
+ spawnProcess(command, options) {
8649
+ const child = childSpawn("docker", [
8650
+ "compose",
8651
+ "-f",
8652
+ this.composeFile,
8653
+ "-p",
8654
+ this.projectName,
8655
+ "exec",
8656
+ "-T",
8657
+ ...buildDockerExecFlags(options),
8658
+ this.service,
8659
+ "sh",
8660
+ "-c",
8661
+ command
8662
+ ]);
8663
+ return toSandboxProcess(child, options?.signal);
8664
+ }
8665
+ async stopContainer(_containerId) {
8666
+ try {
8667
+ await spawn3("docker", [
8668
+ "compose",
8669
+ "-f",
8670
+ this.composeFile,
8671
+ "-p",
8672
+ this.projectName,
8673
+ "down"
8674
+ ]);
8675
+ } catch {
8676
+ }
8305
8677
  }
8306
8678
  };
8307
- function githubRelease(options) {
8308
- return new GithubReleaseInstaller(options);
8679
+ async function createDockerSandbox(options = {}) {
8680
+ let strategy;
8681
+ if (isComposeOptions(options)) {
8682
+ strategy = new ComposeStrategy({
8683
+ compose: options.compose,
8684
+ service: options.service,
8685
+ resources: options.resources
8686
+ });
8687
+ } else if (isDockerfileOptions(options)) {
8688
+ strategy = new DockerfileStrategy({
8689
+ dockerfile: options.dockerfile,
8690
+ context: options.context,
8691
+ showBuildLogs: options.showBuildLogs,
8692
+ volumes: options.volumes,
8693
+ resources: options.resources,
8694
+ env: options.env,
8695
+ name: options.name,
8696
+ command: options.command
8697
+ });
8698
+ } else {
8699
+ strategy = new RuntimeStrategy({
8700
+ image: options.image,
8701
+ installers: options.installers,
8702
+ volumes: options.volumes,
8703
+ resources: options.resources,
8704
+ env: options.env,
8705
+ name: options.name,
8706
+ command: options.command
8707
+ });
8708
+ }
8709
+ return strategy.create();
8710
+ }
8711
+ async function useSandbox(options, fn) {
8712
+ const sandbox = await createDockerSandbox(options);
8713
+ try {
8714
+ return await fn(sandbox);
8715
+ } finally {
8716
+ await sandbox.dispose();
8717
+ }
8718
+ }
8719
+
8720
+ // packages/context/src/lib/sandbox/gcs.ts
8721
+ function gcs(options) {
8722
+ return {
8723
+ type: "bind",
8724
+ hostPath: options.hostPath,
8725
+ containerPath: options.mountPath,
8726
+ readOnly: options.readOnly ?? false
8727
+ };
8309
8728
  }
8310
8729
 
8311
8730
  // packages/context/src/lib/sandbox/subcommand.ts
@@ -11654,10 +12073,10 @@ export {
11654
12073
  PostgresStreamStore,
11655
12074
  PromiseResolver,
11656
12075
  RuntimeStrategy,
11657
- SnapshotFailedError,
11658
12076
  SqlServerContextStore,
11659
12077
  SqliteContextStore,
11660
12078
  SqliteStreamStore,
12079
+ StraceUnavailableError,
11661
12080
  StreamManager,
11662
12081
  StreamStore,
11663
12082
  TitleGenerator,
@@ -11682,7 +12101,7 @@ export {
11682
12101
  applyInlineReminder,
11683
12102
  applyPartReminder,
11684
12103
  applyReminderToMessage,
11685
- applyToolOutputRemindersToMessage,
12104
+ applyRemindersToToolOutput,
11686
12105
  asStaticWordPartText,
11687
12106
  asStaticWordText,
11688
12107
  assertCountSpec,
@@ -11726,7 +12145,7 @@ export {
11726
12145
  executorContext,
11727
12146
  explain,
11728
12147
  fail,
11729
- findSingleOutputAvailableToolPart,
12148
+ first,
11730
12149
  firstN,
11731
12150
  fragment,
11732
12151
  fromFragment,
@@ -11750,6 +12169,7 @@ export {
11750
12169
  isFragmentObject,
11751
12170
  isMessageFragment,
11752
12171
  isRecord,
12172
+ isSyntheticSteerMessage,
11753
12173
  lastAssistantLength,
11754
12174
  loadSkillMetadata,
11755
12175
  localeReminder,
@@ -11765,10 +12185,10 @@ export {
11765
12185
  not,
11766
12186
  npm,
11767
12187
  nullUsage,
11768
- observeSandboxFileEvents,
11769
12188
  once,
11770
12189
  or,
11771
12190
  parseFrontmatter,
12191
+ parseStraceTrace,
11772
12192
  pass,
11773
12193
  persistedWriter,
11774
12194
  persona,
@@ -11795,7 +12215,8 @@ export {
11795
12215
  seasonChanged,
11796
12216
  seasonReminder,
11797
12217
  selfCritique,
11798
- shellQuote3 as shellQuote,
12218
+ selfTestStrace,
12219
+ shellQuote,
11799
12220
  skills,
11800
12221
  skillsReminder,
11801
12222
  socraticPrompting,
@@ -11806,6 +12227,7 @@ export {
11806
12227
  stripTextByRanges,
11807
12228
  structuredOutput,
11808
12229
  styleGuide,
12230
+ synthesizeSteerUserMessage,
11809
12231
  temporalReminder,
11810
12232
  term,
11811
12233
  timeReminder,
@@ -11815,6 +12237,7 @@ export {
11815
12237
  toolCallCount,
11816
12238
  toolCalled,
11817
12239
  toolFailed,
12240
+ traceFileChanges,
11818
12241
  uploadSkills,
11819
12242
  urlBinary,
11820
12243
  usageExceeds,