@deepagents/context 5.0.0 → 6.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.
package/dist/index.js CHANGED
@@ -380,7 +380,8 @@ var Agent = class _Agent {
380
380
  }),
381
381
  stopWhen: this.#options.stopWhen ?? DEFAULT_STOP_WHEN,
382
382
  prepareStep: this.#options.context.createPrepareStep({
383
- steer: false
383
+ steer: false,
384
+ sandbox: this.#options.sandbox
384
385
  }),
385
386
  tools: this.tools,
386
387
  runtimeContext: contextVariables,
@@ -417,7 +418,8 @@ var Agent = class _Agent {
417
418
  throw new Error(`Agent ${this.#options.name} is missing a model.`);
418
419
  }
419
420
  const prepareStep = this.#options.context.createPrepareStep({
420
- additionalInput: this.#options.prepareStepInput
421
+ additionalInput: this.#options.prepareStepInput,
422
+ sandbox: this.#options.sandbox
421
423
  });
422
424
  const result = await this.#createRawStream(
423
425
  contextVariables,
@@ -462,7 +464,7 @@ var Agent = class _Agent {
462
464
  }),
463
465
  repairToolCall: createRepairToolCall(model, config?.abortSignal),
464
466
  stopWhen: this.#options.stopWhen ?? DEFAULT_STOP_WHEN,
