@deepagents/context 3.0.0 → 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 (44) 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 +1669 -1298
  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/daytona-sandbox.d.ts +6 -18
  23. package/dist/lib/sandbox/daytona-sandbox.d.ts.map +1 -1
  24. package/dist/lib/sandbox/docker-sandbox.d.ts +14 -0
  25. package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -1
  26. package/dist/lib/sandbox/file-changes.d.ts +83 -0
  27. package/dist/lib/sandbox/file-changes.d.ts.map +1 -0
  28. package/dist/lib/sandbox/index.d.ts +1 -1
  29. package/dist/lib/sandbox/index.d.ts.map +1 -1
  30. package/dist/lib/sandbox/types.d.ts +0 -8
  31. package/dist/lib/sandbox/types.d.ts.map +1 -1
  32. package/dist/lib/save/reminder-eval.d.ts +19 -0
  33. package/dist/lib/save/reminder-eval.d.ts.map +1 -0
  34. package/dist/lib/save/save-pipeline.d.ts +8 -2
  35. package/dist/lib/save/save-pipeline.d.ts.map +1 -1
  36. package/package.json +2 -2
  37. package/dist/lib/sandbox/file-events.d.ts +0 -42
  38. package/dist/lib/sandbox/file-events.d.ts.map +0 -1
  39. package/dist/lib/save/reminder-target-handler.d.ts +0 -36
  40. package/dist/lib/save/reminder-target-handler.d.ts.map +0 -1
  41. package/dist/lib/save/tool-output-target-handler.d.ts +0 -7
  42. package/dist/lib/save/tool-output-target-handler.d.ts.map +0 -1
  43. package/dist/lib/save/user-target-handler.d.ts +0 -7
  44. 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
- }
7101
+ await collectSpawn(traceFile).catch(() => {
7102
+ });
6248
7103
  }
6249
7104
  })();
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
- };
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;
@@ -6605,15 +7506,6 @@ var DaytonaSandboxError = class extends Error {
6605
7506
  this.cause = cause;
6606
7507
  }
6607
7508
  };
