@diffexai/diffex 0.2.6 → 0.3.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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/core/agent-session.d.ts +5 -0
  3. package/dist/core/agent-session.js +89 -19
  4. package/dist/core/compaction/compaction.js +3 -1
  5. package/dist/core/model-runtime.js +3 -3
  6. package/dist/core/provider-composer.js +1 -1
  7. package/dist/core/sdk.d.ts +2 -1
  8. package/dist/core/sdk.js +18 -6
  9. package/dist/core/subagents/controller.d.ts +4 -3
  10. package/dist/core/subagents/controller.js +173 -37
  11. package/dist/core/subagents/types.d.ts +13 -0
  12. package/dist/core/tools/bash.d.ts +4 -0
  13. package/dist/core/tools/bash.js +14 -5
  14. package/dist/core/tools/subagents.d.ts +12 -10
  15. package/dist/core/tools/subagents.js +61 -49
  16. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
  17. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +1 -1
  18. package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -1
  19. package/dist/node_modules/@diffexai/diffex-ai/dist/api/lazy.d.ts +1 -1
  20. package/dist/node_modules/@diffexai/diffex-ai/dist/api/lazy.js +8 -8
  21. package/dist/node_modules/@diffexai/diffex-ai/dist/models.js +4 -4
  22. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
  23. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode.json +1 -1
  24. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
  25. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  26. package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
  27. package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +8 -8
  28. package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -1
  29. package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
  30. package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
  31. package/dist/node_modules/@diffexai/diffex-client/package.json +1 -1
  32. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
  33. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
  34. package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -1
  35. package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
  36. package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
  37. package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -1
  38. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
  39. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
  40. package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -1
  41. package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
  42. package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
  43. package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -1
  44. package/dist/utils/owned-shell-processes.d.ts +8 -0
  45. package/dist/utils/owned-shell-processes.js +61 -0
  46. package/distribution-components.json +11 -11
  47. package/distribution-files.json +51 -43
  48. package/npm-shrinkwrap.json +2 -2
  49. package/package.json +1 -1
  50. package/release/distribution-manifest.json +4 -4
  51. package/release/install-package-lock.json +5 -5
  52. package/release/install-package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-09-20
