@namzu/sdk 3.0.0 → 3.2.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 +223 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
- package/dist/advisory/__tests__/consultation-context.test.js +124 -0
- package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
- package/dist/advisory/context.d.ts +25 -0
- package/dist/advisory/context.d.ts.map +1 -1
- package/dist/advisory/context.js +18 -0
- package/dist/advisory/context.js.map +1 -1
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +25 -3
- package/dist/advisory/executor.js.map +1 -1
- package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
- package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/context-reducer.test.js +197 -0
- package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
- package/dist/compaction/factory.d.ts +7 -0
- package/dist/compaction/factory.d.ts.map +1 -1
- package/dist/compaction/factory.js +7 -0
- package/dist/compaction/factory.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -0
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/interface.d.ts +13 -0
- package/dist/compaction/interface.d.ts.map +1 -1
- package/dist/compaction/managers/null.d.ts +3 -0
- package/dist/compaction/managers/null.d.ts.map +1 -1
- package/dist/compaction/managers/null.js +3 -0
- package/dist/compaction/managers/null.js.map +1 -1
- package/dist/compaction/managers/slidingWindow.d.ts +6 -0
- package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
- package/dist/compaction/managers/slidingWindow.js +6 -0
- package/dist/compaction/managers/slidingWindow.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts +10 -0
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +10 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/reducer.d.ts +86 -0
- package/dist/compaction/reducer.d.ts.map +1 -0
- package/dist/compaction/reducer.js +77 -0
- package/dist/compaction/reducer.js.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +24 -2
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/http.test.js +18 -2
- package/dist/connector/builtins/http.test.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +52 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +86 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +12 -1
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +19 -4
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +2 -2
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -1
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/server.d.ts +42 -1
- package/dist/connector/mcp/server.d.ts.map +1 -1
- package/dist/connector/mcp/server.js +77 -4
- package/dist/connector/mcp/server.js.map +1 -1
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
- package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
- package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
- package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
- package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
- package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts +41 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +29 -1
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/loader.d.ts +39 -3
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +37 -4
- package/dist/plugin/loader.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 +11 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
- package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
- package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
- package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
- package/dist/rag/ingestion.d.ts.map +1 -1
- package/dist/rag/ingestion.js +1 -0
- package/dist/rag/ingestion.js.map +1 -1
- package/dist/rag/retriever.d.ts.map +1 -1
- package/dist/rag/retriever.js +2 -0
- package/dist/rag/retriever.js.map +1 -1
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js +6 -0
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +113 -109
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
- package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +10 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +10 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +26 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +118 -34
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +95 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
- 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/resume-run.d.ts +70 -0
- package/dist/runtime/query/resume-run.d.ts.map +1 -0
- package/dist/runtime/query/resume-run.js +46 -0
- package/dist/runtime/query/resume-run.js.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.js +124 -0
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
- package/dist/tools/advisory/index.js +1 -1
- package/dist/tools/advisory/index.js.map +1 -1
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/edit.test.js +242 -1
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
- package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
- package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
- package/dist/tools/builtins/atomic-write-file.js +31 -2
- package/dist/tools/builtins/atomic-write-file.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
- package/dist/tools/builtins/content-fingerprint.js +30 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +172 -31
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +5 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +75 -27
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +75 -5
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
- package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
- package/dist/tools/coordinator/plan-dependencies.js +148 -0
- package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
- package/dist/types/agent/supervisor.d.ts +15 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/connector/core.d.ts +34 -0
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +10 -0
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +13 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/rag/retrieval.d.ts +16 -0
- package/dist/types/rag/retrieval.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +9 -0
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +11 -0
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/types/router/task-router.d.ts +19 -0
- package/dist/types/router/task-router.d.ts.map +1 -1
- package/dist/types/run/prepare-step.d.ts +56 -3
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +17 -1
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/toolset/index.d.ts +22 -0
- package/dist/types/toolset/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/__tests__/consultation-context.test.ts +191 -0
- package/src/advisory/context.ts +32 -0
- package/src/advisory/executor.ts +30 -3
- package/src/compaction/__tests__/context-reducer.test.ts +239 -0
- package/src/compaction/factory.ts +7 -0
- package/src/compaction/index.ts +8 -0
- package/src/compaction/interface.ts +13 -0
- package/src/compaction/managers/null.ts +3 -0
- package/src/compaction/managers/slidingWindow.ts +6 -0
- package/src/compaction/managers/structured.ts +10 -0
- package/src/compaction/reducer.ts +154 -0
- package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
- package/src/connector/builtins/http.test.ts +28 -2
- package/src/connector/builtins/http.ts +26 -2
- package/src/connector/index.ts +6 -2
- package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
- package/src/connector/mcp/client.ts +95 -0
- package/src/connector/mcp/discovery.ts +19 -4
- package/src/connector/mcp/index.ts +6 -2
- package/src/connector/mcp/server.ts +101 -3
- package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
- package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
- package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
- package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
- package/src/plugin/lifecycle.ts +57 -1
- package/src/plugin/loader.ts +57 -3
- package/src/public-runtime.ts +16 -1
- package/src/public-types.ts +5 -0
- package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
- package/src/rag/ingestion.ts +1 -0
- package/src/rag/retriever.ts +2 -0
- package/src/rag/vector-store.ts +5 -0
- package/src/registry/tool/execute.ts +123 -119
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
- package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
- package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
- package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
- package/src/runtime/query/executor.ts +9 -1
- package/src/runtime/query/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +130 -34
- package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
- package/src/runtime/query/iteration/phases/compaction.ts +106 -5
- package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/resume-run.ts +93 -0
- package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
- package/src/telemetry/__tests__/span-closure.test.ts +153 -0
- package/src/tools/advisory/index.ts +1 -1
- package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
- package/src/tools/builtins/__tests__/edit.test.ts +324 -1
- package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
- package/src/tools/builtins/atomic-write-file.ts +30 -2
- package/src/tools/builtins/content-fingerprint.ts +33 -0
- package/src/tools/builtins/edit.ts +188 -36
- package/src/tools/builtins/read-file.ts +5 -2
- package/src/tools/builtins/write-file.ts +79 -28
- package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
- package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +116 -42
- package/src/tools/coordinator/plan-dependencies.ts +175 -0
- package/src/types/agent/supervisor.ts +15 -0
- package/src/types/connector/core.ts +34 -0
- package/src/types/connector/definition.ts +10 -0
- package/src/types/connector/mcp.ts +14 -0
- package/src/types/rag/retrieval.ts +16 -0
- package/src/types/rag/storage.ts +9 -0
- package/src/types/rag/vector.ts +11 -0
- package/src/types/router/task-router.ts +19 -0
- package/src/types/run/prepare-step.ts +58 -3
- package/src/types/tool/index.ts +17 -1
- package/src/types/toolset/index.ts +22 -0
|
@@ -5,11 +5,13 @@ import {
|
|
|
5
5
|
DEFAULT_STRUCTURED_OUTPUT_RETRIES,
|
|
6
6
|
STRUCTURED_OUTPUT_REPROMPT,
|
|
7
7
|
} from '../../../constants/tools/index.js'
|
|
8
|
+
import { renderSkillsSection } from '../../../persona/assembler.js'
|
|
8
9
|
import { collect } from '../../../provider/collect.js'
|
|
9
10
|
import {
|
|
10
11
|
GENAI,
|
|
11
12
|
NAMZU,
|
|
12
13
|
agentIterationSpanName,
|
|
14
|
+
chatSpanName,
|
|
13
15
|
parentContext,
|
|
14
16
|
} from '../../../telemetry/attributes.js'
|
|
15
17
|
import { getTracer } from '../../../telemetry/runtime-accessors.js'
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
createSystemMessage,
|
|
22
24
|
createUserMessage,
|
|
23
25
|
} from '../../../types/message/index.js'
|
|
26
|
+
import type { ToolChoice } from '../../../types/provider/chat.js'
|
|
24
27
|
import { classifyProviderError } from '../../../types/provider/errors.js'
|
|
25
28
|
import type { ChatCompletionResponse } from '../../../types/provider/index.js'
|
|
26
29
|
import type { AnswerReview } from '../../../types/run/answer-review.js'
|
|
@@ -30,6 +33,8 @@ import type {
|
|
|
30
33
|
StepResult,
|
|
31
34
|
StopReason,
|
|
32
35
|
} from '../../../types/run/index.js'
|
|
36
|
+
import type { Skill } from '../../../types/skills/index.js'
|
|
37
|
+
import type { LLMToolSchema, ToolRegistryContract } from '../../../types/tool/index.js'
|
|
33
38
|
import { toErrorMessage } from '../../../utils/error.js'
|
|
34
39
|
import { generateMessageId } from '../../../utils/id.js'
|
|
35
40
|
import type { ToolCallOutcome } from '../executor.js'
|
|
@@ -149,23 +154,30 @@ export class IterationOrchestrator {
|
|
|
149
154
|
{},
|
|
150
155
|
parentContext(this.ctx.rootSpan),
|
|
151
156
|
)
|
|
152
|
-
//
|
|
153
|
-
|
|
157
|
+
// Declared out here so the iteration's own finally can close it on
|
|
158
|
+
// any path that does not reach its success branch.
|
|
159
|
+
let chatSpan: Span | undefined
|
|
160
|
+
try {
|
|
161
|
+
// Tool spans for this turn belong under this iteration. Inside
|
|
162
|
+
// the try rather than before it: a throw from any of these left
|
|
163
|
+
// the span open, and an iteration span that never ends is a
|
|
164
|
+
// trace that never closes — the export is incomplete for exactly
|
|
165
|
+
// the run that failed.
|
|
166
|
+
this.ctx.toolExecutor.setParentSpan(iterSpan)
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
iterSpan.setAttributes({
|
|
169
|
+
[NAMZU.ITERATION]: iterationNum,
|
|
170
|
+
[NAMZU.RUN_ID]: runMgr.id,
|
|
171
|
+
[GENAI.REQUEST_MODEL]: model,
|
|
172
|
+
})
|
|
160
173
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
await this.ctx.emitEvent({
|
|
175
|
+
type: 'iteration_started',
|
|
176
|
+
runId: runMgr.id,
|
|
177
|
+
iteration: iterationNum,
|
|
178
|
+
})
|
|
179
|
+
yield* this.ctx.drainPending()
|
|
167
180
|
|
|
168
|
-
try {
|
|
169
181
|
if (this.ctx.pluginManager) {
|
|
170
182
|
const hookResults = await this.ctx.pluginManager.executeHooks(
|
|
171
183
|
'iteration_start',
|
|
@@ -200,6 +212,7 @@ export class IterationOrchestrator {
|
|
|
200
212
|
const step = await this.prepareStep(iterationNum)
|
|
201
213
|
|
|
202
214
|
const llmTools = this.ctx.tools.toLLMTools(step.allowedTools ?? this.ctx.allowedTools)
|
|
215
|
+
const enforceToolInputSchema = enforcedModelInputToolNames(this.ctx.tools, llmTools)
|
|
203
216
|
const stepModel = step.model ?? model
|
|
204
217
|
|
|
205
218
|
const baseMessages = forceFinalize
|
|
@@ -223,8 +236,17 @@ export class IterationOrchestrator {
|
|
|
223
236
|
// exactly this reason. Shallow is enough: the defect is array
|
|
224
237
|
// mutation, and per-iteration this is trivial next to the model
|
|
225
238
|
// call it precedes.
|
|
226
|
-
|
|
227
|
-
|
|
239
|
+
// A step's skills and its guidance ride the same ephemeral
|
|
240
|
+
// trailing system message. Appending leaves the cached prefix
|
|
241
|
+
// intact; rewriting the run's own prompt to carry a phase's
|
|
242
|
+
// skills would invalidate it on every iteration.
|
|
243
|
+
// `renderSkillsSection` already answers null for an empty list, so
|
|
244
|
+
// there is no length check here — a second guard for the same
|
|
245
|
+
// case is one more thing to keep in agreement with the first.
|
|
246
|
+
const stepSkills = step.skills ? renderSkillsSection([...step.skills]) : null
|
|
247
|
+
const stepPreamble = [step.system, stepSkills].filter(Boolean).join('\n\n')
|
|
248
|
+
const messages = stepPreamble
|
|
249
|
+
? [...baseMessages, createSystemMessage(stepPreamble)]
|
|
228
250
|
: [...baseMessages]
|
|
229
251
|
|
|
230
252
|
if (this.ctx.pluginManager) {
|
|
@@ -260,13 +282,45 @@ export class IterationOrchestrator {
|
|
|
260
282
|
// aggregated `ChatCompletionResponse` for the legacy
|
|
261
283
|
// downstream paths (assistantMsg construction, working
|
|
262
284
|
// state extraction, telemetry attribute stamping).
|
|
285
|
+
// The model call gets its own span. There was none at all —
|
|
286
|
+
// `chatSpanName` shipped with zero call sites — so a run's traces
|
|
287
|
+
// carried no LLM latency whatsoever, and the one thing anybody
|
|
288
|
+
// opens a trace to find (which turn was slow, and why) was the
|
|
289
|
+
// one thing not in it.
|
|
290
|
+
chatSpan = tracer.startSpan(chatSpanName(stepModel), {}, parentContext(iterSpan))
|
|
291
|
+
chatSpan.setAttributes({
|
|
292
|
+
[GENAI.OPERATION_NAME]: 'chat',
|
|
293
|
+
[GENAI.SYSTEM]: this.ctx.provider.id,
|
|
294
|
+
[GENAI.REQUEST_MODEL]: stepModel,
|
|
295
|
+
...((step.temperature ?? runConfig.temperature) !== undefined
|
|
296
|
+
? { [GENAI.REQUEST_TEMPERATURE]: (step.temperature ?? runConfig.temperature) as number }
|
|
297
|
+
: {}),
|
|
298
|
+
...((step.maxResponseTokens ?? runConfig.maxResponseTokens) !== undefined
|
|
299
|
+
? {
|
|
300
|
+
[GENAI.REQUEST_MAX_TOKENS]: (step.maxResponseTokens ??
|
|
301
|
+
runConfig.maxResponseTokens) as number,
|
|
302
|
+
}
|
|
303
|
+
: {}),
|
|
304
|
+
})
|
|
305
|
+
|
|
263
306
|
const { response, messageId } = yield* streamProviderTurn(
|
|
264
307
|
this.ctx.provider,
|
|
265
308
|
{
|
|
266
309
|
model: stepModel,
|
|
267
310
|
messages,
|
|
268
311
|
tools: llmTools.length > 0 ? llmTools : undefined,
|
|
269
|
-
|
|
312
|
+
...(enforceToolInputSchema ? { enforceToolInputSchema } : {}),
|
|
313
|
+
// The forced-final turn wins: a step that asked to force a
|
|
314
|
+
// tool cannot override the loop's own decision to stop
|
|
315
|
+
// asking for them. Otherwise the step's choice applies —
|
|
316
|
+
// and only to this step, because the next one is prepared
|
|
317
|
+
// from scratch.
|
|
318
|
+
toolChoice:
|
|
319
|
+
forceFinalize && llmTools.length > 0
|
|
320
|
+
? 'none'
|
|
321
|
+
: llmTools.length > 0
|
|
322
|
+
? step.toolChoice
|
|
323
|
+
: undefined,
|
|
270
324
|
temperature: step.temperature ?? runConfig.temperature,
|
|
271
325
|
maxTokens: step.maxResponseTokens ?? runConfig.maxResponseTokens,
|
|
272
326
|
cacheControl: { type: 'auto' },
|
|
@@ -285,6 +339,27 @@ export class IterationOrchestrator {
|
|
|
285
339
|
iterSpan,
|
|
286
340
|
)
|
|
287
341
|
|
|
342
|
+
// Stamped on the call that produced them. The token counts also
|
|
343
|
+
// stay on the iteration span below, where they have always been:
|
|
344
|
+
// moving them would silently break whatever reads them today,
|
|
345
|
+
// and one turn per iteration makes the two agree.
|
|
346
|
+
chatSpan.setAttributes({
|
|
347
|
+
[GENAI.RESPONSE_MODEL]: response.model || stepModel,
|
|
348
|
+
[GENAI.RESPONSE_ID]: response.id,
|
|
349
|
+
[GENAI.USAGE_INPUT_TOKENS]: response.usage.promptTokens,
|
|
350
|
+
[GENAI.USAGE_OUTPUT_TOKENS]: response.usage.completionTokens,
|
|
351
|
+
// An array, per the semantic convention: one call can finish
|
|
352
|
+
// several ways when a provider returns more than one choice.
|
|
353
|
+
[GENAI.RESPONSE_FINISH_REASONS]: [response.finishReason ?? 'stop'],
|
|
354
|
+
[NAMZU.CACHE_READ_TOKENS]: response.usage.cachedTokens ?? 0,
|
|
355
|
+
[NAMZU.CACHE_WRITE_TOKENS]: response.usage.cacheWriteTokens ?? 0,
|
|
356
|
+
})
|
|
357
|
+
chatSpan.setStatus({ code: SpanStatusCode.OK })
|
|
358
|
+
chatSpan.end()
|
|
359
|
+
// Closed here for an accurate duration, and cleared so the
|
|
360
|
+
// iteration finally does not close it a second time.
|
|
361
|
+
chatSpan = undefined
|
|
362
|
+
|
|
288
363
|
// Main-loop turn: also records the prompt size compaction reads.
|
|
289
364
|
runMgr.recordTurnUsage(response.usage)
|
|
290
365
|
|
|
@@ -443,7 +518,6 @@ export class IterationOrchestrator {
|
|
|
443
518
|
hasToolCalls: false,
|
|
444
519
|
})
|
|
445
520
|
yield* this.ctx.drainPending()
|
|
446
|
-
iterSpan.end()
|
|
447
521
|
continue
|
|
448
522
|
}
|
|
449
523
|
|
|
@@ -461,7 +535,6 @@ export class IterationOrchestrator {
|
|
|
461
535
|
attempts: attempt - 1,
|
|
462
536
|
})
|
|
463
537
|
runMgr.setStopReason('structured_output_failed')
|
|
464
|
-
iterSpan.end()
|
|
465
538
|
break
|
|
466
539
|
}
|
|
467
540
|
this.ctx.log.info('Re-prompting for structured output', {
|
|
@@ -477,7 +550,6 @@ export class IterationOrchestrator {
|
|
|
477
550
|
hasToolCalls: false,
|
|
478
551
|
})
|
|
479
552
|
yield* this.ctx.drainPending()
|
|
480
|
-
iterSpan.end()
|
|
481
553
|
continue
|
|
482
554
|
}
|
|
483
555
|
|
|
@@ -506,7 +578,6 @@ export class IterationOrchestrator {
|
|
|
506
578
|
limit,
|
|
507
579
|
})
|
|
508
580
|
runMgr.setStopReason('answer_rejected')
|
|
509
|
-
iterSpan.end()
|
|
510
581
|
break
|
|
511
582
|
}
|
|
512
583
|
this.ctx.log.info('Answer rejected — returning it to the model', {
|
|
@@ -522,7 +593,6 @@ export class IterationOrchestrator {
|
|
|
522
593
|
hasToolCalls: false,
|
|
523
594
|
})
|
|
524
595
|
yield* this.ctx.drainPending()
|
|
525
|
-
iterSpan.end()
|
|
526
596
|
continue
|
|
527
597
|
}
|
|
528
598
|
}
|
|
@@ -550,11 +620,9 @@ export class IterationOrchestrator {
|
|
|
550
620
|
if (this.ctx.abortController.signal.aborted) {
|
|
551
621
|
runMgr.setStopReason('cancelled')
|
|
552
622
|
runMgr.markCancelled()
|
|
553
|
-
iterSpan.end()
|
|
554
623
|
break
|
|
555
624
|
}
|
|
556
625
|
runMgr.setStopReason('end_turn')
|
|
557
|
-
iterSpan.end()
|
|
558
626
|
break
|
|
559
627
|
}
|
|
560
628
|
|
|
@@ -576,12 +644,10 @@ export class IterationOrchestrator {
|
|
|
576
644
|
})
|
|
577
645
|
|
|
578
646
|
if (reviewOutcome.decision === 'stop') {
|
|
579
|
-
iterSpan.end()
|
|
580
647
|
return
|
|
581
648
|
}
|
|
582
649
|
|
|
583
650
|
if (reviewOutcome.decision === 'rejected') {
|
|
584
|
-
iterSpan.end()
|
|
585
651
|
continue
|
|
586
652
|
}
|
|
587
653
|
|
|
@@ -601,7 +667,6 @@ export class IterationOrchestrator {
|
|
|
601
667
|
hasToolCalls: true,
|
|
602
668
|
})
|
|
603
669
|
yield* this.ctx.drainPending()
|
|
604
|
-
iterSpan.end()
|
|
605
670
|
break
|
|
606
671
|
}
|
|
607
672
|
|
|
@@ -628,7 +693,6 @@ export class IterationOrchestrator {
|
|
|
628
693
|
hasToolCalls: true,
|
|
629
694
|
})
|
|
630
695
|
yield* this.ctx.drainPending()
|
|
631
|
-
iterSpan.end()
|
|
632
696
|
break
|
|
633
697
|
}
|
|
634
698
|
|
|
@@ -648,13 +712,11 @@ export class IterationOrchestrator {
|
|
|
648
712
|
hasToolCalls: true,
|
|
649
713
|
})
|
|
650
714
|
yield* this.ctx.drainPending()
|
|
651
|
-
iterSpan.end()
|
|
652
715
|
break
|
|
653
716
|
}
|
|
654
717
|
|
|
655
718
|
const checkpointSignal = yield* runIterationCheckpoint(this.ctx, iterationNum)
|
|
656
719
|
if (checkpointSignal === 'stop') {
|
|
657
|
-
iterSpan.end()
|
|
658
720
|
return
|
|
659
721
|
}
|
|
660
722
|
|
|
@@ -677,7 +739,6 @@ export class IterationOrchestrator {
|
|
|
677
739
|
hasToolCalls: true,
|
|
678
740
|
})
|
|
679
741
|
yield* this.ctx.drainPending()
|
|
680
|
-
iterSpan.end()
|
|
681
742
|
} catch (err) {
|
|
682
743
|
// A Stop that aborted the in-flight turn surfaces here as a
|
|
683
744
|
// thrown abort (the provider stream was raced against the run
|
|
@@ -689,7 +750,6 @@ export class IterationOrchestrator {
|
|
|
689
750
|
if (this.ctx.abortController.signal.aborted) {
|
|
690
751
|
runMgr.setStopReason('cancelled')
|
|
691
752
|
runMgr.markCancelled()
|
|
692
|
-
iterSpan.end()
|
|
693
753
|
break
|
|
694
754
|
}
|
|
695
755
|
|
|
@@ -719,7 +779,6 @@ export class IterationOrchestrator {
|
|
|
719
779
|
if (iterationActivity) {
|
|
720
780
|
this.ctx.activityStore.complete(iterationActivity.id)
|
|
721
781
|
}
|
|
722
|
-
iterSpan.end()
|
|
723
782
|
continue
|
|
724
783
|
}
|
|
725
784
|
}
|
|
@@ -733,8 +792,15 @@ export class IterationOrchestrator {
|
|
|
733
792
|
message: toErrorMessage(err),
|
|
734
793
|
})
|
|
735
794
|
iterSpan.recordException(err instanceof Error ? err : new Error(String(err)))
|
|
736
|
-
iterSpan.end()
|
|
737
795
|
throw err
|
|
796
|
+
} finally {
|
|
797
|
+
// A model call that threw never reached its own close above.
|
|
798
|
+
chatSpan?.end()
|
|
799
|
+
// The only place the iteration span ends. It used to be ended at each of
|
|
800
|
+
// seventeen exits, which is a rule every future edit has to
|
|
801
|
+
// remember; a generator abandoned by its consumer never reached
|
|
802
|
+
// any of them.
|
|
803
|
+
iterSpan.end()
|
|
738
804
|
}
|
|
739
805
|
}
|
|
740
806
|
}
|
|
@@ -749,8 +815,10 @@ export class IterationOrchestrator {
|
|
|
749
815
|
*/
|
|
750
816
|
private async prepareStep(stepNumber: number): Promise<{
|
|
751
817
|
allowedTools?: string[]
|
|
818
|
+
toolChoice?: ToolChoice
|
|
752
819
|
model?: string
|
|
753
820
|
system?: string
|
|
821
|
+
skills?: readonly Skill[]
|
|
754
822
|
temperature?: number
|
|
755
823
|
maxResponseTokens?: number
|
|
756
824
|
}> {
|
|
@@ -786,8 +854,10 @@ export class IterationOrchestrator {
|
|
|
786
854
|
|
|
787
855
|
const prepared: {
|
|
788
856
|
allowedTools?: string[]
|
|
857
|
+
toolChoice?: ToolChoice
|
|
789
858
|
model?: string
|
|
790
859
|
system?: string
|
|
860
|
+
skills?: readonly Skill[]
|
|
791
861
|
temperature?: number
|
|
792
862
|
maxResponseTokens?: number
|
|
793
863
|
} = {}
|
|
@@ -806,8 +876,10 @@ export class IterationOrchestrator {
|
|
|
806
876
|
}
|
|
807
877
|
prepared.allowedTools = known
|
|
808
878
|
}
|
|
879
|
+
if (result.toolChoice !== undefined) prepared.toolChoice = result.toolChoice
|
|
809
880
|
if (result.model !== undefined) prepared.model = result.model
|
|
810
881
|
if (result.system !== undefined) prepared.system = result.system
|
|
882
|
+
if (result.skills !== undefined) prepared.skills = result.skills
|
|
811
883
|
if (result.temperature !== undefined) prepared.temperature = result.temperature
|
|
812
884
|
if (result.maxResponseTokens !== undefined) {
|
|
813
885
|
prepared.maxResponseTokens = result.maxResponseTokens
|
|
@@ -1042,11 +1114,13 @@ export class IterationOrchestrator {
|
|
|
1042
1114
|
// tools param identical to prior iterations (cache prefix intact,
|
|
1043
1115
|
// no 400 on tool blocks in history) and forbid use via tool_choice.
|
|
1044
1116
|
const finalTools = this.ctx.tools.toLLMTools(this.ctx.allowedTools)
|
|
1117
|
+
const finalEnforced = enforcedModelInputToolNames(this.ctx.tools, finalTools)
|
|
1045
1118
|
const response = await collect(
|
|
1046
1119
|
this.ctx.provider.chatStream({
|
|
1047
1120
|
model,
|
|
1048
1121
|
messages: finalMessages,
|
|
1049
1122
|
tools: finalTools.length > 0 ? finalTools : undefined,
|
|
1123
|
+
...(finalEnforced ? { enforceToolInputSchema: finalEnforced } : {}),
|
|
1050
1124
|
toolChoice: finalTools.length > 0 ? 'none' : undefined,
|
|
1051
1125
|
temperature: this.ctx.runConfig.temperature,
|
|
1052
1126
|
maxTokens: this.ctx.runConfig.maxResponseTokens,
|
|
@@ -1102,3 +1176,25 @@ function subtractUsage(after: TokenUsage, before: TokenUsage): TokenUsage {
|
|
|
1102
1176
|
function round6(n: number): number {
|
|
1103
1177
|
return Math.round(n * 1e6) / 1e6
|
|
1104
1178
|
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Which of the tools going out on this request have a closed model schema.
|
|
1182
|
+
*
|
|
1183
|
+
* A driver reads `enforceToolInputSchema` to decide which tool schemas to
|
|
1184
|
+
* constrain generation against. Nothing populated it, so every driver that
|
|
1185
|
+
* consumed it — three of them — was reading a permanently undefined field
|
|
1186
|
+
* and `enforceModelInput: true` on a tool meant nothing end to end.
|
|
1187
|
+
*
|
|
1188
|
+
* Computed per request rather than once, because the allowed set changes:
|
|
1189
|
+
* a deferred tool activated mid-run has to start being enforced from the
|
|
1190
|
+
* next call, not the next process.
|
|
1191
|
+
*/
|
|
1192
|
+
function enforcedModelInputToolNames(
|
|
1193
|
+
registry: ToolRegistryContract,
|
|
1194
|
+
tools: readonly LLMToolSchema[],
|
|
1195
|
+
): readonly string[] | undefined {
|
|
1196
|
+
const names = tools
|
|
1197
|
+
.map((tool) => tool.function.name)
|
|
1198
|
+
.filter((name) => registry.get(name)?.enforceModelInput === true)
|
|
1199
|
+
return names.length > 0 ? names : undefined
|
|
1200
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { WorkingStateManager } from '../../../../compaction/manager.js'
|
|
4
|
+
import { CompactionConfigSchema } from '../../../../config/runtime.js'
|
|
5
|
+
import type { CompactionConfig } from '../../../../config/runtime.js'
|
|
6
|
+
import type { Message } from '../../../../types/message/index.js'
|
|
7
|
+
import type { TaskRouterConfig } from '../../../../types/router/index.js'
|
|
8
|
+
import { runCompactionCheck } from './compaction.js'
|
|
9
|
+
import type { IterationContext } from './context.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The compaction summary is the only model call a run makes that nobody
|
|
13
|
+
* asked for, and it was hardwired to the primary model. `taskRouter` had been
|
|
14
|
+
* accepted, schema-validated and threaded through four types since it was
|
|
15
|
+
* added, with `resolveTaskModel` exported and never called — so a host who
|
|
16
|
+
* pointed compaction at a cheap model kept paying the expensive one.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function harness(taskRouter?: TaskRouterConfig): {
|
|
20
|
+
ctx: IterationContext
|
|
21
|
+
modelsUsed: string[]
|
|
22
|
+
} {
|
|
23
|
+
const messages: Message[] = [
|
|
24
|
+
{ role: 'system', content: 'prompt', timestamp: 1 },
|
|
25
|
+
...Array.from({ length: 40 }, (_, i) => ({
|
|
26
|
+
role: 'user' as const,
|
|
27
|
+
content: `turn ${i} `.repeat(200),
|
|
28
|
+
timestamp: 1,
|
|
29
|
+
})),
|
|
30
|
+
]
|
|
31
|
+
const config: CompactionConfig = {
|
|
32
|
+
...CompactionConfigSchema.parse({}),
|
|
33
|
+
contextWindowTokens: 1_000,
|
|
34
|
+
keepRecentMessages: 4,
|
|
35
|
+
llmVerification: true,
|
|
36
|
+
// Keep the slot count under the rich-state threshold so the verified
|
|
37
|
+
// summary path — the one that calls a model — is the one taken.
|
|
38
|
+
richStateThreshold: 1_000,
|
|
39
|
+
}
|
|
40
|
+
const modelsUsed: string[] = []
|
|
41
|
+
|
|
42
|
+
const ctx = {
|
|
43
|
+
compactionConfig: config,
|
|
44
|
+
workingStateManager: new WorkingStateManager(config),
|
|
45
|
+
runConfig: { model: 'primary-model' },
|
|
46
|
+
...(taskRouter ? { taskRouter } : {}),
|
|
47
|
+
runMgr: {
|
|
48
|
+
id: 'run_routing',
|
|
49
|
+
messages,
|
|
50
|
+
accumulateUsage: vi.fn(),
|
|
51
|
+
clearLastPromptTokens: vi.fn(),
|
|
52
|
+
},
|
|
53
|
+
provider: {
|
|
54
|
+
chatStream: async function* (params: { model: string }) {
|
|
55
|
+
modelsUsed.push(params.model)
|
|
56
|
+
yield { id: 'c1', delta: { content: 'a summary' } }
|
|
57
|
+
yield {
|
|
58
|
+
id: 'c1',
|
|
59
|
+
delta: {},
|
|
60
|
+
finishReason: 'stop',
|
|
61
|
+
usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
66
|
+
} as unknown as IterationContext
|
|
67
|
+
|
|
68
|
+
return { ctx, modelsUsed }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('the compaction summary goes to the model a host routed it to', () => {
|
|
72
|
+
it('uses the primary model when nothing is routed', async () => {
|
|
73
|
+
const h = harness()
|
|
74
|
+
|
|
75
|
+
await runCompactionCheck(h.ctx)
|
|
76
|
+
|
|
77
|
+
expect(h.modelsUsed).toEqual(['primary-model'])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('uses the compaction model when one is named', async () => {
|
|
81
|
+
const h = harness({ compaction: 'small-model' })
|
|
82
|
+
|
|
83
|
+
await runCompactionCheck(h.ctx)
|
|
84
|
+
|
|
85
|
+
expect(h.modelsUsed).toEqual(['small-model'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('falls back to the router default', async () => {
|
|
89
|
+
const h = harness({ default: 'fallback-model' })
|
|
90
|
+
|
|
91
|
+
await runCompactionCheck(h.ctx)
|
|
92
|
+
|
|
93
|
+
expect(h.modelsUsed).toEqual(['fallback-model'])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('prefers the specific key over the default', async () => {
|
|
97
|
+
const h = harness({ compaction: 'small-model', default: 'fallback-model' })
|
|
98
|
+
|
|
99
|
+
await runCompactionCheck(h.ctx)
|
|
100
|
+
|
|
101
|
+
expect(h.modelsUsed).toEqual(['small-model'])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('ignores a key that names no model', async () => {
|
|
105
|
+
// `null` is what the schema produces for an explicitly cleared key, and
|
|
106
|
+
// it has to mean "unrouted" rather than "route to nothing" — sending an
|
|
107
|
+
// empty model id is an endpoint error on backends where the id IS the
|
|
108
|
+
// endpoint.
|
|
109
|
+
const h = harness({ compaction: null, default: 'fallback-model' })
|
|
110
|
+
|
|
111
|
+
await runCompactionCheck(h.ctx)
|
|
112
|
+
|
|
113
|
+
expect(h.modelsUsed).toEqual(['fallback-model'])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('does not route a key the runtime does not consult', async () => {
|
|
117
|
+
// `coding` is documented as inert. Pinning that keeps the docs honest:
|
|
118
|
+
// if a future change starts consulting it, this test says so.
|
|
119
|
+
const h = harness({ coding: 'coding-model' })
|
|
120
|
+
|
|
121
|
+
await runCompactionCheck(h.ctx)
|
|
122
|
+
|
|
123
|
+
expect(h.modelsUsed).toEqual(['primary-model'])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { resolveContextWindow } from '../../../../compaction/context-window.js'
|
|
2
|
-
import { findSafeTrimIndex } from '../../../../compaction/dangling.js'
|
|
2
|
+
import { findDanglingMessages, findSafeTrimIndex } from '../../../../compaction/dangling.js'
|
|
3
|
+
import type { ContextReducer, ContextReduction } from '../../../../compaction/reducer.js'
|
|
4
|
+
import { createSlidingWindowReducer } from '../../../../compaction/reducer.js'
|
|
3
5
|
import { findRetainedIndices } from '../../../../compaction/retention.js'
|
|
4
6
|
import { serializeState } from '../../../../compaction/serializer.js'
|
|
5
7
|
import { clearStaleToolResults } from '../../../../compaction/tool-result-editing.js'
|
|
6
8
|
import { buildVerifiedSummary } from '../../../../compaction/verifier.js'
|
|
7
9
|
import { CHARS_PER_TOKEN } from '../../../../constants/limits.js'
|
|
10
|
+
import { resolveTaskModel } from '../../../../router/task-router.js'
|
|
8
11
|
import type { Message } from '../../../../types/message/index.js'
|
|
9
12
|
import { createSystemMessage } from '../../../../types/message/index.js'
|
|
10
13
|
import type { IterationContext } from './context.js'
|
|
@@ -202,6 +205,78 @@ function totalChars(messages: readonly { content: unknown }[]): number {
|
|
|
202
205
|
return total
|
|
203
206
|
}
|
|
204
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Run a reducer and install what it returns, or leave the history alone.
|
|
210
|
+
*
|
|
211
|
+
* Three ways to decline, all of them ending the same way — the run keeps its
|
|
212
|
+
* full history. `undefined` is the reducer saying so; a throw is treated as
|
|
213
|
+
* the same answer, because a broken reduction hook should not kill a healthy
|
|
214
|
+
* run any more than a broken `prepareStep` should; and a result that splits a
|
|
215
|
+
* tool pair is REFUSED rather than repaired.
|
|
216
|
+
*
|
|
217
|
+
* That last one is the least obvious and the most important. `tool_result`
|
|
218
|
+
* without its `tool_use` is a provider 400 on the next turn, so quietly
|
|
219
|
+
* repairing it would trade a clear "this reducer split a tool pair" for an
|
|
220
|
+
* opaque rejection a call later, with the reducer never implicated. The
|
|
221
|
+
* invariant is written on {@link ContextReducer}; enforcing it where it is
|
|
222
|
+
* violated is what makes it true rather than aspirational.
|
|
223
|
+
*/
|
|
224
|
+
async function applyReducer(
|
|
225
|
+
ctx: IterationContext,
|
|
226
|
+
reducer: ContextReducer,
|
|
227
|
+
reduction: ContextReduction,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const messages = ctx.runMgr.messages
|
|
230
|
+
const before = messages.length
|
|
231
|
+
const beforeChars = totalChars(messages)
|
|
232
|
+
|
|
233
|
+
let next: readonly Message[] | undefined
|
|
234
|
+
try {
|
|
235
|
+
next = await reducer(reduction)
|
|
236
|
+
} catch (error) {
|
|
237
|
+
ctx.log.warn('Context reducer threw — keeping the full history', {
|
|
238
|
+
runId: ctx.runMgr.id,
|
|
239
|
+
reason: reduction.reason,
|
|
240
|
+
error: error instanceof Error ? error.message : String(error),
|
|
241
|
+
})
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!next || next.length >= before) {
|
|
246
|
+
ctx.log.debug('Context reducer shed nothing', {
|
|
247
|
+
runId: ctx.runMgr.id,
|
|
248
|
+
reason: reduction.reason,
|
|
249
|
+
messages: before,
|
|
250
|
+
})
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!findDanglingMessages([...next]).isValid) {
|
|
255
|
+
ctx.log.warn('Context reducer split a tool pair — refusing its result', {
|
|
256
|
+
runId: ctx.runMgr.id,
|
|
257
|
+
reason: reduction.reason,
|
|
258
|
+
hint: 'use findSafeTrimIndex to move a cut off a tool_use/tool_result boundary',
|
|
259
|
+
})
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
messages.length = 0
|
|
264
|
+
for (const message of next) messages.push(message)
|
|
265
|
+
|
|
266
|
+
// The provider's count described the pre-reduction prompt. Same reasoning
|
|
267
|
+
// as the structured path: leaving it would have the next trigger check
|
|
268
|
+
// compare an old size against the new history and reduce again.
|
|
269
|
+
ctx.runMgr.clearLastPromptTokens?.()
|
|
270
|
+
|
|
271
|
+
ctx.log.info('Context reduced', {
|
|
272
|
+
runId: ctx.runMgr.id,
|
|
273
|
+
reason: reduction.reason,
|
|
274
|
+
oldMessageCount: before,
|
|
275
|
+
newMessageCount: messages.length,
|
|
276
|
+
charsShed: beforeChars - totalChars(messages),
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
205
280
|
export async function runCompactionCheck(
|
|
206
281
|
ctx: IterationContext,
|
|
207
282
|
options?: { force?: boolean },
|
|
@@ -210,9 +285,6 @@ export async function runCompactionCheck(
|
|
|
210
285
|
if (!config) return
|
|
211
286
|
if (config.strategy === 'disabled') return
|
|
212
287
|
|
|
213
|
-
const manager = ctx.workingStateManager
|
|
214
|
-
if (!manager) return
|
|
215
|
-
|
|
216
288
|
const measured = measureContext(ctx)
|
|
217
289
|
const estimatedTokens = measured.tokens
|
|
218
290
|
|
|
@@ -232,6 +304,29 @@ export async function runCompactionCheck(
|
|
|
232
304
|
// rejected the prompt, which is stronger evidence than any estimate.
|
|
233
305
|
if (!options?.force && usage < config.triggerThreshold) return
|
|
234
306
|
|
|
307
|
+
// A reducer, when the run has one, OWNS reduction — the structured pass
|
|
308
|
+
// below does not also run. `strategy: 'sliding-window'` resolves to the
|
|
309
|
+
// built-in one; a host-supplied reducer outranks the strategy entirely,
|
|
310
|
+
// because someone who wrote a reducer has said what they want more
|
|
311
|
+
// specifically than an enum can.
|
|
312
|
+
const reducer =
|
|
313
|
+
ctx.contextReducer ??
|
|
314
|
+
(config.strategy === 'sliding-window' ? createSlidingWindowReducer() : undefined)
|
|
315
|
+
if (reducer) {
|
|
316
|
+
await applyReducer(ctx, reducer, {
|
|
317
|
+
messages: ctx.runMgr.messages,
|
|
318
|
+
reason: options?.force ? 'overflow' : 'threshold',
|
|
319
|
+
estimatedTokens,
|
|
320
|
+
contextWindowTokens: budget,
|
|
321
|
+
model: ctx.runConfig.model,
|
|
322
|
+
keepRecentMessages: config.keepRecentMessages,
|
|
323
|
+
})
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const manager = ctx.workingStateManager
|
|
328
|
+
if (!manager) return
|
|
329
|
+
|
|
235
330
|
ctx.log.info('Compaction threshold reached — compacting context', {
|
|
236
331
|
runId: ctx.runMgr.id,
|
|
237
332
|
contextTokens: estimatedTokens,
|
|
@@ -380,7 +475,13 @@ export async function runCompactionCheck(
|
|
|
380
475
|
ctx.provider,
|
|
381
476
|
config,
|
|
382
477
|
(usage) => ctx.runMgr.accumulateUsage(usage),
|
|
383
|
-
|
|
478
|
+
// The one model call a run makes that the user never asked for. It
|
|
479
|
+
// reads a transcript and writes a summary, which is the cheapest
|
|
480
|
+
// thing a small model does well, and it fires on exactly the long
|
|
481
|
+
// runs where the primary model is most expensive. `taskRouter` had
|
|
482
|
+
// been accepted, validated and threaded through four types since it
|
|
483
|
+
// was added, and nothing ever consulted it.
|
|
484
|
+
resolveTaskModel('compaction', ctx.taskRouter, ctx.runConfig.model),
|
|
384
485
|
)
|
|
385
486
|
} else {
|
|
386
487
|
compactedContent = serializeState(manager.getState())
|