6608
- var DaytonaNotAvailableError = class extends DaytonaSandboxError {
6609
- constructor(cause) {
6610
- super(
6611
- "@daytona/sdk is not installed. Install it with: npm install @daytona/sdk",
6612
- cause
6613
- );
6614
- this.name = "DaytonaNotAvailableError";
6615
- }
6616
- };
6617
7509
  var DaytonaCreationError = class extends DaytonaSandboxError {
6618
7510
  constructor(message2, cause) {
6619
7511
  super(`Failed to create Daytona sandbox: ${message2}`, cause);
@@ -6626,54 +7518,56 @@ var DaytonaCommandError = class extends DaytonaSandboxError {
6626
7518
  this.name = "DaytonaCommandError";
6627
7519
  }
6628
7520
  };
6629
- async function createDaytonaSandbox(options = {}) {
7521
+ async function createDaytonaSandbox(client, options = {}) {
6630
7522
  validateDaytonaOptions(options);
6631
- const sdk = await importDaytonaSdk();
6632
- let client;
6633
- try {
6634
- client = new sdk.Daytona(createDaytonaConfig(options));
6635
- } catch (error) {
6636
- throw normalizeDaytonaError(error, sdk);
6637
- }
6638
- const attached = options.sandboxId !== void 0;
6639
- const reuseByName = !attached && options.name !== void 0;
6640
- const deleteOnDispose = options.deleteOnDispose ?? (!attached && !reuseByName);
7523
+ const sdk = await import("@daytona/sdk");
6641
7524
  let sandbox;
6642
7525
  try {
6643
- if (reuseByName) {
6644
- sandbox = await acquireReusedSandbox(client, options, sdk);
6645
- } else if (attached) {
7526
+ if (options.sandboxId !== void 0) {
6646
7527
  sandbox = await client.get(options.sandboxId);
6647
7528
  await startIfStopped(sandbox, options);
6648
7529
  } else {
6649
- sandbox = await createSandbox(client, options);
7530
+ sandbox = await acquireReusedSandbox(client, options, sdk);
6650
7531
  }
6651
7532
  } catch (error) {
6652
7533
  throw normalizeDaytonaError(error, sdk);
6653
7534
  }
6654
7535
  return createDaytonaSandboxMethods({
6655
- client,
6656
7536
  sandbox,
6657
- commandTimeout: options.commandTimeout,
6658
- deleteOnDispose,
6659
- deleteTimeout: options.deleteTimeout
7537
+ commandTimeout: options.commandTimeout
6660
7538
  });
6661
7539
  }
7540
+ var UNRECOVERABLE_SANDBOX_STATES = /* @__PURE__ */ new Set([
7541
+ "error",
7542
+ "build_failed",
7543
+ "destroyed"
7544
+ ]);
6662
7545
  async function acquireReusedSandbox(client, options, sdk) {
7546
+ let existing;
6663
7547
  try {
6664
- const sandbox = await client.get(options.name);
6665
- await startIfStopped(sandbox, options);
6666
- return sandbox;
7548
+ existing = await client.get(options.name);
6667
7549
  } catch (error) {
6668
- if (!(error instanceof sdk.DaytonaNotFoundError)) {
6669
- throw error;
7550
+ if (error instanceof sdk.DaytonaNotFoundError) {
7551
+ return createSandbox(client, options);
6670
7552
  }
7553
+ throw error;
7554
+ }
7555
+ if (existing.state && UNRECOVERABLE_SANDBOX_STATES.has(existing.state)) {
7556
+ await deleteSandboxQuietly(existing, options);
6671
7557
  return createSandbox(client, options);
6672
7558
  }
7559
+ await startIfStopped(existing, options);
7560
+ return existing;
7561
+ }
7562
+ async function deleteSandboxQuietly(sandbox, options) {
7563
+ try {
7564
+ await sandbox.delete(options.deleteTimeout);
7565
+ } catch {
7566
+ }
6673
7567
  }
6674
7568
  async function startIfStopped(sandbox, options) {
6675
7569
  if (sandbox.state && sandbox.state !== "started") {
6676
- await sandbox.start?.(options.startTimeout ?? options.createTimeout);
7570
+ await sandbox.start(options.startTimeout ?? options.createTimeout);
6677
7571
  }
6678
7572
  }
6679
7573
  function normalizeDaytonaError(error, sdk) {
@@ -6683,37 +7577,6 @@ function normalizeDaytonaError(error, sdk) {
6683
7577
  }
6684
7578
  return new DaytonaCreationError(err.message, err);
6685
7579
  }
6686
- async function importDaytonaSdk() {
6687
- try {
6688
- const mod = await import("@daytona/sdk");
6689
- if (typeof mod.Daytona !== "function") {
6690
- throw new DaytonaSandboxError(
6691
- "@daytona/sdk did not export a Daytona constructor"
6692
- );
6693
- }
6694
- return mod;
6695
- } catch (error) {
6696
- if (error instanceof DaytonaSandboxError) {
6697
- throw error;
6698
- }
6699
- const err = toError(error);
6700
- if (isMissingDaytonaSdk(err)) {
6701
- throw new DaytonaNotAvailableError(err);
6702
- }
6703
- throw err;
6704
- }
6705
- }
6706
- function createDaytonaConfig(options) {
6707
- return compactObject({
6708
- apiKey: options.apiKey,
6709
- jwtToken: options.jwtToken,
6710
- organizationId: options.organizationId,
6711
- apiUrl: options.apiUrl,
6712
- target: options.target,
6713
- otelEnabled: options.otelEnabled,
6714
- _experimental: options.experimental
6715
- });
6716
- }
6717
7580
  function createSandbox(client, options) {
6718
7581
  const base = compactObject({
6719
7582
  name: options.name,
@@ -6745,9 +7608,7 @@ function createSandbox(client, options) {
6745
7608
  ...base,
6746
7609
  snapshot: options.snapshot
6747
7610
  });
6748
- return client.create(Object.keys(params).length > 0 ? params : void 0, {
6749
- timeout: options.createTimeout
6750
- });
7611
+ return client.create(params, { timeout: options.createTimeout });
6751
7612
  }
6752
7613
  function validateDaytonaOptions(options) {
6753
7614
  if (options.image && options.snapshot) {
@@ -6760,6 +7621,11 @@ function validateDaytonaOptions(options) {
6760
7621
  'Daytona sandbox options can only include "resources" when creating from "image". The Daytona SDK does not apply resources during default or snapshot creation.'
6761
7622
  );
6762
7623
  }
7624
+ if (options.sandboxId === void 0 && options.name === void 0) {
7625
+ throw new DaytonaSandboxError(
7626
+ 'Daytona sandbox options require "name" (get-or-create) or "sandboxId" (attach). An unnamed sandbox cannot be reclaimed, since dispose() does not delete it.'
7627
+ );
7628
+ }
6763
7629
  if (!options.sandboxId) {
6764
7630
  return;
6765
7631
  }
@@ -6792,8 +7658,7 @@ function validateDaytonaOptions(options) {
6792
7658
  }
6793
7659
  }
6794
7660
  function createDaytonaSandboxMethods(args) {
6795
- const { client, sandbox, commandTimeout, deleteOnDispose, deleteTimeout } = args;
6796
- let disposed = false;
7661
+ const { sandbox, commandTimeout } = args;
6797
7662
  const executeCommand = async (command, options) => {
6798
7663
  if (options?.signal?.aborted) {
6799
7664
  return abortedCommandResult();
@@ -6876,17 +7741,6 @@ function createDaytonaSandboxMethods(args) {
6876
7741
  }
6877
7742
  },
6878
7743
  async dispose() {
6879
- if (disposed) return;
6880
- disposed = true;
6881
- try {
6882
- if (deleteOnDispose) {
6883
- if (sandbox.delete) await sandbox.delete(deleteTimeout);
6884
- else if (client.delete) await client.delete(sandbox, deleteTimeout);
6885
- }
6886
- } finally {
6887
- await client[Symbol.asyncDispose]?.().catch(() => {
6888
- });
6889
- }
6890
7744
  }
6891
7745
  };
6892
7746
  }
@@ -7074,7 +7928,7 @@ function validateEnvKey(key) {
7074
7928
  }
7075
7929
  }
7076
7930
  function createSessionId(kind) {
7077
- return `deepagents-${kind}-${randomUUID()}`;
7931
+ return `deepagents-${kind}-${randomUUID2()}`;
7078
7932
  }
7079
7933
  function abortedCommandResult() {
7080
7934
  return {
@@ -7112,10 +7966,6 @@ function compactObject(input) {
7112
7966
  }
7113
7967
  return output;
7114
7968
  }
7115
- function isMissingDaytonaSdk(error) {
7116
- const maybeCode = error.code;
7117
- return maybeCode === "ERR_MODULE_NOT_FOUND" || maybeCode === "MODULE_NOT_FOUND" || error.message.includes("@daytona/sdk");
7118
- }
7119
7969
  function toError(error) {
7120
7970
  return error instanceof Error ? error : new Error(String(error));
7121
7971
  }
@@ -7126,243 +7976,7 @@ import spawn3 from "nano-spawn";
7126
7976
  import { spawn as childSpawn } from "node:child_process";
7127
7977
  import { createHash } from "node:crypto";
7128
7978
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
7129
- import { Readable as Readable2 } from "node:stream";
7130
-
7131
- // packages/context/src/lib/sandbox/docker-sandbox-errors.ts
7132
- var DockerSandboxError = class extends Error {
7133
- containerId;
7134
- constructor(message2, containerId) {
7135
- super(message2);
7136
- this.name = "DockerSandboxError";
7137
- this.containerId = containerId;
7138
- }
7139
- };
7140
- var DockerNotAvailableError = class extends DockerSandboxError {
7141
- constructor() {
7142
- super("Docker is not available. Ensure Docker daemon is running.");
7143
- this.name = "DockerNotAvailableError";
7144
- }
7145
- };
7146
- var ContainerCreationError = class extends DockerSandboxError {
7147
- image;
7148
- cause;
7149
- constructor(message2, image, cause) {
7150
- super(`Failed to create container from image "${image}": ${message2}`);
7151
- this.name = "ContainerCreationError";
7152
- this.image = image;
7153
- this.cause = cause;
7154
- }
7155
- };
7156
- var PackageInstallError = class extends DockerSandboxError {
7157
- packages;
7158
- image;
7159
- packageManager;
7160
- stderr;
7161
- constructor(packages, image, packageManager, stderr, containerId) {
7162
- super(
7163
- `Package installation failed for [${packages.join(", ")}] using ${packageManager} on ${image}: ${stderr}`,
7164
- containerId
7165
- );
7166
- this.name = "PackageInstallError";
7167
- this.packages = packages;
7168
- this.image = image;
7169
- this.packageManager = packageManager;
7170
- this.stderr = stderr;
7171
- }
7172
- };
7173
- var InstallError = class extends DockerSandboxError {
7174
- target;
7175
- source;
7176
- reason;
7177
- url;
7178
- constructor(opts) {
7179
- const where = opts.url ? `${opts.source} (${opts.url})` : opts.source;
7180
- super(
7181
- `Failed to install "${opts.target}" via ${where}: ${opts.reason}`,
7182
- opts.containerId
7183
- );
7184
- this.name = "InstallError";
7185
- this.target = opts.target;
7186
- this.source = opts.source;
7187
- this.reason = opts.reason;
7188
- this.url = opts.url;
7189
- }
7190
- };
7191
- var MissingRuntimeError = class extends DockerSandboxError {
7192
- runtime;
7193
- required;
7194
- constructor(runtime, required, details, containerId) {
7195
- const base = `Required runtime "${runtime}" is not installed (needs: ${required.join(", ")}).`;
7196
- super(details ? `${base} ${details}` : base, containerId);
7197
- this.name = "MissingRuntimeError";
7198
- this.runtime = runtime;
7199
- this.required = required;
7200
- }
7201
- };
7202
- var VolumePathError = class extends DockerSandboxError {
7203
- source;
7204
- containerPath;
7205
- reason;
7206
- constructor(source, containerPath, reason) {
7207
- super(
7208
- `Invalid Docker volume path "${source}" -> "${containerPath}": ${reason}`
7209
- );
7210
- this.name = "VolumePathError";
7211
- this.source = source;
7212
- this.containerPath = containerPath;
7213
- this.reason = reason;
7214
- }
7215
- };
7216
- var VolumeInspectError = class extends DockerSandboxError {
7217
- volume;
7218
- reason;
7219
- constructor(volume, reason) {
7220
- super(`Failed to inspect Docker volume "${volume}": ${reason}`);
7221
- this.name = "VolumeInspectError";
7222
- this.volume = volume;
7223
- this.reason = reason;
7224
- }
7225
- };
7226
- var VolumeCreateError = class extends DockerSandboxError {
7227
- volume;
7228
- reason;
7229
- constructor(volume, reason) {
7230
- super(`Failed to create Docker volume "${volume}": ${reason}`);
7231
- this.name = "VolumeCreateError";
7232
- this.volume = volume;
7233
- this.reason = reason;
7234
- }
7235
- };
7236
- var VolumeRemoveError = class extends DockerSandboxError {
7237
- volume;
7238
- reason;
7239
- constructor(volume, reason) {
7240
- super(`Failed to remove Docker volume "${volume}": ${reason}`);
7241
- this.name = "VolumeRemoveError";
7242
- this.volume = volume;
7243
- this.reason = reason;
7244
- }
7245
- };
7246
- var DockerfileBuildError = class extends DockerSandboxError {
7247
- stderr;
7248
- constructor(stderr) {
7249
- super(`Dockerfile build failed: ${stderr}`);
7250
- this.name = "DockerfileBuildError";
7251
- this.stderr = stderr;
7252
- }
7253
- };
7254
- var ComposeStartError = class extends DockerSandboxError {
7255
- composeFile;
7256
- stderr;
7257
- constructor(composeFile, stderr) {
7258
- super(`Docker Compose failed to start: ${stderr}`);
7259
- this.name = "ComposeStartError";
7260
- this.composeFile = composeFile;
7261
- this.stderr = stderr;
7262
- }
7263
- };
7264
-
7265
- // packages/context/src/lib/sandbox/installers/installer.ts
7266
- import "bash-tool";
7267
- import spawn2 from "nano-spawn";
7268
- var Installer = class {
7269
- };
7270
- function isDebianBased(image) {
7271
- const lower = image.toLowerCase();
7272
- if (lower.includes("alpine")) return false;
7273
- const debianPatterns = ["debian", "ubuntu", "node", "python"];
7274
- return debianPatterns.some((pattern) => lower.includes(pattern));
7275
- }
7276
- function shellQuote3(s) {
7277
- return `'${s.replace(/'/g, `'\\''`)}'`;
7278
- }
7279
- function createInstallerContext(containerId, image) {
7280
- const packageManager = isDebianBased(image) ? "apt-get" : "apk";
7281
- let archPromise = null;
7282
- const ensuredTools = /* @__PURE__ */ new Set();
7283
- let aptUpdated = false;
7284
- const exec = async (command) => {
7285
- try {
7286
- const result = await spawn2("docker", [
7287
- "exec",
7288
- containerId,
7289
- "sh",
7290
- "-c",
7291
- command
7292
- ]);
7293
- return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
7294
- } catch (error) {
7295
- const err = error;
7296
- return {
7297
- stdout: err.stdout ?? "",
7298
- stderr: err.stderr ?? err.message ?? "",
7299
- exitCode: err.exitCode ?? 1
7300
- };
7301
- }
7302
- };
7303
- const arch = async () => {
7304
- if (!archPromise) {
7305
- const attempt = (async () => {
7306
- const result = await exec("uname -m");
7307
- if (result.exitCode !== 0) {
7308
- throw new DockerSandboxError(
7309
- `Failed to detect container architecture: ${result.stderr}`,
7310
- containerId
7311
- );
7312
- }
7313
- return result.stdout.trim();
7314
- })();
7315
- archPromise = attempt.catch((err) => {
7316
- archPromise = null;
7317
- throw err;
7318
- });
7319
- }
7320
- return archPromise;
7321
- };
7322
- const installPackages = async (packages) => {
7323
- if (packages.length === 0) return;
7324
- const quoted = packages.map(shellQuote3).join(" ");
7325
- let cmd;
7326
- if (packageManager === "apt-get") {
7327
- cmd = aptUpdated ? `apt-get install -y ${quoted}` : `apt-get update && apt-get install -y ${quoted}`;
7328
- } else {
7329
- cmd = `apk add --no-cache ${quoted}`;
7330
- }
7331
- const result = await exec(cmd);
7332
- if (result.exitCode !== 0) {
7333
- throw new PackageInstallError(
7334
- packages,
7335
- image,
7336
- packageManager,
7337
- result.stderr,
7338
- containerId
7339
- );
7340
- }
7341
- if (packageManager === "apt-get") aptUpdated = true;
7342
- };
7343
- const ensureTool = async (checkName, installName) => {
7344
- const cacheKey = installName ?? checkName;
7345
- if (ensuredTools.has(cacheKey)) return;
7346
- const check = await exec(`which ${shellQuote3(checkName)}`);
7347
- if (check.exitCode === 0) {
7348
- ensuredTools.add(cacheKey);
7349
- return;
7350
- }
7351
- await installPackages([installName ?? checkName]);
7352
- ensuredTools.add(cacheKey);
7353
- };
7354
- return {
7355
- containerId,
7356
- image,
7357
- packageManager,
7358
- arch,
7359
- exec,
7360
- installPackages,
7361
- ensureTool
7362
- };
7363
- }
7364
-
7365
- // packages/context/src/lib/sandbox/docker-sandbox.ts
7979
+ import { Readable as Readable2 } from "node:stream";
7366
7980
  function isDockerfileOptions(opts) {
7367
7981
  return "dockerfile" in opts;
7368
7982
  }
@@ -7803,560 +8417,314 @@ function buildDockerExecFlags(options) {
7803
8417
  flags.push("-w", options.cwd);
7804
8418
  }
7805
8419
  if (options?.env) {
7806
- for (const [key, value] of Object.entries(options.env)) {
7807
- validateEnvKey2(key);
7808
- flags.push("-e", `${key}=${value}`);
7809
- }
7810
- }
7811
- return flags;
7812
- }
7813
- function toSandboxProcess(child, abortSignal) {
7814
- if (!child.stdout || !child.stderr) {
7815
- child.kill("SIGKILL");
7816
- throw new DockerSandboxError("docker exec child missing stdio streams");
7817
- }
7818
- const onAbort = abortSignal ? () => child.kill("SIGKILL") : void 0;
7819
- if (abortSignal && onAbort) {
7820
- if (abortSignal.aborted) onAbort();
7821
- else abortSignal.addEventListener("abort", onAbort, { once: true });
7822
- }
7823
- return {
7824
- stdout: Readable2.toWeb(child.stdout),
7825
- stderr: Readable2.toWeb(child.stderr),
7826
- exit: new Promise((resolve4, reject) => {
7827
- const settle = () => {
7828
- child.removeListener("exit", onExitEvent);
7829
- child.removeListener("error", onError);
7830
- if (abortSignal && onAbort) {
7831
- abortSignal.removeEventListener("abort", onAbort);
7832
- }
7833
- };
7834
- const onError = (err) => {
7835
- settle();
7836
- reject(err);
7837
- };
7838
- const onExitEvent = (code, exitSignal) => {
7839
- settle();
7840
- resolve4({ code, signal: exitSignal, success: code === 0 });
7841
- };
7842
- child.on("exit", onExitEvent);
7843
- child.on("error", onError);
7844
- })
7845
- };
7846
- }
7847
- var RuntimeStrategy = class extends DockerSandboxStrategy {
7848
- image;
7849
- installers;
7850
- constructor(args = {}) {
7851
- super({
7852
- volumes: args.volumes,
7853
- resources: args.resources,
7854
- env: args.env,
7855
- name: args.name,
7856
- command: args.command
7857
- });
7858
- this.image = args.image ?? "alpine:latest";
7859
- this.installers = args.installers ?? [];
7860
- }
7861
- async getImage() {
7862
- return this.image;
7863
- }
7864
- async configure() {
7865
- const ctx = createInstallerContext(this.context.containerId, this.image);
7866
- for (const installer of this.installers) {
7867
- await installer.install(ctx);
7868
- }
7869
- }
7870
- };
7871
- var DockerfileStrategy = class extends DockerSandboxStrategy {
7872
- imageTag;
7873
- dockerfile;
7874
- dockerContext;
7875
- constructor(args) {
7876
- super({
7877
- volumes: args.volumes,
7878
- resources: args.resources,
7879
- env: args.env,
7880
- name: args.name,
7881
- command: args.command
7882
- });
7883
- this.dockerfile = args.dockerfile;
7884
- this.dockerContext = args.context ?? ".";
7885
- this.imageTag = this.computeImageTag();
7886
- }
7887
- computeImageTag() {
7888
- const content = this.isInlineDockerfile() ? this.dockerfile : readFileSync2(this.dockerfile, "utf-8");
7889
- const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
7890
- return `sandbox-${hash}`;
7891
- }
7892
- isInlineDockerfile() {
7893
- return this.dockerfile.includes("\n");
7894
- }
7895
- async getImage() {
7896
- const exists = await this.imageExists();
7897
- if (!exists) {
7898
- await this.buildImage();
7899
- }
7900
- return this.imageTag;
7901
- }
7902
- async configure() {
7903
- }
7904
- async imageExists() {
7905
- try {
7906
- await spawn3("docker", ["image", "inspect", this.imageTag]);
7907
- return true;
7908
- } catch {
7909
- return false;
7910
- }
7911
- }
7912
- async buildImage() {
7913
- try {
7914
- if (this.isInlineDockerfile()) {
7915
- const buildCmd = `echo '${this.dockerfile.replace(/'/g, "'\\''")}' | docker build -t ${this.imageTag} -f - ${this.dockerContext}`;
7916
- await spawn3("sh", ["-c", buildCmd]);
7917
- } else {
7918
- await spawn3("docker", [
7919
- "build",
7920
- "-t",
7921
- this.imageTag,
7922
- "-f",
7923
- this.dockerfile,
7924
- this.dockerContext
7925
- ]);
7926
- }
7927
- } catch (error) {
7928
- const err = error;
7929
- throw new DockerfileBuildError(err.stderr || err.message);
7930
- }
7931
- }
7932
- };
7933
- var ComposeStrategy = class extends DockerSandboxStrategy {
7934
- projectName;
7935
- composeFile;
7936
- service;
7937
- constructor(args) {
7938
- super({ resources: args.resources });
7939
- this.composeFile = args.compose;
7940
- this.service = args.service;
7941
- this.projectName = this.computeProjectName();
7942
- }
7943
- computeProjectName() {
7944
- const content = readFileSync2(this.composeFile, "utf-8");
7945
- const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
7946
- return `sandbox-${hash}`;
7947
- }
7948
- async getImage() {
7949
- return "";
7950
- }
7951
- async startContainer(_image, _containerId) {
7952
- try {
7953
- await spawn3("docker", [
7954
- "compose",
7955
- "-f",
7956
- this.composeFile,
7957
- "-p",
7958
- this.projectName,
7959
- "up",
7960
- "-d"
7961
- ]);
7962
- } catch (error) {
7963
- const err = error;
7964
- if (err.stderr?.includes("Cannot connect")) {
7965
- throw new DockerNotAvailableError();
7966
- }
7967
- throw new ComposeStartError(this.composeFile, err.stderr || err.message);
7968
- }
7969
- }
7970
- defaultContainerId() {
7971
- return this.projectName;
7972
- }
7973
- async configure() {
7974
- }
7975
- async exec(command, options) {
7976
- try {
7977
- const result = await spawn3(
7978
- "docker",
7979
- [
7980
- "compose",
7981
- "-f",
7982
- this.composeFile,
7983
- "-p",
7984
- this.projectName,
7985
- "exec",
7986
- "-T",
7987
- this.service,
7988
- "sh",
7989
- "-c",
7990
- command
7991
- ],
7992
- { signal: options?.signal }
7993
- );
7994
- return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
7995
- } catch (error) {
7996
- const err = error;
7997
- return {
7998
- stdout: err.stdout || "",
7999
- stderr: err.stderr || err.message || "",
8000
- exitCode: err.exitCode ?? 1
8001
- };
8002
- }
8003
- }
8004
- spawnProcess(command, options) {
8005
- const child = childSpawn("docker", [
8006
- "compose",
8007
- "-f",
8008
- this.composeFile,
8009
- "-p",
8010
- this.projectName,
8011
- "exec",
8012
- "-T",
8013
- ...buildDockerExecFlags(options),
8014
- this.service,
8015
- "sh",
8016
- "-c",
8017
- command
8018
- ]);
8019
- return toSandboxProcess(child, options?.signal);
8020
- }
8021
- async stopContainer(_containerId) {
8022
- try {
8023
- await spawn3("docker", [
8024
- "compose",
8025
- "-f",
8026
- this.composeFile,
8027
- "-p",
8028
- this.projectName,
8029
- "down"
8030
- ]);
8031
- } catch {
8032
- }
8033
- }
8034
- };
8035
- async function createDockerSandbox(options = {}) {
8036
- let strategy;
8037
- if (isComposeOptions(options)) {
8038
- strategy = new ComposeStrategy({
8039
- compose: options.compose,
8040
- service: options.service,
8041
- resources: options.resources
8042
- });
8043
- } else if (isDockerfileOptions(options)) {
8044
- strategy = new DockerfileStrategy({
8045
- dockerfile: options.dockerfile,
8046
- context: options.context,
8047
- volumes: options.volumes,
8048
- resources: options.resources,
8049
- env: options.env,
8050
- name: options.name,
8051
- command: options.command
8052
- });
8053
- } else {
8054
- strategy = new RuntimeStrategy({
8055
- image: options.image,
8056
- installers: options.installers,
8057
- volumes: options.volumes,
8058
- resources: options.resources,
8059
- env: options.env,
8060
- name: options.name,
8061
- command: options.command
8062
- });
8420
+ for (const [key, value] of Object.entries(options.env)) {
8421
+ validateEnvKey2(key);
8422
+ flags.push("-e", `${key}=${value}`);
8423
+ }
8063
8424
  }
8064
- return strategy.create();
8425
+ return flags;
8065
8426
  }
