@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.2] - 2026-09-29
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- Added persistent plan context with stable step IDs and optional success criteria, attached to every model request and restored across session resume, compaction, and tree navigation.
|
|
8
|
+
See [Plan Mode](docs/usage.md#plan-mode) ([#249](https://github.com/diffexai/diffex/pull/249)).
|
|
9
|
+
- Added parallel tool execution phases that overlap independent calls around sequential tool barriers.
|
|
10
|
+
See [Extensions](docs/extensions.md) ([#250](https://github.com/diffexai/diffex/pull/250)).
|
|
11
|
+
- Added a cache hit rate to the interactive footer alongside context usage ([#249](https://github.com/diffexai/diffex/pull/249)).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added persistent plan step identity with stable IDs and optional success criteria, attached as hidden current-task context to every model request and restored across session resume, compaction, and tree navigation.
|
|
16
|
+
See [Plan Mode](docs/usage.md#plan-mode) and [SDK tools](docs/sdk.md#tools) ([#249](https://github.com/diffexai/diffex/pull/249)).
|
|
17
|
+
- Added a cache hit rate to the interactive footer alongside context usage ([#249](https://github.com/diffexai/diffex/pull/249)).
|
|
18
|
+
- Added parallel tool execution phases that overlap consecutive parallel-capable calls around individual sequential barriers, such as `update_plan`.
|
|
19
|
+
See [Extensions](docs/extensions.md#tool_execution_start--tool_execution_update--tool_execution_end) ([#250](https://github.com/diffexai/diffex/pull/250)).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Excluded plan tool history and saved plan snapshots from compaction and branch summaries; the latest attached snapshot supersedes retained plan context ([#251](https://github.com/diffexai/diffex/pull/251)).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Fixed plan cache-prefix preservation so ordinary requests reuse the existing prompt prefix ([#249](https://github.com/diffexai/diffex/pull/249)).
|
|
28
|
+
- Fixed plan restoration on tree navigation to use the destination branch's authoritative plan snapshot ([#249](https://github.com/diffexai/diffex/pull/249)).
|
|
29
|
+
- Fixed settled tool output being remeasured on every keystroke, keeping typing responsive as session history grows ([#249](https://github.com/diffexai/diffex/pull/249)).
|
|
30
|
+
|
|
3
31
|
## [0.3.1] - 2026-09-20
|
|
4
32
|
|
|
5
33
|
### New Features
|
|
@@ -253,7 +253,6 @@ export declare class AgentSession {
|
|
|
253
253
|
private _followUpMessages;
|
|
254
254
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
255
255
|
private _pendingNextTurnMessages;
|
|
256
|
-
private _pendingCurrentPlanRestoration;
|
|
257
256
|
private readonly _skillReferenceSnapshots;
|
|
258
257
|
private readonly _skillEnablementOverrides;
|
|
259
258
|
private _activeSkillTrace;
|
|
@@ -337,10 +336,6 @@ export declare class AgentSession {
|
|
|
337
336
|
*/
|
|
338
337
|
private _installAgentToolHooks;
|
|
339
338
|
private _installAgentNextTurnRefresh;
|
|
340
|
-
private _installAgentContextRestoration;
|
|
341
|
-
private _armInitialCurrentPlanRestoration;
|
|
342
|
-
private _armCurrentPlanRestorationIfActive;
|
|
343
|
-
private _consumePendingCurrentPlanRestoration;
|
|
344
339
|
/** Emit an event to all listeners */
|
|
345
340
|
private _emit;
|
|
346
341
|
private _emitQueueUpdate;
|
|
@@ -26,7 +26,7 @@ import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth
|
|
|
26
26
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
27
27
|
import { CHANGE_COLLABORATION_MODE, COLLABORATION_MODE_CUSTOM_TYPE, GET_COLLABORATION_MODE, IS_COLLABORATION_MODE_BUSY, PLAN_MODE_SYSTEM_PROMPT, PLAN_MODE_TOOL_NAMES, planHandoffSettlementGuards, SUBSCRIBE_PROMPT_ADMISSION, } from "./collaboration-mode.js";
|
|
28
28
|
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
29
|
-
import { createSessionUpdatePlanToolDefinition,
|
|
29
|
+
import { createSessionUpdatePlanToolDefinition, formatCurrentPlanSnapshot } from "./current-plan-state.js";
|
|
30
30
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
31
31
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
32
32
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
@@ -109,8 +109,6 @@ function estimateMessagesTokens(messages) {
|
|
|
109
109
|
// ============================================================================
|
|
110
110
|
/** Standard thinking levels */
|
|
111
111
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
112
|
-
const CURRENT_PLAN_RESTORATION_CUSTOM_TYPE = "diffex.current-plan-restoration";
|
|
113
|
-
const CURRENT_PLAN_RESTORATION_REMINDER = "Reminder: restored context is not proof of completion. Verify current state before claiming work is done.";
|
|
114
112
|
const MAX_PENDING_SUBAGENT_ESCALATION_CHARACTERS = 8_000;
|
|
115
113
|
const INCOMPATIBLE_SUBAGENT_PARENT_HOOKS = [
|
|
116
114
|
"resources_discover",
|
|
@@ -145,7 +143,6 @@ export class AgentSession {
|
|
|
145
143
|
_followUpMessages = [];
|
|
146
144
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
147
145
|
_pendingNextTurnMessages = [];
|
|
148
|
-
_pendingCurrentPlanRestoration = false;
|
|
149
146
|
_skillReferenceSnapshots = new Map();
|
|
150
147
|
_skillEnablementOverrides = new Map();
|
|
151
148
|
_activeSkillTrace;
|
|
@@ -245,12 +242,10 @@ export class AgentSession {
|
|
|
245
242
|
: createEvolvedSkillCatalog(config.harnessVersionBundle, config.workspaceIdentity));
|
|
246
243
|
this._telemetryContext = config.telemetryContext ?? NOOP_TELEMETRY_CONTEXT;
|
|
247
244
|
this._lifecycleObserver = config.lifecycleObserver;
|
|
248
|
-
this._armInitialCurrentPlanRestoration();
|
|
249
245
|
// Always subscribe to agent events for internal handling
|
|
250
246
|
// (session persistence, extensions, auto-compaction, retry logic)
|
|
251
247
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
252
248
|
this._installAgentToolHooks();
|
|
253
|
-
this._installAgentContextRestoration();
|
|
254
249
|
this._installAgentNextTurnRefresh();
|
|
255
250
|
this._buildRuntime({
|
|
256
251
|
activeToolNames: this._initialActiveToolNames,
|
|
@@ -434,55 +429,6 @@ export class AgentSession {
|
|
|
434
429
|
};
|
|
435
430
|
};
|
|
436
431
|
}
|
|
437
|
-
_installAgentContextRestoration() {
|
|
438
|
-
const previousTransformContext = this.agent.transformContext;
|
|
439
|
-
this.agent.transformContext = async (messages, signal) => {
|
|
440
|
-
const transformedMessages = previousTransformContext
|
|
441
|
-
? await previousTransformContext(messages, signal)
|
|
442
|
-
: messages;
|
|
443
|
-
return this._consumePendingCurrentPlanRestoration(transformedMessages);
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
_armInitialCurrentPlanRestoration() {
|
|
447
|
-
if (this._sessionStartEvent.reason !== "startup" && this._sessionStartEvent.reason !== "resume") {
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
this._armCurrentPlanRestorationIfActive();
|
|
451
|
-
}
|
|
452
|
-
_armCurrentPlanRestorationIfActive() {
|
|
453
|
-
if (resolveCurrentPlanState(this.sessionManager).status === "active") {
|
|
454
|
-
this._pendingCurrentPlanRestoration = true;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
_consumePendingCurrentPlanRestoration(messages) {
|
|
458
|
-
if (!this._pendingCurrentPlanRestoration) {
|
|
459
|
-
return messages;
|
|
460
|
-
}
|
|
461
|
-
const state = resolveCurrentPlanState(this.sessionManager);
|
|
462
|
-
this._pendingCurrentPlanRestoration = false;
|
|
463
|
-
if (state.status !== "active") {
|
|
464
|
-
return messages;
|
|
465
|
-
}
|
|
466
|
-
const lines = [
|
|
467
|
-
"Restored unfinished update_plan checklist from the current session. Use this hidden context to continue the work.",
|
|
468
|
-
];
|
|
469
|
-
if (state.operation.explanation !== undefined) {
|
|
470
|
-
lines.push("", "Explanation:", state.operation.explanation);
|
|
471
|
-
}
|
|
472
|
-
lines.push("", "Checklist:");
|
|
473
|
-
for (const [index, item] of state.operation.plan.entries()) {
|
|
474
|
-
lines.push(`${index + 1}. [${item.status}] ${item.step}`);
|
|
475
|
-
}
|
|
476
|
-
lines.push("", CURRENT_PLAN_RESTORATION_REMINDER);
|
|
477
|
-
const restoredContext = {
|
|
478
|
-
role: "custom",
|
|
479
|
-
customType: CURRENT_PLAN_RESTORATION_CUSTOM_TYPE,
|
|
480
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
481
|
-
display: false,
|
|
482
|
-
timestamp: Date.now(),
|
|
483
|
-
};
|
|
484
|
-
return [...messages, restoredContext];
|
|
485
|
-
}
|
|
486
432
|
// =========================================================================
|
|
487
433
|
// Event Subscription
|
|
488
434
|
// =========================================================================
|
|
@@ -1809,7 +1755,6 @@ export class AgentSession {
|
|
|
1809
1755
|
return;
|
|
1810
1756
|
pendingCompaction.commit();
|
|
1811
1757
|
pendingCompaction = undefined;
|
|
1812
|
-
this._armCurrentPlanRestorationIfActive();
|
|
1813
1758
|
};
|
|
1814
1759
|
const rollbackPendingCompaction = () => {
|
|
1815
1760
|
if (!pendingCompaction)
|
|
@@ -2729,7 +2674,8 @@ export class AgentSession {
|
|
|
2729
2674
|
this._branchSummaryAbortController?.abort();
|
|
2730
2675
|
}
|
|
2731
2676
|
async _recordSuccessfulCompaction(options) {
|
|
2732
|
-
const
|
|
2677
|
+
const summary = `${options.summary}\n\n${formatCurrentPlanSnapshot(this.sessionManager)}`;
|
|
2678
|
+
const transaction = appendCompactionTransaction(this.sessionManager, summary, options.firstKeptEntryId, options.tokensBefore, options.details, options.fromExtension, options.usage);
|
|
2733
2679
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2734
2680
|
this.agent.state.messages = sessionContext.messages;
|
|
2735
2681
|
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
@@ -2763,15 +2709,13 @@ export class AgentSession {
|
|
|
2763
2709
|
else
|
|
2764
2710
|
transaction.commit();
|
|
2765
2711
|
const compactionResult = {
|
|
2766
|
-
summary
|
|
2712
|
+
summary,
|
|
2767
2713
|
firstKeptEntryId: options.firstKeptEntryId,
|
|
2768
2714
|
tokensBefore: options.tokensBefore,
|
|
2769
2715
|
estimatedTokensAfter,
|
|
2770
2716
|
usage: options.usage,
|
|
2771
2717
|
details: options.details,
|
|
2772
2718
|
};
|
|
2773
|
-
if (!options.deferCommit)
|
|
2774
|
-
this._armCurrentPlanRestorationIfActive();
|
|
2775
2719
|
return compactionResult;
|
|
2776
2720
|
}
|
|
2777
2721
|
/**
|
|
@@ -3759,7 +3703,8 @@ export class AgentSession {
|
|
|
3759
3703
|
let summaryEntry;
|
|
3760
3704
|
const mutateLeaf = () => {
|
|
3761
3705
|
if (summaryText) {
|
|
3762
|
-
const
|
|
3706
|
+
const summary = `${summaryText}\n\nThe plan below is authoritative for the selected conversation branch. Plan references in the exploration summary above are historical.\n${formatCurrentPlanSnapshot(this.sessionManager, newLeafId)}`;
|
|
3707
|
+
const summaryId = this.sessionManager.branchWithSummary(newLeafId, summary, summaryDetails, fromExtension, summaryUsage);
|
|
3763
3708
|
summaryEntry = this.sessionManager.getEntry(summaryId);
|
|
3764
3709
|
if (label)
|
|
3765
3710
|
this.sessionManager.appendLabelChange(summaryId, label);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { contentText } from "@diffexai/diffex-ai";
|
|
8
8
|
import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
9
9
|
import { completeSummarization, estimateTokens } from "./compaction.js";
|
|
10
|
-
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
|
10
|
+
import { computeFileLists, createFileOps, excludePlanItems, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Entry Collection
|
|
13
13
|
// ============================================================================
|
|
@@ -120,7 +120,8 @@ export function prepareBranchEntries(entries, tokenBudget = 0) {
|
|
|
120
120
|
// Second pass: walk from newest to oldest, adding messages until token budget
|
|
121
121
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
122
122
|
const entry = entries[i];
|
|
123
|
-
const
|
|
123
|
+
const rawMessage = getMessageFromEntry(entry);
|
|
124
|
+
const message = rawMessage ? excludePlanItems([rawMessage])[0] : undefined;
|
|
124
125
|
if (!message)
|
|
125
126
|
continue;
|
|
126
127
|
// Extract file ops from assistant messages (tool calls)
|
|
@@ -8,8 +8,9 @@ import { NOOP_TELEMETRY_CONTEXT, startAiSpan, } from "@diffexai/diffex-agent-cor
|
|
|
8
8
|
import { contentText, retryAssistantCall, uuidv7 } from "@diffexai/diffex-ai";
|
|
9
9
|
import { completeSimple } from "@diffexai/diffex-ai/compat";
|
|
10
10
|
import { convertToLlm } from "../messages.js";
|
|
11
|
+
import { stripCurrentPlanSnapshot } from "../plan-snapshot.js";
|
|
11
12
|
import { buildSessionContext, sessionEntryToContextMessages, } from "../session-manager.js";
|
|
12
|
-
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
|
13
|
+
import { computeFileLists, createFileOps, excludePlanItems, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
|
13
14
|
/**
|
|
14
15
|
* Extract file operations from messages and previous compaction entries.
|
|
15
16
|
*/
|
|
@@ -494,13 +495,14 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
|
|
|
494
495
|
export async function generateSummaryWithUsage(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks, telemetryContext) {
|
|
495
496
|
const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
|
|
496
497
|
// Use update prompt if we have a previous summary, otherwise initial prompt
|
|
498
|
+
previousSummary = previousSummary === undefined ? undefined : stripCurrentPlanSnapshot(previousSummary);
|
|
497
499
|
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
|
498
500
|
if (customInstructions) {
|
|
499
501
|
basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`;
|
|
500
502
|
}
|
|
501
503
|
// Serialize conversation to text so model doesn't try to continue it
|
|
502
504
|
// Convert to LLM messages first (handles custom types like bashExecution, custom, etc.)
|
|
503
|
-
const llmMessages = convertToLlm(currentMessages);
|
|
505
|
+
const llmMessages = convertToLlm(excludePlanItems(currentMessages));
|
|
504
506
|
const conversationText = serializeConversation(llmMessages);
|
|
505
507
|
// Build the prompt with conversation wrapped in tags
|
|
506
508
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
@@ -538,7 +540,7 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
538
540
|
let boundaryStart = 0;
|
|
539
541
|
if (prevCompactionIndex >= 0) {
|
|
540
542
|
const prevCompaction = pathEntries[prevCompactionIndex];
|
|
541
|
-
previousSummary = prevCompaction.summary;
|
|
543
|
+
previousSummary = stripCurrentPlanSnapshot(prevCompaction.summary);
|
|
542
544
|
const firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);
|
|
543
545
|
boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
|
|
544
546
|
}
|
|
@@ -581,8 +583,8 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
581
583
|
}
|
|
582
584
|
return {
|
|
583
585
|
firstKeptEntryId,
|
|
584
|
-
messagesToSummarize,
|
|
585
|
-
turnPrefixMessages,
|
|
586
|
+
messagesToSummarize: excludePlanItems(messagesToSummarize),
|
|
587
|
+
turnPrefixMessages: excludePlanItems(turnPrefixMessages),
|
|
586
588
|
isSplitTurn: cutPoint.isSplitTurn,
|
|
587
589
|
tokensBefore,
|
|
588
590
|
previousSummary,
|
|
@@ -620,7 +622,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
|
|
|
620
622
|
let summary;
|
|
621
623
|
let summaryUsage;
|
|
622
624
|
if (isSplitTurn && turnPrefixMessages.length > 0) {
|
|
623
|
-
let historyText = "No prior history.";
|
|
625
|
+
let historyText = previousSummary ?? "No prior history.";
|
|
624
626
|
let historyUsage;
|
|
625
627
|
if (messagesToSummarize.length > 0) {
|
|
626
628
|
const historyResult = await generateSummaryWithUsage(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks, telemetryContext);
|
|
@@ -657,7 +659,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
|
|
|
657
659
|
*/
|
|
658
660
|
async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, env, signal, thinkingLevel, streamFn, retry, callbacks, telemetryContext) {
|
|
659
661
|
const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); // Smaller budget for turn prefix
|
|
660
|
-
const llmMessages = convertToLlm(messages);
|
|
662
|
+
const llmMessages = convertToLlm(excludePlanItems(messages));
|
|
661
663
|
const conversationText = serializeConversation(llmMessages);
|
|
662
664
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
663
665
|
const summarizationMessages = [
|
|
@@ -25,6 +25,8 @@ export declare function computeFileLists(fileOps: FileOperations): {
|
|
|
25
25
|
* Format file operations as XML tags for summary.
|
|
26
26
|
*/
|
|
27
27
|
export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string;
|
|
28
|
+
/** Exclude plan data from a context projection without changing persisted messages. */
|
|
29
|
+
export declare function excludePlanItems(messages: AgentMessage[]): AgentMessage[];
|
|
28
30
|
/**
|
|
29
31
|
* Serialize LLM messages to text for summarization.
|
|
30
32
|
* This prevents the model from treating it as a conversation to continue.
|
|
@@ -32,6 +34,7 @@ export declare function formatFileOperations(readFiles: string[], modifiedFiles:
|
|
|
32
34
|
*
|
|
33
35
|
* Tool results are truncated to keep the summarization request within
|
|
34
36
|
* reasonable token budgets. Full content is not needed for summarization.
|
|
37
|
+
* Plan calls and results are omitted because the latest plan is attached separately.
|
|
35
38
|
*/
|
|
36
39
|
export declare function serializeConversation(messages: Message[]): string;
|
|
37
40
|
export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Shared utilities for compaction and branch summarization.
|
|
3
3
|
*/
|
|
4
4
|
import { contentText } from "@diffexai/diffex-ai";
|
|
5
|
+
import { stripCurrentPlanSnapshot } from "../plan-snapshot.js";
|
|
5
6
|
export function createFileOps() {
|
|
6
7
|
return {
|
|
7
8
|
read: new Set(),
|
|
@@ -71,6 +72,21 @@ export function formatFileOperations(readFiles, modifiedFiles) {
|
|
|
71
72
|
// ============================================================================
|
|
72
73
|
// Message Serialization
|
|
73
74
|
// ============================================================================
|
|
75
|
+
/** Exclude plan data from a context projection without changing persisted messages. */
|
|
76
|
+
export function excludePlanItems(messages) {
|
|
77
|
+
return messages.flatMap((message) => {
|
|
78
|
+
if (message.role === "toolResult" && message.toolName === "update_plan")
|
|
79
|
+
return [];
|
|
80
|
+
if (message.role === "assistant") {
|
|
81
|
+
const content = message.content.filter((block) => block.type !== "toolCall" || block.name !== "update_plan");
|
|
82
|
+
return content.length > 0 ? [{ ...message, content }] : [];
|
|
83
|
+
}
|
|
84
|
+
if (message.role === "compactionSummary" || message.role === "branchSummary") {
|
|
85
|
+
return [{ ...message, summary: stripCurrentPlanSnapshot(message.summary) }];
|
|
86
|
+
}
|
|
87
|
+
return [message];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
74
90
|
/** Maximum characters for a tool result in serialized summaries. */
|
|
75
91
|
const TOOL_RESULT_MAX_CHARS = 2000;
|
|
76
92
|
/**
|
|
@@ -90,6 +106,7 @@ function truncateForSummary(text, maxChars) {
|
|
|
90
106
|
*
|
|
91
107
|
* Tool results are truncated to keep the summarization request within
|
|
92
108
|
* reasonable token budgets. Full content is not needed for summarization.
|
|
109
|
+
* Plan calls and results are omitted because the latest plan is attached separately.
|
|
93
110
|
*/
|
|
94
111
|
export function serializeConversation(messages) {
|
|
95
112
|
const parts = [];
|
|
@@ -107,6 +124,8 @@ export function serializeConversation(messages) {
|
|
|
107
124
|
thinkingParts.push(block.thinking);
|
|
108
125
|
}
|
|
109
126
|
else if (block.type === "toolCall") {
|
|
127
|
+
if (block.name === "update_plan")
|
|
128
|
+
continue;
|
|
110
129
|
const args = block.arguments;
|
|
111
130
|
const argsStr = Object.entries(args)
|
|
112
131
|
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
@@ -125,6 +144,8 @@ export function serializeConversation(messages) {
|
|
|
125
144
|
}
|
|
126
145
|
}
|
|
127
146
|
else if (msg.role === "toolResult") {
|
|
147
|
+
if (msg.toolName === "update_plan")
|
|
148
|
+
continue;
|
|
128
149
|
const content = contentText(msg.content, "");
|
|
129
150
|
if (content) {
|
|
130
151
|
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
|
|
@@ -15,5 +15,6 @@ export type CurrentPlanState = {
|
|
|
15
15
|
operation: UpdatePlanToolDetails;
|
|
16
16
|
};
|
|
17
17
|
export declare function appendCurrentPlanRecord(sessionManager: Pick<SessionManager, "appendCustomEntry">, operation: UpdatePlanToolDetails): string;
|
|
18
|
-
export declare function resolveCurrentPlanState(sessionManager: Pick<SessionManager, "getBranch"
|
|
19
|
-
export declare function
|
|
18
|
+
export declare function resolveCurrentPlanState(sessionManager: Pick<SessionManager, "getBranch">, fromId?: string | null): CurrentPlanState;
|
|
19
|
+
export declare function formatCurrentPlanSnapshot(sessionManager: Pick<SessionManager, "getBranch">, fromId?: string | null): string;
|
|
20
|
+
export declare function createSessionUpdatePlanToolDefinition(sessionManager: Pick<SessionManager, "appendCustomEntry" | "getBranch">): ReturnType<typeof createUpdatePlanToolDefinition>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CURRENT_PLAN_SNAPSHOT_PREFIX } from "./plan-snapshot.js";
|
|
2
|
+
import { createUpdatePlanToolDefinition, resolveUpdatePlanOperation, } from "./tools/update-plan.js";
|
|
2
3
|
export const CURRENT_PLAN_CUSTOM_ENTRY_TYPE = "diffex.current-plan";
|
|
3
4
|
export function appendCurrentPlanRecord(sessionManager, operation) {
|
|
4
|
-
return sessionManager.appendCustomEntry(CURRENT_PLAN_CUSTOM_ENTRY_TYPE,
|
|
5
|
+
return sessionManager.appendCustomEntry(CURRENT_PLAN_CUSTOM_ENTRY_TYPE, resolveUpdatePlanOperation(operation));
|
|
5
6
|
}
|
|
6
|
-
export function resolveCurrentPlanState(sessionManager) {
|
|
7
|
-
const branch = sessionManager.getBranch();
|
|
7
|
+
export function resolveCurrentPlanState(sessionManager, fromId) {
|
|
8
|
+
const branch = fromId === null ? [] : sessionManager.getBranch(fromId);
|
|
8
9
|
for (let index = branch.length - 1; index >= 0; index--) {
|
|
9
10
|
const entry = branch[index];
|
|
10
11
|
if (entry.type !== "custom" || entry.customType !== CURRENT_PLAN_CUSTOM_ENTRY_TYPE) {
|
|
@@ -12,7 +13,7 @@ export function resolveCurrentPlanState(sessionManager) {
|
|
|
12
13
|
}
|
|
13
14
|
let operation;
|
|
14
15
|
try {
|
|
15
|
-
operation =
|
|
16
|
+
operation = resolveUpdatePlanOperation(entry.data);
|
|
16
17
|
}
|
|
17
18
|
catch {
|
|
18
19
|
return { status: "corrupt" };
|
|
@@ -24,8 +25,24 @@ export function resolveCurrentPlanState(sessionManager) {
|
|
|
24
25
|
}
|
|
25
26
|
return { status: "absent" };
|
|
26
27
|
}
|
|
28
|
+
export function formatCurrentPlanSnapshot(sessionManager, fromId) {
|
|
29
|
+
const state = resolveCurrentPlanState(sessionManager, fromId);
|
|
30
|
+
const plan = state.status === "active"
|
|
31
|
+
? state.operation.plan
|
|
32
|
+
: state.status === "inactive" && "plan" in state.operation
|
|
33
|
+
? state.operation.plan
|
|
34
|
+
: [];
|
|
35
|
+
return `${CURRENT_PLAN_SNAPSHOT_PREFIX}${JSON.stringify({ plan }, null, 2)}`;
|
|
36
|
+
}
|
|
27
37
|
export function createSessionUpdatePlanToolDefinition(sessionManager) {
|
|
28
|
-
const definition = createUpdatePlanToolDefinition()
|
|
38
|
+
const definition = createUpdatePlanToolDefinition(undefined, () => {
|
|
39
|
+
const state = resolveCurrentPlanState(sessionManager);
|
|
40
|
+
if (state.status === "active")
|
|
41
|
+
return state.operation.plan;
|
|
42
|
+
if (state.status === "inactive" && "plan" in state.operation)
|
|
43
|
+
return state.operation.plan;
|
|
44
|
+
return undefined;
|
|
45
|
+
});
|
|
29
46
|
const execute = definition.execute;
|
|
30
47
|
return {
|
|
31
48
|
...definition,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const CURRENT_PLAN_SNAPSHOT_PREFIX = "Current task context (latest plan):\n";
|
|
2
|
+
// Branch navigation attaches this authority paragraph with the snapshot.
|
|
3
|
+
const BRANCH_PLAN_AUTHORITY = "The plan below is authoritative for the selected conversation branch. Plan references in the exploration summary above are historical.\n";
|
|
4
|
+
/** Remove only the structured plan snapshot appended to a summary. */
|
|
5
|
+
export function stripCurrentPlanSnapshot(summary) {
|
|
6
|
+
const index = summary.lastIndexOf(CURRENT_PLAN_SNAPSHOT_PREFIX);
|
|
7
|
+
const authorityStart = index - BRANCH_PLAN_AUTHORITY.length;
|
|
8
|
+
const hasBranchAuthority = authorityStart >= 0 && summary.slice(authorityStart, index) === BRANCH_PLAN_AUTHORITY;
|
|
9
|
+
const attachmentStart = hasBranchAuthority ? authorityStart : index;
|
|
10
|
+
if (index < 0 || (attachmentStart > 0 && summary.slice(attachmentStart - 2, attachmentStart) !== "\n\n")) {
|
|
11
|
+
return summary;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const snapshot = JSON.parse(summary.slice(index + CURRENT_PLAN_SNAPSHOT_PREFIX.length));
|
|
15
|
+
if (typeof snapshot !== "object" || snapshot === null || !("plan" in snapshot) || !Array.isArray(snapshot.plan)) {
|
|
16
|
+
return summary;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return summary;
|
|
21
|
+
}
|
|
22
|
+
return summary.slice(0, attachmentStart === 0 ? 0 : attachmentStart - 2);
|
|
23
|
+
}
|
|
@@ -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
|
/**
|
|
@@ -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 {};
|