@namzu/sdk 36.0.0 → 37.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 +180 -0
- package/README.md +68 -41
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +2 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +2 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/compaction/extractor.d.ts +1 -1
- package/dist/compaction/extractor.d.ts.map +1 -1
- package/dist/compaction/extractor.js +15 -7
- package/dist/compaction/extractor.js.map +1 -1
- package/dist/compaction/manager.d.ts +2 -0
- package/dist/compaction/manager.d.ts.map +1 -1
- package/dist/compaction/manager.js +12 -1
- package/dist/compaction/manager.js.map +1 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +82 -15
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/contracts/schemas.d.ts +10 -10
- package/dist/eval/harness-verification.d.ts +63 -0
- package/dist/eval/harness-verification.d.ts.map +1 -0
- package/dist/eval/harness-verification.js +165 -0
- package/dist/eval/harness-verification.js.map +1 -0
- package/dist/eval/index.d.ts +2 -0
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +1 -0
- package/dist/eval/index.js.map +1 -1
- package/dist/execution/code-runtime/json-value.d.ts +10 -0
- package/dist/execution/code-runtime/json-value.d.ts.map +1 -0
- package/dist/execution/code-runtime/json-value.js +89 -0
- package/dist/execution/code-runtime/json-value.js.map +1 -0
- package/dist/execution/code-runtime/worker-program.d.ts +8 -0
- package/dist/execution/code-runtime/worker-program.d.ts.map +1 -0
- package/dist/execution/code-runtime/worker-program.js +240 -0
- package/dist/execution/code-runtime/worker-program.js.map +1 -0
- package/dist/execution/code-runtime/worker.d.ts +22 -0
- package/dist/execution/code-runtime/worker.d.ts.map +1 -1
- package/dist/execution/code-runtime/worker.js +148 -111
- package/dist/execution/code-runtime/worker.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +12 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +306 -37
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +2 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +6 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/provider/capabilities.d.ts +3 -1
- package/dist/provider/capabilities.d.ts.map +1 -1
- package/dist/provider/capabilities.js +13 -0
- package/dist/provider/capabilities.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +2 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -1
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +1 -1
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/tool/execute.d.ts +3 -0
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +11 -80
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/run/command-gate.d.ts.map +1 -1
- package/dist/run/command-gate.js +65 -12
- package/dist/run/command-gate.js.map +1 -1
- package/dist/run/index.d.ts +2 -0
- package/dist/run/index.d.ts.map +1 -1
- package/dist/run/index.js +1 -0
- package/dist/run/index.js.map +1 -1
- package/dist/run/memory-promoter.d.ts +3 -35
- package/dist/run/memory-promoter.d.ts.map +1 -1
- package/dist/run/memory-promoter.js +32 -40
- package/dist/run/memory-promoter.js.map +1 -1
- package/dist/run/memory-recall.d.ts +25 -0
- package/dist/run/memory-recall.d.ts.map +1 -0
- package/dist/run/memory-recall.js +228 -0
- package/dist/run/memory-recall.js.map +1 -0
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +1 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/run/token-budget.d.ts +2 -0
- package/dist/run/token-budget.d.ts.map +1 -1
- package/dist/run/token-budget.js +4 -0
- package/dist/run/token-budget.js.map +1 -1
- package/dist/run/workspace-fingerprint.d.ts +10 -3
- package/dist/run/workspace-fingerprint.d.ts.map +1 -1
- package/dist/run/workspace-fingerprint.js +17 -8
- package/dist/run/workspace-fingerprint.js.map +1 -1
- package/dist/runtime/query/checkpoint.d.ts +14 -0
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +82 -1
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +5 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +70 -14
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +5 -8
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +54 -7
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +10 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +325 -36
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/native-output.d.ts +10 -0
- package/dist/runtime/query/iteration/native-output.d.ts.map +1 -0
- package/dist/runtime/query/iteration/native-output.js +32 -0
- package/dist/runtime/query/iteration/native-output.js.map +1 -0
- package/dist/runtime/query/iteration/phases/advisory.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/advisory.js +18 -13
- package/dist/runtime/query/iteration/phases/advisory.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +32 -11
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -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/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/phases/working-memory.d.ts +1 -1
- package/dist/runtime/query/iteration/phases/working-memory.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/working-memory.js +23 -6
- package/dist/runtime/query/iteration/phases/working-memory.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +2 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +6 -2
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +17 -2
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tool-call-budget.d.ts +19 -0
- package/dist/runtime/query/tool-call-budget.d.ts.map +1 -0
- package/dist/runtime/query/tool-call-budget.js +110 -0
- package/dist/runtime/query/tool-call-budget.js.map +1 -0
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +4 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +1 -0
- package/dist/scheduler/local.js.map +1 -1
- package/dist/session/handoff/capacity.d.ts +2 -2
- package/dist/session/handoff/capacity.d.ts.map +1 -1
- package/dist/session/handoff/capacity.js +3 -2
- package/dist/session/handoff/capacity.js.map +1 -1
- package/dist/store/memory/disk.d.ts +6 -2
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +42 -25
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/index.d.ts +4 -0
- package/dist/store/memory/index.d.ts.map +1 -1
- package/dist/store/memory/index.js +55 -19
- package/dist/store/memory/index.js.map +1 -1
- package/dist/store/memory/memory.d.ts +3 -2
- package/dist/store/memory/memory.d.ts.map +1 -1
- package/dist/store/memory/memory.js +11 -2
- package/dist/store/memory/memory.js.map +1 -1
- package/dist/store/memory/operation-lock.d.ts +11 -0
- package/dist/store/memory/operation-lock.d.ts.map +1 -0
- package/dist/store/memory/operation-lock.js +94 -0
- package/dist/store/memory/operation-lock.js.map +1 -0
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +60 -10
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/run-code.d.ts +6 -0
- package/dist/tools/builtins/run-code.d.ts.map +1 -1
- package/dist/tools/builtins/run-code.js +15 -4
- package/dist/tools/builtins/run-code.js.map +1 -1
- package/dist/tools/builtins/search-tools.d.ts.map +1 -1
- package/dist/tools/builtins/search-tools.js +14 -2
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/defineTool.d.ts +2 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +3 -0
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/memory/delete.d.ts +4 -0
- package/dist/tools/memory/delete.d.ts.map +1 -0
- package/dist/tools/memory/delete.js +26 -0
- package/dist/tools/memory/delete.js.map +1 -0
- package/dist/tools/memory/index.d.ts +2 -0
- package/dist/tools/memory/index.d.ts.map +1 -1
- package/dist/tools/memory/index.js +6 -0
- package/dist/tools/memory/index.js.map +1 -1
- package/dist/tools/memory/save.d.ts.map +1 -1
- package/dist/tools/memory/save.js +3 -2
- package/dist/tools/memory/save.js.map +1 -1
- package/dist/tools/memory/search.d.ts.map +1 -1
- package/dist/tools/memory/search.js +12 -5
- package/dist/tools/memory/search.js.map +1 -1
- package/dist/tools/memory/update.d.ts +4 -0
- package/dist/tools/memory/update.d.ts.map +1 -0
- package/dist/tools/memory/update.js +47 -0
- package/dist/tools/memory/update.js.map +1 -0
- package/dist/types/agent/scheduler.d.ts +5 -0
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +10 -0
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/computer-use/index.d.ts +6 -0
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/errors/catalog.js +1 -1
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/hitl/index.d.ts +11 -1
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/memory/index.d.ts +14 -1
- package/dist/types/memory/index.d.ts.map +1 -1
- package/dist/types/project/entity.d.ts +1 -0
- package/dist/types/project/entity.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +2 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/model.d.ts +4 -0
- package/dist/types/provider/model.d.ts.map +1 -1
- package/dist/types/run/answer-review.d.ts +2 -0
- package/dist/types/run/answer-review.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +9 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/prepare-step.d.ts +19 -1
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/structured-output/index.d.ts +8 -2
- package/dist/types/structured-output/index.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +15 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/utils/json-snapshot.d.ts +3 -0
- package/dist/utils/json-snapshot.d.ts.map +1 -0
- package/dist/utils/json-snapshot.js +79 -0
- package/dist/utils/json-snapshot.js.map +1 -0
- package/package.json +4 -2
- package/src/bridge/a2a/mapper.ts +2 -0
- package/src/bridge/sse/mapper.ts +2 -0
- package/src/compaction/extractor.ts +21 -8
- package/src/compaction/manager.ts +11 -1
- package/src/compaction/verifier.ts +80 -18
- package/src/eval/harness-verification.ts +270 -0
- package/src/eval/index.ts +10 -0
- package/src/execution/code-runtime/json-value.ts +82 -0
- package/src/execution/code-runtime/worker-program.ts +239 -0
- package/src/execution/code-runtime/worker.ts +164 -111
- package/src/manager/agent/lifecycle.ts +340 -45
- package/src/manager/run/persistence.ts +7 -0
- package/src/provider/capabilities.ts +25 -1
- package/src/provider/fallback.ts +2 -0
- package/src/public-runtime.ts +4 -0
- package/src/public-tools.ts +5 -1
- package/src/public-types.ts +1 -0
- package/src/registry/tool/execute.ts +17 -79
- package/src/run/command-gate.ts +61 -13
- package/src/run/index.ts +2 -0
- package/src/run/memory-promoter.ts +32 -40
- package/src/run/memory-recall.ts +254 -0
- package/src/run/reporter.ts +1 -0
- package/src/run/token-budget.ts +4 -0
- package/src/run/workspace-fingerprint.ts +17 -8
- package/src/runtime/query/checkpoint.ts +96 -2
- package/src/runtime/query/executor.ts +77 -14
- package/src/runtime/query/index.ts +73 -16
- package/src/runtime/query/iteration/index.ts +377 -44
- package/src/runtime/query/iteration/native-output.ts +35 -0
- package/src/runtime/query/iteration/phases/advisory.ts +21 -11
- package/src/runtime/query/iteration/phases/compaction.ts +36 -14
- package/src/runtime/query/iteration/phases/context.ts +9 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +5 -1
- package/src/runtime/query/iteration/phases/working-memory.ts +23 -5
- package/src/runtime/query/iteration/stream-turn.ts +2 -0
- package/src/runtime/query/steering.ts +19 -2
- package/src/runtime/query/tool-call-budget.ts +115 -0
- package/src/runtime/query/tooling.ts +6 -0
- package/src/scheduler/local.ts +1 -0
- package/src/session/handoff/capacity.ts +7 -3
- package/src/store/memory/disk.ts +52 -30
- package/src/store/memory/index.ts +75 -27
- package/src/store/memory/memory.ts +18 -6
- package/src/store/memory/operation-lock.ts +99 -0
- package/src/tools/builtins/computer-use.ts +61 -7
- package/src/tools/builtins/run-code.ts +24 -5
- package/src/tools/builtins/search-tools.ts +18 -3
- package/src/tools/defineTool.ts +5 -0
- package/src/tools/memory/delete.ts +29 -0
- package/src/tools/memory/index.ts +6 -0
- package/src/tools/memory/save.ts +4 -2
- package/src/tools/memory/search.ts +12 -5
- package/src/tools/memory/update.ts +50 -0
- package/src/types/agent/scheduler.ts +6 -0
- package/src/types/agent/task.ts +13 -0
- package/src/types/computer-use/index.ts +6 -0
- package/src/types/errors/catalog.ts +1 -1
- package/src/types/hitl/index.ts +11 -1
- package/src/types/memory/index.ts +16 -1
- package/src/types/project/entity.ts +1 -0
- package/src/types/provider/config.ts +2 -0
- package/src/types/provider/model.ts +4 -0
- package/src/types/run/answer-review.ts +2 -0
- package/src/types/run/events.ts +10 -0
- package/src/types/run/prepare-step.ts +19 -1
- package/src/types/structured-output/index.ts +15 -2
- package/src/types/tool/index.ts +16 -0
- package/src/utils/json-snapshot.ts +76 -0
|
@@ -5,6 +5,7 @@ import { toolResultToText } from '../../../../types/message/content.js'
|
|
|
5
5
|
import { createRuntimeContextMessage } from '../../../../types/message/index.js'
|
|
6
6
|
import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
|
|
7
7
|
import { toErrorMessage } from '../../../../utils/error.js'
|
|
8
|
+
import { activeContextWindow, measureContext } from './compaction.js'
|
|
8
9
|
import type { IterationContext } from './context.js'
|
|
9
10
|
|
|
10
11
|
function countToolCalls(ctx: IterationContext): number {
|
|
@@ -18,9 +19,8 @@ function countToolCalls(ctx: IterationContext): number {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function estimateContextWindowPercent(ctx: IterationContext): number {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
return (ctx.runMgr.tokenUsage.totalTokens / budget) * 100
|
|
22
|
+
const window = activeContextWindow(ctx)
|
|
23
|
+
return (measureContext(ctx).tokens / window.tokens) * 100
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function computeCostBudgetPercent(ctx: IterationContext): number | undefined {
|
|
@@ -67,7 +67,7 @@ export async function runAdvisoryPhase(
|
|
|
67
67
|
contextWindowPercent: estimateContextWindowPercent(ctx),
|
|
68
68
|
totalCostUsd: ctx.runMgr.costInfo.totalCost,
|
|
69
69
|
costBudgetPercent: computeCostBudgetPercent(ctx),
|
|
70
|
-
lastError:
|
|
70
|
+
lastError: extractCurrentToolErrors(ctx, response),
|
|
71
71
|
lastToolCategory: extractLastToolCategory(ctx, response),
|
|
72
72
|
advisoryCallCount: advisoryCtx.callHistory.length,
|
|
73
73
|
}
|
|
@@ -190,16 +190,26 @@ export async function runAdvisoryPhase(
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function
|
|
193
|
+
function extractCurrentToolErrors(
|
|
194
|
+
ctx: IterationContext,
|
|
195
|
+
response: ChatCompletionResponse,
|
|
196
|
+
): string | undefined {
|
|
197
|
+
const calls = new Map(response.message.toolCalls?.map((call) => [call.id, call.function.name]))
|
|
198
|
+
if (calls.size === 0) return undefined
|
|
194
199
|
const messages = ctx.runMgr.messages
|
|
200
|
+
const errors: string[] = []
|
|
195
201
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
196
202
|
const msg = messages[i]
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
// The current assistant turn starts this batch. Never resurrect an old
|
|
204
|
+
// failure, even if a provider reused a call ID in another iteration.
|
|
205
|
+
if (msg?.role === 'assistant') break
|
|
206
|
+
if (msg?.role === 'tool' && msg.isError === true && calls.has(msg.toolCallId)) {
|
|
207
|
+
errors.push(
|
|
208
|
+
toolResultToText(msg.content).trim() ||
|
|
209
|
+
`Tool ${calls.get(msg.toolCallId)} reported an error.`,
|
|
210
|
+
)
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
|
-
|
|
213
|
+
// A successful sibling does not erase failed results in this same batch.
|
|
214
|
+
return errors.length > 0 ? errors.reverse().join('\n') : undefined
|
|
205
215
|
}
|
|
@@ -28,6 +28,16 @@ import { isWorkingMemoryMessage } from './working-memory.js'
|
|
|
28
28
|
// because `runtime/query/index.ts` already imports it from this path.
|
|
29
29
|
export { isCompactionMessage } from '../../../../compaction/summary.js'
|
|
30
30
|
|
|
31
|
+
/** The selected request model owns the denominator, including overflow recovery. */
|
|
32
|
+
export function activeContextWindow(ctx: IterationContext) {
|
|
33
|
+
const model = ctx.contextModel ?? ctx.runConfig.model
|
|
34
|
+
return resolveContextWindow(
|
|
35
|
+
ctx.compactionConfig?.contextWindowTokens,
|
|
36
|
+
model,
|
|
37
|
+
model === ctx.runConfig.model ? ctx.providerContextWindow : ctx.activeProviderContextWindow,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
/**
|
|
32
42
|
* How much a forced pass must shed before the turn is worth retrying.
|
|
33
43
|
*
|
|
@@ -147,7 +157,12 @@ async function commitToolResultClear(
|
|
|
147
157
|
plan: ToolResultClearPlan,
|
|
148
158
|
contextWindowTokens: number,
|
|
149
159
|
windowSource: 'config' | 'provider' | 'model-table' | 'default',
|
|
160
|
+
reason: 'threshold' | 'overflow',
|
|
150
161
|
): Promise<void> {
|
|
162
|
+
// A shorter replacement also removes evidence. Preserve its original
|
|
163
|
+
// content in the same archive used by whole-message compaction before
|
|
164
|
+
// changing the live history or publishing the clear.
|
|
165
|
+
await recordShed(ctx, [...ctx.runMgr.messages], plan.messages, reason)
|
|
151
166
|
installMessages(ctx.runMgr.messages, plan.messages)
|
|
152
167
|
// The provider counted the pre-edit prompt. Leaving that measurement live
|
|
153
168
|
// makes the next trigger compare the old prompt against the new history.
|
|
@@ -547,11 +562,7 @@ async function runCompactionCheckInner(
|
|
|
547
562
|
// number. Nothing in the estate ever set `contextWindowTokens`, so the
|
|
548
563
|
// fallback WAS the behavior, and the shipped CLI's 1M budget put the
|
|
549
564
|
// trigger at ~700k. See `compaction/context-window.ts`.
|
|
550
|
-
const window =
|
|
551
|
-
config.contextWindowTokens,
|
|
552
|
-
ctx.runConfig.model,
|
|
553
|
-
ctx.providerContextWindow,
|
|
554
|
-
)
|
|
565
|
+
const window = activeContextWindow(ctx)
|
|
555
566
|
const budget = window.tokens
|
|
556
567
|
|
|
557
568
|
const usage = estimatedTokens / budget
|
|
@@ -659,7 +670,7 @@ async function runCompactionCheckInner(
|
|
|
659
670
|
...(openTasks ? { openTasks } : {}),
|
|
660
671
|
})
|
|
661
672
|
if (working.reclaimedTokens > 0) {
|
|
662
|
-
await commitToolResultClear(ctx, working, budget, window.source)
|
|
673
|
+
await commitToolResultClear(ctx, working, budget, window.source, pass.reason)
|
|
663
674
|
}
|
|
664
675
|
// Under the trigger, the working set was the whole pass: the summary
|
|
665
676
|
// path is for the trigger, not for the soft target.
|
|
@@ -684,7 +695,7 @@ async function runCompactionCheckInner(
|
|
|
684
695
|
// the NEXT turn will correct it, and an over-eager summarization is
|
|
685
696
|
// far more costly than one late pass.
|
|
686
697
|
if (clearPlan.reliefWasEnough) {
|
|
687
|
-
await commitToolResultClear(ctx, clearPlan, budget, window.source)
|
|
698
|
+
await commitToolResultClear(ctx, clearPlan, budget, window.source, pass.reason)
|
|
688
699
|
return
|
|
689
700
|
}
|
|
690
701
|
}
|
|
@@ -736,7 +747,7 @@ async function runCompactionCheckInner(
|
|
|
736
747
|
// There is no opaque provider wait on this path, so publish that single
|
|
737
748
|
// complete edit now rather than discarding it.
|
|
738
749
|
if (stagedClear) {
|
|
739
|
-
await commitToolResultClear(ctx, stagedClear, budget, window.source)
|
|
750
|
+
await commitToolResultClear(ctx, stagedClear, budget, window.source, pass.reason)
|
|
740
751
|
}
|
|
741
752
|
return
|
|
742
753
|
}
|
|
@@ -823,6 +834,20 @@ async function runCompactionCheckInner(
|
|
|
823
834
|
|
|
824
835
|
const live = ctx.runMgr.messages
|
|
825
836
|
const oldCount = live.length
|
|
837
|
+
// Retained history can survive in full while a summary adds another copy
|
|
838
|
+
// of its facts. Even an unretained span may be smaller than the cumulative
|
|
839
|
+
// summary. Compare both candidates with the same estimator, rather than
|
|
840
|
+
// comparing provider usage against an estimate or trusting message count.
|
|
841
|
+
// When a clear was staged, require the summary itself to improve on it;
|
|
842
|
+
// otherwise publish the useful clear without consuming its savings.
|
|
843
|
+
if (estimateMessagesTokens(newMessages) >= estimateMessagesTokens(messages)) {
|
|
844
|
+
if (stagedClear && stagedClear.reclaimedTokens > 0) {
|
|
845
|
+
await commitToolResultClear(ctx, stagedClear, budget, window.source, pass.reason)
|
|
846
|
+
} else {
|
|
847
|
+
await declined(ctx, 'shed_nothing', oldCount)
|
|
848
|
+
}
|
|
849
|
+
return
|
|
850
|
+
}
|
|
826
851
|
await recordShed(ctx, [...live], newMessages, options?.force ? 'overflow' : 'threshold')
|
|
827
852
|
|
|
828
853
|
installMessages(live, newMessages)
|
|
@@ -840,12 +865,9 @@ async function runCompactionCheckInner(
|
|
|
840
865
|
// edits are installed and no verifier can still fail between them.
|
|
841
866
|
if (stagedClear) await emitToolResultClear(ctx, stagedClear)
|
|
842
867
|
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
// time, for nothing. Report the shortfall rather than repeating a move
|
|
847
|
-
// that demonstrably does not work; `resetThreshold` was declared and
|
|
848
|
-
// CLI-set but read by nothing until now.
|
|
868
|
+
// Report remaining pressure after a useful edit. This diagnostic does not
|
|
869
|
+
// suppress future passes: appended evidence may become reclaimable even
|
|
870
|
+
// when the protected floor and recent history remain above the target.
|
|
849
871
|
const reachedReset = newEstimate / budget <= config.resetThreshold
|
|
850
872
|
if (!reachedReset) {
|
|
851
873
|
ctx.log.warn('Compaction did not reach its reset threshold — context may still be tight', {
|
|
@@ -131,6 +131,8 @@ export interface IterationContext {
|
|
|
131
131
|
* drained and no tool result is extended.
|
|
132
132
|
*/
|
|
133
133
|
readonly steering?: SteeringChannel
|
|
134
|
+
/** Records operator intent only after guidance was accepted by a tool result. */
|
|
135
|
+
readonly onSteeringDelivered?: (text: string) => void
|
|
134
136
|
/** Exit notices for the run's background jobs, drained into the next tool result. */
|
|
135
137
|
readonly jobNotices?: SteeringChannel
|
|
136
138
|
readonly checkpointMgr: CheckpointManager
|
|
@@ -179,6 +181,11 @@ export interface IterationContext {
|
|
|
179
181
|
* through to the table.
|
|
180
182
|
*/
|
|
181
183
|
readonly providerContextWindow?: number
|
|
184
|
+
/** Selected request model; its window must not reuse another model's metadata. */
|
|
185
|
+
contextModel?: string
|
|
186
|
+
activeProviderContextWindow?: number
|
|
187
|
+
/** Bounded, run-cached provider metadata lookup for a newly selected model. */
|
|
188
|
+
readonly resolveModelContextWindow?: (model: string) => Promise<number | undefined>
|
|
182
189
|
|
|
183
190
|
/**
|
|
184
191
|
* Text queued for this run since its last turn.
|
|
@@ -188,6 +195,8 @@ export interface IterationContext {
|
|
|
188
195
|
* after tool results and before the next turn.
|
|
189
196
|
*/
|
|
190
197
|
readonly inboundMessages?: () => readonly import('../../../../types/message/index.js').Message[]
|
|
198
|
+
/** Known topic-queue arrivals appended after the restored checkpoint history. */
|
|
199
|
+
readonly resumedInput?: readonly import('../../../../types/message/index.js').Message[]
|
|
191
200
|
/** Observes pending input without draining it; abort releases the waiter. */
|
|
192
201
|
readonly waitForInbound?: (signal: AbortSignal) => Promise<void>
|
|
193
202
|
|
|
@@ -146,7 +146,11 @@ export async function* runToolReview(
|
|
|
146
146
|
// user message wedged between them is rejected by the provider. Same
|
|
147
147
|
// delivery a denial already uses, without the refusal.
|
|
148
148
|
for (const msg of attachRepeatNotice(
|
|
149
|
-
attachNotice(
|
|
149
|
+
attachNotice(
|
|
150
|
+
attachSteering(batch.messages, ctx.steering, ctx.onSteeringDelivered),
|
|
151
|
+
ctx.jobNotices,
|
|
152
|
+
formatJobNote,
|
|
153
|
+
),
|
|
150
154
|
notices,
|
|
151
155
|
)) {
|
|
152
156
|
ctx.runMgr.pushMessage(msg)
|
|
@@ -14,6 +14,10 @@ import type { IterationContext } from './context.js'
|
|
|
14
14
|
export const WORKING_MEMORY_HEADER =
|
|
15
15
|
'[WORKING MEMORY] Authoritative state for this conversation — you produced these.'
|
|
16
16
|
|
|
17
|
+
// Stored in the message, so ownership survives checkpoint JSON. Unmarked
|
|
18
|
+
// inherited blocks may be opaque host ledgers and must never be guessed away.
|
|
19
|
+
const TOOL_PINS_HEADER = `${WORKING_MEMORY_HEADER}\n[Tool-managed pins]`
|
|
20
|
+
|
|
17
21
|
/**
|
|
18
22
|
* True when `content` is the pinned working-memory slot (header identity only).
|
|
19
23
|
* Shared with `compaction.ts` so the survival guard re-pins by the SAME rule.
|
|
@@ -34,16 +38,15 @@ export function isWorkingMemoryMessage(content: string | null | undefined): bool
|
|
|
34
38
|
*
|
|
35
39
|
* Failure-isolated (the `web-search` seam rule): a throwing/slow provider
|
|
36
40
|
* degrades to "no refresh this turn" (keeps the prior slot), never breaks the
|
|
37
|
-
* run.
|
|
41
|
+
* run. With no provider or tool-owned slot, the history is left unchanged.
|
|
38
42
|
*/
|
|
39
43
|
export async function refreshWorkingMemory(ctx: IterationContext): Promise<void> {
|
|
40
44
|
const provider = ctx.workingMemoryProvider
|
|
41
45
|
// What the tools pinned joins the slot beside what the host provides: a
|
|
42
46
|
// pin exists to be in front of the model every iteration, and until this
|
|
43
47
|
// the working state reached the history only through a compaction
|
|
44
|
-
// summary.
|
|
48
|
+
// summary. When the final pin is removed, retire only a tool-owned slot.
|
|
45
49
|
const pinned = ctx.workingStateManager ? renderPins(ctx.workingStateManager.getState()) : null
|
|
46
|
-
if (!provider && !pinned) return
|
|
47
50
|
|
|
48
51
|
let block = ''
|
|
49
52
|
if (provider) {
|
|
@@ -80,17 +83,32 @@ export async function refreshWorkingMemory(ctx: IterationContext): Promise<void>
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
if (!provider && !pinned) {
|
|
87
|
+
if (!ctx.workingStateManager) return
|
|
88
|
+
const prior = msgs[idx]?.content
|
|
89
|
+
if (typeof prior !== 'string' || !prior.startsWith(TOOL_PINS_HEADER)) return
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
if (!block.trim()) {
|
|
84
93
|
// Empty block ⇒ remove the slot (byte-identical-when-empty).
|
|
85
|
-
if (idx >= 0)
|
|
94
|
+
if (idx >= 0) {
|
|
95
|
+
msgs.splice(idx, 1)
|
|
96
|
+
ctx.runMgr.clearLastPromptTokens?.()
|
|
97
|
+
}
|
|
86
98
|
return
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
const
|
|
101
|
+
const header = provider ? WORKING_MEMORY_HEADER : TOOL_PINS_HEADER
|
|
102
|
+
const content = `${header}\n\n${block}`
|
|
103
|
+
if (idx >= 0 && msgs[idx]?.content === content) return
|
|
104
|
+
const wm = createSystemMessage(content, 'ephemeral')
|
|
90
105
|
if (idx >= 0) {
|
|
91
106
|
msgs[idx] = wm
|
|
92
107
|
} else {
|
|
93
108
|
// Insert as the LAST leading system message (after static + dynamic).
|
|
94
109
|
msgs.splice(leadEnd, 0, wm)
|
|
95
110
|
}
|
|
111
|
+
// The old provider reading measured a different prefix, and an insertion
|
|
112
|
+
// also moves its tail watermark. Re-estimate until the next request reports.
|
|
113
|
+
ctx.runMgr.clearLastPromptTokens?.()
|
|
96
114
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Span, SpanStatusCode } from '@opentelemetry/api'
|
|
2
|
+
import { assertNativeStructuredOutputSupported } from '../../../provider/capabilities.js'
|
|
2
3
|
import { isProviderRequestError } from '../../../provider/errors.js'
|
|
3
4
|
import { GENAI, NAMZU, chatSpanName, parentContext } from '../../../telemetry/attributes.js'
|
|
4
5
|
import {
|
|
@@ -153,6 +154,7 @@ export async function* streamProviderTurn(
|
|
|
153
154
|
readonly onAccepted: (identity: RequestImageIdentity) => Promise<void>
|
|
154
155
|
},
|
|
155
156
|
): AsyncGenerator<RunEvent, StreamingTurnResult> {
|
|
157
|
+
assertNativeStructuredOutputSupported(provider, params)
|
|
156
158
|
// The `chat {model}` span the GenAI conventions require. There was none:
|
|
157
159
|
// `chatSpanName` existed with zero call sites, so a trace carried no LLM
|
|
158
160
|
// latency at all and the token counts landed on the iteration span
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import type { Message } from '../../types/message/index.js'
|
|
1
|
+
import type { Message, UserMessage } from '../../types/message/index.js'
|
|
2
|
+
|
|
3
|
+
/** User-role runtime reports are context, not new operator intent. */
|
|
4
|
+
export function isOperatorUserMessage(message: Message): message is UserMessage {
|
|
5
|
+
if (message.role !== 'user') return false
|
|
6
|
+
const source = message.source
|
|
7
|
+
return (
|
|
8
|
+
!source ||
|
|
9
|
+
source.type === 'goal-round' ||
|
|
10
|
+
(source.type === 'runtime-context' && source.kind === 'steering')
|
|
11
|
+
)
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
/**
|
|
4
15
|
* Guidance a host hands to a turn that is already running.
|
|
@@ -134,6 +145,8 @@ export function formatSteeringNote(text: string): string {
|
|
|
134
145
|
export function attachSteering(
|
|
135
146
|
messages: readonly Message[],
|
|
136
147
|
channel: SteeringChannel | undefined,
|
|
148
|
+
/** Receives accepted operator text, never inferred from a tool's output. */
|
|
149
|
+
onDelivered?: (text: string) => void,
|
|
137
150
|
): readonly Message[] {
|
|
138
151
|
if (!channel?.pending) return messages
|
|
139
152
|
|
|
@@ -162,6 +175,10 @@ export function attachSteering(
|
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
const next = [...messages]
|
|
165
|
-
next[lastToolIndex] = {
|
|
178
|
+
next[lastToolIndex] = {
|
|
179
|
+
...target,
|
|
180
|
+
content: target.content + formatSteeringNote(guidance),
|
|
181
|
+
}
|
|
182
|
+
onDelivered?.(guidance)
|
|
166
183
|
return next
|
|
167
184
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { RunId } from '../../types/ids/index.js'
|
|
2
|
+
import type { RunEvent } from '../../types/run/index.js'
|
|
3
|
+
|
|
4
|
+
export function assertMaxToolCalls(limit: number | undefined): void {
|
|
5
|
+
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
6
|
+
throw new RangeError('maxToolCalls must be a nonnegative safe integer.')
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A run-owned admission ledger; durability belongs to the run's event store. */
|
|
11
|
+
export class ToolCallBudget {
|
|
12
|
+
private used = 0
|
|
13
|
+
private initialized?: Promise<void>
|
|
14
|
+
private chain: Promise<void> = Promise.resolve()
|
|
15
|
+
private failed: unknown
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly limit: number,
|
|
19
|
+
private readonly runId: RunId,
|
|
20
|
+
private readonly emit: (event: RunEvent) => Promise<void>,
|
|
21
|
+
private readonly read?: () => Promise<readonly RunEvent[]>,
|
|
22
|
+
) {
|
|
23
|
+
assertMaxToolCalls(limit)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private async initialize(): Promise<void> {
|
|
27
|
+
const events = (await this.read?.()) ?? []
|
|
28
|
+
if (events.length > 100_000) throw new Error('Tool-call budget recovery exceeds 100000 events.')
|
|
29
|
+
let initialized = false
|
|
30
|
+
let expectedSeq = 0
|
|
31
|
+
for (const event of events) {
|
|
32
|
+
// A replayed transcript must be complete and belong to this run.
|
|
33
|
+
if (event.runId !== this.runId || event.seq !== ++expectedSeq)
|
|
34
|
+
throw new Error('Tool-call budget recovery requires a complete run event log.')
|
|
35
|
+
if (event.type === 'tool_calls_admitted') {
|
|
36
|
+
if (
|
|
37
|
+
!Number.isSafeInteger(event.count) ||
|
|
38
|
+
event.count < 0 ||
|
|
39
|
+
!Number.isSafeInteger(event.used) ||
|
|
40
|
+
event.used !== this.used + event.count ||
|
|
41
|
+
!Number.isSafeInteger(event.limit) ||
|
|
42
|
+
event.limit < event.used ||
|
|
43
|
+
!['initialize', 'batch', 'nested', 'retry'].includes(event.kind) ||
|
|
44
|
+
(event.kind === 'initialize'
|
|
45
|
+
? initialized || event.count !== 0
|
|
46
|
+
: !initialized || event.count === 0)
|
|
47
|
+
)
|
|
48
|
+
throw new Error('Invalid tool-call admission ledger.')
|
|
49
|
+
initialized = true
|
|
50
|
+
this.used = event.used
|
|
51
|
+
} else if (
|
|
52
|
+
!initialized &&
|
|
53
|
+
(event.type === 'tool_executing' || event.type === 'tool_completed')
|
|
54
|
+
) {
|
|
55
|
+
throw new Error('Cannot establish tool-call usage for an earlier unbudgeted execution.')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!initialized) {
|
|
59
|
+
await this.emit({
|
|
60
|
+
type: 'tool_calls_admitted',
|
|
61
|
+
runId: this.runId,
|
|
62
|
+
kind: 'initialize',
|
|
63
|
+
count: 0,
|
|
64
|
+
used: 0,
|
|
65
|
+
limit: this.limit,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Reserve before work, never refund. A refused group consumes no new slots. */
|
|
71
|
+
async admit(
|
|
72
|
+
count: number,
|
|
73
|
+
kind: 'batch' | 'nested' | 'retry',
|
|
74
|
+
signal: AbortSignal,
|
|
75
|
+
): Promise<string | undefined> {
|
|
76
|
+
signal.throwIfAborted()
|
|
77
|
+
const previous = this.chain
|
|
78
|
+
let release!: () => void
|
|
79
|
+
this.chain = new Promise<void>((resolve) => {
|
|
80
|
+
release = resolve
|
|
81
|
+
})
|
|
82
|
+
try {
|
|
83
|
+
await previous
|
|
84
|
+
signal.throwIfAborted()
|
|
85
|
+
if (this.failed !== undefined) throw this.failed
|
|
86
|
+
this.initialized ??= this.initialize()
|
|
87
|
+
await this.initialized
|
|
88
|
+
signal.throwIfAborted()
|
|
89
|
+
if (count > Math.max(0, this.limit - this.used)) {
|
|
90
|
+
return `Tool-call budget exhausted: this ${kind} needs ${count} slots; ${Math.max(0, this.limit - this.used)} remain of maxToolCalls=${this.limit}. No calls in this admission were executed.`
|
|
91
|
+
}
|
|
92
|
+
if (count === 0) return undefined
|
|
93
|
+
const used = this.used + count
|
|
94
|
+
try {
|
|
95
|
+
await this.emit({
|
|
96
|
+
type: 'tool_calls_admitted',
|
|
97
|
+
runId: this.runId,
|
|
98
|
+
kind,
|
|
99
|
+
count,
|
|
100
|
+
used,
|
|
101
|
+
limit: this.limit,
|
|
102
|
+
})
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// An uncertain write must not be retried as a fresh allowance.
|
|
105
|
+
this.failed = error ?? new Error('Tool-call admission persistence failed.')
|
|
106
|
+
throw this.failed
|
|
107
|
+
}
|
|
108
|
+
this.used = used
|
|
109
|
+
signal.throwIfAborted()
|
|
110
|
+
return undefined
|
|
111
|
+
} finally {
|
|
112
|
+
release()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -43,6 +43,8 @@ export interface ToolingBootstrapConfig {
|
|
|
43
43
|
web?: import('../../types/tool/index.js').ToolContext['web']
|
|
44
44
|
toolRetryBackoff?: Partial<BackoffPolicy>
|
|
45
45
|
maxToolConcurrency?: number
|
|
46
|
+
maxToolCalls?: number
|
|
47
|
+
readToolCallBudgetEvents?: () => Promise<readonly RunEvent[]>
|
|
46
48
|
maxToolOutputChars?: number
|
|
47
49
|
maxToolContentBytes?: number
|
|
48
50
|
toolOutputDir?: string
|
|
@@ -84,6 +86,10 @@ export class ToolingBootstrap {
|
|
|
84
86
|
...(config.toolRetryBackoff !== undefined
|
|
85
87
|
? { toolRetryBackoff: config.toolRetryBackoff }
|
|
86
88
|
: {}),
|
|
89
|
+
...(config.maxToolCalls !== undefined ? { maxToolCalls: config.maxToolCalls } : {}),
|
|
90
|
+
...(config.readToolCallBudgetEvents
|
|
91
|
+
? { readToolCallBudgetEvents: config.readToolCallBudgetEvents }
|
|
92
|
+
: {}),
|
|
87
93
|
...(config.maxToolConcurrency !== undefined
|
|
88
94
|
? { maxToolConcurrency: config.maxToolConcurrency }
|
|
89
95
|
: {}),
|
package/src/scheduler/local.ts
CHANGED
|
@@ -111,6 +111,7 @@ export class LocalTaskScheduler implements TaskScheduler {
|
|
|
111
111
|
const task = await this.agentManager.sendMessage(
|
|
112
112
|
{
|
|
113
113
|
agentId: options.agentId,
|
|
114
|
+
beforeStart: options.beforeStart,
|
|
114
115
|
input: {
|
|
115
116
|
messages: [createUserMessage(options.prompt)],
|
|
116
117
|
workingDirectory: options.workingDirectory,
|
|
@@ -62,7 +62,7 @@ export interface CapacityValidator {
|
|
|
62
62
|
): Promise<void>
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* Asserts `
|
|
65
|
+
* Asserts `liveDirectChildren + pendingNewChildren ≤ projectMaxWidth`
|
|
66
66
|
* under `parentSessionId`. The width cap applies to a single spawn call —
|
|
67
67
|
* a broadcast of N recipients passes `pendingNewChildren = N`.
|
|
68
68
|
*/
|
|
@@ -77,7 +77,7 @@ export interface CapacityValidator {
|
|
|
77
77
|
/**
|
|
78
78
|
* Default validator backed by {@link SessionStore}. Uses `getAncestry` for
|
|
79
79
|
* depth (root-to-self chain length) and `getChildren` for width (count of
|
|
80
|
-
*
|
|
80
|
+
* pending or active direct sub-sessions). Historical edges stay queryable.
|
|
81
81
|
*/
|
|
82
82
|
export class DefaultCapacityValidator implements CapacityValidator {
|
|
83
83
|
constructor(private readonly store: SessionStore) {}
|
|
@@ -108,7 +108,11 @@ export class DefaultCapacityValidator implements CapacityValidator {
|
|
|
108
108
|
tenantId: TenantId,
|
|
109
109
|
): Promise<void> {
|
|
110
110
|
const existing = await this.store.getChildren(parentSessionId, tenantId)
|
|
111
|
-
const
|
|
111
|
+
const live = existing.filter(
|
|
112
|
+
(child) =>
|
|
113
|
+
child.status !== 'idle' && child.status !== 'failed' && child.status !== 'archived',
|
|
114
|
+
)
|
|
115
|
+
const total = live.length + pendingNewChildren
|
|
112
116
|
if (total > projectMaxWidth) {
|
|
113
117
|
throw new DelegationCapacityExceeded({
|
|
114
118
|
dimension: 'width',
|
package/src/store/memory/disk.ts
CHANGED
|
@@ -6,16 +6,24 @@ import type {
|
|
|
6
6
|
CreateMemoryParams,
|
|
7
7
|
MemoryContent,
|
|
8
8
|
MemoryIndexEntry,
|
|
9
|
+
MemoryRecord,
|
|
9
10
|
MemorySearchParams,
|
|
10
11
|
MemorySearchResult,
|
|
11
12
|
MemoryStore,
|
|
13
|
+
UpdateMemoryParams,
|
|
12
14
|
} from '../../types/memory/index.js'
|
|
15
|
+
import { assertMemoryStatus } from '../../types/memory/index.js'
|
|
13
16
|
import { generateMemoryId, isEntityId } from '../../utils/id.js'
|
|
14
17
|
import { SCOPE_ATTRIBUTE } from '../../utils/log/types.js'
|
|
15
18
|
import { type Logger, resolveLogger } from '../../utils/logger.js'
|
|
16
19
|
import { DiskRecordStore } from '../kv/record-store.js'
|
|
17
20
|
import { defineSchema } from '../schema.js'
|
|
18
|
-
import { InMemoryMemoryIndex } from './index.js'
|
|
21
|
+
import { InMemoryMemoryIndex, searchMemoryEntries } from './index.js'
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_MEMORY_LOCK_TIMEOUT_MS,
|
|
24
|
+
acquireMemoryOperationLock,
|
|
25
|
+
validateMemoryLockTimeout,
|
|
26
|
+
} from './operation-lock.js'
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
29
|
* This store's on-disk format, versioned as a unit — which is how a
|
|
@@ -32,27 +40,6 @@ interface StorageLocation {
|
|
|
32
40
|
readonly contentDir: string
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
const operationTails = new Map<string, Promise<void>>()
|
|
36
|
-
|
|
37
|
-
/** Serialize every operation over one canonical disk projection in this process. */
|
|
38
|
-
async function withMemoryOperationLock<T>(key: string, operation: () => Promise<T>): Promise<T> {
|
|
39
|
-
const previous = operationTails.get(key) ?? Promise.resolve()
|
|
40
|
-
let release = () => {}
|
|
41
|
-
const current = new Promise<void>((resolveCurrent) => {
|
|
42
|
-
release = resolveCurrent
|
|
43
|
-
})
|
|
44
|
-
const tail = previous.then(() => current)
|
|
45
|
-
operationTails.set(key, tail)
|
|
46
|
-
|
|
47
|
-
await previous
|
|
48
|
-
try {
|
|
49
|
-
return await operation()
|
|
50
|
-
} finally {
|
|
51
|
-
release()
|
|
52
|
-
if (operationTails.get(key) === tail) operationTails.delete(key)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
43
|
function invalidIndex(
|
|
57
44
|
reason: string,
|
|
58
45
|
details: { entryIndex?: number; field?: string } = {},
|
|
@@ -163,11 +150,14 @@ function assertMemoryContent(value: unknown, expectedId: MemoryId): asserts valu
|
|
|
163
150
|
export interface DiskMemoryStoreConfig {
|
|
164
151
|
baseDir: string
|
|
165
152
|
logger?: Logger
|
|
153
|
+
/** Maximum wait for another process's operation; default 10 seconds. Never breaks stale locks. */
|
|
154
|
+
lockTimeoutMs?: number
|
|
166
155
|
}
|
|
167
156
|
|
|
168
157
|
export class DiskMemoryStore implements MemoryStore {
|
|
169
158
|
private readonly baseDir: string
|
|
170
159
|
private readonly log: Logger
|
|
160
|
+
private readonly lockTimeoutMs: number
|
|
171
161
|
private readonly index = new InMemoryMemoryIndex()
|
|
172
162
|
private location?: StorageLocation
|
|
173
163
|
// Two instances of one primitive rather than two copies of its body.
|
|
@@ -180,6 +170,8 @@ export class DiskMemoryStore implements MemoryStore {
|
|
|
180
170
|
constructor(config: DiskMemoryStoreConfig) {
|
|
181
171
|
this.baseDir = join(config.baseDir, 'memory')
|
|
182
172
|
this.log = resolveLogger(config.logger).child({ [SCOPE_ATTRIBUTE]: 'store/memory/disk' })
|
|
173
|
+
this.lockTimeoutMs = config.lockTimeoutMs ?? DEFAULT_MEMORY_LOCK_TIMEOUT_MS
|
|
174
|
+
validateMemoryLockTimeout(this.lockTimeoutMs)
|
|
183
175
|
}
|
|
184
176
|
|
|
185
177
|
private get contentDir(): string {
|
|
@@ -235,10 +227,16 @@ export class DiskMemoryStore implements MemoryStore {
|
|
|
235
227
|
operation: (location: StorageLocation) => Promise<T>,
|
|
236
228
|
): Promise<T> {
|
|
237
229
|
const location = await this.storageLocation()
|
|
238
|
-
|
|
230
|
+
const release = await acquireMemoryOperationLock(
|
|
231
|
+
join(dirname(location.indexPath), 'operation.lock'),
|
|
232
|
+
this.lockTimeoutMs,
|
|
233
|
+
)
|
|
234
|
+
try {
|
|
239
235
|
await this.reloadIndex(location)
|
|
240
|
-
return operation(location)
|
|
241
|
-
}
|
|
236
|
+
return await operation(location)
|
|
237
|
+
} finally {
|
|
238
|
+
await release()
|
|
239
|
+
}
|
|
242
240
|
}
|
|
243
241
|
|
|
244
242
|
private async readContent(location: StorageLocation, id: MemoryId): Promise<MemoryContent> {
|
|
@@ -307,10 +305,17 @@ export class DiskMemoryStore implements MemoryStore {
|
|
|
307
305
|
})
|
|
308
306
|
}
|
|
309
307
|
|
|
310
|
-
async
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
308
|
+
async getRecord(id: MemoryId): Promise<MemoryRecord | undefined> {
|
|
309
|
+
return this.withAuthoritativeIndex(async (location) => {
|
|
310
|
+
assertStorageMemoryId(id, (reason) => invalidContent(id, reason, { field: 'id' }))
|
|
311
|
+
const entry = this.index.getEntry(id)
|
|
312
|
+
if (!entry) return undefined
|
|
313
|
+
return structuredClone({ entry, content: await this.readContent(location, id) })
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async update(id: MemoryId, updates: UpdateMemoryParams): Promise<MemoryIndexEntry | undefined> {
|
|
318
|
+
if (updates.status !== undefined) assertMemoryStatus(updates.status)
|
|
314
319
|
return this.withAuthoritativeIndex(async (location) => {
|
|
315
320
|
assertStorageMemoryId(id, (reason) => invalidContent(id, reason, { field: 'id' }))
|
|
316
321
|
const existing = this.index.getEntry(id)
|
|
@@ -321,6 +326,7 @@ export class DiskMemoryStore implements MemoryStore {
|
|
|
321
326
|
title: updates.title ?? existing.title,
|
|
322
327
|
summary: updates.summary ?? existing.summary,
|
|
323
328
|
tags: updates.tags ? [...updates.tags] : existing.tags,
|
|
329
|
+
status: updates.status ?? existing.status,
|
|
324
330
|
updatedAt: Date.now(),
|
|
325
331
|
}
|
|
326
332
|
const updatesContent =
|
|
@@ -367,7 +373,23 @@ export class DiskMemoryStore implements MemoryStore {
|
|
|
367
373
|
}
|
|
368
374
|
|
|
369
375
|
async list(params?: MemorySearchParams): Promise<MemorySearchResult> {
|
|
370
|
-
return this.withAuthoritativeIndex(async () =>
|
|
376
|
+
return this.withAuthoritativeIndex(async (location) => {
|
|
377
|
+
if (!params?.query?.trim() && !params?.requiredIdentifiers?.length)
|
|
378
|
+
return this.index.search(params ?? {})
|
|
379
|
+
// Search the current bodies inside the same operation as the index
|
|
380
|
+
// snapshot. No retained cache can hide another process's update.
|
|
381
|
+
const candidates = this.index.search({
|
|
382
|
+
...params,
|
|
383
|
+
query: undefined,
|
|
384
|
+
limit: undefined,
|
|
385
|
+
requiredIdentifiers: undefined,
|
|
386
|
+
})
|
|
387
|
+
const contents = new Map<MemoryId, string>()
|
|
388
|
+
for (const entry of candidates.entries) {
|
|
389
|
+
contents.set(entry.id, (await this.readContent(location, entry.id)).content)
|
|
390
|
+
}
|
|
391
|
+
return searchMemoryEntries(candidates.entries, params, (id) => contents.get(id) ?? '')
|
|
392
|
+
})
|
|
371
393
|
}
|
|
372
394
|
|
|
373
395
|
getIndex(): InMemoryMemoryIndex {
|