8066
- async function useSandbox(options, fn) {
8067
- const sandbox = await createDockerSandbox(options);
8068
- try {
8069
- return await fn(sandbox);
8070
- } finally {
8071
- 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 });
8072
8436
  }
8073
- }
8074
-
8075
- // packages/context/src/lib/sandbox/gcs.ts
8076
- function gcs(options) {
8077
8437
  return {
8078
- type: "bind",
8079
- hostPath: options.hostPath,
8080
- containerPath: options.mountPath,
8081
- 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
+ })
8082
8459
  };
8083
8460
  }
8084
-
8085
- // packages/context/src/lib/sandbox/installers/package-manager.ts
8086
- var PackageInstaller = class extends Installer {
8087
- kind;
8088
- packages;
8089
- constructor(packages) {
8090
- super();
8091
- this.packages = packages;
8092
- this.kind = packages.length === 0 ? "pkg:<empty>" : `pkg:${packages.join(",")}`;
8093
- }
8094
- async install(ctx) {
8095
- if (this.packages.length === 0) return;
8096
- 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 ?? [];
8097
8474
  }
8098
- };
8099
- function pkg(packages) {
8100
- return new PackageInstaller(packages);
8101
- }
8102
-
8103
- // packages/context/src/lib/sandbox/installers/url-binary.ts
8104
- var UrlBinaryInstaller = class extends Installer {
8105
- kind;
8106
- options;
8107
- constructor(options) {
8108
- super();
8109
- this.options = options;
8110
- this.kind = `url-binary:${options.name}`;
8475
+ async getImage() {
8476
+ return this.image;
8111
8477
  }
8112
- async install(ctx) {
8113
- const url = await resolveUrl(ctx, this.options);
8114
- await downloadAndInstall(
8115
- ctx,
8116
- this.options.name,
8117
- url,
8118
- this.options.binaryPath
8119
- );
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
+ }
8120
8483
  }
8121
8484
  };
