@diffexai/diffex 0.2.7 → 0.3.1
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/CHANGELOG.md +34 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +39 -0
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session.d.ts +9 -0
- package/dist/core/agent-session.js +62 -8
- package/dist/core/compaction/compaction.js +3 -1
- package/dist/core/sdk.d.ts +5 -1
- package/dist/core/sdk.js +19 -6
- package/dist/core/subagents/controller.d.ts +4 -3
- package/dist/core/subagents/controller.js +173 -37
- package/dist/core/subagents/services.d.ts +2 -0
- package/dist/core/subagents/services.js +2 -0
- package/dist/core/subagents/types.d.ts +13 -0
- package/dist/core/tools/bash.d.ts +4 -0
- package/dist/core/tools/bash.js +55 -11
- package/dist/core/tools/subagents.d.ts +12 -10
- package/dist/core/tools/subagents.js +61 -49
- package/dist/index.d.ts +1 -0
- package/dist/main.js +4 -0
- package/dist/modes/print-mode.js +16 -13
- package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
- package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode-go.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
- package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +6 -6
- package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
- package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-client/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -1
- package/dist/utils/child-process.d.ts +5 -1
- package/dist/utils/child-process.js +9 -1
- package/dist/utils/owned-shell-processes.d.ts +29 -0
- package/dist/utils/owned-shell-processes.js +171 -0
- package/dist/utils/shell-process-identity.d.ts +11 -0
- package/dist/utils/shell-process-identity.js +61 -0
- package/dist/utils/shell.d.ts +1 -1
- package/dist/utils/shell.js +5 -5
- package/distribution-components.json +11 -11
- package/distribution-files.json +68 -52
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/release/distribution-manifest.json +4 -4
- package/release/install-package-lock.json +5 -5
- package/release/install-package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-09-20
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- Added opt-in background services that survive CLI and subagent exit, with explicit environment ownership.
|
|
8
|
+
See [Background process lifetime](docs/background-processes.md) ([#242](https://github.com/diffexai/diffex/pull/242)).
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added CLI and SDK options for environment-owned background processes on Linux and macOS, with durable ownership records and inherited policy for child sessions.
|
|
13
|
+
See [Background process lifetime](docs/background-processes.md) ([#242](https://github.com/diffexai/diffex/pull/242)).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed successful background services being terminated after ownership transfers to the environment, including on later failures, interruption, session replacement, and disposal ([#242](https://github.com/diffexai/diffex/pull/242)).
|
|
18
|
+
- Fixed print-mode shutdown to await shared runtime disposal and handle SIGINT cleanup ([#242](https://github.com/diffexai/diffex/pull/242)).
|
|
19
|
+
|
|
20
|
+
## [0.3.0] - 2026-09-20
|
|
21
|
+
|
|
22
|
+
### New Features
|
|
23
|
+
|
|
24
|
+
- 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)).
|
|
25
|
+
- 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)).
|
|
26
|
+
|
|
27
|
+
### Breaking Changes
|
|
28
|
+
|
|
29
|
+
- 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)).
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Fixed automatic compaction to check complete tool-result batches ([#233](https://github.com/diffexai/diffex/pull/233)).
|
|
34
|
+
- Fixed subagent completion and cancellation, including renewable work-allowance supervision ([#233](https://github.com/diffexai/diffex/pull/233)).
|
|
35
|
+
- Fixed session and subagent cancellation to terminate owned background shell processes ([#233](https://github.com/diffexai/diffex/pull/233)).
|
|
36
|
+
|
|
3
37
|
## [0.2.7] - 2026-09-18
|
|
4
38
|
|
|
5
39
|
### Fixed
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { ExtensionFlag } from "../core/extensions/types.ts";
|
|
|
6
6
|
import type { TuiMode } from "../core/settings-manager.ts";
|
|
7
7
|
export type Mode = "text" | "json" | "rpc";
|
|
8
8
|
export interface Args {
|
|
9
|
+
backgroundProcessLifetime?: "session" | "environment";
|
|
10
|
+
backgroundProcessOwnershipDir?: string;
|
|
9
11
|
provider?: string;
|
|
10
12
|
model?: string;
|
|
11
13
|
apiKey?: string;
|
package/dist/cli/args.js
CHANGED
|
@@ -28,6 +28,29 @@ export function parseArgs(args) {
|
|
|
28
28
|
result.mode = mode;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
else if (arg === "--background-process-lifetime" || arg.startsWith("--background-process-lifetime=")) {
|
|
32
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : args[i + 1];
|
|
33
|
+
if (!arg.includes("=") && value !== undefined && !value.startsWith("-"))
|
|
34
|
+
i++;
|
|
35
|
+
if (value === "session" || value === "environment")
|
|
36
|
+
result.backgroundProcessLifetime = value;
|
|
37
|
+
else
|
|
38
|
+
result.diagnostics.push({
|
|
39
|
+
type: "error",
|
|
40
|
+
message: "--background-process-lifetime requires session or environment",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else if (arg === "--background-process-ownership-dir" ||
|
|
44
|
+
arg.startsWith("--background-process-ownership-dir=")) {
|
|
45
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : args[i + 1];
|
|
46
|
+
if (value && !value.startsWith("-")) {
|
|
47
|
+
result.backgroundProcessOwnershipDir = value;
|
|
48
|
+
if (!arg.includes("="))
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
result.diagnostics.push({ type: "error", message: "--background-process-ownership-dir requires a path" });
|
|
53
|
+
}
|
|
31
54
|
else if (arg === "--continue" || arg === "-c") {
|
|
32
55
|
result.continue = true;
|
|
33
56
|
}
|
|
@@ -213,6 +236,18 @@ export function parseArgs(args) {
|
|
|
213
236
|
result.messages.push(arg);
|
|
214
237
|
}
|
|
215
238
|
}
|
|
239
|
+
if (result.backgroundProcessLifetime === "environment" && !result.backgroundProcessOwnershipDir) {
|
|
240
|
+
result.diagnostics.push({
|
|
241
|
+
type: "error",
|
|
242
|
+
message: "Environment lifetime requires --background-process-ownership-dir",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (result.backgroundProcessOwnershipDir && result.backgroundProcessLifetime !== "environment") {
|
|
246
|
+
result.diagnostics.push({
|
|
247
|
+
type: "error",
|
|
248
|
+
message: "--background-process-ownership-dir requires environment lifetime",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
216
251
|
return result;
|
|
217
252
|
}
|
|
218
253
|
export function printHelp(extensionFlags) {
|
|
@@ -248,6 +283,10 @@ ${chalk.bold("Options:")}
|
|
|
248
283
|
--append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)
|
|
249
284
|
--mode <mode> Output mode: text (default), json, or rpc
|
|
250
285
|
--print, -p Non-interactive mode: process prompt and exit
|
|
286
|
+
--background-process-lifetime <session|environment>
|
|
287
|
+
Background shell ownership (default: session)
|
|
288
|
+
--background-process-ownership-dir <path>
|
|
289
|
+
Supervisor handoff records (required for environment lifetime)
|
|
251
290
|
--continue, -c Continue previous session
|
|
252
291
|
--resume, -r Select a session to resume
|
|
253
292
|
--session <path|id> Use specific session file or partial UUID
|
|
@@ -46,6 +46,7 @@ export interface CreateAgentSessionServicesOptions {
|
|
|
46
46
|
* have been resolved against those services.
|
|
47
47
|
*/
|
|
48
48
|
export interface CreateAgentSessionFromServicesOptions {
|
|
49
|
+
backgroundProcessPolicy?: CreateAgentSessionOptions["backgroundProcessPolicy"];
|
|
49
50
|
services: AgentSessionServices;
|
|
50
51
|
sessionManager: SessionManager;
|
|
51
52
|
sessionStartEvent?: SessionStartEvent;
|
|
@@ -137,6 +137,7 @@ export async function createAgentSessionFromServices(options) {
|
|
|
137
137
|
export async function createInternalAgentSessionFromServices(options, internalCustomTools = []) {
|
|
138
138
|
registerSubagentRuntimeDiagnosticSink(options.sessionManager, options.services.diagnostics);
|
|
139
139
|
return createInternalAgentSession({
|
|
140
|
+
backgroundProcessPolicy: options.backgroundProcessPolicy,
|
|
140
141
|
cwd: options.services.cwd,
|
|
141
142
|
agentDir: options.services.agentDir,
|
|
142
143
|
modelRuntime: options.services.modelRuntime,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { type Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type TelemetryContext, type ThinkingLevel } from "@diffexai/diffex-agent-core";
|
|
16
16
|
import type { ImageContent, Model, TextContent } from "@diffexai/diffex-ai/compat";
|
|
17
17
|
import type { HarnessStateStore, HarnessVersion, HarnessVersionBundle, HarnessVersionList, HarnessWorkspaceIdentity } from "@diffexai/diffex-harness-state";
|
|
18
|
+
import { type BackgroundProcessPolicy } from "../utils/owned-shell-processes.ts";
|
|
18
19
|
import type { AgentSessionLifecycleObserver } from "./agent-session-lifecycle.ts";
|
|
19
20
|
import { type BashResult } from "./bash-executor.ts";
|
|
20
21
|
import { CHANGE_COLLABORATION_MODE, type CollaborationMode, type CollaborationModeTransitionResult, GET_COLLABORATION_MODE, IS_COLLABORATION_MODE_BUSY, SUBSCRIBE_PROMPT_ADMISSION } from "./collaboration-mode.ts";
|
|
@@ -135,6 +136,8 @@ export type AgentSessionEvent = Exclude<AgentEvent, {
|
|
|
135
136
|
/** Listener function for agent session events */
|
|
136
137
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
137
138
|
export interface AgentSessionConfig {
|
|
139
|
+
/** Lifetime of background groups launched by successful local shell commands. */
|
|
140
|
+
backgroundProcessPolicy?: BackgroundProcessPolicy;
|
|
138
141
|
agent: Agent;
|
|
139
142
|
sessionManager: SessionManager;
|
|
140
143
|
settingsManager: SettingsManager;
|
|
@@ -263,6 +266,8 @@ export declare class AgentSession {
|
|
|
263
266
|
private readonly _bashAbortControllers;
|
|
264
267
|
private _pendingBashMessages;
|
|
265
268
|
private readonly _ownedOperations;
|
|
269
|
+
private readonly _shellProcesses;
|
|
270
|
+
private readonly _bashOperations;
|
|
266
271
|
private _extensionRunner;
|
|
267
272
|
private _turnIndex;
|
|
268
273
|
private _resourceLoader;
|
|
@@ -561,6 +566,10 @@ export declare class AgentSession {
|
|
|
561
566
|
*/
|
|
562
567
|
abort(): Promise<void>;
|
|
563
568
|
private _abort;
|
|
569
|
+
/** Stop and verify all shell processes owned by this session, including background jobs. */
|
|
570
|
+
terminateOwnedShellProcesses(): Promise<void>;
|
|
571
|
+
get backgroundProcessPolicy(): BackgroundProcessPolicy;
|
|
572
|
+
get ownedShellProcessCount(): number;
|
|
564
573
|
waitForIdle(): Promise<void>;
|
|
565
574
|
private _emitModelSelect;
|
|
566
575
|
/**
|
|
@@ -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;
|
|
166
|
+
_bashOperations = new Set();
|
|
164
167
|
// Extension system
|
|
165
168
|
_extensionRunner;
|
|
166
169
|
_turnIndex = 0;
|
|
@@ -217,6 +220,7 @@ export class AgentSession {
|
|
|
217
220
|
_agentRunAborted = false;
|
|
218
221
|
_disposePromise;
|
|
219
222
|
constructor(config) {
|
|
223
|
+
this._shellProcesses = new OwnedShellProcesses(config.backgroundProcessPolicy);
|
|
220
224
|
this.agent = config.agent;
|
|
221
225
|
this.sessionManager = config.sessionManager;
|
|
222
226
|
this.settingsManager = config.settingsManager;
|
|
@@ -402,10 +406,25 @@ export class AgentSession {
|
|
|
402
406
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
403
407
|
const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
|
|
404
408
|
const previousContext = previousSnapshot?.context ?? turn.context;
|
|
409
|
+
let messages = previousContext.messages;
|
|
410
|
+
// Tool output is part of the next request even though the model's usage
|
|
411
|
+
// only describes the request before that output. Check every complete batch.
|
|
412
|
+
if (turn.toolResults.length > 0 && !signal?.aborted && this.model) {
|
|
413
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
414
|
+
if (shouldCompact(estimateContextTokens(messages).tokens, this.model.contextWindow, settings)) {
|
|
415
|
+
const beforeCompaction = this.agent.state.messages;
|
|
416
|
+
await this._runAutoCompaction("threshold", true, signal);
|
|
417
|
+
if (this.agent.state.messages !== beforeCompaction) {
|
|
418
|
+
messages = this.agent.state.messages;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
signal?.throwIfAborted();
|
|
405
423
|
return {
|
|
406
424
|
...previousSnapshot,
|
|
407
425
|
context: {
|
|
408
426
|
...previousContext,
|
|
427
|
+
messages,
|
|
409
428
|
systemPrompt: this._activeRunCollaborationState?.effectiveSystemPrompt ??
|
|
410
429
|
this._deriveEffectivePrompt(this._systemPromptOverride ?? this._baseSystemPrompt, this._activeRunCollaborationMode ?? this._collaborationMode),
|
|
411
430
|
tools: (this._activeRunCollaborationState?.tools ?? this.agent.state.tools).slice(),
|
|
@@ -836,7 +855,7 @@ export class AgentSession {
|
|
|
836
855
|
failures.push(error);
|
|
837
856
|
}
|
|
838
857
|
const ownedOperationsIdle = this._waitForOwnedOperations();
|
|
839
|
-
const cleanupResults = await Promise.allSettled([childCleanup, rootIdle, ownedOperationsIdle].filter((promise) => promise !== undefined));
|
|
858
|
+
const cleanupResults = await Promise.allSettled([childCleanup, rootIdle, ownedOperationsIdle, this.terminateOwnedShellProcesses()].filter((promise) => promise !== undefined));
|
|
840
859
|
for (const result of cleanupResults) {
|
|
841
860
|
if (result.status !== "rejected")
|
|
842
861
|
continue;
|
|
@@ -1651,6 +1670,22 @@ export class AgentSession {
|
|
|
1651
1670
|
if (coordination.exitReason === "cancellation" || this._agentRunAborted) {
|
|
1652
1671
|
this._exitSubagentCoordination("cancellation");
|
|
1653
1672
|
}
|
|
1673
|
+
else if (!coordination.completionChecked) {
|
|
1674
|
+
coordination.completionChecked = true;
|
|
1675
|
+
const active = this.getSubagents().filter((child) => coordination.names.includes(child.name) && (child.status === "running" || child.status === "queued"));
|
|
1676
|
+
if (active.length > 0) {
|
|
1677
|
+
await this.sendCustomMessage({
|
|
1678
|
+
customType: "subagent_completion_check",
|
|
1679
|
+
content: JSON.stringify({
|
|
1680
|
+
type: "subagent_completion_check",
|
|
1681
|
+
agents: active,
|
|
1682
|
+
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.",
|
|
1683
|
+
}),
|
|
1684
|
+
display: false,
|
|
1685
|
+
}, { deliverAs: "steer" });
|
|
1686
|
+
return "continue";
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1654
1689
|
const delivery = await this._waitForSubagentCoordination(coordination);
|
|
1655
1690
|
if (delivery.reason === "cancellation")
|
|
1656
1691
|
return "settled";
|
|
@@ -2354,11 +2389,24 @@ export class AgentSession {
|
|
|
2354
2389
|
}
|
|
2355
2390
|
async _abort() {
|
|
2356
2391
|
this.abortRetry();
|
|
2392
|
+
this.abortCompaction();
|
|
2393
|
+
this.abortBranchSummary();
|
|
2394
|
+
this.abortBash();
|
|
2357
2395
|
if (this._isAgentRunActive)
|
|
2358
2396
|
this._agentRunAborted = true;
|
|
2359
2397
|
this._exitSubagentCoordination("cancellation");
|
|
2360
2398
|
this.agent.abort();
|
|
2361
|
-
await this.waitForIdle();
|
|
2399
|
+
await Promise.all([this.waitForIdle(), this.terminateOwnedShellProcesses(), ...this._bashOperations]);
|
|
2400
|
+
}
|
|
2401
|
+
/** Stop and verify all shell processes owned by this session, including background jobs. */
|
|
2402
|
+
async terminateOwnedShellProcesses() {
|
|
2403
|
+
await this._shellProcesses.terminateAll();
|
|
2404
|
+
}
|
|
2405
|
+
get backgroundProcessPolicy() {
|
|
2406
|
+
return this._shellProcesses.policy;
|
|
2407
|
+
}
|
|
2408
|
+
get ownedShellProcessCount() {
|
|
2409
|
+
return this._shellProcesses.count;
|
|
2362
2410
|
}
|
|
2363
2411
|
async waitForIdle() {
|
|
2364
2412
|
if (this.isIdle) {
|
|
@@ -2847,6 +2895,9 @@ export class AgentSession {
|
|
|
2847
2895
|
return false;
|
|
2848
2896
|
}
|
|
2849
2897
|
this._autoCompactionAbortController = new AbortController();
|
|
2898
|
+
const compactionSignal = signal
|
|
2899
|
+
? AbortSignal.any([signal, this._autoCompactionAbortController.signal])
|
|
2900
|
+
: this._autoCompactionAbortController.signal;
|
|
2850
2901
|
this._emit({ type: "compaction_start", reason });
|
|
2851
2902
|
started = true;
|
|
2852
2903
|
let extensionCompaction;
|
|
@@ -2859,7 +2910,7 @@ export class AgentSession {
|
|
|
2859
2910
|
customInstructions: undefined,
|
|
2860
2911
|
reason,
|
|
2861
2912
|
willRetry,
|
|
2862
|
-
signal:
|
|
2913
|
+
signal: compactionSignal,
|
|
2863
2914
|
}));
|
|
2864
2915
|
if (extensionResult?.cancel) {
|
|
2865
2916
|
this._emit({
|
|
@@ -2891,14 +2942,14 @@ export class AgentSession {
|
|
|
2891
2942
|
}
|
|
2892
2943
|
else {
|
|
2893
2944
|
// Generate compaction result
|
|
2894
|
-
const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined,
|
|
2945
|
+
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);
|
|
2895
2946
|
summary = compactResult.summary;
|
|
2896
2947
|
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
2897
2948
|
tokensBefore = compactResult.tokensBefore;
|
|
2898
2949
|
usage = compactResult.usage;
|
|
2899
2950
|
details = compactResult.details;
|
|
2900
2951
|
}
|
|
2901
|
-
if (
|
|
2952
|
+
if (compactionSignal.aborted) {
|
|
2902
2953
|
this._emit({
|
|
2903
2954
|
type: "compaction_end",
|
|
2904
2955
|
reason,
|
|
@@ -3257,7 +3308,7 @@ export class AgentSession {
|
|
|
3257
3308
|
: {
|
|
3258
3309
|
...createAllToolDefinitions(this._cwd, {
|
|
3259
3310
|
read: { autoResizeImages },
|
|
3260
|
-
bash: { commandPrefix: shellCommandPrefix, shellPath },
|
|
3311
|
+
bash: { commandPrefix: shellCommandPrefix, shellPath, owner: this._shellProcesses },
|
|
3261
3312
|
}),
|
|
3262
3313
|
update_plan: createSessionUpdatePlanToolDefinition(this.sessionManager),
|
|
3263
3314
|
};
|
|
@@ -3458,7 +3509,7 @@ export class AgentSession {
|
|
|
3458
3509
|
* @param options.operations Custom BashOperations for remote execution
|
|
3459
3510
|
*/
|
|
3460
3511
|
executeBash(command, onChunk, options) {
|
|
3461
|
-
|
|
3512
|
+
const operation = this._runOwnedOperation(() => this._traceOperation("bash", () => startCodingAgentSpan(this.agent.telemetryContext, "diffex.coding_agent.terminal", { "diffex.session.id": this.sessionId }, async (span) => {
|
|
3462
3513
|
const result = await this._executeBash(command, onChunk, options);
|
|
3463
3514
|
const outcome = result.cancelled ? "cancelled" : result.exitCode === 0 ? "completed" : "failed";
|
|
3464
3515
|
span.setAttributes({
|
|
@@ -3469,6 +3520,9 @@ export class AgentSession {
|
|
|
3469
3520
|
span.setStatus({ status: "error" });
|
|
3470
3521
|
return result;
|
|
3471
3522
|
})));
|
|
3523
|
+
this._bashOperations.add(operation);
|
|
3524
|
+
void operation.then(() => this._bashOperations.delete(operation), () => this._bashOperations.delete(operation));
|
|
3525
|
+
return operation;
|
|
3472
3526
|
}
|
|
3473
3527
|
async _executeBash(command, onChunk, options) {
|
|
3474
3528
|
const abortController = new AbortController();
|
|
@@ -3478,7 +3532,7 @@ export class AgentSession {
|
|
|
3478
3532
|
const shellPath = this.settingsManager.getShellPath();
|
|
3479
3533
|
const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
|
|
3480
3534
|
try {
|
|
3481
|
-
const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath }), {
|
|
3535
|
+
const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath, owner: this._shellProcesses }), {
|
|
3482
3536
|
onChunk: (delta) => {
|
|
3483
3537
|
onChunk?.(delta);
|
|
3484
3538
|
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
|
-
//
|
|
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];
|
package/dist/core/sdk.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type TelemetryContext, type ThinkingLevel } from "@diffexai/diffex-agent-core";
|
|
2
2
|
import { type Model } from "@diffexai/diffex-ai/compat";
|
|
3
3
|
import type { HarnessStateStore, HarnessVersionBundle } from "@diffexai/diffex-harness-state";
|
|
4
|
+
import type { BackgroundProcessPolicy } from "../utils/owned-shell-processes.ts";
|
|
4
5
|
import { AgentSession } from "./agent-session.ts";
|
|
5
6
|
import type { AgentSessionLifecycleObserver } from "./agent-session-lifecycle.ts";
|
|
6
7
|
import type { LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
|
|
@@ -8,12 +9,15 @@ import { ModelRuntime } from "./model-runtime.ts";
|
|
|
8
9
|
import type { ResourceLoader } from "./resource-loader.ts";
|
|
9
10
|
import { SessionManager } from "./session-manager.ts";
|
|
10
11
|
import { SettingsManager } from "./settings-manager.ts";
|
|
12
|
+
import type { SubagentSupervisionOptions } from "./subagents/types.ts";
|
|
11
13
|
import { createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createUpdatePlanTool, createWriteTool, withFileMutationQueue } from "./tools/index.ts";
|
|
12
|
-
export interface SubagentOptions {
|
|
14
|
+
export interface SubagentOptions extends SubagentSupervisionOptions {
|
|
13
15
|
/** Maximum number of child agents that may run concurrently. Default: 4 */
|
|
14
16
|
maxConcurrent?: number;
|
|
15
17
|
}
|
|
16
18
|
export interface CreateAgentSessionOptions {
|
|
19
|
+
/** Local shell background lifetime. Defaults to session-owned cleanup. */
|
|
20
|
+
backgroundProcessPolicy?: BackgroundProcessPolicy;
|
|
17
21
|
/** Working directory for project-local discovery. Default: process.cwd() */
|
|
18
22
|
cwd?: string;
|
|
19
23
|
/** Global config directory. Default: ~/.diffex/agent */
|
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", "
|
|
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 (
|
|
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 {
|
|
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(
|
|
81
|
+
function createRootSubagentController(options, agentDir, telemetryContext, parentSessionManager) {
|
|
70
82
|
const sessionManagerFactory = new RootLocalEphemeralSubagentSessionManagerFactory();
|
|
71
83
|
return new SubagentController({
|
|
72
|
-
|
|
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
|
|
254
|
+
: createRootSubagentController(subagentOptions, agentDir, telemetryContext, sessionManager);
|
|
243
255
|
const customTools = [
|
|
244
256
|
...(options.customTools ?? []),
|
|
245
257
|
...(subagentController?.createToolDefinitions() ?? []),
|
|
@@ -424,6 +436,7 @@ export async function createInternalAgentSession(options = {}, internalCustomToo
|
|
|
424
436
|
sessionManager.appendThinkingLevelChange(thinkingLevel);
|
|
425
437
|
}
|
|
426
438
|
const session = new AgentSession({
|
|
439
|
+
backgroundProcessPolicy: options.backgroundProcessPolicy,
|
|
427
440
|
agent,
|
|
428
441
|
sessionManager,
|
|
429
442
|
settingsManager,
|
|
@@ -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;
|