@namzu/sdk 56.1.0 → 57.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +1 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/plan.d.ts +20 -0
- package/dist/agents/plan.d.ts.map +1 -0
- package/dist/agents/plan.js +27 -0
- package/dist/agents/plan.js.map +1 -0
- package/dist/compaction/context-breakdown.d.ts +1 -1
- package/dist/compaction/context-breakdown.d.ts.map +1 -1
- package/dist/compaction/context-breakdown.js +9 -2
- package/dist/compaction/context-breakdown.js.map +1 -1
- package/dist/compaction/dangling.d.ts +12 -1
- package/dist/compaction/dangling.d.ts.map +1 -1
- package/dist/compaction/dangling.js +17 -3
- package/dist/compaction/dangling.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -2
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/manager.d.ts +16 -0
- package/dist/compaction/manager.d.ts.map +1 -1
- package/dist/compaction/manager.js +55 -0
- package/dist/compaction/manager.js.map +1 -1
- package/dist/compaction/manual.d.ts +2 -0
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +33 -4
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/restore.d.ts +51 -0
- package/dist/compaction/restore.d.ts.map +1 -0
- package/dist/compaction/restore.js +123 -0
- package/dist/compaction/restore.js.map +1 -0
- package/dist/compaction/types.d.ts +22 -0
- package/dist/compaction/types.d.ts.map +1 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +3 -1
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/compaction/wire.d.ts +3 -1
- package/dist/compaction/wire.d.ts.map +1 -1
- package/dist/compaction/wire.js +16 -0
- package/dist/compaction/wire.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/plugin/shell-hook.d.ts +19 -1
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +21 -5
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +2 -2
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +3 -2
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/errors.d.ts +3 -0
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +23 -3
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +34 -3
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/retry.d.ts +12 -5
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +22 -8
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +42 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +77 -0
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/calls-that-never-started.d.ts +12 -0
- package/dist/runtime/query/calls-that-never-started.d.ts.map +1 -0
- package/dist/runtime/query/calls-that-never-started.js +41 -0
- package/dist/runtime/query/calls-that-never-started.js.map +1 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts +2 -2
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +13 -9
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +16 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +45 -2
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/file-evidence-context.d.ts +21 -4
- package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
- package/dist/runtime/query/file-evidence-context.js +70 -8
- package/dist/runtime/query/file-evidence-context.js.map +1 -1
- package/dist/runtime/query/index.d.ts +6 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +7 -11
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +1 -2
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +30 -19
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +20 -0
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/recorded-context.d.ts +18 -0
- package/dist/runtime/query/iteration/recorded-context.d.ts.map +1 -0
- package/dist/runtime/query/iteration/recorded-context.js +37 -0
- package/dist/runtime/query/iteration/recorded-context.js.map +1 -0
- package/dist/runtime/query/iteration/step-context.d.ts +15 -51
- package/dist/runtime/query/iteration/step-context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-context.js +24 -134
- package/dist/runtime/query/iteration/step-context.js.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.d.ts +20 -8
- package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.js +52 -26
- package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +45 -25
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +10 -1
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts +27 -0
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +90 -23
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/store/session-index/fts.d.ts.map +1 -1
- package/dist/store/session-index/fts.js +6 -0
- package/dist/store/session-index/fts.js.map +1 -1
- package/dist/test-support/file-evidence-held.d.ts +11 -0
- package/dist/test-support/file-evidence-held.d.ts.map +1 -0
- package/dist/test-support/file-evidence-held.js +28 -0
- package/dist/test-support/file-evidence-held.js.map +1 -0
- package/dist/test-support/request-only.d.ts +10 -0
- package/dist/test-support/request-only.d.ts.map +1 -0
- package/dist/test-support/request-only.js +15 -0
- package/dist/test-support/request-only.js.map +1 -0
- package/dist/tools/admit-facts.d.ts.map +1 -1
- package/dist/tools/admit-facts.js +1 -0
- package/dist/tools/admit-facts.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +42 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +115 -48
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +55 -5
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +28 -0
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/tools/untrusted-envelope.d.ts +7 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +3 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/turn/evidence-recall.d.ts +19 -8
- package/dist/turn/evidence-recall.d.ts.map +1 -1
- package/dist/turn/evidence-recall.js +134 -53
- package/dist/turn/evidence-recall.js.map +1 -1
- package/dist/turn/memory-recall.d.ts +18 -0
- package/dist/turn/memory-recall.d.ts.map +1 -1
- package/dist/turn/memory-recall.js +43 -4
- package/dist/turn/memory-recall.js.map +1 -1
- package/dist/types/agent/base.d.ts +6 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/errors/catalog.d.ts +3 -0
- package/dist/types/errors/catalog.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +24 -2
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/message/index.d.ts +46 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +78 -1
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/provider/error.d.ts +7 -0
- package/dist/types/provider/error.d.ts.map +1 -1
- package/dist/types/provider/errors.d.ts +24 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +101 -1
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +8 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/prepare-step.d.ts +24 -0
- package/dist/types/session/prepare-step.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +39 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +4 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/agents/index.ts +1 -0
- package/src/agents/plan.ts +30 -0
- package/src/compaction/context-breakdown.ts +11 -3
- package/src/compaction/dangling.ts +35 -3
- package/src/compaction/index.ts +4 -0
- package/src/compaction/manager.ts +53 -0
- package/src/compaction/manual.ts +42 -10
- package/src/compaction/restore.ts +159 -0
- package/src/compaction/types.ts +25 -0
- package/src/compaction/verifier.ts +3 -1
- package/src/compaction/wire.ts +26 -1
- package/src/index.ts +2 -0
- package/src/plugin/shell-hook.ts +33 -5
- package/src/prompt/coding-agent-doctrine.ts +3 -2
- package/src/provider/errors.ts +27 -3
- package/src/provider/fallback.ts +34 -2
- package/src/provider/retry.ts +30 -10
- package/src/public-runtime.ts +15 -0
- package/src/public-types.ts +3 -0
- package/src/runtime/jobs/registry.ts +105 -0
- package/src/runtime/query/calls-that-never-started.ts +37 -0
- package/src/runtime/query/executor/tool-call-admission.ts +12 -9
- package/src/runtime/query/executor.ts +57 -2
- package/src/runtime/query/file-evidence-context.ts +79 -11
- package/src/runtime/query/index.ts +14 -10
- package/src/runtime/query/iteration/index.ts +32 -26
- package/src/runtime/query/iteration/phases/compaction.ts +19 -0
- package/src/runtime/query/iteration/phases/context.ts +3 -0
- package/src/runtime/query/iteration/recorded-context.ts +48 -0
- package/src/runtime/query/iteration/step-context.ts +27 -155
- package/src/runtime/query/iteration/step-shaping.ts +64 -40
- package/src/runtime/query/iteration/stream-turn.ts +47 -25
- package/src/runtime/query/prepare-turn.ts +10 -1
- package/src/scheduler/completion-inbox.ts +89 -22
- package/src/store/session-index/fts.ts +5 -0
- package/src/test-support/file-evidence-held.ts +27 -0
- package/src/test-support/request-only.ts +19 -0
- package/src/tools/admit-facts.ts +1 -0
- package/src/tools/builtins/bash.ts +149 -54
- package/src/tools/builtins/read-file.ts +66 -4
- package/src/tools/file-read-tracker.ts +25 -1
- package/src/tools/untrusted-envelope.ts +10 -1
- package/src/turn/evidence-recall.ts +189 -58
- package/src/turn/memory-recall.ts +59 -5
- package/src/types/agent/base.ts +7 -0
- package/src/types/errors/catalog.ts +31 -2
- package/src/types/message/index.ts +108 -1
- package/src/types/provider/error.ts +7 -0
- package/src/types/provider/errors.ts +120 -1
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +8 -0
- package/src/types/session/prepare-step.ts +26 -0
- package/src/types/tool/index.ts +40 -0
- package/src/types/tool/presentation.ts +4 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The delegate that plans without touching anything.
|
|
3
|
+
*
|
|
4
|
+
* A parent that wants a design before it changes code has two bad options:
|
|
5
|
+
* plan in its own context, which spends the window on exploration, or hand
|
|
6
|
+
* the whole job to a delegate that can also edit. This type is the third:
|
|
7
|
+
* the same roster as `explore` (`EXPLORE_AGENT_ID`, built by filtering each
|
|
8
|
+
* contributing toolset with `isTrustedReadOnly`), under a prompt that asks
|
|
9
|
+
* for a step plan naming the files it touches and the risks it sees. The
|
|
10
|
+
* roster is the host's to build; only the identity and the prompt live here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const PLAN_AGENT_ID = 'plan'
|
|
14
|
+
|
|
15
|
+
export const PLAN_AGENT_DESCRIPTION =
|
|
16
|
+
'A read-only sub-agent that studies the code and returns a step plan with the files it touches and the risks.'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What a plan delegate is told. It states its limits in the first person so a
|
|
20
|
+
* model does not describe a change as made, and fixes the shape of the answer
|
|
21
|
+
* because the parent acts on the steps and opens the files.
|
|
22
|
+
*/
|
|
23
|
+
export const PLAN_AGENT_PROMPT = [
|
|
24
|
+
'You are a read-only sub-agent dispatched to plan a change: you study the code and return a plan, never the change itself.',
|
|
25
|
+
'You cannot see the parent conversation — work only from the prompt you were given.',
|
|
26
|
+
'You have reading and searching tools only. You cannot edit, write or run commands, and you must not describe a change as made.',
|
|
27
|
+
'Read first: find the code the change touches, the patterns it should follow and the tests that cover it. Prefer what the code already does over a design of your own.',
|
|
28
|
+
'Return the plan in this shape: a one-paragraph approach; numbered steps in the order to do them, each naming the files it changes; the files to read or change first, as paths with line numbers where it helps; the risks and open questions, including what could break and how to check it.',
|
|
29
|
+
'Say plainly what you could not determine. End with the plan; do not ask the parent questions.',
|
|
30
|
+
].join('\n')
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { CHARS_PER_TOKEN } from '../constants/limits.js'
|
|
15
|
-
import type
|
|
15
|
+
import { type Message, isTurnContextEntry } from '../types/message/index.js'
|
|
16
16
|
import type { LLMToolSchema } from '../types/tool/index.js'
|
|
17
17
|
import { TRIGGER_BUFFER_TOKENS, TRIGGER_OUTPUT_RESERVE_CAP_TOKENS } from './context-window.js'
|
|
18
18
|
import { estimateMessageTokens, estimateTextTokens } from './token-estimate.js'
|
|
@@ -166,6 +166,9 @@ function breakdownMessages(messages: readonly Message[]): ContextMessageBreakdow
|
|
|
166
166
|
const name = nameOfCall.get(message.toolCallId) ?? 'unknown'
|
|
167
167
|
toolResultsByType[name] = (toolResultsByType[name] ?? 0) + tokensOfContent(message)
|
|
168
168
|
} else if (message.role === 'user') {
|
|
169
|
+
// Recorded context is what the host recalled for the model, not
|
|
170
|
+
// something the operator said, so it is counted under memory.
|
|
171
|
+
if (isTurnContextEntry(message)) continue
|
|
169
172
|
userTokens += tokensOfContent(message)
|
|
170
173
|
}
|
|
171
174
|
}
|
|
@@ -175,13 +178,15 @@ function breakdownMessages(messages: readonly Message[]): ContextMessageBreakdow
|
|
|
175
178
|
/** Split a window into the parts that fill it. */
|
|
176
179
|
export function analyzeContextUsage(input: ContextUsageInput): ContextBreakdown {
|
|
177
180
|
const windowTokens = Math.max(0, Math.round(input.windowTokens))
|
|
181
|
+
const recorded = (input.messages ?? []).filter(isTurnContextEntry)
|
|
182
|
+
const recordedTokens = recorded.reduce((sum, message) => sum + tokensOfContent(message), 0)
|
|
178
183
|
const fixed: Record<(typeof CONTEXT_CATEGORY_ORDER)[number], number> = {
|
|
179
184
|
'system-prompt': nonNegative(input.systemPromptTokens),
|
|
180
185
|
'system-tools': nonNegative(input.systemToolTokens),
|
|
181
186
|
'mcp-tools': nonNegative(input.mcpToolTokens),
|
|
182
187
|
'mcp-instructions': nonNegative(input.mcpInstructionTokens),
|
|
183
188
|
agents: nonNegative(input.agentTokens),
|
|
184
|
-
memory: nonNegative(input.memoryTokens),
|
|
189
|
+
memory: nonNegative(input.memoryTokens) + recordedTokens,
|
|
185
190
|
skills: nonNegative(input.skillTokens),
|
|
186
191
|
messages: 0,
|
|
187
192
|
}
|
|
@@ -195,7 +200,10 @@ export function analyzeContextUsage(input: ContextUsageInput): ContextBreakdown
|
|
|
195
200
|
const rawMessages =
|
|
196
201
|
measured > 0
|
|
197
202
|
? Math.max(0, measured - fixedTotal)
|
|
198
|
-
: messages.reduce(
|
|
203
|
+
: messages.reduce(
|
|
204
|
+
(sum, message) => (isTurnContextEntry(message) ? sum : sum + tokensOfContent(message)),
|
|
205
|
+
0,
|
|
206
|
+
)
|
|
199
207
|
fixed.messages = Math.min(rawMessages, Math.max(0, windowTokens - fixedTotal - bufferTokens))
|
|
200
208
|
|
|
201
209
|
const totalTokens = measured > 0 ? Math.max(measured, fixedTotal) : fixedTotal + rawMessages
|
|
@@ -231,6 +231,8 @@ export interface ToolHistoryRepairReport {
|
|
|
231
231
|
readonly orphanedToolResultsRemoved: number
|
|
232
232
|
/** Error results inserted for calls whose durable outcome is unavailable. */
|
|
233
233
|
readonly syntheticToolResultsInserted: number
|
|
234
|
+
/** Of those, the results that say the call never started. Absent when none. */
|
|
235
|
+
readonly neverRanToolResultsInserted?: number
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
/** A repaired copy and the exact changes made to it. */
|
|
@@ -274,7 +276,30 @@ function validateToolCallIds(messages: readonly Message[]): void {
|
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
|
|
277
|
-
|
|
279
|
+
/** What is known about the unanswered calls of a history being repaired. */
|
|
280
|
+
export interface ToolHistoryRepairOptions {
|
|
281
|
+
/**
|
|
282
|
+
* Ids of calls known never to have started (queued, or waiting for a
|
|
283
|
+
* person's approval, when the session ended). Their synthetic result says
|
|
284
|
+
* nothing ran, instead of calling the outcome unknown.
|
|
285
|
+
*/
|
|
286
|
+
readonly neverRan?: ReadonlySet<string>
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function unknownOutcomeResult(
|
|
290
|
+
call: ToolCall,
|
|
291
|
+
assistant: AssistantMessage,
|
|
292
|
+
neverRan: boolean,
|
|
293
|
+
): ToolMessage {
|
|
294
|
+
if (neverRan) {
|
|
295
|
+
return {
|
|
296
|
+
role: CONSTANTS.TOOL_ROLE,
|
|
297
|
+
toolCallId: call.id,
|
|
298
|
+
isError: true,
|
|
299
|
+
content: `The \`${call.function.name}\` call did not run: the session ended before it started (it may have been waiting for the user's approval, which was never given). Nothing was changed. If it is still needed, call it again.`,
|
|
300
|
+
...(assistant.timestamp !== undefined ? { timestamp: assistant.timestamp } : {}),
|
|
301
|
+
}
|
|
302
|
+
}
|
|
278
303
|
return {
|
|
279
304
|
role: CONSTANTS.TOOL_ROLE,
|
|
280
305
|
toolCallId: call.id,
|
|
@@ -294,13 +319,17 @@ function unknownOutcomeResult(call: ToolCall, assistant: AssistantMessage): Tool
|
|
|
294
319
|
* fail closed: changing ids or tool calls would invalidate opaque native replay
|
|
295
320
|
* state carried by the assistant message.
|
|
296
321
|
*/
|
|
297
|
-
export function repairToolMessageHistory(
|
|
322
|
+
export function repairToolMessageHistory(
|
|
323
|
+
messages: readonly Message[],
|
|
324
|
+
options: ToolHistoryRepairOptions = {},
|
|
325
|
+
): ToolHistoryRepairResult {
|
|
298
326
|
validateToolCallIds(messages)
|
|
299
327
|
|
|
300
328
|
const repaired: Message[] = []
|
|
301
329
|
let duplicateToolResultsRemoved = 0
|
|
302
330
|
let orphanedToolResultsRemoved = 0
|
|
303
331
|
let syntheticToolResultsInserted = 0
|
|
332
|
+
let neverRanToolResultsInserted = 0
|
|
304
333
|
let index = 0
|
|
305
334
|
|
|
306
335
|
while (index < messages.length) {
|
|
@@ -352,8 +381,10 @@ export function repairToolMessageHistory(messages: readonly Message[]): ToolHist
|
|
|
352
381
|
|
|
353
382
|
for (const call of calls) {
|
|
354
383
|
if (answered.has(call.id)) continue
|
|
355
|
-
|
|
384
|
+
const neverRan = options.neverRan?.has(call.id) === true
|
|
385
|
+
repaired.push(unknownOutcomeResult(call, assistant, neverRan))
|
|
356
386
|
syntheticToolResultsInserted++
|
|
387
|
+
if (neverRan) neverRanToolResultsInserted++
|
|
357
388
|
}
|
|
358
389
|
index = resultEnd
|
|
359
390
|
}
|
|
@@ -364,6 +395,7 @@ export function repairToolMessageHistory(messages: readonly Message[]): ToolHist
|
|
|
364
395
|
duplicateToolResultsRemoved,
|
|
365
396
|
orphanedToolResultsRemoved,
|
|
366
397
|
syntheticToolResultsInserted,
|
|
398
|
+
...(neverRanToolResultsInserted > 0 ? { neverRanToolResultsInserted } : {}),
|
|
367
399
|
},
|
|
368
400
|
}
|
|
369
401
|
}
|
package/src/compaction/index.ts
CHANGED
|
@@ -4,6 +4,9 @@ export type {
|
|
|
4
4
|
FileSlot,
|
|
5
5
|
PinSlot,
|
|
6
6
|
PlanSlot,
|
|
7
|
+
RestorableFile,
|
|
8
|
+
RestorableSkill,
|
|
9
|
+
RestorableState,
|
|
7
10
|
ToolResultSlot,
|
|
8
11
|
WorkingState,
|
|
9
12
|
WorkingStatePin,
|
|
@@ -11,6 +14,7 @@ export type {
|
|
|
11
14
|
|
|
12
15
|
export type {
|
|
13
16
|
DanglingResult,
|
|
17
|
+
ToolHistoryRepairOptions,
|
|
14
18
|
ToolHistoryRepairReport,
|
|
15
19
|
ToolHistoryRepairResult,
|
|
16
20
|
} from './dangling.js'
|
|
@@ -4,10 +4,18 @@ import type {
|
|
|
4
4
|
FileSlot,
|
|
5
5
|
PinSlot,
|
|
6
6
|
PlanSlot,
|
|
7
|
+
RestorableState,
|
|
7
8
|
ToolResultSlot,
|
|
8
9
|
WorkingState,
|
|
9
10
|
} from './types.js'
|
|
10
11
|
|
|
12
|
+
/** Skills kept for restoring; the oldest is dropped past this. */
|
|
13
|
+
export const MAX_RESTORABLE_SKILLS = 8
|
|
14
|
+
/** Files kept for restoring; the oldest is dropped past this. */
|
|
15
|
+
export const MAX_RESTORABLE_FILES = 12
|
|
16
|
+
/** Characters of one skill or file kept for restoring (a little over the 5,000 tokens a restore shows, so the cut is visible). */
|
|
17
|
+
export const MAX_RESTORABLE_CHARS = 24_000
|
|
18
|
+
|
|
11
19
|
/** Pins kept at most; the oldest is dropped past this, and the drop is counted. */
|
|
12
20
|
export const MAX_PINS = 40
|
|
13
21
|
/** Characters one pin may carry; a pin is a fact, not a document. */
|
|
@@ -23,6 +31,10 @@ export function truncateStateText(text: string, max: number): string {
|
|
|
23
31
|
return `${head}${marker}`
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
function normalizeRestorablePath(path: string): string {
|
|
35
|
+
return path.trim().replace(/^\.\//, '')
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
function createEmptyState(): WorkingState {
|
|
27
39
|
return {
|
|
28
40
|
task: '',
|
|
@@ -90,6 +102,47 @@ export class WorkingStateManager {
|
|
|
90
102
|
}
|
|
91
103
|
}
|
|
92
104
|
|
|
105
|
+
private restorableState(): RestorableState {
|
|
106
|
+
if (!this.state.restorable) this.state.restorable = { skills: [], files: [] }
|
|
107
|
+
return this.state.restorable
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remember a skill's instructions so a compaction can put them back.
|
|
112
|
+
* `append` adds a later page of a skill that arrives in pages.
|
|
113
|
+
*/
|
|
114
|
+
recordSkillBody(name: string, body: string, append = false): void {
|
|
115
|
+
const key = name.trim()
|
|
116
|
+
if (key.length === 0 || body.trim().length === 0) return
|
|
117
|
+
const restorable = this.restorableState()
|
|
118
|
+
const index = restorable.skills.findIndex((skill) => skill.name === key)
|
|
119
|
+
const previous = index >= 0 ? restorable.skills[index] : undefined
|
|
120
|
+
const joined = append && previous ? `${previous.body}${body}` : body
|
|
121
|
+
if (index >= 0) restorable.skills.splice(index, 1)
|
|
122
|
+
restorable.skills.push({ name: key, body: joined.slice(0, MAX_RESTORABLE_CHARS) })
|
|
123
|
+
while (restorable.skills.length > MAX_RESTORABLE_SKILLS) restorable.skills.shift()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Remember what a read returned so a compaction can put it back. */
|
|
127
|
+
recordFileText(path: string, text: string): void {
|
|
128
|
+
const key = normalizeRestorablePath(path)
|
|
129
|
+
if (key.length === 0 || text.trim().length === 0) return
|
|
130
|
+
const restorable = this.restorableState()
|
|
131
|
+
const index = restorable.files.findIndex((file) => file.path === key)
|
|
132
|
+
if (index >= 0) restorable.files.splice(index, 1)
|
|
133
|
+
restorable.files.push({ path: key, text: text.slice(0, MAX_RESTORABLE_CHARS) })
|
|
134
|
+
while (restorable.files.length > MAX_RESTORABLE_FILES) restorable.files.shift()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A file that was changed after it was read no longer has a body worth restoring. */
|
|
138
|
+
forgetFileText(path: string): void {
|
|
139
|
+
const files = this.state.restorable?.files
|
|
140
|
+
if (!files) return
|
|
141
|
+
const key = normalizeRestorablePath(path)
|
|
142
|
+
const index = files.findIndex((file) => file.path === key)
|
|
143
|
+
if (index >= 0) files.splice(index, 1)
|
|
144
|
+
}
|
|
145
|
+
|
|
93
146
|
addDecision(decision: string): void {
|
|
94
147
|
this.pushWithEviction('decisions', this.state.decisions, decision, this.config.maxListSize)
|
|
95
148
|
}
|
package/src/compaction/manual.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from './extractor.js'
|
|
18
18
|
import { WorkingStateManager } from './manager.js'
|
|
19
19
|
import { planCompaction } from './plan.js'
|
|
20
|
+
import { recordRestorable, renderRestoredContext } from './restore.js'
|
|
20
21
|
import { findRetainedIndices } from './retention.js'
|
|
21
22
|
import { buildCompactionMessage, isCompactionMessage } from './summary.js'
|
|
22
23
|
import { type CompactionVerificationOptions, buildVerifiedSummary } from './verifier.js'
|
|
@@ -69,6 +70,8 @@ export interface CompactNowInput extends CompactionVerificationOptions {
|
|
|
69
70
|
* model at the originals ({@link COMPACTION_RECOVERY_NOTE}). Absent adds none.
|
|
70
71
|
*/
|
|
71
72
|
readonly recoveryNote?: string
|
|
73
|
+
/** Text the host wants carried across, such as where its plan file is; added to the restored context. */
|
|
74
|
+
readonly restoreNote?: string
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
async function publishManualCompaction(
|
|
@@ -116,6 +119,23 @@ function splice(
|
|
|
116
119
|
return { messages: [...preservedSystem, summary, ...retainedOlder, ...recentMessages], summary }
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
/** The summary with the loaded skills and read files put back after it. */
|
|
123
|
+
function withRestoredContext(
|
|
124
|
+
body: string,
|
|
125
|
+
manager: WorkingStateManager,
|
|
126
|
+
input: CompactNowInput,
|
|
127
|
+
): string {
|
|
128
|
+
const window = resolveContextWindow(
|
|
129
|
+
input.contextWindowTokens ?? input.config.contextWindowTokens,
|
|
130
|
+
input.model,
|
|
131
|
+
)
|
|
132
|
+
const restored = renderRestoredContext(manager.getState(), {
|
|
133
|
+
contextWindowTokens: window.tokens,
|
|
134
|
+
...(input.restoreNote ? { hostNote: input.restoreNote } : {}),
|
|
135
|
+
})
|
|
136
|
+
return restored ? `${body}\n\n${restored}` : body
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
/** Build the state a host-triggered pass is about to replace. */
|
|
120
140
|
function populateWorkingState(
|
|
121
141
|
manager: WorkingStateManager,
|
|
@@ -124,6 +144,7 @@ function populateWorkingState(
|
|
|
124
144
|
): void {
|
|
125
145
|
let firstUser = true
|
|
126
146
|
const toolNames = new Map<string, string>()
|
|
147
|
+
const toolInputs = new Map<string, unknown>()
|
|
127
148
|
|
|
128
149
|
for (const message of messages) {
|
|
129
150
|
switch (message.role) {
|
|
@@ -139,17 +160,26 @@ function populateWorkingState(
|
|
|
139
160
|
if (message.content) extractFromAssistantMessage(manager, message.content, config)
|
|
140
161
|
for (const call of message.toolCalls ?? []) {
|
|
141
162
|
toolNames.set(call.id, call.function.name)
|
|
163
|
+
try {
|
|
164
|
+
toolInputs.set(call.id, JSON.parse(call.function.arguments))
|
|
165
|
+
} catch {
|
|
166
|
+
// Unparseable arguments restore nothing.
|
|
167
|
+
}
|
|
142
168
|
extractFromToolCall(manager, call.function.name, call.function.arguments)
|
|
143
169
|
}
|
|
144
170
|
break
|
|
145
|
-
case 'tool':
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
171
|
+
case 'tool': {
|
|
172
|
+
const toolName = toolNames.get(message.toolCallId) ?? 'tool'
|
|
173
|
+
const text = toolResultToText(message.content)
|
|
174
|
+
extractFromToolResult(manager, toolName, text, message.isError === true)
|
|
175
|
+
recordRestorable(manager, {
|
|
176
|
+
toolName,
|
|
177
|
+
input: toolInputs.get(message.toolCallId),
|
|
178
|
+
output: text,
|
|
179
|
+
success: message.isError !== true,
|
|
180
|
+
})
|
|
152
181
|
break
|
|
182
|
+
}
|
|
153
183
|
case 'system':
|
|
154
184
|
break
|
|
155
185
|
}
|
|
@@ -218,7 +248,7 @@ export async function compactNow(input: CompactNowInput): Promise<CompactionResu
|
|
|
218
248
|
|
|
219
249
|
const { messages, summary } = splice(
|
|
220
250
|
preservedSystem,
|
|
221
|
-
body,
|
|
251
|
+
withRestoredContext(body, manager, input),
|
|
222
252
|
retainedOlder,
|
|
223
253
|
plan.recentMessages,
|
|
224
254
|
input.recoveryNote,
|
|
@@ -230,7 +260,6 @@ export async function compactNow(input: CompactNowInput): Promise<CompactionResu
|
|
|
230
260
|
usage,
|
|
231
261
|
})
|
|
232
262
|
}
|
|
233
|
-
|
|
234
263
|
export interface CompactRegionInput extends CompactNowInput {
|
|
235
264
|
/** First index to summarise, inclusive. */
|
|
236
265
|
readonly start: number
|
|
@@ -311,7 +340,10 @@ export async function compactRegion(input: CompactRegionInput): Promise<Compacti
|
|
|
311
340
|
|
|
312
341
|
// Same cross-run ownership as compactNow: a selected region has been
|
|
313
342
|
// replaced outside a turn, so its only surviving account is pinned.
|
|
314
|
-
const summary = {
|
|
343
|
+
const summary = {
|
|
344
|
+
...buildCompactionMessage(withRestoredContext(body, manager, input), input.recoveryNote),
|
|
345
|
+
retain: true,
|
|
346
|
+
}
|
|
315
347
|
const out = [...messages.slice(0, start), summary, ...retainedSelected, ...messages.slice(end)]
|
|
316
348
|
|
|
317
349
|
// Checked after the splice, not only before it. The edges being
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { resolve as resolvePath } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { CHARS_PER_TOKEN } from '../constants/limits.js'
|
|
4
|
+
import type { WorkingStateManager } from './manager.js'
|
|
5
|
+
import type { WorkingState } from './types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What a compaction puts back after the summary.
|
|
9
|
+
*
|
|
10
|
+
* A summary says a skill was loaded and a file was read; it cannot give the
|
|
11
|
+
* model the skill's steps or the file's lines, so the next step either guesses
|
|
12
|
+
* from a paraphrase or loads and reads the same things again. This puts the
|
|
13
|
+
* most recent of each back, verbatim, inside fixed caps — and inside a share
|
|
14
|
+
* of the window, so a small window is not refilled by the thing that was meant
|
|
15
|
+
* to free it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Skills restored per pass, most recent first. */
|
|
19
|
+
export const RESTORE_MAX_SKILLS = 8
|
|
20
|
+
/** Tokens of one restored skill. */
|
|
21
|
+
export const RESTORE_SKILL_TOKENS = 5_000
|
|
22
|
+
/** Tokens of all restored skills together. */
|
|
23
|
+
export const RESTORE_SKILLS_TOTAL_TOKENS = 25_000
|
|
24
|
+
/** Files restored per pass, most recent first. */
|
|
25
|
+
export const RESTORE_MAX_FILES = 5
|
|
26
|
+
/** Tokens of one restored file. */
|
|
27
|
+
export const RESTORE_FILE_TOKENS = 5_000
|
|
28
|
+
/** Tokens of all restored files together. */
|
|
29
|
+
export const RESTORE_FILES_TOTAL_TOKENS = 50_000
|
|
30
|
+
/** The share of the window all restored files may take. */
|
|
31
|
+
export const RESTORE_FILES_WINDOW_SHARE = 0.1
|
|
32
|
+
/** The share of the window all restored skills may take. */
|
|
33
|
+
export const RESTORE_SKILLS_WINDOW_SHARE = 0.05
|
|
34
|
+
|
|
35
|
+
const CUT_MARKER = '\n… [cut to fit the restore budget]'
|
|
36
|
+
const SKILL_CONTINUATION = /\n\n\[More skill instructions remain\.[^\]]*\]/g
|
|
37
|
+
|
|
38
|
+
function isInstructionPath(path: string): boolean {
|
|
39
|
+
const normal = path.replace(/\\/g, '/')
|
|
40
|
+
const name = normal.slice(normal.lastIndexOf('/') + 1)
|
|
41
|
+
return name === 'AGENTS.md' || name === 'AGENTS.local.md' || normal.includes('/.namzu/rules/')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveAgainst(path: string, workingDirectory: string | undefined): string {
|
|
45
|
+
return workingDirectory ? resolvePath(workingDirectory, path) : path
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isPlanPath(path: string): boolean {
|
|
49
|
+
return /(^|\/)plans\/plan\.md$/.test(path.replace(/\\/g, '/'))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Note a finished tool call, if it is one whose result a compaction restores.
|
|
54
|
+
* Called with the call's input and the text the model was shown.
|
|
55
|
+
*/
|
|
56
|
+
export function recordRestorable(
|
|
57
|
+
manager: WorkingStateManager,
|
|
58
|
+
call: {
|
|
59
|
+
readonly toolName: string
|
|
60
|
+
readonly input: unknown
|
|
61
|
+
readonly output: string
|
|
62
|
+
readonly success: boolean
|
|
63
|
+
/** The read was answered as unchanged: the body is already stored from the earlier read. */
|
|
64
|
+
readonly unchanged?: boolean
|
|
65
|
+
/** Resolves a relative path, so one file is one entry however the model spelled it. */
|
|
66
|
+
readonly workingDirectory?: string
|
|
67
|
+
},
|
|
68
|
+
): void {
|
|
69
|
+
const input =
|
|
70
|
+
typeof call.input === 'object' && call.input !== null
|
|
71
|
+
? (call.input as Record<string, unknown>)
|
|
72
|
+
: undefined
|
|
73
|
+
if (!input) return
|
|
74
|
+
if (call.toolName === 'skill') {
|
|
75
|
+
if (!call.success || typeof input.name !== 'string') return
|
|
76
|
+
manager.recordSkillBody(
|
|
77
|
+
input.name,
|
|
78
|
+
call.output.replace(SKILL_CONTINUATION, ''),
|
|
79
|
+
typeof input.cursor === 'string' && input.cursor.length > 0,
|
|
80
|
+
)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (call.toolName === 'read') {
|
|
84
|
+
const path = typeof input.path === 'string' ? input.path : undefined
|
|
85
|
+
if (!path || !call.success || call.unchanged) return
|
|
86
|
+
manager.recordFileText(resolveAgainst(path, call.workingDirectory), call.output)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (call.toolName === 'write' || call.toolName === 'edit') {
|
|
90
|
+
const path = typeof input.path === 'string' ? input.path : undefined
|
|
91
|
+
// The body a read showed is behind the file now.
|
|
92
|
+
if (path && call.success) manager.forgetFileText(resolveAgainst(path, call.workingDirectory))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function fit(text: string, tokens: number): string {
|
|
97
|
+
const room = tokens * CHARS_PER_TOKEN
|
|
98
|
+
return text.length <= room
|
|
99
|
+
? text
|
|
100
|
+
: `${text.slice(0, Math.max(0, room - CUT_MARKER.length))}${CUT_MARKER}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface RestoreRenderOptions {
|
|
104
|
+
/** The window the pass is working against; caps scale down on a small one. */
|
|
105
|
+
readonly contextWindowTokens?: number
|
|
106
|
+
/** A host's own line to carry across, such as where its plan file is. */
|
|
107
|
+
readonly hostNote?: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The section to append after a summary, or `undefined` when there is nothing to put back. */
|
|
111
|
+
export function renderRestoredContext(
|
|
112
|
+
state: WorkingState,
|
|
113
|
+
options: RestoreRenderOptions = {},
|
|
114
|
+
): string | undefined {
|
|
115
|
+
const window = options.contextWindowTokens
|
|
116
|
+
const skillsTotal =
|
|
117
|
+
window === undefined
|
|
118
|
+
? RESTORE_SKILLS_TOTAL_TOKENS
|
|
119
|
+
: Math.min(RESTORE_SKILLS_TOTAL_TOKENS, Math.floor(window * RESTORE_SKILLS_WINDOW_SHARE))
|
|
120
|
+
const filesTotal =
|
|
121
|
+
window === undefined
|
|
122
|
+
? RESTORE_FILES_TOTAL_TOKENS
|
|
123
|
+
: Math.min(RESTORE_FILES_TOTAL_TOKENS, Math.floor(window * RESTORE_FILES_WINDOW_SHARE))
|
|
124
|
+
|
|
125
|
+
const sections: string[] = []
|
|
126
|
+
|
|
127
|
+
let skillBudget = skillsTotal
|
|
128
|
+
const skills = [...(state.restorable?.skills ?? [])].reverse()
|
|
129
|
+
for (const skill of skills) {
|
|
130
|
+
if (sections.length >= RESTORE_MAX_SKILLS || skillBudget <= 0) break
|
|
131
|
+
const body = fit(skill.body, Math.min(RESTORE_SKILL_TOKENS, skillBudget))
|
|
132
|
+
skillBudget -= Math.ceil(body.length / CHARS_PER_TOKEN)
|
|
133
|
+
sections.push(`### Skill: ${skill.name}\n\n${body}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let fileBudget = filesTotal
|
|
137
|
+
let files = 0
|
|
138
|
+
const candidates = [...(state.restorable?.files ?? [])].reverse()
|
|
139
|
+
for (const file of candidates) {
|
|
140
|
+
if (files >= RESTORE_MAX_FILES || fileBudget <= 0) break
|
|
141
|
+
if (isInstructionPath(file.path) || isPlanPath(file.path)) continue
|
|
142
|
+
const text = fit(file.text, Math.min(RESTORE_FILE_TOKENS, fileBudget))
|
|
143
|
+
fileBudget -= Math.ceil(text.length / CHARS_PER_TOKEN)
|
|
144
|
+
files += 1
|
|
145
|
+
sections.push(`### File: ${file.path}\n\n${text}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const note = options.hostNote?.trim()
|
|
149
|
+
if (note) sections.push(note)
|
|
150
|
+
if (sections.length === 0) return undefined
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
'## Restored after compaction',
|
|
154
|
+
'',
|
|
155
|
+
'These were loaded or read earlier in this conversation and are put back here so you can keep using them. A file below may have changed on disk since it was read; read it again before you rely on it for an edit.',
|
|
156
|
+
'',
|
|
157
|
+
sections.join('\n\n'),
|
|
158
|
+
].join('\n')
|
|
159
|
+
}
|
package/src/compaction/types.ts
CHANGED
|
@@ -28,6 +28,31 @@ export interface WorkingState {
|
|
|
28
28
|
* of the state".
|
|
29
29
|
*/
|
|
30
30
|
evicted: Record<string, number>
|
|
31
|
+
/**
|
|
32
|
+
* Bodies a compaction puts back after the summary: the skills the model
|
|
33
|
+
* loaded and the files it read. Kept apart from the slots above because
|
|
34
|
+
* they are verbatim text, not a summary of it, and are never rendered into
|
|
35
|
+
* the working-memory block.
|
|
36
|
+
*/
|
|
37
|
+
restorable?: RestorableState
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A skill the model loaded, as its instructions read. */
|
|
41
|
+
export interface RestorableSkill {
|
|
42
|
+
readonly name: string
|
|
43
|
+
readonly body: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A file the model read, as the read returned it. */
|
|
47
|
+
export interface RestorableFile {
|
|
48
|
+
readonly path: string
|
|
49
|
+
readonly text: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Oldest first; a later load or read of the same name or path replaces and moves to the end. */
|
|
53
|
+
export interface RestorableState {
|
|
54
|
+
skills: RestorableSkill[]
|
|
55
|
+
files: RestorableFile[]
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
export interface PinSlot {
|
|
@@ -55,7 +55,9 @@ function isStandingContext(message: Message): boolean {
|
|
|
55
55
|
return (
|
|
56
56
|
source?.type === 'project-instructions' ||
|
|
57
57
|
(source?.type === 'runtime-context' &&
|
|
58
|
-
(source.kind === 'step-context' ||
|
|
58
|
+
(source.kind === 'step-context' ||
|
|
59
|
+
source.kind === 'reminder' ||
|
|
60
|
+
source.kind === 'turn-context'))
|
|
59
61
|
)
|
|
60
62
|
}
|
|
61
63
|
|
package/src/compaction/wire.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { CompactionConfig } from '../config/runtime.js'
|
|
2
2
|
import { WorkingStateManager } from './manager.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
FileSlot,
|
|
5
|
+
PinSlot,
|
|
6
|
+
PlanSlot,
|
|
7
|
+
RestorableState,
|
|
8
|
+
ToolResultSlot,
|
|
9
|
+
WorkingState,
|
|
10
|
+
} from './types.js'
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
13
|
* The working state in a form that survives a process boundary.
|
|
@@ -42,6 +49,8 @@ export interface WorkingStateSnapshot {
|
|
|
42
49
|
* completeness it does not have.
|
|
43
50
|
*/
|
|
44
51
|
readonly evicted: Readonly<Record<string, number>>
|
|
52
|
+
/** Absent in snapshots written before bodies were restored; restored as none. */
|
|
53
|
+
readonly restorable?: RestorableState
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
export function snapshotWorkingState(manager: WorkingStateManager): WorkingStateSnapshot {
|
|
@@ -59,6 +68,14 @@ export function snapshotWorkingState(manager: WorkingStateManager): WorkingState
|
|
|
59
68
|
userRequirements: [...state.userRequirements],
|
|
60
69
|
assistantNotes: [...state.assistantNotes],
|
|
61
70
|
evicted: { ...state.evicted },
|
|
71
|
+
...(state.restorable
|
|
72
|
+
? {
|
|
73
|
+
restorable: {
|
|
74
|
+
skills: state.restorable.skills.map((skill) => ({ ...skill })),
|
|
75
|
+
files: state.restorable.files.map((file) => ({ ...file })),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
81
|
|
|
@@ -89,6 +106,14 @@ export function restoreWorkingState(
|
|
|
89
106
|
userRequirements: [...snapshot.userRequirements],
|
|
90
107
|
assistantNotes: [...snapshot.assistantNotes],
|
|
91
108
|
evicted: { ...snapshot.evicted },
|
|
109
|
+
...(snapshot.restorable
|
|
110
|
+
? {
|
|
111
|
+
restorable: {
|
|
112
|
+
skills: snapshot.restorable.skills.map((skill) => ({ ...skill })),
|
|
113
|
+
files: snapshot.restorable.files.map((file) => ({ ...file })),
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
92
117
|
}
|
|
93
118
|
manager.replaceState(state)
|
|
94
119
|
return manager
|