8122
- function urlBinary(options) {
8123
- return new UrlBinaryInstaller(options);
8124
- }
8125
- async function resolveUrl(ctx, options) {
8126
- if (typeof options.url === "string") return options.url;
8127
- const arch = await ctx.arch();
8128
- const archUrl = options.url[arch];
8129
- if (!archUrl) {
8130
- throw new InstallError({
8131
- target: options.name,
8132
- source: "url",
8133
- reason: `No URL provided for architecture "${arch}". Available: ${Object.keys(options.url).join(", ")}`,
8134
- 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
8135
8497
  });
8498
+ this.dockerfile = args.dockerfile;
8499
+ this.dockerContext = args.context ?? ".";
8500
+ this.showBuildLogs = args.showBuildLogs ?? false;
8501
+ this.imageTag = this.computeImageTag();
8136
8502
  }
8137
- return archUrl;
8138
- }
8139
- async function downloadAndInstall(ctx, name, url, binaryPath, source = "url") {
8140
- await ctx.ensureTool("curl");
8141
- const isTarGz = url.endsWith(".tar.gz") || url.endsWith(".tgz");
8142
- const installCmd = isTarGz ? buildTarGzInstallCmd(name, url, binaryPath ?? name) : buildRawInstallCmd(name, url);
8143
- const result = await ctx.exec(installCmd);
8144
- if (result.exitCode !== 0) {
8145
- throw new InstallError({
8146
- target: name,
8147
- source,
8148
- url,
8149
- reason: result.stderr,
8150
- containerId: ctx.containerId
8151
- });
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}`;
8152
8507
  }
8153
- }
8154
- function buildTarGzInstallCmd(name, url, binaryPathInArchive) {
8155
- return `
8156
- set -e
8157
- NAME=${shellQuote3(name)}
8158
- URL=${shellQuote3(url)}
8159
- BIN_IN_ARCHIVE=${shellQuote3(binaryPathInArchive)}
8160
- TMPDIR=$(mktemp -d)
8161
- cd "$TMPDIR"
8162
- curl -fsSL "$URL" -o archive.tar.gz
8163
- tar -xzf archive.tar.gz
8164
- BINARY_FILE=$(find . -name "$BIN_IN_ARCHIVE" -o -name "$NAME" | head -1)
8165
- if [ -z "$BINARY_FILE" ]; then
8166
- echo "Binary not found in archive. Contents:" >&2
8167
- find . -type f >&2
8168
- exit 1
8169
- fi
8170
- chmod +x "$BINARY_FILE"
8171
- mv "$BINARY_FILE" "/usr/local/bin/$NAME"
8172
- cd /
8173
- rm -rf "$TMPDIR"
8174
- `;
8175
- }
8176
- function buildRawInstallCmd(name, url) {
8177
- return `
8178
- NAME=${shellQuote3(name)}
8179
- URL=${shellQuote3(url)}
8180
- curl -fsSL "$URL" -o "/usr/local/bin/$NAME"
8181
- chmod +x "/usr/local/bin/$NAME"
8182
- `;
8183
- }
8184
-
8185
- // packages/context/src/lib/sandbox/installers/npm.ts
8186
- var NODE_BINARIES = ["node", "npm"];
8187
- var ENSURE_RUNTIME_HINT = "Pass `{ ensureRuntime: true }` to auto-install, or add it via `pkg([...])`.";
8188
- var NpmInstaller = class extends Installer {
8189
- kind;
8190
- packageName;
8191
- options;
8192
- constructor(packageName, options = {}) {
8193
- super();
8194
- this.packageName = packageName;
8195
- this.options = options;
8196
- this.kind = `npm:${packageName}`;
8508
+ isInlineDockerfile() {
8509
+ return this.dockerfile.includes("\n");
8197
8510
  }
8198
- async install(ctx) {
8199
- await ensureNodeRuntime(ctx, this.options.ensureRuntime ?? false);
8200
- const spec = this.options.version ? `${this.packageName}@${this.options.version}` : this.packageName;
8201
- const result = await ctx.exec(`npm install -g ${spec}`);
8202
- if (result.exitCode !== 0) {
8203
- throw new InstallError({
8204
- target: this.packageName,
8205
- source: "npm",
8206
- reason: result.stderr,
8207
- containerId: ctx.containerId
8208
- });
8511
+ async getImage() {
8512
+ const exists = await this.imageExists();
8513
+ if (!exists) {
8514
+ await this.buildImage();
8209
8515
  }
8516
+ return this.imageTag;
8210
8517
  }
8211
- };
8212
- function npm(packageName, options) {
8213
- return new NpmInstaller(packageName, options);
8214
- }
8215
- async function ensureNodeRuntime(ctx, ensure) {
8216
- if (ensure) {
8217
- await ctx.ensureTool("node", "nodejs");
8218
- await ctx.ensureTool("npm");
8219
- return;
8518
+ async configure() {
8220
8519
  }
8221
- const check = await ctx.exec("which node && which npm");
8222
- if (check.exitCode !== 0) {
8223
- throw new MissingRuntimeError(
8224
- "npm",
8225
- NODE_BINARIES,
8226
- ENSURE_RUNTIME_HINT,
8227
- ctx.containerId
8228
- );
8520
+ async imageExists() {
8521
+ try {
8522
+ await spawn3("docker", ["image", "inspect", this.imageTag]);
8523
+ return true;
8524
+ } catch {
8525
+ return false;
8526
+ }
8229
8527
  }
8230
- }
8231
-
8232
- // packages/context/src/lib/sandbox/installers/bin.ts
8233
- import { basename, posix } from "node:path";
8234
- var NOT_FOUND_EXIT = 11;
8235
- var CHMOD_FAILED_EXIT = 12;
8236
- var BinInstaller = class extends Installer {
8237
- kind;
8238
- binary;
8239
- options;
8240
- #name;
8241
- #target;
8242
- constructor(binary, options = {}) {
8243
- super();
8244
- this.binary = binary;
8245
- this.options = options;
8246
- this.#name = resolveName(binary, options);
8247
- this.#target = options.target ?? posix.join("/usr/local/bin", this.#name);
8248
- 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
+ }
8249
8556
  }
8250
- async install(ctx) {
8251
- const b = shellQuote3(this.binary);
8252
- const t = shellQuote3(this.#target);
8253
- const dir = shellQuote3(posix.dirname(this.#target));
8254
- const result = await ctx.exec(
8255
- `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}`
8256
- );
8257
- if (result.exitCode === 0) return;
8258
- throw new InstallError({
8259
- target: this.#name,
8260
- source: "bin",
8261
- reason: explainFailure(result.exitCode, result.stderr, this.binary),
8262
- 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
+ );
8263
8574
  });
8264
8575
  }
8265
8576
  };
8266
- function bin(binary, options) {
8267
- return new BinInstaller(binary, options);
8268
- }
8269
- function explainFailure(exitCode, stderr, binary) {
8270
- if (exitCode === NOT_FOUND_EXIT) {
8271
- 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();
8272
8586
  }
8273
- if (exitCode === CHMOD_FAILED_EXIT && /read-only file system/i.test(stderr)) {
8274
- 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}`;
8275
8591
  }