465
- prepareStep: prepareStep ?? context.createPrepareStep(),
467
+ prepareStep: prepareStep ?? context.createPrepareStep({ sandbox: this.#options.sandbox }),
466
468
  experimental_transform: config?.transform ?? smoothStream(),
467
469
  tools: this.tools,
468
470
  runtimeContext: contextVariables,
@@ -2302,6 +2304,16 @@ function toolCallCount(name, spec) {
2302
2304
  return checkCount(count, spec);
2303
2305
  };
2304
2306
  }
2307
+ function everyNToolCalls(n) {
2308
+ if (!Number.isInteger(n) || n < 1) {
2309
+ throw new Error("everyNToolCalls(n) requires a positive integer");
2310
+ }
2311
+ return (ctx) => toolPartsOf(ctx.lastAssistantMessage).filter(
2312
+ (part) => ["output-available", "output-error", "output-denied"].includes(
2313
+ part.state
2314
+ )
2315
+ ).length >= n;
2316
+ }
2305
2317
  function toolFailureStreak(ctx, name) {
2306
2318
  const parts = (ctx.lastAssistantMessages ?? []).flatMap(
2307
2319
  (message2) => toolPartsOf(message2).filter((part) => matchesName(name, toolNameOf(part)))
@@ -3838,6 +3850,42 @@ function estimateMessageContent(data) {
3838
3850
  function isLanguageModelUsage(value) {
3839
3851
  return typeof value === "object" && value !== null && "totalTokens" in value;
3840
3852
  }
3853
+ function cloneFragmentData(data, seen) {
3854
+ if (typeof data !== "object" || data === null) return data;
3855
+ const existing = seen.get(data);
3856
+ if (existing !== void 0) return existing;
3857
+ if (isFragment(data)) {
3858
+ const clone = { ...data };
3859
+ seen.set(data, clone);
3860
+ if ("data" in data) {
3861
+ clone.data = cloneFragmentData(data.data, seen);
3862
+ }
3863
+ return clone;
3864
+ }
3865
+ if (Array.isArray(data)) {
3866
+ const clone = [];
3867
+ seen.set(data, clone);
3868
+ clone.push(...data.map((item) => cloneFragmentData(item, seen)));
3869
+ return clone;
3870
+ }
3871
+ if (isFragmentObject(data)) {
3872
+ const clone = {};
3873
+ seen.set(data, clone);
3874
+ for (const [key, value] of Object.entries(data)) {
3875
+ clone[key] = cloneFragmentData(value, seen);
3876
+ }
3877
+ return clone;
3878
+ }
3879
+ return data;
3880
+ }
3881
+ function cloneFragment(fragment2) {
3882
+ const clone = { ...fragment2 };
3883
+ const seen = new WeakMap([[fragment2, clone]]);
3884
+ if ("data" in fragment2) {
3885
+ clone.data = cloneFragmentData(fragment2.data, seen);
3886
+ }
3887
+ return clone;
3888
+ }
3841
3889
  function addUsageValue(current, next) {
3842
3890
  if (current === void 0 && next === void 0) {
3843
3891
  return void 0;
@@ -3911,13 +3959,6 @@ function toolOutcomesFromStep(content) {
3911
3959
  }
3912
3960
  return outcomes;
3913
3961
  }
3914
- function stepStartPartIndices(parts) {
3915
- const indices = [];
3916
- for (let i = 0; i < parts.length; i++) {
3917
- if (parts[i].type === "step-start") indices.push(i);
3918
- }
3919
- return indices;
3920
- }
3921
3962
  var ContextEngine = class _ContextEngine {
3922
3963
  /** Non-message fragments (role, hints, etc.) - not persisted in graph */
3923
3964
  #fragments = [];
@@ -4072,7 +4113,7 @@ var ContextEngine = class _ContextEngine {
4072
4113
  * Count user turns in the conversation and return the previous saved user message context.
4073
4114
  * Includes persisted messages and pending messages in the turn count.
4074
4115
  */
4075
- async #getChainContext() {
4116
+ async #getChainContext(pending = this.#pendingMessages) {
4076
4117
  await this.#ensureInitialized();
4077
4118
  const builder = new ChainSummaryBuilder();
4078
4119
  if (this.#branch?.headMessageId) {
@@ -4081,8 +4122,7 @@ var ContextEngine = class _ContextEngine {
4081
4122
  );
4082
4123
  for (const msg of chain) builder.ingestStored(msg);
4083
4124
  }
4084
- for (const fragment2 of this.#pendingMessages)
4085
- builder.ingestPending(fragment2);
4125
+ for (const fragment2 of pending) builder.ingestPending(fragment2);
4086
4126
  return builder.build();
4087
4127
  }
4088
4128
  async getTurnCount() {
@@ -4229,14 +4269,14 @@ var ContextEngine = class _ContextEngine {
4229
4269
  * Add fragments to the context.
4230
4270
  *
4231
4271
  * - Message fragments (user/assistant) are queued for persistence
4232
- * - Non-message fragments (role/hint) are kept in memory for system prompt
4272
+ * - Non-message fragments are cloned so lazy resolution stays engine-local
4233
4273
  */
4234
4274
  set(...fragments) {
4235
4275
  for (const fragment2 of fragments) {
4236
4276
  if (isMessageFragment(fragment2)) {
4237
4277
  this.#pendingMessages.push(fragment2);
4238
4278
  } else {
4239
- this.#fragments.push(fragment2);
4279
+ this.#fragments.push(cloneFragment(fragment2));
4240
4280
  }
4241
4281
  }
4242
4282
  return this;
@@ -4299,12 +4339,16 @@ var ContextEngine = class _ContextEngine {
4299
4339
  return { headMessageId: this.#branch?.headMessageId ?? void 0 };
4300
4340
  }
4301
4341
  const pending = this.#pendingMessages;
4302
- const pipeline = new SavePipeline(this.#asSavePipelineEngine(), pending);
4303
- await pipeline.applyUpdateBranching(options?.branch ?? true);
4304
- await this.#foldUserReminders(pending);
4305
- const result = await pipeline.persist();
4306
4342
  this.#pendingMessages = [];
4307
- return result;
4343
+ const pipeline = new SavePipeline(this.#asSavePipelineEngine(), pending);
4344
+ try {
4345
+ await pipeline.applyUpdateBranching(options?.branch ?? true);
4346
+ await this.#foldUserReminders(pending);
4347
+ return await pipeline.persist();
4348
+ } catch (error) {
4349
+ this.#pendingMessages.unshift(...pending);
4350
+ throw error;
4351
+ }
4308
4352
  }
4309
4353
  #currentReminderSession;
4310
4354
  /**
@@ -4318,27 +4362,19 @@ var ContextEngine = class _ContextEngine {
4318
4362
  * latch is desired. AI SDK v7 carries prepared message overrides forward to
4319
4363
  * subsequent steps.
4320
4364
  *
4321
- * writeAssistantSegment persists the matching
4322
- * `[assistant, reminder, assistant]` split so later requests reproduce the
4323
- * exact prompt prefix the model saw.
4365
+ * Each injected input is persisted with a following assistant placeholder
4366
+ * before prepareStep returns. writeAssistantSegment fills that placeholder,
4367
+ * so process loss cannot make a delivered reminder eligible again and later
4368
+ * requests reproduce the exact prompt prefix the model saw.
4324
4369
  */
4325
4370
  createPrepareStep(options = {}) {
4326
4371
  const enableSteer = options.steer ?? true;
4327
4372
  const session = {
4328
4373
  firedOnceIds: /* @__PURE__ */ new Set(),
4329
- fired: [],
4330
- currentSegStart: 0,
4331
- materialized: 0
4374
+ currentSegStart: 0
4332
4375
  };
4333
4376
  this.#currentReminderSession = session;
4334
- let stepOffset = 0;
4335
- let previousStepNumber;
4336
4377
  return async ({ steps, stepNumber, messages }) => {
4337
- if (previousStepNumber !== void 0 && stepNumber <= previousStepNumber) {
4338
- stepOffset += previousStepNumber + 1;
4339
- }
4340
- previousStepNumber = stepNumber;
4341
- const cumulativeStepNumber = stepOffset + stepNumber;
4342
4378
  const priorStep = stepNumber >= 1 ? steps[stepNumber - 1] : void 0;
4343
4379
  const hasSafeBoundary = (priorStep?.content?.length ?? 0) > 0;
4344
4380
  const canFire = enableSteer && hasSafeBoundary;
@@ -4346,13 +4382,18 @@ var ContextEngine = class _ContextEngine {
4346
4382
  const outcomes = toolOutcomesFromStep(steps.at(-1)?.content ?? []);
4347
4383
  const needsChain = steerConfigs.length > 0 || outcomes.length > 0 && this.#remindersFor("tool-output").length > 0;
4348
4384
  const chain = needsChain ? await this.#getChainContext() : void 0;
4349
- const toolReminders = chain ? await this.#evaluateToolOutputReminders(outcomes, session, chain) : { texts: [], onceIds: [] };
4385
+ const toolReminders = chain ? await this.#evaluateToolOutputReminders(
4386
+ outcomes,
4387
+ session,
4388
+ chain,
4389
+ options.sandbox
4390
+ ) : { texts: [], onceIds: [] };
4350
4391
  const additionalInput = await options.additionalInput?.();
4351
4392
  let reminderInput;
4352
4393
  if (canFire && chain) {
4353
4394
  const configs = steerConfigs;
4354
4395
  if (configs.length > 0) {
4355
- const whenCtx = this.#steerWhenCtx(chain, session);
4396
+ const whenCtx = this.#steerWhenCtx(chain, session, options.sandbox);
4356
4397
  const matched = await evaluateFiredReminders(configs, whenCtx);
4357
4398
  if (matched.length > 0) {
4358
4399
  const onceIds = [
@@ -4388,10 +4429,35 @@ var ContextEngine = class _ContextEngine {
4388
4429
  const inputModel = await convertToModelMessages2(inputs, {
4389
4430
  ignoreIncompleteToolCalls: true
4390
4431
  });
4391
- session.fired.push({
4392
- afterStep: cumulativeStepNumber - 1,
4393
- messages: inputs
4394
- });
4432
+ const head = await this.headMessage();
4433
+ if (head?.name !== "assistant") {
4434
+ throw new Error(
4435
+ "createPrepareStep: expected an assistant message at chain head."
4436
+ );
4437
+ }
4438
+ const currentSegmentId = session.currentSegId ?? head.id;
4439
+ const currentSegment = await this.#store.getMessage(currentSegmentId);
4440
+ if (!currentSegment || currentSegment.name !== "assistant") {
4441
+ throw new Error(
4442
+ "createPrepareStep: expected the open assistant segment to be persisted."
4443
+ );
4444
+ }
4445
+ const currentMessage = requireUIMessage(
4446
+ currentSegment.data,
4447
+ `Stored assistant message "${currentSegment.id}"`
4448
+ );
4449
+ const nextSegmentId = generateId5();
4450
+ for (const input of inputs) this.set(user(input));
4451
+ this.set(
4452
+ assistant({
4453
+ id: nextSegmentId,
4454
+ role: "assistant",
4455
+ parts: []
4456
+ })
4457
+ );
4458
+ await this.save({ branch: false });
4459
+ session.currentSegId = nextSegmentId;
4460
+ session.currentSegStart += currentMessage.parts.length;
4395
4461
  return {
4396
4462
  messages: [...messages, ...inputModel]
4397
4463
  };
@@ -4400,16 +4466,15 @@ var ContextEngine = class _ContextEngine {
4400
4466
  };
4401
4467
  }
4402
4468
  /**
4403
- * Persist the streamed assistant message, carving it at reminder boundaries.
4469
+ * Persist the streamed assistant message into its reserved segment.
4404
4470
  *
4405
4471
  * Called from chat()'s onStepEnd/onEnd (and the guardrail path) with the
4406
- * cumulative response message. Segment boundaries come from the `step-start`
4407
- * markers in the message itself — no cross-track store read — so the carve is
4408
- * race-free. Idempotent: finalized segments keep stable ids; the open segment
4409
- * is updated in place. With no fired reminder it degrades to a plain in-place
4410
- * write of the whole message to the reserved head.
4472
+ * cumulative response message. prepareStep has already persisted reminder
4473
+ * boundaries and reserved the open segment id, so this only updates that
4474
+ * segment in place with the cumulative prefix removed. With no fired reminder
4475
+ * it writes the whole message to the original reserved head.
4411
4476
  */
4412
- async writeAssistantSegment(message2, options = {}) {
4477
+ async writeAssistantSegment(message2) {
4413
4478
  const head = await this.headMessage();
4414
4479
  if (head?.name !== "assistant") {
4415
4480
  throw new Error(
@@ -4417,37 +4482,11 @@ var ContextEngine = class _ContextEngine {
4417
4482
  );
4418
4483
  }
4419
4484
  const session = this.#currentReminderSession;
4420
- if (!session || session.fired.length === 0) {
4421
- this.set(assistant({ ...message2, id: head.id }));
4422
- await this.save({ branch: false });
4423
- return;
4424
- }
4425
- if (session.currentSegId === void 0) {
4426
- session.currentSegId = head.id;
4427
- session.currentSegStart = 0;
4428
- }
4429
- const stepStarts = stepStartPartIndices(message2.parts);
4430
- while (session.materialized < session.fired.length) {
4431
- const fire = session.fired[session.materialized];
4432
- const boundary = stepStarts[fire.afterStep + 1] ?? (options.final ? message2.parts.length : void 0);
4433
- if (boundary === void 0) break;
4434
- this.set(
4435
- assistant({
4436
- id: session.currentSegId,
4437
- role: "assistant",
4438
- parts: message2.parts.slice(session.currentSegStart, boundary)
4439
- })
4440
- );
4441
- for (const input of fire.messages) this.set(user(input));
4442
- session.currentSegId = generateId5();
4443
- session.currentSegStart = boundary;
4444
- session.materialized++;
4445
- }
4446
4485
  this.set(
4447
4486
  assistant({
4448
4487
  ...message2,
4449
- id: session.currentSegId,
4450
- parts: message2.parts.slice(session.currentSegStart)
4488
+ id: session?.currentSegId ?? head.id,
4489
+ parts: message2.parts.slice(session?.currentSegStart ?? 0)
4451
4490
  })
4452
4491
  );
4453
4492
  await this.save({ branch: false });
@@ -4503,7 +4542,7 @@ var ContextEngine = class _ContextEngine {
4503
4542
  fragment2.codec.encode(),
4504
4543
  `Pending user fragment "${fragment2.name}"`
4505
4544
  );
4506
- const chain = await this.#getChainContext();
4545
+ const chain = await this.#getChainContext(pending);
4507
4546
  const matched = await evaluateFiredReminders(configs, {
4508
4547
  ...this.#buildWhenCtx(chain, message2),
4509
4548
  firedOnceIds: chain.firedOnceIds
@@ -4544,7 +4583,7 @@ var ContextEngine = class _ContextEngine {
4544
4583
  * elapsedExceeds keeps firing every step once crossed — by design; compose
4545
4584
  * once() for control.
4546
4585
  */
4547
- #steerWhenCtx(chain, session) {
4586
+ #steerWhenCtx(chain, session, sandbox) {
4548
4587
  const currentMessage = chain.lastMessage;
4549
4588
  if (!currentMessage) {
4550
4589
  throw new Error(
@@ -4553,10 +4592,11 @@ var ContextEngine = class _ContextEngine {
4553
4592
  }
4554
4593
  return {
4555
4594
  ...this.#buildWhenCtx(chain, currentMessage),
4595
+ sandbox,
4556
4596
  firedOnceIds: /* @__PURE__ */ new Set([...chain.firedOnceIds, ...session.firedOnceIds])
4557
4597
  };
4558
4598
  }
4559
- async #evaluateToolOutputReminders(outcomes, session, chain) {
4599
+ async #evaluateToolOutputReminders(outcomes, session, chain, sandbox) {
4560
4600
  const configs = this.#remindersFor("tool-output");
4561
4601
  if (configs.length === 0 || outcomes.length === 0) {
4562
4602
  return { texts: [], onceIds: [] };
@@ -4567,6 +4607,7 @@ var ContextEngine = class _ContextEngine {
4567
4607
  const onceIds = /* @__PURE__ */ new Set();
4568
4608
  for (const outcome of outcomes) {
4569
4609
  const whenCtx = this.#buildWhenCtx(chain, currentMessage);
4610
+ whenCtx.sandbox = sandbox;
4570
4611
  whenCtx.toolOutcome = outcome;
4571
4612
  whenCtx.firedOnceIds = /* @__PURE__ */ new Set([
4572
4613
  ...chain.firedOnceIds,
@@ -5540,7 +5581,7 @@ async function chat(agent2, options = {}) {
5540
5581
  if (Object.keys(mergedMetadata).length > 0) {
5541
5582
  message2 = { ...message2, metadata: mergedMetadata };
5542
5583
  }
5543
- await context.writeAssistantSegment(message2, { final: true });
5584
+ await context.writeAssistantSegment(message2);
5544
5585
  const usage = await result.usage;
5545
5586
  if (usage !== void 0) {
5546
5587
  await context.trackUsage(usage);
@@ -5888,6 +5929,300 @@ function fromFragment(fragment2, options) {
5888
5929
  throw new Error(`Fragment "${fragment2.name}" is missing codec`);
5889
5930
  }
5890
5931
 
5932
+ // packages/context/src/lib/fragments/plan.ts
5933
+ import dedent from "dedent";
5934
+ import z3 from "zod";
5935
+ var PLAN_FILE_PATH = "/workspace/.deepagents/plan.json";
5936
+ var PLAN_INSTRUCTIONS = dedent`
5937
+ You own the plan. The user is not required to provide a structured objective, success criteria, constraints, or task breakdown.
5938
+
5939
+ Build and maintain the plan from the best available evidence, in this order:
5940
+ 1. The user's request and later corrections.
5941
+ 2. Higher-priority system, developer, and workspace instructions.
5942
+ 3. Workspace guidance such as AGENTS.md.
5943
+ 4. Existing source, public contracts, tests, documentation, issues, and generated artifacts.
5944
+ 5. The current branch, working-tree changes, and pre-existing work.
5945
+ 6. Runtime observations, reproduction results, errors, logs, and tool output.
5946
+ 7. Sandbox, permission, and available-tool constraints.
5947
+ 8. Existing plan state when resuming.
5948
+ 9. Dependencies and risks discovered during execution.
5949
+
5950
+ Describe the objective as the outcome the user is trying to achieve, not merely the activity requested. Derive observable success criteria from that outcome and the workspace's real validation mechanisms.
5951
+
5952
+ Preserve the basis of every important plan statement:
5953
+ - Explicit requirement: stated by the user or a higher-priority instruction.
5954
+ - Discovered constraint: demonstrated by source, a public contract, a test, or runtime evidence.
5955
+ - Inference: supported by available evidence but not directly stated.
5956
+ - Assumption: unresolved and potentially requiring confirmation.
5957
+
5958
+ Never silently promote an assumption into a requirement or constraint. Ask the user only when an unresolved choice materially changes the outcome, cannot be answered from the workspace or runtime, or requires authority beyond the request. Do not ask merely because the user did not provide structured plan fields.
5959
+
5960
+ Maintain the authoritative plan as JSON at ${PLAN_FILE_PATH}. Read the latest file before changing it and write the complete document in one operation. The plan has this shape:
5961
+ - revision: a positive integer, incremented whenever the objective, criteria, constraints, assumptions, tasks, or dependencies change; recording lastReview alone does not increment it;
5962
+ - objective: { text, basis, source };
5963
+ - successCriteria: [{ id, text, basis, source, evidence: [{ summary, source }] }];
5964
+ - constraints and assumptions: [{ text, basis, source }];
5965
+ - tasks: [{ id, title, status, blockedBy, evidence }], where status is pending, in_progress, or completed;
5966
+ - lastReview, when present: { revision, decision, summary }.
5967
+
5968
+ Use basis values explicit_requirement, discovered_constraint, inference, or assumption. Store only blockedBy dependency edges. Never store blocks, readiness, or a blocked task status; those are derived when the plan is read. A task may be in_progress only when every blocker is completed, and a completed task must include concrete evidence.
5969
+
5970
+ Whenever plan review fires, record lastReview before continuing. Its revision is the plan revision that was evaluated. If the decision is revise or replace, increment the plan revision when applying the resulting changes while preserving the evaluated revision in lastReview.
5971
+
5972
+ Revise the plan when new evidence invalidates its objective, criteria, constraints, assumptions, sequencing, or dependencies. Before recording a complete decision or claiming completion, map every success criterion to concrete evidence. Completed task statuses alone do not prove completion.
5973
+ `;
5974
+ var REVIEW_REMINDER = [
5975
+ "Re-read the current plan and consider the evidence gathered since the previous review.",
5976
+ "",
5977
+ "Is the current plan still valid given the latest evidence?",
5978
+ "",
5979
+ "If yes, continue. If not, revise it before taking another action.",
5980
+ "Before claiming completion, verify the success criteria against evidence."
5981
+ ].join("\n");
5982
+ var nonEmptyString = z3.string().trim().min(1);
5983
+ var statementBasis = z3.enum([
5984
+ "explicit_requirement",
5985
+ "discovered_constraint",
5986
+ "inference",
5987
+ "assumption"
5988
+ ]);
5989
+ var statement = z3.strictObject({
5990
+ text: nonEmptyString,
5991
+ basis: statementBasis,
5992
+ source: nonEmptyString
5993
+ });
5994
+ var evidence = z3.strictObject({
5995
+ summary: nonEmptyString,
5996
+ source: nonEmptyString
5997
+ });
5998
+ var successCriterion = statement.extend({
5999
+ id: nonEmptyString,
6000
+ evidence: z3.array(evidence)
6001
+ });
6002
+ var task = z3.strictObject({
6003
+ id: nonEmptyString,
6004
+ title: nonEmptyString,
6005
+ status: z3.enum(["pending", "in_progress", "completed"]),
6006
+ blockedBy: z3.array(nonEmptyString),
6007
+ evidence: z3.array(evidence)
6008
+ });
6009
+ var lastReview = z3.strictObject({
6010
+ revision: z3.number().int().positive(),
6011
+ decision: z3.enum(["continue", "revise", "replace", "complete", "blocked"]),
6012
+ summary: nonEmptyString
6013
+ });
6014
+ var planState = z3.strictObject({
6015
+ revision: z3.number().int().positive(),
6016
+ objective: statement,
6017
+ successCriteria: z3.array(successCriterion).min(1),
6018
+ constraints: z3.array(statement),
6019
+ assumptions: z3.array(statement),
6020
+ tasks: z3.array(task),
6021
+ lastReview: lastReview.optional()
6022
+ });
6023
+ function uniqueIds(items, label) {
6024
+ const byId = /* @__PURE__ */ new Map();
6025
+ for (const item of items) {
6026
+ if (byId.has(item.id)) {
6027
+ throw new Error(`duplicate ${label} id "${item.id}"`);
6028
+ }
6029
+ byId.set(item.id, item);
6030
+ }
6031
+ return byId;
6032
+ }
6033
+ function validatePlan(state) {
6034
+ uniqueIds(state.successCriteria, "success criterion");
6035
+ const tasks = uniqueIds(state.tasks, "task");
6036
+ const blocks = new Map(state.tasks.map(({ id }) => [id, []]));
6037
+ for (const item of state.tasks) {
6038
+ const blockers = /* @__PURE__ */ new Set();
6039
+ for (const blocker of item.blockedBy) {
6040
+ if (blocker === item.id) {
6041
+ throw new Error(`task "${item.id}" cannot block itself`);
6042
+ }
6043
+ if (!tasks.has(blocker)) {
6044
+ throw new Error(
6045
+ `task "${item.id}" references missing blocker "${blocker}"`
6046
+ );
6047
+ }
6048
+ if (blockers.has(blocker)) {
6049
+ throw new Error(`task "${item.id}" repeats blocker "${blocker}"`);
6050
+ }
6051
+ blockers.add(blocker);
6052
+ blocks.get(blocker)?.push(item.id);
6053
+ }
6054
+ }
6055
+ const visited = /* @__PURE__ */ new Set();
6056
+ const visiting = /* @__PURE__ */ new Set();
6057
+ const stack = [];
6058
+ const visit = (id) => {
6059
+ if (visited.has(id)) return;
6060
+ if (visiting.has(id)) {
6061
+ const cycleStart = stack.indexOf(id);
6062
+ throw new Error(
6063
+ `dependency cycle: ${[...stack.slice(cycleStart), id].join(" -> ")}`
6064
+ );
6065
+ }
6066
+ visiting.add(id);
6067
+ stack.push(id);
6068
+ for (const blocker of tasks.get(id)?.blockedBy ?? []) visit(blocker);
6069
+ stack.pop();
6070
+ visiting.delete(id);
6071
+ visited.add(id);
6072
+ };
6073
+ for (const { id } of state.tasks) visit(id);
6074
+ for (const item of state.tasks) {
6075
+ const incompleteBlockers = item.blockedBy.filter(
6076
+ (id) => tasks.get(id)?.status !== "completed"
6077
+ );
6078
+ if (item.status !== "pending" && incompleteBlockers.length > 0) {
6079
+ throw new Error(
6080
+ `task "${item.id}" is ${item.status} while blocked by ${incompleteBlockers.join(", ")}`
6081
+ );
6082
+ }
6083
+ if (item.status === "completed" && item.evidence.length === 0) {
6084
+ throw new Error(`completed task "${item.id}" has no evidence`);
6085
+ }
6086
+ }
6087
+ if (state.lastReview && state.lastReview.revision > state.revision) {
6088
+ throw new Error(
6089
+ `last review references future revision ${state.lastReview.revision}`
6090
+ );
6091
+ }
6092
+ if (state.lastReview?.decision === "complete" && state.successCriteria.some(({ evidence: evidence2 }) => evidence2.length === 0)) {
6093
+ throw new Error(
6094
+ "a complete review requires evidence for every success criterion"
6095
+ );
6096
+ }
6097
+ return blocks;
6098
+ }
6099
+ function parsePlan(raw) {
6100
+ let parsed;
6101
+ try {
6102
+ parsed = JSON.parse(raw);
6103
+ } catch (error) {
6104
+ const message2 = error instanceof Error ? error.message : String(error);
6105
+ throw new Error(`invalid JSON: ${message2}`, { cause: error });
6106
+ }
6107
+ const result = planState.safeParse(parsed);
6108
+ if (!result.success) {
6109
+ const issues = result.error.issues.slice(0, 5).map(({ path: path3, message: message2 }) => `${path3.join(".") || "plan"}: ${message2}`);
6110
+ const remaining = result.error.issues.length - issues.length;
6111
+ throw new Error(
6112
+ `schema errors: ${issues.join("; ")}${remaining > 0 ? `; ${remaining} more` : ""}`
6113
+ );
6114
+ }
6115
+ return { state: result.data, blocks: validatePlan(result.data) };
6116
+ }
6117
+ function lineItems(items) {
6118
+ return items.length > 0 ? items.map((item) => `- ${item}`) : ["- none"];
6119
+ }
6120
+ function latestEvidence(items) {
6121
+ const latest = items.at(-1);
6122
+ return latest ? ` Evidence: ${latest.summary} (${latest.source})` : "";
6123
+ }
6124
+ function formatTask(item, blocks) {
6125
+ const blocked = blocks.get(item.id) ?? [];
6126
+ const suffix = blocked.length > 0 ? `; blocks ${blocked.join(", ")}` : "";
6127
+ return `${item.id}: ${item.title}${suffix}${latestEvidence(item.evidence)}`;
6128
+ }
6129
+ function formatPlanReview(state, blocks) {
6130
+ const tasks = new Map(state.tasks.map((item) => [item.id, item]));
6131
+ const active = state.tasks.filter(({ status }) => status === "in_progress");
6132
+ const ready = state.tasks.filter(
6133
+ ({ status, blockedBy }) => status === "pending" && blockedBy.every((id) => tasks.get(id)?.status === "completed")
6134
+ );
6135
+ const waiting = state.tasks.filter(
6136
+ ({ status, blockedBy }) => status === "pending" && blockedBy.some((id) => tasks.get(id)?.status !== "completed")
6137
+ );
6138
+ const completed = state.tasks.filter(({ status }) => status === "completed");
6139
+ return [
6140
+ `Current plan (revision ${state.revision})`,
6141
+ `Objective: ${state.objective.text} [${state.objective.basis}; ${state.objective.source}]`,
6142
+ "",
6143
+ "Success criteria:",
6144
+ ...lineItems(
6145
+ state.successCriteria.map(
6146
+ (criterion) => `${criterion.evidence.length > 0 ? "[supported]" : "[unsupported]"} ${criterion.id}: ${criterion.text}${latestEvidence(criterion.evidence)}`
6147
+ )
6148
+ ),
6149
+ "",
6150
+ "Constraints:",
6151
+ ...lineItems(
6152
+ state.constraints.map(
6153
+ (item) => `${item.text} [${item.basis}; ${item.source}]`
6154
+ )
6155
+ ),
6156
+ "",
6157
+ "Assumptions:",
6158
+ ...lineItems(
6159
+ state.assumptions.map(
6160
+ (item) => `${item.text} [${item.basis}; ${item.source}]`
6161
+ )
6162
+ ),
6163
+ "",
6164
+ "Active tasks:",
6165
+ ...lineItems(active.map((item) => formatTask(item, blocks))),
6166
+ "",
6167
+ "Ready tasks:",
6168
+ ...lineItems(ready.map((item) => formatTask(item, blocks))),
6169
+ "",
6170
+ "Waiting tasks:",
6171
+ ...lineItems(
6172
+ waiting.map(
6173
+ (item) => `${formatTask(item, blocks)}; waiting for ${item.blockedBy.filter((id) => tasks.get(id)?.status !== "completed").join(", ")}`
6174
+ )
6175
+ ),
6176
+ "",
6177
+ "Completed tasks:",
6178
+ ...lineItems(completed.map((item) => formatTask(item, blocks))),
6179
+ ...state.lastReview ? [
6180
+ "",
6181
+ `Last review: ${state.lastReview.decision} at revision ${state.lastReview.revision} \u2014 ${state.lastReview.summary}`
6182
+ ] : [],
6183
+ "",
6184
+ REVIEW_REMINDER
6185
+ ].join("\n");
6186
+ }
6187
+ function errorMessage(error) {
6188
+ const message2 = error instanceof Error ? error.message : String(error);
6189
+ return message2.length > 500 ? `${message2.slice(0, 497)}...` : message2;
6190
+ }
6191
+ async function resolvePlanReview(sandbox) {
6192
+ let raw;
6193
+ try {
6194
+ raw = await sandbox.sandbox.readFile(PLAN_FILE_PATH);
6195
+ } catch (error) {
6196
+ return [
6197
+ `The plan file at ${PLAN_FILE_PATH} could not be read: ${errorMessage(error)}`,
6198
+ "Create or repair it before taking another action.",
6199
+ "",
6200
+ REVIEW_REMINDER
6201
+ ].join("\n");
6202
+ }
6203
+ try {
6204
+ const { state, blocks } = parsePlan(raw);
6205
+ return formatPlanReview(state, blocks);
6206
+ } catch (error) {
6207
+ return [
6208
+ `The plan file at ${PLAN_FILE_PATH} is invalid: ${errorMessage(error)}`,
6209
+ "Repair the plan before taking another action.",
6210
+ "",
6211
+ REVIEW_REMINDER
6212
+ ].join("\n");
6213
+ }
6214
+ }
6215
+ function instructions() {
6216
+ return fragment("plan_instructions", PLAN_INSTRUCTIONS);
6217
+ }
6218
+ function review(options) {
6219
+ return reminder(({ sandbox }) => resolvePlanReview(sandbox), {
6220
+ when: options.when,
6221
+ target: "steer"
6222
+ });
6223
+ }
6224
+ var plan = { path: PLAN_FILE_PATH, instructions, review };
6225
+
5891
6226
  // packages/context/src/lib/fragments/reasoning.ts
5892
6227
  function reasoningFramework() {
5893
6228
  return [
@@ -7520,7 +7855,7 @@ import fg from "fast-glob";
7520
7855
  import { BashTransformPipeline, TeePlugin } from "just-bash";
7521
7856
  import { readFile } from "node:fs/promises";
7522
7857
  import * as path from "node:path";
7523
- import z3 from "zod";
7858
+ import z4 from "zod";
7524
7859
  import { withHostOnlyToolMetadata as withHostOnlyToolMetadata2 } from "@deepagents/agent";
7525
7860
  var DEFAULT_DESTINATION = "/workspace";
7526
7861
  var DEFAULT_MAX_FILES = 1e3;
@@ -7543,6 +7878,7 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
7543
7878
  "head",
7544
7879
  "html-to-markdown",
7545
7880
  "iconv",
7881
+ "js-exec",
7546
7882
  "join",
7547
7883
  "jq",
7548
7884
  "node",
@@ -7687,6 +8023,7 @@ async function createToolPrompt(sandbox, filenames, customPrompt) {
7687
8023
  const available = new Set(
7688
8024
  result.stdout.split("\n").filter((entry) => KNOWN_TOOLS.has(entry))
7689
8025
  );
8026
+ if (available.has("js-exec")) available.delete("node");
7690
8027
  if (available.size === 0) return "";
7691
8028
  const lines = [
7692
8029
  `Available tools: ${[...available].sort().join(", ")}, and more`
@@ -7764,9 +8101,9 @@ async function createBashTool(options) {
7764
8101
  extraInstructions,
7765
8102
  experimentalTeeTransform
7766
8103
  }),
7767
- inputSchema: z3.object({
7768
- command: z3.string().describe("The bash command to execute"),
7769
- reasoning: z3.string().trim().min(1).describe("Brief reason for executing this command")
8104
+ inputSchema: z4.object({
8105
+ command: z4.string().describe("The bash command to execute"),
8106
+ reasoning: z4.string().trim().min(1).describe("Brief reason for executing this command")
7770
8107
  }),
7771
8108
  execute: async ({ command: originalCommand }, executionOptions) => {
7772
8109
  let command = originalCommand;
@@ -7808,8 +8145,8 @@ async function createBashTool(options) {
7808
8145
  });
7809
8146
  const readFile2 = tool2({
7810
8147
  description: "Read the contents of a file from the sandbox.",
7811
- inputSchema: z3.object({
7812
- path: z3.string().describe("The path to the file to read")
8148
+ inputSchema: z4.object({
8149
+ path: z4.string().describe("The path to the file to read")
7813
8150
  }),
7814
8151
  execute: async ({ path: filePath }) => ({
7815
8152
  content: await sandbox.readFile(
@@ -7819,9 +8156,9 @@ async function createBashTool(options) {
7819
8156
  });
7820
8157
  const writeFile2 = tool2({
7821
8158
  description: "Write content to a file in the sandbox. Creates parent directories if needed.",
7822
- inputSchema: z3.object({
7823
- path: z3.string().describe("The path where the file should be written"),
7824
- content: z3.string().describe("The content to write to the file")
8159
+ inputSchema: z4.object({
8160
+ path: z4.string().describe("The path where the file should be written"),
8161
+ content: z4.string().describe("The content to write to the file")
7825
8162
  }),
7826
8163
  execute: async ({ path: filePath, content }) => {
7827
8164
  try {
@@ -9902,12 +10239,15 @@ function buildSubcommandRepair(name, subcommands) {
9902
10239
  }
9903
10240
 
9904
10241
  // packages/context/src/lib/sandbox/virtual-sandbox.ts
9905
- import { Bash } from "just-bash";
10242
+ import {
10243
+ Bash
10244
+ } from "just-bash";
9906
10245
  async function createVirtualSandbox(options) {
9907
10246
  const bash = new Bash({
9908
10247
  fs: options.fs,
9909
10248
  cwd: options.cwd,
9910
10249
  env: options.env,
10250
+ javascript: options.javascript,
9911
10251
  customCommands: options.customCommands
9912
10252
  });
9913
10253
  const sandbox = {
@@ -9952,7 +10292,7 @@ async function createVirtualSandbox(options) {
9952
10292
  }
9953
10293
 
9954
10294
  // packages/context/src/lib/skills/fragments.ts
9955
- import dedent from "dedent";
10295
+ import dedent2 from "dedent";
9956
10296
  function skills(availableSkills) {
9957
10297
  const skillSnapshot = availableSkills.map(({ name, description, path: path3 }) => ({
9958
10298
  name,
@@ -9983,7 +10323,7 @@ function skills(availableSkills) {
9983
10323
  metadata: { skills: skillSnapshot }
9984
10324
  };
9985
10325
  }
9986
- var SKILLS_INSTRUCTIONS = dedent`A skill is a set of local instructions to follow that is stored in a \`SKILL.md\` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
10326
+ var SKILLS_INSTRUCTIONS = dedent2`A skill is a set of local instructions to follow that is stored in a \`SKILL.md\` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
9987
10327
 
9988
10328
  ### How to use skills
9989
10329
  - Discovery: The list below shows the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
@@ -13152,29 +13492,55 @@ var StreamManager = class {
13152
13492
  const detect = async () => {
13153
13493
  if (detected) return;
13154
13494
  detected = true;
13155
- await this.#notifyCancellation(streamId, options);
13156
13495
  ac.abort();
13496
+ await this.#notifyCancellation(streamId, options);
13157
13497
  };
13498
+ const retryState = createAdaptivePollingState(DEFAULT_CANCEL_POLLING);
13158
13499
  const listeners = this.#localCancellationListeners.get(streamId) ?? /* @__PURE__ */ new Set();
13159
13500
  listeners.add(detect);
13160
13501
  this.#localCancellationListeners.set(streamId, listeners);
13161
13502
  try {
13162
- for await (const change of this.#changeSource.subscribe(
13163
- streamId,
13164
- ac.signal
13165
- )) {
13166
- if (change.kind === "chunks") continue;
13167
- const status = await this.#store.getStreamStatus(streamId);
13168
- if (status === void 0) {
13169
- ac.abort();
13170
- return;
13503
+ while (!ac.signal.aborted && !detected) {
13504
+ try {
13505
+ const initialStatus = await this.#store.getStreamStatus(streamId);
13506
+ if (initialStatus === void 0) {
13507
+ ac.abort();
13508
+ return;
13509
+ }
13510
+ if (initialStatus === "cancelled") {
13511
+ await detect();
13512
+ return;
13513
+ }
13514
+ if (isTerminal3(initialStatus)) return;
13515
+ const subscribedAt = Date.now();
13516
+ let subscriptionStabilized = false;
13517
+ for await (const change of this.#changeSource.subscribe(
13518
+ streamId,
13519
+ ac.signal
13520
+ )) {
13521
+ if (!subscriptionStabilized && Date.now() - subscribedAt >= DEFAULT_CANCEL_POLLING.maxMs) {
13522
+ resetAdaptivePolling(retryState);
13523
+ subscriptionStabilized = true;
13524
+ }
13525
+ if (change.kind === "chunks") continue;
13526
+ const status = await this.#store.getStreamStatus(streamId);
13527
+ if (status === void 0) {
13528
+ ac.abort();
13529
+ return;
13530
+ }
13531
+ if (status === "cancelled") {
13532
+ await detect();
13533
+ return;
13534
+ }
13535
+ if (isTerminal3(status)) return;
13536
+ }
13537
+ } catch {
13538
+ if (ac.signal.aborted || detected) return;
13171
13539
  }
13172
- if (status === "cancelled") {
13173
- await detect();
13540
+ if (!await waitForDelay2(nextAdaptivePollingDelay(retryState), ac.signal)) {
13174
13541
  return;
13175
13542
  }
13176
13543
  }
13177
- } catch {
13178
13544
  } finally {
13179
13545
  listeners.delete(detect);
13180
13546
  if (listeners.size === 0) {
@@ -13188,7 +13554,11 @@ var StreamManager = class {
13188
13554
  await Promise.all([...listeners].map((listener) => listener()));
13189
13555
  }
13190
13556
  async #notifyCancellation(streamId, options) {
13191
- const current = await this.#store.getStream(streamId);
13557
+ let current;
13558
+ try {
13559
+ current = await this.#store.getStream(streamId);
13560
+ } catch {
13561
+ }
13192
13562
  const latencyMs = current?.cancelRequestedAt != null ? Math.max(0, Date.now() - current.cancelRequestedAt) : null;
13193
13563
  this.#emit({ type: "persist:cancel-detected", streamId, latencyMs });
13194
13564
  if (!options?.onCancelDetected) return;
@@ -13303,6 +13673,20 @@ var StreamManager = class {
13303
13673
  }
13304
13674
  }
13305
13675
  };
13676
+ function waitForDelay2(ms, signal) {
13677
+ if (signal.aborted) return Promise.resolve(false);
13678
+ return new Promise((resolve3) => {
13679
+ const timeout = globalThis.setTimeout(() => {
13680
+ signal.removeEventListener("abort", onAbort);
13681
+ resolve3(true);
13682
+ }, ms);
13683
+ const onAbort = () => {
13684
+ globalThis.clearTimeout(timeout);
13685
+ resolve3(false);
13686
+ };
13687
+ signal.addEventListener("abort", onAbort, { once: true });
13688
+ });
13689
+ }
13306
13690
  async function drainAvailable(controller, store, streamId, lastSeqRef, pageSize, onPage) {
13307
13691
  let total = 0;
13308
13692
  while (true) {
@@ -13530,6 +13914,7 @@ export {
13530
13914
  errorRecoveryGuardrail,
13531
13915
  estimate,
13532
13916
  evaluateFiredReminders,
13917
+ everyNToolCalls,
13533
13918
  everyNTurns,
13534
13919
  everyOfLastN,
13535
13920
  example,
@@ -13585,6 +13970,7 @@ export {
13585
13970
  persona,
13586
13971
  pip,
13587
13972
  pkg,
13973
+ plan,
13588
13974
  policy,
13589
13975
  preference,
13590
13976
  principle,