4
+
5
+ ### New Features
6
+
7
+ - Added nonblocking `agent_status` for immediate child-status and result inspection, with automatic coordination for child completion. See [Automatic Sub-agent Coordination](docs/usage.md#automatic-sub-agent-coordination) ([#234](https://github.com/diffexai/diffex/pull/234)).
8
+ - Added automatic subagent supervision with renewable work allowances and parent-deadline bounds. See [SDK tools](docs/sdk.md#tools) ([#233](https://github.com/diffexai/diffex/pull/233)).
9
+
10
+ ### Breaking Changes
11
+
12
+ - Replaced the blocking `wait_for_agents` tool with nonblocking `agent_status`; rely on automatic child-result coordination instead of tool-managed waits ([#234](https://github.com/diffexai/diffex/pull/234)).
13
+
14
+ ### Fixed
15
+
16
+ - Fixed automatic compaction to check complete tool-result batches ([#233](https://github.com/diffexai/diffex/pull/233)).
17
+ - Fixed subagent completion and cancellation, including renewable work-allowance supervision ([#233](https://github.com/diffexai/diffex/pull/233)).
18
+ - Fixed session and subagent cancellation to terminate owned background shell processes ([#233](https://github.com/diffexai/diffex/pull/233)).
19
+
20
+ ## [0.2.7] - 2026-09-18
21
+
22
+ ### Fixed
23
+
24
+ - Improved stability and fixed UI issues.
25
+
3
26
  ## [0.2.6] - 2026-09-18
4
27
 
5
28
  ### Fixed
@@ -263,6 +263,8 @@ export declare class AgentSession {
263
263
  private readonly _bashAbortControllers;
264
264
  private _pendingBashMessages;
265
265
  private readonly _ownedOperations;
266
+ private readonly _shellProcesses;
267
+ private readonly _bashOperations;
266
268
  private _extensionRunner;
267
269
  private _turnIndex;
268
270
  private _resourceLoader;
@@ -561,6 +563,9 @@ export declare class AgentSession {
561
563
  */
562
564
  abort(): Promise<void>;
563
565
  private _abort;
566
+ /** Stop and verify all shell processes owned by this session, including background jobs. */
567
+ terminateOwnedShellProcesses(): Promise<void>;
568
+ get ownedShellProcessCount(): number;
564
569
  waitForIdle(): Promise<void>;
565
570
  private _emitModelSelect;
566
571
  /**
@@ -18,6 +18,7 @@ import { NOOP_TELEMETRY_CONTEXT, uuidv7, } from "@diffexai/diffex-agent-core";
18
18
  import { contentText } from "@diffexai/diffex-ai";
19
19
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@diffexai/diffex-ai/compat";
20
20
  import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
21
+ import { OwnedShellProcesses } from "../utils/owned-shell-processes.js";
21
22
  import { canonicalizePath, resolvePath } from "../utils/paths.js";
22
23
  import { sleep } from "../utils/sleep.js";
23
24
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
@@ -161,6 +162,8 @@ export class AgentSession {
161
162
  _bashAbortControllers = new Set();
162
163
  _pendingBashMessages = [];
163
164
  _ownedOperations = new Set();
165
+ _shellProcesses = new OwnedShellProcesses();
166
+ _bashOperations = new Set();
164
167
  // Extension system
165
168
  _extensionRunner;
166
169
  _turnIndex = 0;
@@ -402,10 +405,25 @@ export class AgentSession {
402
405
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
403
406
  const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
404
407
  const previousContext = previousSnapshot?.context ?? turn.context;
408
+ let messages = previousContext.messages;
409
+ // Tool output is part of the next request even though the model's usage
410
+ // only describes the request before that output. Check every complete batch.
411
+ if (turn.toolResults.length > 0 && !signal?.aborted && this.model) {
412
+ const settings = this.settingsManager.getCompactionSettings();
413
+ if (shouldCompact(estimateContextTokens(messages).tokens, this.model.contextWindow, settings)) {
414
+ const beforeCompaction = this.agent.state.messages;
415
+ await this._runAutoCompaction("threshold", true, signal);
416
+ if (this.agent.state.messages !== beforeCompaction) {
417
+ messages = this.agent.state.messages;
418
+ }
419
+ }
420
+ }
421
+ signal?.throwIfAborted();
405
422
  return {
406
423
  ...previousSnapshot,
407
424
  context: {
408
425
  ...previousContext,
426
+ messages,
409
427
  systemPrompt: this._activeRunCollaborationState?.effectiveSystemPrompt ??
410
428
  this._deriveEffectivePrompt(this._systemPromptOverride ?? this._baseSystemPrompt, this._activeRunCollaborationMode ?? this._collaborationMode),
411
429
  tools: (this._activeRunCollaborationState?.tools ?? this.agent.state.tools).slice(),
@@ -836,7 +854,7 @@ export class AgentSession {
836
854
  failures.push(error);
837
855
  }
838
856
  const ownedOperationsIdle = this._waitForOwnedOperations();
839
- const cleanupResults = await Promise.allSettled([childCleanup, rootIdle, ownedOperationsIdle].filter((promise) => promise !== undefined));
857
+ const cleanupResults = await Promise.allSettled([childCleanup, rootIdle, ownedOperationsIdle, this.terminateOwnedShellProcesses()].filter((promise) => promise !== undefined));
840
858
  for (const result of cleanupResults) {
841
859
  if (result.status !== "rejected")
842
860
  continue;
@@ -1429,7 +1447,10 @@ export class AgentSession {
1429
1447
  else
1430
1448
  await this.agent.continue();
1431
1449
  firstRun = false;
1432
- if (await this._handlePostAgentRun()) {
1450
+ const next = await this._handlePostAgentRun();
1451
+ if (next === "failed")
1452
+ break;
1453
+ if (next === "continue") {
1433
1454
  if (this._agentRunAborted) {
1434
1455
  const coordination = this._subagentCoordination;
1435
1456
  if (coordination) {
@@ -1605,10 +1626,10 @@ export class AgentSession {
1605
1626
  const msg = this._lastAssistantMessage;
1606
1627
  this._lastAssistantMessage = undefined;
1607
1628
  if (!msg) {
1608
- return false;
1629
+ return "settled";
1609
1630
  }
1610
1631
  if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
1611
- return true;
1632
+ return "continue";
1612
1633
  }
1613
1634
  if (msg.stopReason === "error" && this._retryAttempt > 0) {
1614
1635
  this._emit({
@@ -1620,12 +1641,27 @@ export class AgentSession {
1620
1641
  this._retryAttempt = 0;
1621
1642
  }
1622
1643
  if (await this._checkCompaction(msg)) {
1623
- return true;
1644
+ return "continue";
1645
+ }
1646
+ if (msg.stopReason === "error") {
1647
+ // Recovery may have removed the failed response before giving up.
1648
+ if (!this.agent.state.messages.includes(msg))
1649
+ this.agent.state.messages.push(msg);
1650
+ while (true) {
1651
+ const coordination = this._getOrCreateSubagentCoordination();
1652
+ if (!coordination)
1653
+ break;
1654
+ this._exitSubagentCoordination("cancellation");
1655
+ await this._waitForSubagentCoordination(coordination);
1656
+ }
1657
+ // Queued reports and input must not bypass an exhausted recovery budget.
1658
+ // Leave them available for an explicit prompt or queue recovery by the caller.
1659
+ return "failed";
1624
1660
  }
1625
1661
  // Reports already queued by this run must enter context before a new batch is frozen
1626
1662
  // or an escalation-suspended coordination resumes waiting.
1627
1663
  if (this.agent.hasQueuedMessages())
1628
- return true;
1664
+ return "continue";
1629
1665
  while (true) {
1630
1666
  const coordination = this._getOrCreateSubagentCoordination();
1631
1667
  if (!coordination)
@@ -1633,28 +1669,44 @@ export class AgentSession {
1633
1669
  if (coordination.exitReason === "cancellation" || this._agentRunAborted) {
1634
1670
  this._exitSubagentCoordination("cancellation");
1635
1671
  }
1672
+ else if (!coordination.completionChecked) {
1673
+ coordination.completionChecked = true;
1674
+ const active = this.getSubagents().filter((child) => coordination.names.includes(child.name) && (child.status === "running" || child.status === "queued"));
1675
+ if (active.length > 0) {
1676
+ await this.sendCustomMessage({
1677
+ customType: "subagent_completion_check",
1678
+ content: JSON.stringify({
1679
+ type: "subagent_completion_check",
1680
+ agents: active,
1681
+ instruction: "Review active children before finishing: cancel obsolete work with agent_action interrupt. If needed work remains, end your response to wait automatically. Renew only justified work.",
1682
+ }),
1683
+ display: false,
1684
+ }, { deliverAs: "steer" });
1685
+ return "continue";
1686
+ }
1687
+ }
1636
1688
  const delivery = await this._waitForSubagentCoordination(coordination);
1637
1689
  if (delivery.reason === "cancellation")
1638
- return false;
1690
+ return "settled";
1639
1691
  if (delivery.reason === "escalation") {
1640
1692
  if (coordination.exitReason === "cancellation" || this._agentRunAborted) {
1641
1693
  this._exitSubagentCoordination("cancellation");
1642
1694
  await this._waitForSubagentCoordination(coordination);
1643
- return false;
1695
+ return "settled";
1644
1696
  }
1645
- return true;
1697
+ return "continue";
1646
1698
  }
1647
1699
  if (delivery.results.length > 0) {
1648
1700
  const message = this._createSubagentResultMessage(delivery.results);
1649
1701
  await this.sendCustomMessage(message, { deliverAs: "steer" });
1650
- return true;
1702
+ return "continue";
1651
1703
  }
1652
1704
  // Explicit agents consumption can empty this batch while the same continuation
1653
1705
  // starts another coordinated child. Freeze that pending run before settling.
1654
1706
  }
1655
1707
  // The agent loop drains both queues before emitting agent_end. Any messages
1656
1708
  // here were queued by agent_end extension handlers and need a continuation.
1657
- return this.agent.hasQueuedMessages();
1709
+ return this.agent.hasQueuedMessages() ? "continue" : "settled";
1658
1710
  }
1659
1711
  /**
1660
1712
  * Send a prompt to the agent.
@@ -2336,11 +2388,21 @@ export class AgentSession {
2336
2388
  }
2337
2389
  async _abort() {
2338
2390
  this.abortRetry();
2391
+ this.abortCompaction();
2392
+ this.abortBranchSummary();
2393
+ this.abortBash();
2339
2394
  if (this._isAgentRunActive)
2340
2395
  this._agentRunAborted = true;
2341
2396
  this._exitSubagentCoordination("cancellation");
2342
2397
  this.agent.abort();
2343
- await this.waitForIdle();
2398
+ await Promise.all([this.waitForIdle(), this.terminateOwnedShellProcesses(), ...this._bashOperations]);
2399
+ }
2400
+ /** Stop and verify all shell processes owned by this session, including background jobs. */
2401
+ async terminateOwnedShellProcesses() {
2402
+ await this._shellProcesses.terminateAll();
2403
+ }
2404
+ get ownedShellProcessCount() {
2405
+ return this._shellProcesses.count;
2344
2406
  }
2345
2407
  async waitForIdle() {
2346
2408
  if (this.isIdle) {
@@ -2798,7 +2860,9 @@ export class AgentSession {
2798
2860
  contextTokens = directContextTokens;
2799
2861
  }
2800
2862
  if (shouldCompact(contextTokens, contextWindow, settings)) {
2801
- return await this._runAutoCompaction("threshold", false, signal, deferCommit);
2863
+ const queuedContinuation = await this._runAutoCompaction("threshold", false, signal, deferCommit);
2864
+ // Threshold compaction does not recover a provider failure.
2865
+ return assistantMessage.stopReason !== "error" && queuedContinuation;
2802
2866
  }
2803
2867
  return false;
2804
2868
  }
@@ -2827,6 +2891,9 @@ export class AgentSession {
2827
2891
  return false;
2828
2892
  }
2829
2893
  this._autoCompactionAbortController = new AbortController();
2894
+ const compactionSignal = signal
2895
+ ? AbortSignal.any([signal, this._autoCompactionAbortController.signal])
2896
+ : this._autoCompactionAbortController.signal;
2830
2897
  this._emit({ type: "compaction_start", reason });
2831
2898
  started = true;
2832
2899
  let extensionCompaction;
@@ -2839,7 +2906,7 @@ export class AgentSession {
2839
2906
  customInstructions: undefined,
2840
2907
  reason,
2841
2908
  willRetry,
2842
- signal: this._autoCompactionAbortController.signal,
2909
+ signal: compactionSignal,
2843
2910
  }));
2844
2911
  if (extensionResult?.cancel) {
2845
2912
  this._emit({
@@ -2871,14 +2938,14 @@ export class AgentSession {
2871
2938
  }
2872
2939
  else {
2873
2940
  // Generate compaction result
2874
- const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }, telemetryContext), telemetryContext);
2941
+ const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined, compactionSignal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }, telemetryContext), telemetryContext);
2875
2942
  summary = compactResult.summary;
2876
2943
  firstKeptEntryId = compactResult.firstKeptEntryId;
2877
2944
  tokensBefore = compactResult.tokensBefore;
2878
2945
  usage = compactResult.usage;
2879
2946
  details = compactResult.details;
2880
2947
  }
2881
- if (this._autoCompactionAbortController.signal.aborted) {
2948
+ if (compactionSignal.aborted) {
2882
2949
  this._emit({
2883
2950
  type: "compaction_end",
2884
2951
  reason,
@@ -3237,7 +3304,7 @@ export class AgentSession {
3237
3304
  : {
3238
3305
  ...createAllToolDefinitions(this._cwd, {
3239
3306
  read: { autoResizeImages },
3240
- bash: { commandPrefix: shellCommandPrefix, shellPath },
3307
+ bash: { commandPrefix: shellCommandPrefix, shellPath, owner: this._shellProcesses },
3241
3308
  }),
3242
3309
  update_plan: createSessionUpdatePlanToolDefinition(this.sessionManager),
3243
3310
  };
@@ -3438,7 +3505,7 @@ export class AgentSession {
3438
3505
  * @param options.operations Custom BashOperations for remote execution
3439
3506
  */
3440
3507
  executeBash(command, onChunk, options) {
3441
- return this._runOwnedOperation(() => this._traceOperation("bash", () => startCodingAgentSpan(this.agent.telemetryContext, "diffex.coding_agent.terminal", { "diffex.session.id": this.sessionId }, async (span) => {
3508
+ const operation = this._runOwnedOperation(() => this._traceOperation("bash", () => startCodingAgentSpan(this.agent.telemetryContext, "diffex.coding_agent.terminal", { "diffex.session.id": this.sessionId }, async (span) => {
3442
3509
  const result = await this._executeBash(command, onChunk, options);
3443
3510
  const outcome = result.cancelled ? "cancelled" : result.exitCode === 0 ? "completed" : "failed";
3444
3511
  span.setAttributes({
@@ -3449,6 +3516,9 @@ export class AgentSession {
3449
3516
  span.setStatus({ status: "error" });
3450
3517
  return result;
3451
3518
  })));
3519
+ this._bashOperations.add(operation);
3520
+ void operation.then(() => this._bashOperations.delete(operation), () => this._bashOperations.delete(operation));
3521
+ return operation;
3452
3522
  }
3453
3523
  async _executeBash(command, onChunk, options) {
3454
3524
  const abortController = new AbortController();
@@ -3458,7 +3528,7 @@ export class AgentSession {
3458
3528
  const shellPath = this.settingsManager.getShellPath();
3459
3529
  const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
3460
3530
  try {
3461
- const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath }), {
3531
+ const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath, owner: this._shellProcesses }), {
3462
3532
  onChunk: (delta) => {
3463
3533
  onChunk?.(delta);
3464
3534
  this._emit({ type: "bash_execution_update", id: options?.id, delta });
@@ -322,7 +322,9 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
322
322
  accumulatedTokens += messageTokens;
323
323
  // Check if we've exceeded the budget
324
324
  if (accumulatedTokens >= keepRecentTokens) {
325
- // Find the closest valid cut point at or after this entry
325
+ // A completed tool batch can end the transcript. If no cut point follows
326
+ // its results, keep the batch's assistant message and all paired results.
327
+ cutIndex = cutPoints[cutPoints.length - 1];
326
328
  for (let c = 0; c < cutPoints.length; c++) {
327
329
  if (cutPoints[c] >= i) {
328
330
  cutIndex = cutPoints[c];
@@ -453,7 +453,7 @@ export class ModelRuntime {
453
453
  return lazyStream(model, async () => {
454
454
  const prepared = await this.prepareRequest(model, options);
455
455
  return prepared.provider.stream(prepared.model, context, prepared.options);
456
- });
456
+ }, options?.signal);
457
457
  }
458
458
  complete(model, context, options) {
459
459
  return this.stream(model, context, options).result();
@@ -462,7 +462,7 @@ export class ModelRuntime {
462
462
  return lazyStream(model, async () => {
463
463
  const prepared = await this.prepareRequest(model, options);
464
464
  return prepared.provider.streamSimple(prepared.model, context, prepared.options);
465
- });
465
+ }, options?.signal);
466
466
  }
467
467
  completeSimple(model, context, options) {
468
468
  return this.streamSimple(model, context, options).result();
@@ -474,7 +474,7 @@ export class ModelRuntime {
474
474
  throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`);
475
475
  }
476
476
  return prepared.provider.fetchDeferred(prepared.model, handle, prepared.options);
477
- }).result();
477
+ }, options?.signal).result();
478
478
  }
479
479
  async cancelDeferred(model, handle, options) {
480
480
  const prepared = await this.prepareRequest(model, options);
@@ -321,7 +321,7 @@ export function composeModelProvider(providerId, base, modelConfig, extension) {
321
321
  return simple
322
322
  ? api.streamSimple(model, context, options)
323
323
  : api.stream(model, context, options);
324
- });
324
+ }, options?.signal);
325
325
  const provider = {
326
326
  id: providerId,
327
327
  name: extension?.name ?? config?.name ?? base?.name ?? extension?.oauth?.name ?? providerId,
@@ -8,8 +8,9 @@ import { ModelRuntime } from "./model-runtime.ts";
8
8
  import type { ResourceLoader } from "./resource-loader.ts";
9
9
  import { SessionManager } from "./session-manager.ts";
10
10
  import { SettingsManager } from "./settings-manager.ts";
11
+ import type { SubagentSupervisionOptions } from "./subagents/types.ts";
11
12
  import { createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createUpdatePlanTool, createWriteTool, withFileMutationQueue } from "./tools/index.ts";
12
- export interface SubagentOptions {
13
+ export interface SubagentOptions extends SubagentSupervisionOptions {
13
14
  /** Maximum number of child agents that may run concurrently. Default: 4 */
14
15
  maxConcurrent?: number;
15
16
  }
package/dist/core/sdk.js CHANGED
@@ -29,7 +29,7 @@ import { createBashTool, createCodingTools, createEditTool, createFindTool, crea
29
29
  // provider-agnostic and does not import Diffex AI compat itself.
30
30
  setDefaultStreamFn(streamSimple);
31
31
  const DEFAULT_SUBAGENT_CONCURRENCY = 4;
32
- const RESERVED_SUBAGENT_TOOL_NAMES = new Set(["spawn_agent", "agent_action", "wait_for_agents", "escalate_to_parent"]);
32
+ const RESERVED_SUBAGENT_TOOL_NAMES = new Set(["spawn_agent", "agent_action", "agent_status", "escalate_to_parent"]);
33
33
  // Re-exports
34
34
  export * from "./agent-session-runtime.js";
35
35
  export { withFileMutationQueue,
@@ -48,7 +48,7 @@ function normalizeSubagentOptions(value) {
48
48
  const options = value;
49
49
  if (options) {
50
50
  for (const key of Object.keys(options)) {
51
- if (key !== "maxConcurrent") {
51
+ if (!["maxConcurrent", "leaseMs", "supervisionIntervalMs", "parentDeadline"].includes(key)) {
52
52
  throw new Error(`Unknown subagents option: ${key}`);
53
53
  }
54
54
  }
@@ -57,7 +57,19 @@ function normalizeSubagentOptions(value) {
57
57
  if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
58
58
  throw new RangeError("subagents.maxConcurrent must be a positive integer");
59
59
  }
60
- return { maxConcurrent };
60
+ return {
61
+ maxConcurrent,
62
+ leaseMs: options?.leaseMs ??
63
+ (process.env.DIFFEX_SUBAGENT_LEASE_MS === undefined
64
+ ? undefined
65
+ : Number(process.env.DIFFEX_SUBAGENT_LEASE_MS)),
66
+ supervisionIntervalMs: options?.supervisionIntervalMs ??
67
+ (process.env.DIFFEX_SUBAGENT_SUPERVISION_MS === undefined
68
+ ? undefined
69
+ : Number(process.env.DIFFEX_SUBAGENT_SUPERVISION_MS)),
70
+ parentDeadline: options?.parentDeadline ??
71
+ (process.env.DIFFEX_TASK_DEADLINE_MS === undefined ? undefined : Number(process.env.DIFFEX_TASK_DEADLINE_MS)),
72
+ };
61
73
  }
62
74
  function assertNoReservedSubagentTools(customTools) {
63
75
  for (const tool of customTools ?? []) {
@@ -66,10 +78,10 @@ function assertNoReservedSubagentTools(customTools) {
66
78
  }
67
79
  }
68
80
  }
69
- function createRootSubagentController(maxConcurrent, agentDir, telemetryContext, parentSessionManager) {
81
+ function createRootSubagentController(options, agentDir, telemetryContext, parentSessionManager) {
70
82
  const sessionManagerFactory = new RootLocalEphemeralSubagentSessionManagerFactory();
71
83
  return new SubagentController({
72
- maxConcurrent,
84
+ ...options,
73
85
  telemetryContext,
74
86
  restoreRoster: (controller, parent) => restoreRootSubagents(controller, parent, agentDir, parentSessionManager),
75
87
  captureSession: (parent, childName, origin) => {
@@ -239,7 +251,7 @@ export async function createInternalAgentSession(options = {}, internalCustomToo
239
251
  const sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));
240
252
  const subagentController = subagentOptions === false
241
253
  ? undefined
242
- : createRootSubagentController(subagentOptions.maxConcurrent, agentDir, telemetryContext, sessionManager);
254
+ : createRootSubagentController(subagentOptions, agentDir, telemetryContext, sessionManager);
243
255
  const customTools = [
244
256
  ...(options.customTools ?? []),
245
257
  ...(subagentController?.createToolDefinitions() ?? []),
@@ -2,14 +2,14 @@ import { type TelemetryContext } from "@diffexai/diffex-agent-core";
2
2
  import type { AgentSession } from "../agent-session.ts";
3
3
  import type { ToolDefinition } from "../extensions/types.ts";
4
4
  import type { ChildSnapshotV1 } from "./persistence.ts";
5
- import type { CapturedSubagentSession, CaptureSubagentSession, CreateSubagentSession, SubagentActionResult, SubagentBatchResult, SubagentBatchWaitOptions, SubagentControllerSnapshot, SubagentDeliveryBatch, SubagentLifecycleListener, SubagentPermit, SubagentQueryOptions, SubagentQueryResult, SubagentSpawnOrigin, SubagentSummary, SubagentWaitOptions, SubagentWaitResult } from "./types.ts";
5
+ import type { CapturedSubagentSession, CaptureSubagentSession, CreateSubagentSession, SubagentActionResult, SubagentBatchResult, SubagentBatchWaitOptions, SubagentControllerSnapshot, SubagentDeliveryBatch, SubagentLifecycleListener, SubagentPermit, SubagentQueryOptions, SubagentQueryResult, SubagentSpawnOrigin, SubagentSummary, SubagentSupervisionOptions, SubagentWaitOptions, SubagentWaitResult } from "./types.ts";
6
6
  export interface PreparedSubagentWake extends CapturedSubagentSession {
7
7
  readonly warning?: string;
8
8
  }
9
9
  export type PrepareSubagentWake = () => Promise<PreparedSubagentWake>;
10
10
  export declare class SubagentController {
11
11
  #private;
12
- constructor(options: {
12
+ constructor(options: SubagentSupervisionOptions & {
13
13
  maxConcurrent: number;
14
14
  captureSession?: CaptureSubagentSession;
15
15
  telemetryContext?: TelemetryContext;
@@ -23,8 +23,9 @@ export declare class SubagentController {
23
23
  restore(snapshot: ChildSnapshotV1, prepareWake: PrepareSubagentWake, isolationError?: string): void;
24
24
  restoreIsolated(name: string, error: string, prepareWake: PrepareSubagentWake): void;
25
25
  spawn(name: string, task: string, createSession: CreateSubagentSession, origin?: SubagentSpawnOrigin, telemetryContext?: TelemetryContext, captured?: CapturedSubagentSession): void;
26
- message(name: string, message: string, wake?: boolean, coordinated?: boolean): Promise<SubagentActionResult>;
26
+ message(name: string, message: string, wake?: boolean, coordinated?: boolean, delivery?: "steer" | "follow_up"): Promise<SubagentActionResult>;
27
27
  interrupt(name: string): Promise<SubagentActionResult>;
28
+ renew(name: string, reason: string): Promise<SubagentActionResult>;
28
29
  attachSession(name: string, session: AgentSession): void;
29
30
  trackRun(name: string, run: Promise<void>): void;
30
31
  markRunning(name: string): void;