8276
- return stderr || `bin installer failed with exit code ${exitCode}`;
8277
- }
8278
- function resolveName(binary, options) {
8279
- if (options.name) return options.name;
8280
- const base = basename(binary);
8281
- const dot = base.lastIndexOf(".");
8282
- return dot > 0 ? base.slice(0, dot) : base;
8283
- }
8284
-
8285
- // packages/context/src/lib/sandbox/installers/pip.ts
8286
- var PYTHON_BINARIES = ["python3", "pip3"];
8287
- var PipInstaller = class extends Installer {
8288
- kind;
8289
- packageName;
8290
- options;
8291
- constructor(packageName, options = {}) {
8292
- super();
8293
- this.packageName = packageName;
8294
- this.options = options;
8295
- this.kind = `pip:${packageName}`;
8592
+ async getImage() {
8593
+ return "";
8296
8594
  }
8297
- async install(ctx) {
8298
- await ensurePythonRuntime(ctx, this.options.ensureRuntime ?? false);
8299
- const spec = this.options.version ? `${this.packageName}${formatVersion(this.options.version)}` : this.packageName;
8300
- const flags = this.options.breakSystemPackages ?? true ? "--break-system-packages" : "";
8301
- const result = await ctx.exec(`pip3 install ${flags} ${spec}`.trim());
8302
- if (result.exitCode !== 0) {
8303
- throw new InstallError({
8304
- target: this.packageName,
8305
- source: "pypi",
8306
- reason: result.stderr,
8307
- containerId: ctx.containerId
8308
- });
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);
8309
8612
  }
8310
8613
  }
8311
- };
8312
- function pip(packageName, options) {
8313
- return new PipInstaller(packageName, options);
8314
- }
8315
- async function ensurePythonRuntime(ctx, ensure) {
8316
- if (ensure) {
8317
- const pipPackage = ctx.packageManager === "apk" ? "py3-pip" : "python3-pip";
8318
- await ctx.ensureTool("python3");
8319
- await ctx.ensureTool("pip3", pipPackage);
8320
- return;
8614
+ defaultContainerId() {
8615
+ return this.projectName;
8321
8616
  }
8322
- const check = await ctx.exec("which python3 && which pip3");
8323
- if (check.exitCode !== 0) {
8324
- throw new MissingRuntimeError(
8325
- "pip",
8326
- PYTHON_BINARIES,
8327
- "Pass `{ ensureRuntime: true }` to auto-install, or add via `pkg([...])` (Alpine: `python3 py3-pip`; Debian: `python3 python3-pip`).",
8328
- ctx.containerId
8329
- );
8617
+ async configure() {
8330
8618
  }
8331
- }
8332
- function formatVersion(version) {
8333
- return /^[<>=!~]/.test(version) ? version : `==${version}`;
8334
- }
8335
-
8336
- // packages/context/src/lib/sandbox/installers/github-release.ts
8337
- var GithubReleaseInstaller = class extends Installer {
8338
- kind;
8339
- options;
8340
- constructor(options) {
8341
- super();
8342
- this.options = options;
8343
- 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
+ }
8344
8647
  }
8345
- async install(ctx) {
8346
- const arch = await ctx.arch();
8347
- const assetName = this.options.asset(arch);
8348
- const url = `https://github.com/${this.options.owner}/${this.options.repo}/releases/download/${this.options.version}/${assetName}`;
8349
- await downloadAndInstall(
8350
- ctx,
8351
- this.options.name,
8352
- url,
8353
- this.options.binaryPath,
8354
- "github-release"
8355
- );
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
+ }
8356
8677
  }
8357
8678
  };
