@diffexai/diffex 0.3.1 → 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 +28 -0
- package/dist/core/agent-session.d.ts +0 -5
- package/dist/core/agent-session.js +6 -61
- 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/session-manager.js +21 -1
- package/dist/core/tools/update-plan.d.ts +27 -6
- package/dist/core/tools/update-plan.js +122 -18
- 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/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/distribution-components.json +11 -11
- package/distribution-files.json +75 -67
- 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
|
@@ -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
|
}
|
|
@@ -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
|
});
|
|
@@ -7,6 +7,7 @@ export class ToolGroupComponent {
|
|
|
7
7
|
pathOrigin;
|
|
8
8
|
reusesLeadingGap;
|
|
9
9
|
settled = false;
|
|
10
|
+
cachedRender;
|
|
10
11
|
constructor(outputPad = 1, pathOrigin = "standalone", options = {}) {
|
|
11
12
|
this.outputPad = outputPad;
|
|
12
13
|
this.pathOrigin = pathOrigin;
|
|
@@ -18,13 +19,17 @@ export class ToolGroupComponent {
|
|
|
18
19
|
return existing;
|
|
19
20
|
}
|
|
20
21
|
this.tools.set(toolCallId, component);
|
|
22
|
+
this.cachedRender = undefined;
|
|
21
23
|
return component;
|
|
22
24
|
}
|
|
23
25
|
getTool(toolCallId) {
|
|
24
26
|
return this.tools.get(toolCallId);
|
|
25
27
|
}
|
|
26
28
|
removeTool(toolCallId) {
|
|
27
|
-
|
|
29
|
+
const removed = this.tools.delete(toolCallId);
|
|
30
|
+
if (removed)
|
|
31
|
+
this.cachedRender = undefined;
|
|
32
|
+
return removed;
|
|
28
33
|
}
|
|
29
34
|
replaceTools(tools) {
|
|
30
35
|
this.tools.clear();
|
|
@@ -32,6 +37,7 @@ export class ToolGroupComponent {
|
|
|
32
37
|
this.tools.delete(toolCallId);
|
|
33
38
|
this.tools.set(toolCallId, component);
|
|
34
39
|
}
|
|
40
|
+
this.cachedRender = undefined;
|
|
35
41
|
}
|
|
36
42
|
hasTools() {
|
|
37
43
|
return this.tools.size > 0;
|
|
@@ -42,34 +48,43 @@ export class ToolGroupComponent {
|
|
|
42
48
|
setPathOrigin(pathOrigin, options = {}) {
|
|
43
49
|
this.pathOrigin = pathOrigin;
|
|
44
50
|
this.reusesLeadingGap = pathOrigin === "standalone" && options.reuseLeadingGap === true;
|
|
51
|
+
this.cachedRender = undefined;
|
|
45
52
|
}
|
|
46
53
|
markSettled() {
|
|
47
54
|
this.settled = true;
|
|
55
|
+
this.cachedRender = undefined;
|
|
48
56
|
}
|
|
49
57
|
setOutputPad(padding) {
|
|
50
58
|
this.outputPad = padding;
|
|
59
|
+
this.cachedRender = undefined;
|
|
51
60
|
}
|
|
52
61
|
setExpanded(expanded) {
|
|
53
62
|
for (const tool of this.tools.values()) {
|
|
54
63
|
tool.setExpanded(expanded);
|
|
55
64
|
}
|
|
65
|
+
this.cachedRender = undefined;
|
|
56
66
|
}
|
|
57
67
|
setShowImages(show) {
|
|
58
68
|
for (const tool of this.tools.values()) {
|
|
59
69
|
tool.setShowImages(show);
|
|
60
70
|
}
|
|
71
|
+
this.cachedRender = undefined;
|
|
61
72
|
}
|
|
62
73
|
setImageWidthCells(width) {
|
|
63
74
|
for (const tool of this.tools.values()) {
|
|
64
75
|
tool.setImageWidthCells(width);
|
|
65
76
|
}
|
|
77
|
+
this.cachedRender = undefined;
|
|
66
78
|
}
|
|
67
79
|
invalidate() {
|
|
68
80
|
for (const tool of this.tools.values()) {
|
|
69
81
|
tool.invalidate();
|
|
70
82
|
}
|
|
83
|
+
this.cachedRender = undefined;
|
|
71
84
|
}
|
|
72
85
|
render(width) {
|
|
86
|
+
if (this.settled && this.cachedRender?.width === width)
|
|
87
|
+
return this.cachedRender.lines;
|
|
73
88
|
const safeWidth = Math.max(1, width);
|
|
74
89
|
const outputPad = Math.min(Math.max(0, this.outputPad), Math.max(0, safeWidth - 1));
|
|
75
90
|
const outerRailWidth = safeWidth - outputPad > 1 ? 1 : 0;
|
|
@@ -87,12 +102,14 @@ export class ToolGroupComponent {
|
|
|
87
102
|
}
|
|
88
103
|
}
|
|
89
104
|
if (renderedTools.length === 0) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
const lines = this.pathOrigin !== "continued"
|
|
106
|
+
? []
|
|
107
|
+
: this.settled
|
|
108
|
+
? new DiffexPathTerminus(outputPad).render(safeWidth)
|
|
109
|
+
: new DiffexPathRail(outputPad).render(safeWidth);
|
|
110
|
+
if (this.settled)
|
|
111
|
+
this.cachedRender = { width, lines };
|
|
112
|
+
return lines;
|
|
96
113
|
}
|
|
97
114
|
const label = this.settled ? "Explored" : "Exploring";
|
|
98
115
|
const continuesPath = this.pathOrigin === "continued";
|
|
@@ -118,6 +135,8 @@ export class ToolGroupComponent {
|
|
|
118
135
|
result.push(visibleWidth(decoratedLine) > safeWidth ? truncateToWidth(decoratedLine, safeWidth, "") : decoratedLine);
|
|
119
136
|
}
|
|
120
137
|
}
|
|
138
|
+
if (this.settled)
|
|
139
|
+
this.cachedRender = { width, lines: result };
|
|
121
140
|
return result;
|
|
122
141
|
}
|
|
123
142
|
}
|
|
@@ -460,11 +460,29 @@ async function failToolCallsFromTruncatedMessage(toolCalls, config, signal, emit
|
|
|
460
460
|
*/
|
|
461
461
|
async function executeToolCalls(currentContext, assistantMessage, config, signal, emit) {
|
|
462
462
|
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
463
|
-
|
|
464
|
-
if (config.toolExecution === "sequential" || hasSequentialToolCall) {
|
|
463
|
+
if (config.toolExecution === "sequential") {
|
|
465
464
|
return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
466
465
|
}
|
|
467
|
-
|
|
466
|
+
const messages = [];
|
|
467
|
+
let terminate = true;
|
|
468
|
+
for (let index = 0; index < toolCalls.length;) {
|
|
469
|
+
if (signal?.aborted)
|
|
470
|
+
break;
|
|
471
|
+
const sequential = currentContext.tools?.find((tool) => tool.name === toolCalls[index].name)?.executionMode === "sequential";
|
|
472
|
+
let end = index + 1;
|
|
473
|
+
if (!sequential) {
|
|
474
|
+
while (end < toolCalls.length &&
|
|
475
|
+
currentContext.tools?.find((tool) => tool.name === toolCalls[end].name)?.executionMode !== "sequential") {
|
|
476
|
+
end++;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const executePhase = sequential ? executeToolCallsSequential : executeToolCallsParallel;
|
|
480
|
+
const phase = await executePhase(currentContext, assistantMessage, toolCalls.slice(index, end), config, signal, emit);
|
|
481
|
+
messages.push(...phase.messages);
|
|
482
|
+
terminate = terminate && phase.terminate;
|
|
483
|
+
index = end;
|
|
484
|
+
}
|
|
485
|
+
return { messages, terminate: messages.length > 0 && terminate };
|
|
468
486
|
}
|
|
469
487
|
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
470
488
|
const finalizedCalls = [];
|
|
@@ -16,9 +16,11 @@ export type StreamFn = (model: Model<Api>, context: Context, options?: SimpleStr
|
|
|
16
16
|
* Configuration for how tool calls from a single assistant message are executed.
|
|
17
17
|
*
|
|
18
18
|
* - "sequential": each tool call is prepared, executed, and finalized before the next one starts.
|
|
19
|
-
* - "parallel":
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* - "parallel": consecutive parallel-capable calls form groups separated by individual
|
|
20
|
+
* tools declaring `executionMode: "sequential"`. Each group is prepared sequentially,
|
|
21
|
+
* then allowed tools execute concurrently. Completion events follow finalization order;
|
|
22
|
+
* tool-result message events follow assistant source order within each phase.
|
|
23
|
+
* Hooks and awaited events settle before the next phase is prepared.
|
|
22
24
|
*/
|
|
23
25
|
export type ToolExecutionMode = "sequential" | "parallel";
|
|
24
26
|
/**
|
|
@@ -237,9 +239,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
237
239
|
/**
|
|
238
240
|
* Tool execution mode.
|
|
239
241
|
* - "sequential": execute tool calls one by one
|
|
240
|
-
* - "parallel":
|
|
241
|
-
*
|
|
242
|
-
*
|
|
242
|
+
* - "parallel": execute consecutive parallel-capable calls concurrently, separated by
|
|
243
|
+
* sequential tool barriers. Preflight each phase sequentially and await execution,
|
|
244
|
+
* result hooks, completion events, and source-ordered result events before the next phase.
|
|
243
245
|
*
|
|
244
246
|
* Default: "parallel"
|
|
245
247
|
*/
|
|
@@ -250,6 +252,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
250
252
|
* Return `{ block: true }` to prevent execution. The loop emits an error tool result instead.
|
|
251
253
|
* A blocked result can also set `terminate: true` to participate in the batch early-termination rule.
|
|
252
254
|
* The hook receives the agent abort signal and is responsible for honoring it.
|
|
255
|
+
* In parallel mode, preflight runs sequentially within the current phase, after all
|
|
256
|
+
* prior phases have settled. Sibling results within that phase are not yet available.
|
|
253
257
|
*/
|
|
254
258
|
beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
|
|
255
259
|
/**
|
|
@@ -264,6 +268,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
264
268
|
*
|
|
265
269
|
* Any omitted fields keep their original values. No deep merge is performed.
|
|
266
270
|
* The hook receives the agent abort signal and is responsible for honoring it.
|
|
271
|
+
* Parallel siblings can finalize concurrently. All hooks and awaited result events
|
|
272
|
+
* in the current phase settle before preflight begins for the next phase.
|
|
267
273
|
*/
|
|
268
274
|
afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
|
|
269
275
|
}
|
|
@@ -362,11 +368,12 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
|
|
362
368
|
/** Execute the tool call. Throw on failure instead of encoding errors in `content`. */
|
|
363
369
|
execute: (toolCallId: string, params: Static<TParameters>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<TDetails>) => Promise<AgentToolResult<TDetails>>;
|
|
364
370
|
/**
|
|
365
|
-
* Per-tool
|
|
366
|
-
* - "sequential": this
|
|
367
|
-
* - "parallel": this
|
|
371
|
+
* Per-tool scheduling metadata under global parallel execution.
|
|
372
|
+
* - "sequential": this call is an exclusive barrier in assistant source order.
|
|
373
|
+
* - "parallel": this call can execute concurrently within its consecutive group.
|
|
368
374
|
*
|
|
369
|
-
*
|
|
375
|
+
* Omitted metadata is parallel-capable. Global sequential execution always runs
|
|
376
|
+
* calls one by one, including tools explicitly declaring "parallel".
|
|
370
377
|
*/
|
|
371
378
|
executionMode?: ToolExecutionMode;
|
|
372
379
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"name": "@diffexai/diffex-agent-core",
|
|
4
|
-
"version": "0.3.
|
|
5
|
-
"sourceCommit": "
|
|
4
|
+
"version": "0.3.2",
|
|
5
|
+
"sourceCommit": "57dbba8a810d2e4cc81e48a30a4f5736e0e2d5bf",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"portions": "Diffex contributions; Pi-derived portions retain MIT terms in LICENSE",
|
|
8
8
|
"notice": "LICENSE",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"name": "@diffexai/diffex-ai",
|
|
13
|
-
"version": "^0.3.
|
|
13
|
+
"version": "^0.3.2",
|
|
14
14
|
"distribution": "resolved separately by package manager; not bundled in this npm tarball"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"name": "@diffexai/diffex-telemetry",
|
|
18
|
-
"version": "^0.3.
|
|
18
|
+
"version": "^0.3.2",
|
|
19
19
|
"distribution": "resolved separately by package manager; not bundled in this npm tarball"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sourceCommit": "
|
|
2
|
+
"sourceCommit": "57dbba8a810d2e4cc81e48a30a4f5736e0e2d5bf",
|
|
3
3
|
"algorithm": "sha256",
|
|
4
4
|
"files": [
|
|
5
5
|
{
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"path": "dist/agent-loop.js",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "b1b846af2909c11fecd35a06cad1dd79c57a8c2ea8ea7240136d71169b4ecfa8"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"path": "dist/agent.d.ts",
|
|
@@ -380,7 +380,7 @@
|
|
|
380
380
|
},
|
|
381
381
|
{
|
|
382
382
|
"path": "dist/types.d.ts",
|
|
383
|
-
"sha256": "
|
|
383
|
+
"sha256": "f64317c4ce1d3ccb8284e6eed635b5540f9a27dfbea94ce709eeed18b4625b28"
|
|
384
384
|
},
|
|
385
385
|
{
|
|
386
386
|
"path": "dist/types.js",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":3,"generatedAt":"2026-09-
|
|
1
|
+
{"schemaVersion":3,"generatedAt":"2026-09-29T21:05:24.416Z","structureHash":"2fd45211a507dbf73974cbe675b576aeb2dfae490b569af9b3b90f9f601d7be5","files":{"amazon-bedrock.json":"1e0b4308b61f51c642616571d53efcca4aada249b20d220ca9068df7e5ea52fd","ant-ling.json":"4979fe79d99ed97382d7ce40170c6132e932e77906de1eb54464c042e7d63633","anthropic.json":"b2976a77dd2a3260d401918df7c523bd2124abae2382f7a67229829e2701d899","azure-openai-responses.json":"dfcc219adb7d04783861274cb94ac503b9a0d9f00a4e77c3637ebfc779ee5eb8","baseten.json":"c7cf9a7e7e6bee9c53005acf4706c25f954adaa64a9058535fb7e5b1d6920852","cerebras.json":"3be228c613905c215c9da3d0b3535e10cc594ff2351b05390744c7c34de89b6c","cloudflare-ai-gateway.json":"fac11bff507133190fcb7cb216656a8ad1b8d3b8c37c1b523a6d85f931b0cbda","cloudflare-workers-ai.json":"6012eb769382a5a9fa96ec5f429f6744848e377327c9063573a15006ef0652ce","deepseek.json":"0dcc807a4e5827b488c6ceac87884ff6e735e01cf4f2ddfec9dd812e6fde041b","fireworks.json":"f3e423850d1509690c0d8cfd4071bdb822f01a05e6d691032c309476edc8a927","github-copilot.json":"418a6da7c705b9043ffecc21597cb7d79bf80b6fb117f95a5a7675b68a032539","google-vertex.json":"97d80ac909d256cc1bf19ac54d9d8131c492e7c0c102ad89f68ade528544e0ad","google.json":"68f6cf6283cc0d69b77b0fa79e9c94774e96524f61ebba616319f05d5c9db56c","groq.json":"e4728fd72e358cd8f25e9a518e9b49a521ef44523d3debe38a7db9e0663c4077","huggingface.json":"2f93082144a7b2a1cbf79148244ce7559e0428d6310c5eb0648bca89b15a8b4b","kimi-coding.json":"9da7ea8e9eb8df5d8f73acc25d3da5b41164f522c0f09b45265b4bf104743ac1","minimax-cn.json":"4f636b47b28a507ad33b5eaba33fc4889e5956504d10f2a0d9c61167ae01ff1d","minimax.json":"9fd1764ffeefebded8579039cf0c48854a43e4c5b724f62361fc2ac54f705aa1","mistral.json":"9da97430d9b86ad7f606b0fc088694e7a04c38bfa8963a33e1205b3141c917eb","moonshotai-cn.json":"c1d6024d0095bb34e33847a3281692e8ddb7ddbb61748a0373cdab45f414d264","moonshotai.json":"08b8a7bbb30aa3a5ed077f02bd59ea5e9d32a1a8257e56d1606be8d722091783","nvidia.json":"6493e09b5db8b8fe9c8fae5bc15dc287645f37f83a4ed3a1ad1181e5830b5ff7","openai-codex.json":"4a73818291987693fcb53e9e61b4be3429ffd78b3f64ea877a34c5f9928c89e1","openai.json":"d8107710ade17d9c177b677135bf1ae88d6b884ee765cd64b74afe501f1f0844","opencode-go.json":"af32bbce67a5b2f705c43cd47e301e9fd4548bd3af03daaebb4b63bca864f9bc","opencode.json":"b3b709db64eb71505560758e36d287458280ce3d361d0d87ab5d616d6d6cf8a3","openrouter.json":"21dbdf7e09616dd36bfbf2121801cee9b8f68b5462e69c009bf1f665a2ba4002","qwen-token-plan-cn.json":"a50420854a624ea82155d9621cd88d6cbd7802498d28fb1fd317eb7a1bc26cc7","qwen-token-plan-individual.json":"0e1578a5c0e11d65644bb42d1d6efee5af97a55a4881d0421ab68bd970d4d4ef","qwen-token-plan.json":"24285f68b2350aa1d98ecaeafe0914bce97940fe8798eff20f2a5c6d8c26bd6f","together.json":"b4971d293e5be6b9acaf7b5d9aa588e6374e5317c116e98437128197c39a2fac","vercel-ai-gateway.json":"79ffb1b0c83cec8d26472ddc519ad6184eeb9cb1c960fd02c7dbf1473dcc5871","xai.json":"d036f5d1ececd7280c09ca1a6249553f39a7a1087ddb129a0d689a427fc944c4","xiaomi-token-plan-ams.json":"9581074003df2c31dbe483221dbbd1891e92d05102fcb816d51cbb22542174cf","xiaomi-token-plan-cn.json":"df9f18cca62ed29398124b3c9f50b157eea83c32167579a40ac129a3a673e4d6","xiaomi-token-plan-sgp.json":"5681a7eed774bc4b4e296cdae1a5028176b856b48af96cbd7776a03ba905aab1","xiaomi.json":"506eef7c189328cabce83768a48ec3e59c3f8a575f840d8ffcefa6966c340562","zai-coding-cn.json":"abf4cbc22507b9d545842072b5e34bce230025fc20f7b1cc67db3782c1dc4b56","zai.json":"089af736a622b0a25e417493819f4e174884afba43235c5a1f17814057875627"}}
|