@diffexai/diffex 0.3.0 → 0.3.2
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 +45 -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 +4 -5
- package/dist/core/agent-session.js +11 -62
- package/dist/core/compaction/branch-summarization.js +3 -2
- package/dist/core/compaction/compaction.js +9 -7
- package/dist/core/compaction/utils.d.ts +3 -0
- package/dist/core/compaction/utils.js +21 -0
- package/dist/core/current-plan-state.d.ts +3 -2
- package/dist/core/current-plan-state.js +23 -6
- package/dist/core/plan-snapshot.d.ts +3 -0
- package/dist/core/plan-snapshot.js +23 -0
- package/dist/core/sdk.d.ts +3 -0
- package/dist/core/sdk.js +1 -0
- package/dist/core/session-manager.js +21 -1
- package/dist/core/subagents/services.d.ts +2 -0
- package/dist/core/subagents/services.js +2 -0
- package/dist/core/tools/bash.js +47 -12
- package/dist/core/tools/update-plan.d.ts +27 -6
- package/dist/core/tools/update-plan.js +122 -18
- package/dist/index.d.ts +1 -0
- package/dist/main.js +4 -0
- package/dist/modes/interactive/components/footer.js +16 -5
- package/dist/modes/interactive/components/tool-group.d.ts +1 -0
- package/dist/modes/interactive/components/tool-group.js +26 -7
- package/dist/modes/print-mode.js +16 -13
- package/dist/node_modules/@diffexai/diffex-agent-core/dist/agent-loop.js +21 -3
- package/dist/node_modules/@diffexai/diffex-agent-core/dist/types.d.ts +17 -10
- package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
- package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +3 -3
- 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/amazon-bedrock.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/anthropic.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/azure-openai-responses.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cerebras.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/fireworks.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/github-copilot.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/google-vertex.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/google.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/huggingface.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/minimax-cn.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/minimax.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/mistral.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/moonshotai.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/nvidia.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openai.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/together.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/dist/providers/data/xai.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi.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 +30 -30
- 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 +23 -2
- package/dist/utils/owned-shell-processes.js +149 -39
- 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 +101 -85
- 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
|
@@ -7,7 +7,9 @@ import { createInterface } from "readline";
|
|
|
7
7
|
import { StringDecoder } from "string_decoder";
|
|
8
8
|
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
|
|
9
9
|
import { normalizePath, resolvePath } from "../utils/paths.js";
|
|
10
|
+
import { excludePlanItems } from "./compaction/utils.js";
|
|
10
11
|
import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "./messages.js";
|
|
12
|
+
import { stripCurrentPlanSnapshot } from "./plan-snapshot.js";
|
|
11
13
|
export const CURRENT_SESSION_VERSION = 3;
|
|
12
14
|
function createSessionId() {
|
|
13
15
|
return uuidv7();
|
|
@@ -237,7 +239,25 @@ export function buildContextEntries(entries, leafId, byId) {
|
|
|
237
239
|
export function buildSessionContext(entries, leafId, byId) {
|
|
238
240
|
const path = buildSessionPath(entries, leafId, byId);
|
|
239
241
|
const { thinkingLevel, model } = getSessionContextSettings(path);
|
|
240
|
-
const
|
|
242
|
+
const contextEntries = buildContextEntries(entries, leafId, byId);
|
|
243
|
+
const contextEntryIds = new Set(contextEntries.map((entry) => entry.id));
|
|
244
|
+
// Context order puts the latest compaction before its retained tail. Use path
|
|
245
|
+
// order to find the snapshot that supersedes earlier plan calls and summaries.
|
|
246
|
+
let snapshotIndex = -1;
|
|
247
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
248
|
+
const entry = path[i];
|
|
249
|
+
if (contextEntryIds.has(entry.id) &&
|
|
250
|
+
(entry.type === "compaction" || entry.type === "branch_summary") &&
|
|
251
|
+
stripCurrentPlanSnapshot(entry.summary) !== entry.summary) {
|
|
252
|
+
snapshotIndex = i;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const supersededEntryIds = new Set(path.slice(0, Math.max(0, snapshotIndex)).map((entry) => entry.id));
|
|
257
|
+
const messages = contextEntries.flatMap((entry) => {
|
|
258
|
+
const projected = sessionEntryToContextMessages(entry);
|
|
259
|
+
return supersededEntryIds.has(entry.id) ? excludePlanItems(projected) : projected;
|
|
260
|
+
});
|
|
241
261
|
return { messages, thinkingLevel, model };
|
|
242
262
|
}
|
|
243
263
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TelemetryContext, ThinkingLevel } from "@diffexai/diffex-agent-core";
|
|
2
2
|
import type { Model } from "@diffexai/diffex-ai";
|
|
3
3
|
import type { HarnessStateStore, HarnessVersionBundle } from "@diffexai/diffex-harness-state";
|
|
4
|
+
import type { BackgroundProcessPolicy } from "../../utils/owned-shell-processes.ts";
|
|
4
5
|
import type { AgentSession } from "../agent-session.ts";
|
|
5
6
|
import { type AgentSessionServices } from "../agent-session-services.ts";
|
|
6
7
|
import type { ModelRuntime } from "../model-runtime.ts";
|
|
@@ -19,6 +20,7 @@ export interface CreateSubagentServicesOptions {
|
|
|
19
20
|
telemetryContext?: TelemetryContext;
|
|
20
21
|
}
|
|
21
22
|
export interface SubagentSessionConfig {
|
|
23
|
+
readonly backgroundProcessPolicy?: BackgroundProcessPolicy;
|
|
22
24
|
readonly cwd: string;
|
|
23
25
|
readonly agentDir: string;
|
|
24
26
|
readonly modelRuntime: ModelRuntime;
|
|
@@ -39,6 +39,7 @@ export function captureSubagentSessionConfig(parent, agentDir) {
|
|
|
39
39
|
.getActiveToolNames()
|
|
40
40
|
.filter((name) => allToolNames.has(name) && toolsByName.get(name)?.sourceInfo.source === "builtin"));
|
|
41
41
|
return Object.freeze({
|
|
42
|
+
backgroundProcessPolicy: parent.backgroundProcessPolicy,
|
|
42
43
|
cwd: parent.sessionManager.getCwd(),
|
|
43
44
|
agentDir,
|
|
44
45
|
modelRuntime: parent.modelRuntime,
|
|
@@ -133,6 +134,7 @@ export async function createSubagentSession(config, options = {}) {
|
|
|
133
134
|
markFixedHarnessSubagentSession(sessionManager);
|
|
134
135
|
const { session } = await createInternalAgentSessionFromServices({
|
|
135
136
|
services,
|
|
137
|
+
backgroundProcessPolicy: config.backgroundProcessPolicy,
|
|
136
138
|
sessionManager,
|
|
137
139
|
model: config.model,
|
|
138
140
|
thinkingLevel: config.thinkingLevel,
|
package/dist/core/tools/bash.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { constants } from "node:fs";
|
|
2
3
|
import { access as fsAccess } from "node:fs/promises";
|
|
3
4
|
import { Container, Text, truncateToWidth } from "@diffexai/diffex-tui";
|
|
@@ -8,6 +9,7 @@ import { truncateToVisualLines } from "../../modes/interactive/components/visual
|
|
|
8
9
|
import { theme } from "../../modes/interactive/theme/theme.js";
|
|
9
10
|
import { waitForChildProcess } from "../../utils/child-process.js";
|
|
10
11
|
import { getShellConfig, getShellEnv, killProcessTree, trackDetachedChildPid, untrackDetachedChildPid, } from "../../utils/shell.js";
|
|
12
|
+
import { SHELL_EXECUTION_ID_ENV } from "../../utils/shell-process-identity.js";
|
|
11
13
|
import { OutputAccumulator } from "./output-accumulator.js";
|
|
12
14
|
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
|
13
15
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
@@ -56,10 +58,11 @@ export function createLocalBashOperations(options) {
|
|
|
56
58
|
}
|
|
57
59
|
signal?.throwIfAborted();
|
|
58
60
|
const commandFromStdin = shellConfig.commandTransport === "stdin";
|
|
61
|
+
const executionId = randomUUID();
|
|
59
62
|
const child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], {
|
|
60
63
|
cwd,
|
|
61
64
|
detached: process.platform !== "win32",
|
|
62
|
-
env: env ?? getShellEnv(),
|
|
65
|
+
env: { ...(env ?? getShellEnv()), [SHELL_EXECUTION_ID_ENV]: executionId },
|
|
63
66
|
stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
64
67
|
windowsHide: true,
|
|
65
68
|
});
|
|
@@ -67,25 +70,52 @@ export function createLocalBashOperations(options) {
|
|
|
67
70
|
child.stdin?.on("error", () => { });
|
|
68
71
|
child.stdin?.end(command);
|
|
69
72
|
}
|
|
70
|
-
if (child.pid) {
|
|
71
|
-
if (options?.owner)
|
|
72
|
-
options.owner.track(child.pid);
|
|
73
|
-
else
|
|
74
|
-
trackDetachedChildPid(child.pid);
|
|
75
|
-
}
|
|
76
73
|
let timedOut = false;
|
|
74
|
+
const environmentLifetime = options?.owner?.policy.lifetime === "environment";
|
|
75
|
+
let completed = false;
|
|
76
|
+
let completionError;
|
|
77
77
|
let timeoutHandle;
|
|
78
78
|
const onAbort = () => {
|
|
79
|
-
if (child.pid)
|
|
80
|
-
|
|
79
|
+
if (child.pid) {
|
|
80
|
+
if (options?.owner)
|
|
81
|
+
options.owner.terminate(child.pid);
|
|
82
|
+
else
|
|
83
|
+
killProcessTree(child.pid);
|
|
84
|
+
}
|
|
81
85
|
};
|
|
86
|
+
const onExit = (code) => {
|
|
87
|
+
completed = true;
|
|
88
|
+
if (environmentLifetime && timeoutHandle)
|
|
89
|
+
clearTimeout(timeoutHandle);
|
|
90
|
+
try {
|
|
91
|
+
if (child.pid)
|
|
92
|
+
options?.owner?.complete(child.pid, code === 0 && !timedOut && !signal?.aborted);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
completionError = error;
|
|
96
|
+
try {
|
|
97
|
+
onAbort();
|
|
98
|
+
}
|
|
99
|
+
catch (cleanupError) {
|
|
100
|
+
completionError = new AggregateError([error, cleanupError], "Shell ownership handoff failed");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
child.once("exit", onExit);
|
|
82
105
|
try {
|
|
106
|
+
if (child.pid) {
|
|
107
|
+
if (options?.owner)
|
|
108
|
+
options.owner.track(child.pid, executionId);
|
|
109
|
+
else
|
|
110
|
+
trackDetachedChildPid(child.pid);
|
|
111
|
+
}
|
|
83
112
|
// Set timeout if provided.
|
|
84
113
|
if (timeoutMs !== undefined) {
|
|
85
114
|
timeoutHandle = setTimeout(() => {
|
|
115
|
+
if (environmentLifetime && completed)
|
|
116
|
+
return;
|
|
86
117
|
timedOut = true;
|
|
87
|
-
|
|
88
|
-
killProcessTree(child.pid);
|
|
118
|
+
onAbort();
|
|
89
119
|
}, timeoutMs);
|
|
90
120
|
}
|
|
91
121
|
// Stream stdout and stderr.
|
|
@@ -100,7 +130,11 @@ export function createLocalBashOperations(options) {
|
|
|
100
130
|
}
|
|
101
131
|
// Handle shell spawn errors and wait for the process to terminate without hanging
|
|
102
132
|
// on inherited stdio handles held by detached descendants.
|
|
103
|
-
const exitCode = await waitForChildProcess(child
|
|
133
|
+
const exitCode = await waitForChildProcess(child, {
|
|
134
|
+
maxPostExitMs: environmentLifetime ? 100 : undefined,
|
|
135
|
+
});
|
|
136
|
+
if (completionError)
|
|
137
|
+
throw completionError;
|
|
104
138
|
if (signal?.aborted) {
|
|
105
139
|
throw new Error("aborted");
|
|
106
140
|
}
|
|
@@ -110,6 +144,7 @@ export function createLocalBashOperations(options) {
|
|
|
110
144
|
return { exitCode };
|
|
111
145
|
}
|
|
112
146
|
finally {
|
|
147
|
+
child.removeListener("exit", onExit);
|
|
113
148
|
if (child.pid) {
|
|
114
149
|
if (options?.owner)
|
|
115
150
|
options.owner.releaseIfExited(child.pid);
|
|
@@ -4,30 +4,51 @@ import type { ToolDefinition } from "../extensions/types.ts";
|
|
|
4
4
|
declare const UPDATE_PLAN_STATUSES: readonly ["pending", "in_progress", "completed"];
|
|
5
5
|
export type UpdatePlanStatus = (typeof UPDATE_PLAN_STATUSES)[number];
|
|
6
6
|
export interface UpdatePlanItem {
|
|
7
|
+
id: string;
|
|
7
8
|
step: string;
|
|
8
9
|
status: UpdatePlanStatus;
|
|
10
|
+
success_criteria?: string[];
|
|
9
11
|
}
|
|
10
12
|
export type UpdatePlanToolInput = {
|
|
11
13
|
plan: UpdatePlanItem[];
|
|
12
14
|
explanation?: string;
|
|
15
|
+
} | {
|
|
16
|
+
update: {
|
|
17
|
+
id: string;
|
|
18
|
+
step?: string;
|
|
19
|
+
status?: UpdatePlanStatus;
|
|
20
|
+
success_criteria?: string[];
|
|
21
|
+
};
|
|
22
|
+
explanation?: string;
|
|
13
23
|
} | {
|
|
14
24
|
clear: true;
|
|
15
25
|
explanation?: string;
|
|
16
26
|
};
|
|
17
|
-
export type UpdatePlanToolDetails = UpdatePlanToolInput
|
|
27
|
+
export type UpdatePlanToolDetails = Exclude<UpdatePlanToolInput, {
|
|
28
|
+
update: unknown;
|
|
29
|
+
}>;
|
|
18
30
|
declare const updatePlanSchema: Type.TObject<{
|
|
19
31
|
plan: Type.TOptional<Type.TArray<Type.TObject<{
|
|
32
|
+
id: Type.TString;
|
|
20
33
|
step: Type.TString;
|
|
21
34
|
status: Type.TUnsafe<"completed" | "in_progress" | "pending">;
|
|
35
|
+
success_criteria: Type.TOptional<Type.TArray<Type.TString>>;
|
|
22
36
|
}>>>;
|
|
37
|
+
update: Type.TOptional<Type.TObject<{
|
|
38
|
+
id: Type.TString;
|
|
39
|
+
step: Type.TOptional<Type.TString>;
|
|
40
|
+
status: Type.TOptional<Type.TUnsafe<"completed" | "in_progress" | "pending">>;
|
|
41
|
+
success_criteria: Type.TOptional<Type.TArray<Type.TString>>;
|
|
42
|
+
}>>;
|
|
23
43
|
clear: Type.TOptional<Type.TLiteral<true>>;
|
|
24
44
|
explanation: Type.TOptional<Type.TString>;
|
|
25
45
|
}>;
|
|
26
46
|
export declare const updatePlanToolSystemPromptContribution: {
|
|
27
|
-
readonly snippet: "
|
|
28
|
-
readonly guidelines: readonly ["Use update_plan for complex, ambiguous, dependent, long-running, or multi-part work, when the user requests a plan, or when checkpoints would improve collaboration; skip it for simple or immediately executable work.", "Write short, ordered, outcome-focused steps
|
|
47
|
+
readonly snippet: "persist an ordered plan with step IDs and optional success criteria";
|
|
48
|
+
readonly guidelines: readonly ["Use update_plan for complex, ambiguous, dependent, long-running, or multi-part work, when the user requests a plan, or when checkpoints would improve collaboration; skip it for simple or immediately executable work.", "Write short, ordered, outcome-focused steps. Give each step a stable ID and add optional success_criteria for subtasks needing validation.", "Include final verification when correctness requires it.", "Update by ID at meaningful milestones; retain IDs when rewording, reordering, or changing status.", "Explain material replanning, omit explanations for routine progress, and use clear: true only when abandoning the checklist.", "Publishing the checklist communicates progress but does not execute, schedule, or prove completion of its steps."];
|
|
29
49
|
};
|
|
30
|
-
export declare function parseUpdatePlanOperation(input: unknown):
|
|
31
|
-
export declare function
|
|
32
|
-
export declare function
|
|
50
|
+
export declare function parseUpdatePlanOperation(input: unknown): UpdatePlanToolInput;
|
|
51
|
+
export declare function resolveUpdatePlanOperation(input: unknown, currentPlan?: readonly UpdatePlanItem[]): UpdatePlanToolDetails;
|
|
52
|
+
export declare function createUpdatePlanToolDefinition(_cwd?: string, getCurrentPlan?: () => readonly UpdatePlanItem[] | undefined): ToolDefinition<typeof updatePlanSchema, UpdatePlanToolDetails>;
|
|
53
|
+
export declare function createUpdatePlanTool(getCurrentPlan?: () => readonly UpdatePlanItem[] | undefined): AgentTool<typeof updatePlanSchema>;
|
|
33
54
|
export {};
|
|
@@ -9,26 +9,34 @@ const updatePlanStatusSchema = Type.Unsafe({
|
|
|
9
9
|
description: "Status of this plan item.",
|
|
10
10
|
});
|
|
11
11
|
const updatePlanItemSchema = Type.Object({
|
|
12
|
+
id: Type.String({ description: "Stable step ID; retain it across edits and reordering." }),
|
|
12
13
|
step: Type.String({ description: "Task step text." }),
|
|
13
14
|
status: updatePlanStatusSchema,
|
|
15
|
+
success_criteria: Type.Optional(Type.Array(Type.String(), { description: "Observable completion criteria." })),
|
|
14
16
|
}, { additionalProperties: false });
|
|
15
17
|
const updatePlanSchema = Type.Object({
|
|
16
18
|
plan: Type.Optional(Type.Array(updatePlanItemSchema, {
|
|
17
19
|
minItems: 1,
|
|
18
|
-
description: "Full ordered plan snapshot.
|
|
20
|
+
description: "Full ordered plan snapshot. Keep existing IDs for retained steps.",
|
|
19
21
|
})),
|
|
22
|
+
update: Type.Optional(Type.Object({
|
|
23
|
+
id: Type.String({ description: "ID of an existing step." }),
|
|
24
|
+
step: Type.Optional(Type.String()),
|
|
25
|
+
status: Type.Optional(updatePlanStatusSchema),
|
|
26
|
+
success_criteria: Type.Optional(Type.Array(Type.String())),
|
|
27
|
+
}, { additionalProperties: false })),
|
|
20
28
|
clear: Type.Optional(Type.Literal(true, {
|
|
21
|
-
description: "Explicitly abandon and clear the current plan.
|
|
29
|
+
description: "Explicitly abandon and clear the current plan.",
|
|
22
30
|
})),
|
|
23
31
|
explanation: Type.Optional(Type.String({ description: "Reason for changing or clearing the plan." })),
|
|
24
32
|
}, { additionalProperties: false });
|
|
25
33
|
export const updatePlanToolSystemPromptContribution = {
|
|
26
|
-
snippet: "
|
|
34
|
+
snippet: "persist an ordered plan with step IDs and optional success criteria",
|
|
27
35
|
guidelines: [
|
|
28
36
|
"Use update_plan for complex, ambiguous, dependent, long-running, or multi-part work, when the user requests a plan, or when checkpoints would improve collaboration; skip it for simple or immediately executable work.",
|
|
29
|
-
"Write short, ordered, outcome-focused steps
|
|
37
|
+
"Write short, ordered, outcome-focused steps. Give each step a stable ID and add optional success_criteria for subtasks needing validation.",
|
|
30
38
|
"Include final verification when correctness requires it.",
|
|
31
|
-
"Update
|
|
39
|
+
"Update by ID at meaningful milestones; retain IDs when rewording, reordering, or changing status.",
|
|
32
40
|
"Explain material replanning, omit explanations for routine progress, and use clear: true only when abandoning the checklist.",
|
|
33
41
|
"Publishing the checklist communicates progress but does not execute, schedule, or prove completion of its steps.",
|
|
34
42
|
],
|
|
@@ -39,28 +47,52 @@ function isRecord(value) {
|
|
|
39
47
|
function isUpdatePlanStatus(value) {
|
|
40
48
|
return typeof value === "string" && UPDATE_PLAN_STATUSES.includes(value);
|
|
41
49
|
}
|
|
50
|
+
function validateCriteria(value, label) {
|
|
51
|
+
if (!Array.isArray(value) ||
|
|
52
|
+
value.some((criterion) => typeof criterion !== "string" || criterion.trim().length === 0)) {
|
|
53
|
+
throw new Error(`${label} must be an array of non-empty strings.`);
|
|
54
|
+
}
|
|
55
|
+
return [...value];
|
|
56
|
+
}
|
|
42
57
|
function validatePlanSnapshot(rawPlan) {
|
|
43
58
|
if (!Array.isArray(rawPlan) || rawPlan.length === 0) {
|
|
44
59
|
throw new Error("Plan must contain at least one item.");
|
|
45
60
|
}
|
|
46
61
|
const plan = [];
|
|
62
|
+
const ids = new Set();
|
|
47
63
|
for (let index = 0; index < rawPlan.length; index++) {
|
|
48
64
|
const rawItem = rawPlan[index];
|
|
49
65
|
if (!isRecord(rawItem)) {
|
|
50
66
|
throw new Error(`Plan item ${index + 1} must be an object.`);
|
|
51
67
|
}
|
|
52
|
-
const unexpectedField = Object.keys(rawItem).find((field) => field !== "step" && field !== "status");
|
|
68
|
+
const unexpectedField = Object.keys(rawItem).find((field) => field !== "id" && field !== "step" && field !== "status" && field !== "success_criteria");
|
|
53
69
|
if (unexpectedField !== undefined) {
|
|
54
70
|
throw new Error(`Plan item ${index + 1} contains unexpected field: ${unexpectedField}.`);
|
|
55
71
|
}
|
|
56
|
-
const { step, status } = rawItem;
|
|
72
|
+
const { id, step, status } = rawItem;
|
|
73
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
74
|
+
throw new Error(`Plan item ${index + 1} id must be a non-empty string.`);
|
|
75
|
+
}
|
|
76
|
+
if (ids.has(id)) {
|
|
77
|
+
throw new Error(`Duplicate plan item id: ${id}.`);
|
|
78
|
+
}
|
|
79
|
+
ids.add(id);
|
|
57
80
|
if (typeof step !== "string" || step.trim().length === 0) {
|
|
58
81
|
throw new Error(`Plan item ${index + 1} step must be a non-empty string.`);
|
|
59
82
|
}
|
|
60
83
|
if (!isUpdatePlanStatus(status)) {
|
|
61
84
|
throw new Error(`Plan item ${index + 1} status must be pending, in_progress, or completed.`);
|
|
62
85
|
}
|
|
63
|
-
plan.push({
|
|
86
|
+
plan.push({
|
|
87
|
+
id,
|
|
88
|
+
step,
|
|
89
|
+
status,
|
|
90
|
+
...(Object.hasOwn(rawItem, "success_criteria")
|
|
91
|
+
? {
|
|
92
|
+
success_criteria: validateCriteria(rawItem.success_criteria, `Plan item ${index + 1} success_criteria`),
|
|
93
|
+
}
|
|
94
|
+
: {}),
|
|
95
|
+
});
|
|
64
96
|
}
|
|
65
97
|
const counts = countPlanStatuses(plan);
|
|
66
98
|
if (counts.completed !== plan.length && counts.inProgress !== 1) {
|
|
@@ -72,14 +104,15 @@ export function parseUpdatePlanOperation(input) {
|
|
|
72
104
|
if (!isRecord(input)) {
|
|
73
105
|
throw new Error("update_plan input must be an object.");
|
|
74
106
|
}
|
|
75
|
-
const unexpectedField = Object.keys(input).find((field) => field !== "plan" && field !== "clear" && field !== "explanation");
|
|
107
|
+
const unexpectedField = Object.keys(input).find((field) => field !== "plan" && field !== "update" && field !== "clear" && field !== "explanation");
|
|
76
108
|
if (unexpectedField !== undefined) {
|
|
77
109
|
throw new Error(`Unexpected update_plan field: ${unexpectedField}.`);
|
|
78
110
|
}
|
|
79
111
|
const hasPlan = Object.hasOwn(input, "plan");
|
|
112
|
+
const hasUpdate = Object.hasOwn(input, "update");
|
|
80
113
|
const hasClear = Object.hasOwn(input, "clear");
|
|
81
|
-
if (hasPlan
|
|
82
|
-
throw new Error("Provide exactly one update_plan operation: plan or clear.");
|
|
114
|
+
if (Number(hasPlan) + Number(hasUpdate) + Number(hasClear) !== 1) {
|
|
115
|
+
throw new Error("Provide exactly one update_plan operation: plan, update, or clear.");
|
|
83
116
|
}
|
|
84
117
|
const hasExplanation = Object.hasOwn(input, "explanation");
|
|
85
118
|
if (hasExplanation && typeof input.explanation !== "string") {
|
|
@@ -92,9 +125,64 @@ export function parseUpdatePlanOperation(input) {
|
|
|
92
125
|
}
|
|
93
126
|
return hasExplanation ? { clear: true, explanation } : { clear: true };
|
|
94
127
|
}
|
|
128
|
+
if (hasUpdate) {
|
|
129
|
+
if (!isRecord(input.update))
|
|
130
|
+
throw new Error("Update must be an object.");
|
|
131
|
+
const unexpectedUpdateField = Object.keys(input.update).find((field) => field !== "id" && field !== "step" && field !== "status" && field !== "success_criteria");
|
|
132
|
+
if (unexpectedUpdateField !== undefined)
|
|
133
|
+
throw new Error(`Unexpected update field: ${unexpectedUpdateField}.`);
|
|
134
|
+
const { id, step, status } = input.update;
|
|
135
|
+
if (typeof id !== "string" || id.trim().length === 0)
|
|
136
|
+
throw new Error("Update id must be a non-empty string.");
|
|
137
|
+
if (step !== undefined && (typeof step !== "string" || step.trim().length === 0)) {
|
|
138
|
+
throw new Error("Update step must be a non-empty string.");
|
|
139
|
+
}
|
|
140
|
+
if (status !== undefined && !isUpdatePlanStatus(status)) {
|
|
141
|
+
throw new Error("Update status must be pending, in_progress, or completed.");
|
|
142
|
+
}
|
|
143
|
+
const hasCriteria = Object.hasOwn(input.update, "success_criteria");
|
|
144
|
+
if (step === undefined && status === undefined && !hasCriteria)
|
|
145
|
+
throw new Error("Update must change a step field.");
|
|
146
|
+
const update = {
|
|
147
|
+
id,
|
|
148
|
+
...(step === undefined ? {} : { step }),
|
|
149
|
+
...(status === undefined ? {} : { status }),
|
|
150
|
+
...(hasCriteria
|
|
151
|
+
? { success_criteria: validateCriteria(input.update.success_criteria, "Update success_criteria") }
|
|
152
|
+
: {}),
|
|
153
|
+
};
|
|
154
|
+
return hasExplanation ? { update, explanation } : { update };
|
|
155
|
+
}
|
|
95
156
|
const plan = validatePlanSnapshot(input.plan);
|
|
96
157
|
return hasExplanation ? { plan, explanation } : { plan };
|
|
97
158
|
}
|
|
159
|
+
export function resolveUpdatePlanOperation(input, currentPlan) {
|
|
160
|
+
const operation = parseUpdatePlanOperation(input);
|
|
161
|
+
if (!("update" in operation))
|
|
162
|
+
return operation;
|
|
163
|
+
if (!currentPlan)
|
|
164
|
+
throw new Error("Cannot update a step without a current plan.");
|
|
165
|
+
const index = currentPlan.findIndex((item) => item.id === operation.update.id);
|
|
166
|
+
if (index < 0)
|
|
167
|
+
throw new Error(`Unknown plan step id: ${operation.update.id}.`);
|
|
168
|
+
const plan = currentPlan.map((item, itemIndex) => {
|
|
169
|
+
if (itemIndex !== index)
|
|
170
|
+
return item;
|
|
171
|
+
const { success_criteria, ...changes } = operation.update;
|
|
172
|
+
const updated = { ...item, ...changes };
|
|
173
|
+
if (success_criteria !== undefined) {
|
|
174
|
+
if (success_criteria.length === 0)
|
|
175
|
+
delete updated.success_criteria;
|
|
176
|
+
else
|
|
177
|
+
updated.success_criteria = success_criteria;
|
|
178
|
+
}
|
|
179
|
+
return updated;
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
plan: validatePlanSnapshot(plan),
|
|
183
|
+
...(operation.explanation === undefined ? {} : { explanation: operation.explanation }),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
98
186
|
function countPlanStatuses(plan) {
|
|
99
187
|
let completed = 0;
|
|
100
188
|
let inProgress = 0;
|
|
@@ -151,27 +239,43 @@ function formatPlanCall(args, theme, showInvalidArgs) {
|
|
|
151
239
|
if ("clear" in details) {
|
|
152
240
|
return `${text}\n${theme.fg("toolOutput", "Plan cleared.")}`;
|
|
153
241
|
}
|
|
242
|
+
if ("update" in details) {
|
|
243
|
+
text += `\n${theme.fg("toolOutput", `Update ${details.update.id}`)}`;
|
|
244
|
+
if (details.update.status)
|
|
245
|
+
text += ` ${formatPlanMarker(details.update.status, theme)}`;
|
|
246
|
+
if (details.update.step)
|
|
247
|
+
text += ` ${formatPlanStep(details.update.step, theme)}`;
|
|
248
|
+
if (details.update.success_criteria) {
|
|
249
|
+
for (const criterion of details.update.success_criteria)
|
|
250
|
+
text += `\n ${theme.fg("toolOutput", `- ${criterion}`)}`;
|
|
251
|
+
}
|
|
252
|
+
return text;
|
|
253
|
+
}
|
|
154
254
|
for (const item of details.plan) {
|
|
155
|
-
text += `\n${formatPlanMarker(item.status, theme)} ${formatPlanStep(item.step, theme)}`;
|
|
255
|
+
text += `\n${formatPlanMarker(item.status, theme)} ${theme.fg("toolOutput", item.id)} ${formatPlanStep(item.step, theme)}`;
|
|
256
|
+
for (const criterion of item.success_criteria ?? [])
|
|
257
|
+
text += `\n ${theme.fg("toolOutput", `- ${criterion}`)}`;
|
|
156
258
|
}
|
|
157
259
|
return text;
|
|
158
260
|
}
|
|
159
|
-
export function createUpdatePlanToolDefinition() {
|
|
261
|
+
export function createUpdatePlanToolDefinition(_cwd, getCurrentPlan) {
|
|
160
262
|
return {
|
|
161
263
|
name: "update_plan",
|
|
162
264
|
label: "update_plan",
|
|
163
|
-
description: "
|
|
265
|
+
description: "Write or clear a plan, or update one step by ID. Use optional success_criteria for validation.",
|
|
164
266
|
promptSnippet: updatePlanToolSystemPromptContribution.snippet,
|
|
165
267
|
promptGuidelines: [...updatePlanToolSystemPromptContribution.guidelines],
|
|
166
268
|
parameters: updatePlanSchema,
|
|
167
269
|
executionMode: "sequential",
|
|
168
270
|
async execute(_toolCallId, input) {
|
|
169
|
-
const details =
|
|
271
|
+
const details = resolveUpdatePlanOperation(input, getCurrentPlan?.());
|
|
170
272
|
return {
|
|
171
273
|
content: [
|
|
172
274
|
{
|
|
173
275
|
type: "text",
|
|
174
|
-
text: "clear" in details
|
|
276
|
+
text: "clear" in details
|
|
277
|
+
? "Plan cleared."
|
|
278
|
+
: `${formatPlanSummary(details.plan)}\n${JSON.stringify({ plan: details.plan }, null, 2)}`,
|
|
175
279
|
},
|
|
176
280
|
],
|
|
177
281
|
details,
|
|
@@ -188,6 +292,6 @@ export function createUpdatePlanToolDefinition() {
|
|
|
188
292
|
},
|
|
189
293
|
};
|
|
190
294
|
}
|
|
191
|
-
export function createUpdatePlanTool() {
|
|
192
|
-
return wrapToolDefinition(createUpdatePlanToolDefinition());
|
|
295
|
+
export function createUpdatePlanTool(getCurrentPlan) {
|
|
296
|
+
return wrapToolDefinition(createUpdatePlanToolDefinition(undefined, getCurrentPlan));
|
|
193
297
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -33,4 +33,5 @@ export { copyToClipboard } from "./utils/clipboard.ts";
|
|
|
33
33
|
export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.ts";
|
|
34
34
|
export { convertToPng } from "./utils/image-convert.ts";
|
|
35
35
|
export { formatDimensionNote, type ResizedImage, resizeImage } from "./utils/image-resize.ts";
|
|
36
|
+
export type { BackgroundProcessPolicy, ShellOwnershipRecord } from "./utils/owned-shell-processes.ts";
|
|
36
37
|
export { getShellConfig } from "./utils/shell.ts";
|
package/dist/main.js
CHANGED
|
@@ -439,6 +439,9 @@ export async function main(args, options) {
|
|
|
439
439
|
: undefined;
|
|
440
440
|
const trustPromptMode = parsed.help || parsed.listModels !== undefined ? "print" : appMode;
|
|
441
441
|
const projectTrustByCwd = new Map();
|
|
442
|
+
const backgroundProcessPolicy = parsed.backgroundProcessLifetime === "environment"
|
|
443
|
+
? { lifetime: "environment", ownershipDirectory: resolvePath(parsed.backgroundProcessOwnershipDir, cwd) }
|
|
444
|
+
: { lifetime: "session" };
|
|
442
445
|
const resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);
|
|
443
446
|
const resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);
|
|
444
447
|
const resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);
|
|
@@ -530,6 +533,7 @@ export async function main(args, options) {
|
|
|
530
533
|
}
|
|
531
534
|
}
|
|
532
535
|
const created = await createAgentSessionFromServices({
|
|
536
|
+
backgroundProcessPolicy,
|
|
533
537
|
services,
|
|
534
538
|
sessionManager,
|
|
535
539
|
sessionStartEvent,
|
|
@@ -131,6 +131,11 @@ export class FooterComponent {
|
|
|
131
131
|
const contextFull = theme.fg("footerContext", `context ${contextPercent}%`) +
|
|
132
132
|
theme.fg("borderMuted", " / ") +
|
|
133
133
|
theme.fg("footerWindow", windowDisplay);
|
|
134
|
+
const { input, cacheRead, cacheWrite } = this.session.getSessionStats().tokens;
|
|
135
|
+
const promptTokens = input + cacheRead + cacheWrite;
|
|
136
|
+
const cacheHitPercent = promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)
|
|
137
|
+
? `${((cacheRead / promptTokens) * 100).toFixed(1)}%`
|
|
138
|
+
: undefined;
|
|
134
139
|
const planMode = getCollaborationMode(this.session) === "plan";
|
|
135
140
|
const planFull = theme.fg("warning", "Plan mode");
|
|
136
141
|
const planCompact = theme.fg("warning", "Plan");
|
|
@@ -176,6 +181,9 @@ export class FooterComponent {
|
|
|
176
181
|
};
|
|
177
182
|
const tryAddField = (field) => trySetFields([...fields, field]);
|
|
178
183
|
const tryUpgradeField = (key, text) => trySetFields(fields.map((field) => (field.key === key ? { ...field, text } : field)));
|
|
184
|
+
if (cacheHitPercent !== undefined) {
|
|
185
|
+
tryAddField({ key: "cache", order: 3, text: theme.fg("dim", `cache ${cacheHitPercent}`) });
|
|
186
|
+
}
|
|
179
187
|
// Extension statuses are public UI state, so admit them before optional
|
|
180
188
|
// detail upgrades and shortcut hints. Truncate only their own field.
|
|
181
189
|
const extensionStatuses = this.footerData.getExtensionStatuses();
|
|
@@ -189,7 +197,7 @@ export class FooterComponent {
|
|
|
189
197
|
if (statusText.length > 0 && availableWidth > 0) {
|
|
190
198
|
tryAddField({
|
|
191
199
|
key: "extensions",
|
|
192
|
-
order:
|
|
200
|
+
order: 5,
|
|
193
201
|
text: truncateToWidth(theme.fg("dim", statusText), availableWidth, glyphs.ellipsis),
|
|
194
202
|
});
|
|
195
203
|
}
|
|
@@ -197,14 +205,17 @@ export class FooterComponent {
|
|
|
197
205
|
tryAddField({ key: "thinking", order: 1, text: theme.fg("thinkingText", thinkingLevel) });
|
|
198
206
|
tryAddField({
|
|
199
207
|
key: "harness",
|
|
200
|
-
order:
|
|
208
|
+
order: 4,
|
|
201
209
|
text: theme.fg("dim", `h:${harnessVersion}`) + evolutionSpinner,
|
|
202
210
|
});
|
|
203
211
|
tryUpgradeField("context", contextMedium);
|
|
204
212
|
tryUpgradeField("thinking", theme.fg("thinkingText", `thinking ${thinkingLevel}`));
|
|
213
|
+
if (cacheHitPercent !== undefined) {
|
|
214
|
+
tryUpgradeField("cache", theme.fg("dim", `input cache hit ${cacheHitPercent}`));
|
|
215
|
+
}
|
|
205
216
|
tryUpgradeField("harness", theme.fg("dim", `harness ${harnessVersion}`) + evolutionSpinner);
|
|
206
217
|
tryUpgradeField("context", contextFull);
|
|
207
|
-
tryAddField({ key: "hotkeys", order:
|
|
218
|
+
tryAddField({ key: "hotkeys", order: 8, text: theme.fg("dim", "/hotkeys") });
|
|
208
219
|
const getBindingHint = (action, label) => {
|
|
209
220
|
try {
|
|
210
221
|
const binding = keyText(action);
|
|
@@ -216,11 +227,11 @@ export class FooterComponent {
|
|
|
216
227
|
};
|
|
217
228
|
const thinkingHint = getBindingHint("app.thinking.cycle", "thinking");
|
|
218
229
|
if (thinkingHint) {
|
|
219
|
-
tryAddField({ key: "thinkingHint", order:
|
|
230
|
+
tryAddField({ key: "thinkingHint", order: 6, text: thinkingHint });
|
|
220
231
|
}
|
|
221
232
|
const modelHint = getBindingHint("app.model.cycleForward", "model");
|
|
222
233
|
if (modelHint) {
|
|
223
|
-
tryAddField({ key: "modelHint", order:
|
|
234
|
+
tryAddField({ key: "modelHint", order: 7, text: modelHint });
|
|
224
235
|
}
|
|
225
236
|
return [renderFields(fields)];
|
|
226
237
|
}
|
|
@@ -7,6 +7,7 @@ export declare class ToolGroupComponent implements Component {
|
|
|
7
7
|
private pathOrigin;
|
|
8
8
|
private reusesLeadingGap;
|
|
9
9
|
private settled;
|
|
10
|
+
private cachedRender?;
|
|
10
11
|
constructor(outputPad?: number, pathOrigin?: DiffexPathOrigin, options?: {
|
|
11
12
|
reuseLeadingGap?: boolean;
|
|
12
13
|
});
|