8358
- function githubRelease(options) {
8359
- 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
+ };
8360
8728
  }
8361
8729
 
8362
8730
  // packages/context/src/lib/sandbox/subcommand.ts
@@ -11677,7 +12045,6 @@ export {
11677
12045
  DEFAULT_WATCH_POLLING,
11678
12046
  DaytonaCommandError,
11679
12047
  DaytonaCreationError,
11680
- DaytonaNotAvailableError,
11681
12048
  DaytonaSandboxError,
11682
12049
  DockerNotAvailableError,
11683
12050
  DockerSandboxError,
@@ -11706,10 +12073,10 @@ export {
11706
12073
  PostgresStreamStore,
11707
12074
  PromiseResolver,
11708
12075
  RuntimeStrategy,
11709
- SnapshotFailedError,
11710
12076
  SqlServerContextStore,
11711
12077
  SqliteContextStore,
11712
12078
  SqliteStreamStore,
12079
+ StraceUnavailableError,
11713
12080
  StreamManager,
11714
12081
  StreamStore,
11715
12082
  TitleGenerator,
@@ -11734,7 +12101,7 @@ export {
11734
12101
  applyInlineReminder,
11735
12102
  applyPartReminder,
11736
12103
  applyReminderToMessage,
11737
- applyToolOutputRemindersToMessage,
12104
+ applyRemindersToToolOutput,
11738
12105
  asStaticWordPartText,
11739
12106
  asStaticWordText,
11740
12107
  assertCountSpec,
@@ -11778,7 +12145,7 @@ export {
11778
12145
  executorContext,
11779
12146
  explain,
11780
12147
  fail,
11781
- findSingleOutputAvailableToolPart,
12148
+ first,
11782
12149
  firstN,
11783
12150
  fragment,
11784
12151
  fromFragment,
@@ -11802,6 +12169,7 @@ export {
11802
12169
  isFragmentObject,
11803
12170
  isMessageFragment,
11804
12171
  isRecord,
12172
+ isSyntheticSteerMessage,
11805
12173
  lastAssistantLength,
11806
12174
  loadSkillMetadata,
11807
12175
  localeReminder,
@@ -11817,10 +12185,10 @@ export {
11817
12185
  not,
11818
12186
  npm,
11819
12187
  nullUsage,
11820
- observeSandboxFileEvents,
11821
12188
  once,
11822
12189
  or,
11823
12190
  parseFrontmatter,
12191
+ parseStraceTrace,
11824
12192
  pass,
11825
12193
  persistedWriter,
11826
12194
  persona,
@@ -11847,7 +12215,8 @@ export {
11847
12215
  seasonChanged,
11848
12216
  seasonReminder,
11849
12217
  selfCritique,
11850
- shellQuote3 as shellQuote,
12218
+ selfTestStrace,
12219
+ shellQuote,
11851
12220
  skills,
11852
12221
  skillsReminder,
11853
12222
  socraticPrompting,
@@ -11858,6 +12227,7 @@ export {
11858
12227
  stripTextByRanges,
11859
12228
  structuredOutput,
11860
12229
  styleGuide,
12230
+ synthesizeSteerUserMessage,
11861
12231
  temporalReminder,
11862
12232
  term,
11863
12233
  timeReminder,
@@ -11867,6 +12237,7 @@ export {
11867
12237
  toolCallCount,
11868
12238
  toolCalled,
11869
12239
  toolFailed,
12240
+ traceFileChanges,
11870
12241
  uploadSkills,
11871
12242
  urlBinary,
11872
12243
  usageExceeds,