@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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { WorkingStateManager } from '../../../../compaction/manager.js'
|
|
4
|
+
import type { ContextReducer } from '../../../../compaction/reducer.js'
|
|
5
|
+
import { CompactionConfigSchema } from '../../../../config/runtime.js'
|
|
6
|
+
import type { CompactionConfig } from '../../../../config/runtime.js'
|
|
7
|
+
import type { Message } from '../../../../types/message/index.js'
|
|
8
|
+
import { runCompactionCheck } from './compaction.js'
|
|
9
|
+
import type { IterationContext } from './context.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `strategy` had three values and two behaviours.
|
|
13
|
+
*
|
|
14
|
+
* The runtime asked only `strategy !== 'disabled'`, so `'sliding-window'` —
|
|
15
|
+
* the name a host picks precisely to AVOID paying for summarization — ran the
|
|
16
|
+
* full structured pass, model call included. These tests are about the
|
|
17
|
+
* dispatch rather than the algorithms: that choosing a strategy selects one,
|
|
18
|
+
* that a host-supplied reducer outranks the enum, and that the structured
|
|
19
|
+
* pass does not also run when something else owns reduction.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const sys = (content: string): Message => ({ role: 'system', content, timestamp: 1 })
|
|
23
|
+
const user = (content: string): Message => ({ role: 'user', content, timestamp: 1 })
|
|
24
|
+
|
|
25
|
+
function longHistory(): Message[] {
|
|
26
|
+
// Long enough to be over any threshold once the window is small.
|
|
27
|
+
return [sys('prompt'), ...Array.from({ length: 40 }, (_, i) => user(`turn ${i} `.repeat(200)))]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Harness {
|
|
31
|
+
readonly ctx: IterationContext
|
|
32
|
+
readonly messages: Message[]
|
|
33
|
+
readonly providerCalls: () => number
|
|
34
|
+
readonly warnings: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function harness(
|
|
38
|
+
over: Partial<IterationContext> & { compaction?: Partial<CompactionConfig> },
|
|
39
|
+
): Harness {
|
|
40
|
+
const messages = longHistory()
|
|
41
|
+
const config: CompactionConfig = {
|
|
42
|
+
...CompactionConfigSchema.parse({}),
|
|
43
|
+
// A tiny window puts the run over the trigger immediately.
|
|
44
|
+
contextWindowTokens: 1_000,
|
|
45
|
+
keepRecentMessages: 4,
|
|
46
|
+
...over.compaction,
|
|
47
|
+
}
|
|
48
|
+
const warnings: string[] = []
|
|
49
|
+
let providerCalls = 0
|
|
50
|
+
|
|
51
|
+
const ctx = {
|
|
52
|
+
compactionConfig: config,
|
|
53
|
+
workingStateManager: new WorkingStateManager(config),
|
|
54
|
+
runConfig: { model: 'mock-model' },
|
|
55
|
+
runMgr: {
|
|
56
|
+
id: 'run_dispatch',
|
|
57
|
+
messages,
|
|
58
|
+
accumulateUsage: vi.fn(),
|
|
59
|
+
clearLastPromptTokens: vi.fn(),
|
|
60
|
+
},
|
|
61
|
+
provider: {
|
|
62
|
+
// The structured path's only observable side effect that a reducer
|
|
63
|
+
// path must not have: it pays for a summary.
|
|
64
|
+
chat: async () => {
|
|
65
|
+
providerCalls++
|
|
66
|
+
return { content: 'summary', usage: { promptTokens: 1, completionTokens: 1 } }
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
log: {
|
|
70
|
+
debug: vi.fn(),
|
|
71
|
+
info: vi.fn(),
|
|
72
|
+
warn: (message: string) => warnings.push(message),
|
|
73
|
+
error: vi.fn(),
|
|
74
|
+
},
|
|
75
|
+
...over,
|
|
76
|
+
} as unknown as IterationContext
|
|
77
|
+
|
|
78
|
+
return { ctx, messages, providerCalls: () => providerCalls, warnings }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('choosing a strategy selects one', () => {
|
|
82
|
+
it("'sliding-window' trims and summarizes nothing", async () => {
|
|
83
|
+
const h = harness({ compaction: { strategy: 'sliding-window', llmVerification: true } })
|
|
84
|
+
|
|
85
|
+
await runCompactionCheck(h.ctx)
|
|
86
|
+
|
|
87
|
+
expect(h.messages.length).toBeLessThan(41)
|
|
88
|
+
// The whole reason a host names this strategy.
|
|
89
|
+
expect(h.providerCalls()).toBe(0)
|
|
90
|
+
// Every survivor is verbatim — no `[COMPACTED CONTEXT]` block appears.
|
|
91
|
+
expect(h.messages.some((m) => String(m.content).includes('COMPACTED CONTEXT'))).toBe(false)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("'structured' still summarizes", async () => {
|
|
95
|
+
const h = harness({ compaction: { strategy: 'structured', llmVerification: false } })
|
|
96
|
+
|
|
97
|
+
await runCompactionCheck(h.ctx)
|
|
98
|
+
|
|
99
|
+
expect(h.messages.some((m) => String(m.content).includes('COMPACTED CONTEXT'))).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("'disabled' leaves the history alone", async () => {
|
|
103
|
+
const h = harness({ compaction: { strategy: 'disabled' } })
|
|
104
|
+
|
|
105
|
+
await runCompactionCheck(h.ctx)
|
|
106
|
+
|
|
107
|
+
expect(h.messages.length).toBe(41)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('a host reducer outranks the enum', () => {
|
|
112
|
+
it('runs instead of the structured pass', async () => {
|
|
113
|
+
// Deliberately NOT a tiny slice: leave more than the structured pass
|
|
114
|
+
// needs to act, so "the reducer ran" and "only the reducer ran" are
|
|
115
|
+
// different observations.
|
|
116
|
+
const reduce: ContextReducer = ({ messages }) => messages.slice(0, 20)
|
|
117
|
+
const h = harness({ contextReducer: reduce, compaction: { strategy: 'structured' } })
|
|
118
|
+
|
|
119
|
+
await runCompactionCheck(h.ctx)
|
|
120
|
+
|
|
121
|
+
expect(h.messages.length).toBe(20)
|
|
122
|
+
expect(h.messages.some((m) => String(m.content).includes('COMPACTED CONTEXT'))).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('is told whether the provider already rejected the prompt', async () => {
|
|
126
|
+
const seen: string[] = []
|
|
127
|
+
const reduce: ContextReducer = ({ messages, reason }) => {
|
|
128
|
+
seen.push(reason)
|
|
129
|
+
return messages.slice(0, 5)
|
|
130
|
+
}
|
|
131
|
+
const h = harness({ contextReducer: reduce })
|
|
132
|
+
|
|
133
|
+
await runCompactionCheck(h.ctx)
|
|
134
|
+
await runCompactionCheck(h.ctx, { force: true })
|
|
135
|
+
|
|
136
|
+
expect(seen).toEqual(['threshold', 'overflow'])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('may be async, so a reducer can call a model of its own', async () => {
|
|
140
|
+
const reduce: ContextReducer = async ({ messages }) => {
|
|
141
|
+
await Promise.resolve()
|
|
142
|
+
return messages.slice(0, 3)
|
|
143
|
+
}
|
|
144
|
+
const h = harness({ contextReducer: reduce })
|
|
145
|
+
|
|
146
|
+
await runCompactionCheck(h.ctx)
|
|
147
|
+
|
|
148
|
+
expect(h.messages.length).toBe(3)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('gets the window it has to fit, not the run token budget', async () => {
|
|
152
|
+
let saw = 0
|
|
153
|
+
const reduce: ContextReducer = ({ contextWindowTokens, messages }) => {
|
|
154
|
+
saw = contextWindowTokens
|
|
155
|
+
return messages.slice(0, 5)
|
|
156
|
+
}
|
|
157
|
+
const h = harness({ contextReducer: reduce, compaction: { contextWindowTokens: 12_345 } })
|
|
158
|
+
|
|
159
|
+
await runCompactionCheck(h.ctx)
|
|
160
|
+
|
|
161
|
+
expect(saw).toBe(12_345)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('a reducer that cannot be trusted is not obeyed', () => {
|
|
166
|
+
it('keeps the full history when the reducer throws', async () => {
|
|
167
|
+
const reduce: ContextReducer = () => {
|
|
168
|
+
throw new Error('boom')
|
|
169
|
+
}
|
|
170
|
+
const h = harness({ contextReducer: reduce })
|
|
171
|
+
|
|
172
|
+
await runCompactionCheck(h.ctx)
|
|
173
|
+
|
|
174
|
+
// Fail open: a broken reduction hook should not take down a healthy run,
|
|
175
|
+
// the same way a broken `prepareStep` does not.
|
|
176
|
+
expect(h.messages.length).toBe(41)
|
|
177
|
+
expect(h.warnings.some((w) => w.includes('threw'))).toBe(true)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('refuses a result that orphans a tool result', async () => {
|
|
181
|
+
const h = harness({
|
|
182
|
+
contextReducer: ({ messages }) => [
|
|
183
|
+
messages[0] as Message,
|
|
184
|
+
{ role: 'tool', content: 'ok', timestamp: 1, toolCallId: 'nobody' } as Message,
|
|
185
|
+
],
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
await runCompactionCheck(h.ctx)
|
|
189
|
+
|
|
190
|
+
// Installing this would trade a nameable "your reducer split a tool
|
|
191
|
+
// pair" for an opaque provider rejection a call later.
|
|
192
|
+
expect(h.messages.length).toBe(41)
|
|
193
|
+
expect(h.warnings.some((w) => w.includes('split a tool pair'))).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('ignores a reducer that returns the history unchanged', async () => {
|
|
197
|
+
const h = harness({ contextReducer: ({ messages }) => messages })
|
|
198
|
+
|
|
199
|
+
await runCompactionCheck(h.ctx)
|
|
200
|
+
|
|
201
|
+
expect(h.messages.length).toBe(41)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('leaves the history alone when the reducer declines', async () => {
|
|
205
|
+
const h = harness({ contextReducer: () => undefined })
|
|
206
|
+
|
|
207
|
+
await runCompactionCheck(h.ctx)
|
|
208
|
+
|
|
209
|
+
expect(h.messages.length).toBe(41)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('the threshold still governs a reducer', () => {
|
|
214
|
+
it('does not run one below the trigger', async () => {
|
|
215
|
+
const reduce = vi.fn(() => undefined)
|
|
216
|
+
const h = harness({
|
|
217
|
+
contextReducer: reduce,
|
|
218
|
+
// A window far larger than the history.
|
|
219
|
+
compaction: { contextWindowTokens: 10_000_000 },
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
await runCompactionCheck(h.ctx)
|
|
223
|
+
|
|
224
|
+
expect(reduce).not.toHaveBeenCalled()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('runs one below the trigger when the provider forced the issue', async () => {
|
|
228
|
+
const reduce = vi.fn(() => undefined)
|
|
229
|
+
const h = harness({
|
|
230
|
+
contextReducer: reduce,
|
|
231
|
+
compaction: { contextWindowTokens: 10_000_000 },
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
await runCompactionCheck(h.ctx, { force: true })
|
|
235
|
+
|
|
236
|
+
expect(reduce).toHaveBeenCalledOnce()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AdvisoryContext } from '../../../../advisory/context.js'
|
|
2
2
|
import type { AgentBus } from '../../../../bus/index.js'
|
|
3
3
|
import type { WorkingStateManager } from '../../../../compaction/manager.js'
|
|
4
|
+
import type { ContextReducer } from '../../../../compaction/reducer.js'
|
|
4
5
|
import type { CompactionConfig } from '../../../../config/runtime.js'
|
|
5
6
|
import type { PlanManager } from '../../../../manager/plan/lifecycle.js'
|
|
6
7
|
import type { RunPersistence } from '../../../../manager/run/persistence.js'
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
} from '../../../../types/hitl/index.js'
|
|
15
16
|
import type { TaskId } from '../../../../types/ids/index.js'
|
|
16
17
|
import type { LLMProvider } from '../../../../types/provider/index.js'
|
|
18
|
+
import type { TaskRouterConfig } from '../../../../types/router/index.js'
|
|
17
19
|
import type { ReviewAnswer } from '../../../../types/run/answer-review.js'
|
|
18
20
|
import type {
|
|
19
21
|
AgentRunConfig,
|
|
@@ -104,10 +106,19 @@ export interface IterationContext {
|
|
|
104
106
|
*/
|
|
105
107
|
readonly toolGrants?: ToolGrantSet
|
|
106
108
|
|
|
109
|
+
/** Per-task model overrides. Consulted for the compaction summary call. */
|
|
110
|
+
readonly taskRouter?: TaskRouterConfig
|
|
111
|
+
|
|
107
112
|
readonly compactionConfig?: CompactionConfig
|
|
108
113
|
|
|
109
114
|
readonly workingStateManager?: WorkingStateManager
|
|
110
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Host-supplied context reduction. Outranks `compactionConfig.strategy`
|
|
118
|
+
* and replaces the structured pass for this run.
|
|
119
|
+
*/
|
|
120
|
+
readonly contextReducer?: ContextReducer
|
|
121
|
+
|
|
111
122
|
readonly workingMemoryProvider?: WorkingMemoryProvider
|
|
112
123
|
|
|
113
124
|
readonly advisoryCtx?: AdvisoryContext
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { PendingDecision } from '../../types/hitl/index.js'
|
|
2
|
+
import type { CheckpointStore } from '../../types/run/checkpoint-store.js'
|
|
3
|
+
import type { Run } from '../../types/run/entity.js'
|
|
4
|
+
import type { RunState } from '../../types/run/state.js'
|
|
5
|
+
import { type QueryParams, drainQuery } from './index.js'
|
|
6
|
+
import { type RunStateScope, loadRunState } from './run-state.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What a resume attempt found.
|
|
10
|
+
*
|
|
11
|
+
* Three outcomes rather than a nullable `Run`, because the two failures
|
|
12
|
+
* mean opposite things to a caller. "No checkpoint" is a dead end — there
|
|
13
|
+
* is nothing to continue and starting fresh would be a different run
|
|
14
|
+
* wearing the same id. "Awaiting decision" is the run working as designed:
|
|
15
|
+
* it parked on a question, and continuing means answering it first.
|
|
16
|
+
*/
|
|
17
|
+
export type ResumeOutcome =
|
|
18
|
+
| { readonly resumed: false; readonly reason: 'no-checkpoint' }
|
|
19
|
+
| {
|
|
20
|
+
readonly resumed: false
|
|
21
|
+
readonly reason: 'awaiting-decision'
|
|
22
|
+
/** What to put in front of a human. */
|
|
23
|
+
readonly pending: PendingDecision
|
|
24
|
+
readonly state: RunState
|
|
25
|
+
}
|
|
26
|
+
| { readonly resumed: true; readonly run: Run; readonly state: RunState }
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The half of a run that cannot be serialized, plus where to look.
|
|
30
|
+
*
|
|
31
|
+
* `messages` is deliberately absent. On resume the history comes from the
|
|
32
|
+
* checkpoint and any passed messages would be discarded, so accepting them
|
|
33
|
+
* would invite a caller to hand over a transcript that is silently ignored.
|
|
34
|
+
*/
|
|
35
|
+
export interface ResumeRunParams
|
|
36
|
+
extends Omit<QueryParams, 'messages' | 'runId' | 'resumeFromCheckpoint'> {
|
|
37
|
+
/** Identifies the run inside the checkpoint store. */
|
|
38
|
+
readonly scope: RunStateScope
|
|
39
|
+
/** Required to find the checkpoint; also threaded into the resumed run. */
|
|
40
|
+
readonly checkpointStore: CheckpointStore
|
|
41
|
+
/**
|
|
42
|
+
* Resume a specific checkpoint instead of the one the store would pick.
|
|
43
|
+
* Absent means the parked checkpoint if there is one, else the newest.
|
|
44
|
+
*/
|
|
45
|
+
readonly checkpointId?: RunState['checkpointId']
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Continue a run that a different process started.
|
|
50
|
+
*
|
|
51
|
+
* Everything this needs already existed — `CheckpointManager` writes the
|
|
52
|
+
* history, budgets, working state, trace context and any park;
|
|
53
|
+
* `loadRunState` reads them back; `query` accepts `runId` +
|
|
54
|
+
* `resumeFromCheckpoint` and restores all of it. What was missing was the
|
|
55
|
+
* few lines that join them, so every host wrote their own — and in this
|
|
56
|
+
* repo nothing outside the SDK ever did, which means the whole path shipped
|
|
57
|
+
* untravelled.
|
|
58
|
+
*
|
|
59
|
+
* The division of labour is the one the mechanism already implies: the
|
|
60
|
+
* CALLER brings what cannot be serialized — the provider client, the tool
|
|
61
|
+
* registry, the sandbox, the working directory — and the STORE brings the
|
|
62
|
+
* state. A snapshot deliberately holds no socket, no client and no open
|
|
63
|
+
* file, so it could never have carried the first half.
|
|
64
|
+
*
|
|
65
|
+
* Refusing beats guessing at both failure points. A missing checkpoint does
|
|
66
|
+
* not silently become a fresh run under a recycled id, and a park is not
|
|
67
|
+
* resumed past without the answer it is waiting for.
|
|
68
|
+
*/
|
|
69
|
+
export async function resumeRun(params: ResumeRunParams): Promise<ResumeOutcome> {
|
|
70
|
+
const { scope, checkpointStore, checkpointId, pendingDecision, ...rest } = params
|
|
71
|
+
|
|
72
|
+
const state = await loadRunState(checkpointStore, scope, checkpointId)
|
|
73
|
+
if (!state?.checkpointId) return { resumed: false, reason: 'no-checkpoint' }
|
|
74
|
+
|
|
75
|
+
// A park is outstanding until it is answered. `resolvedAt` is what marks
|
|
76
|
+
// an answered one, so a checkpoint that carries a resolved decision is an
|
|
77
|
+
// ordinary resume, not a question waiting on anybody.
|
|
78
|
+
const outstanding = state.pending && !state.pending.resolvedAt ? state.pending : undefined
|
|
79
|
+
if (outstanding && !pendingDecision) {
|
|
80
|
+
return { resumed: false, reason: 'awaiting-decision', pending: outstanding, state }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const run = await drainQuery({
|
|
84
|
+
...rest,
|
|
85
|
+
messages: [],
|
|
86
|
+
runId: state.runId,
|
|
87
|
+
resumeFromCheckpoint: state.checkpointId,
|
|
88
|
+
checkpointStore,
|
|
89
|
+
...(pendingDecision ? { pendingDecision } : {}),
|
|
90
|
+
} as QueryParams)
|
|
91
|
+
|
|
92
|
+
return { resumed: true, run, state }
|
|
93
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* There was no span around the model call at all.
|
|
8
|
+
*
|
|
9
|
+
* `chatSpanName` shipped in the telemetry attributes with zero call sites,
|
|
10
|
+
* so a run's traces carried no LLM latency whatsoever — and the one thing
|
|
11
|
+
* anybody opens a trace to find, which turn was slow and why, was the one
|
|
12
|
+
* thing missing from it. The token counts landed on the iteration span
|
|
13
|
+
* instead of the operation that produced them.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface Recorded {
|
|
17
|
+
name: string
|
|
18
|
+
attributes: Record<string, unknown>
|
|
19
|
+
ended: boolean
|
|
20
|
+
status?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const spans: Recorded[] = []
|
|
24
|
+
|
|
25
|
+
vi.mock('../runtime-accessors.js', () => ({
|
|
26
|
+
getTracer: () => ({
|
|
27
|
+
startSpan: (name: string) => {
|
|
28
|
+
const rec: Recorded = { name, attributes: {}, ended: false }
|
|
29
|
+
spans.push(rec)
|
|
30
|
+
return {
|
|
31
|
+
setAttributes: (a: Record<string, unknown>) => Object.assign(rec.attributes, a),
|
|
32
|
+
setAttribute: (k: string, v: unknown) => {
|
|
33
|
+
rec.attributes[k] = v
|
|
34
|
+
},
|
|
35
|
+
setStatus: (s: { code: number }) => {
|
|
36
|
+
rec.status = s.code
|
|
37
|
+
},
|
|
38
|
+
recordException: () => undefined,
|
|
39
|
+
addEvent: () => undefined,
|
|
40
|
+
end: () => {
|
|
41
|
+
rec.ended = true
|
|
42
|
+
},
|
|
43
|
+
spanContext: () => ({ traceId: 't', spanId: 's', traceFlags: 1 }),
|
|
44
|
+
isRecording: () => true,
|
|
45
|
+
updateName: () => undefined,
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
startActiveSpan: (_n: string, _o: unknown, _c: unknown, fn: (s: unknown) => unknown) =>
|
|
49
|
+
fn({
|
|
50
|
+
setAttributes: () => undefined,
|
|
51
|
+
setStatus: () => undefined,
|
|
52
|
+
recordException: () => undefined,
|
|
53
|
+
end: () => undefined,
|
|
54
|
+
spanContext: () => ({ traceId: 't', spanId: 's', traceFlags: 1 }),
|
|
55
|
+
isRecording: () => true,
|
|
56
|
+
}),
|
|
57
|
+
}),
|
|
58
|
+
getMeter: () => ({
|
|
59
|
+
createCounter: () => ({ add: () => undefined }),
|
|
60
|
+
createHistogram: () => ({ record: () => undefined }),
|
|
61
|
+
}),
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
let workdirs: string[] = []
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
spans.length = 0
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterEach(async () => {
|
|
71
|
+
await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
|
|
72
|
+
workdirs = []
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
async function runOnce(turns: { text?: string }[]): Promise<void> {
|
|
76
|
+
const { MockLLMProvider } = await import('../../provider/mock.js')
|
|
77
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
78
|
+
const { drainQuery } = await import('../../runtime/query/index.js')
|
|
79
|
+
const { createUserMessage } = await import('../../types/message/index.js')
|
|
80
|
+
|
|
81
|
+
const dir = await mkdtemp(join(tmpdir(), 'namzu-chatspan-'))
|
|
82
|
+
workdirs.push(dir)
|
|
83
|
+
|
|
84
|
+
await drainQuery({
|
|
85
|
+
provider: new MockLLMProvider({ turns: turns as never }),
|
|
86
|
+
tools: new ToolRegistry(),
|
|
87
|
+
runConfig: {
|
|
88
|
+
model: 'mock-model',
|
|
89
|
+
timeoutMs: 30_000,
|
|
90
|
+
tokenBudget: 100_000,
|
|
91
|
+
maxIterations: 3,
|
|
92
|
+
maxResponseTokens: 256,
|
|
93
|
+
temperature: 0.3,
|
|
94
|
+
},
|
|
95
|
+
agentId: 'agent_cs',
|
|
96
|
+
agentName: 'Chat Span Agent',
|
|
97
|
+
workingDirectory: dir,
|
|
98
|
+
sessionId: 'ses_cs',
|
|
99
|
+
threadId: 'thd_cs',
|
|
100
|
+
projectId: 'prj_cs',
|
|
101
|
+
tenantId: 'tnt_cs',
|
|
102
|
+
messages: [createUserMessage('go')],
|
|
103
|
+
} as never)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const chatSpans = () => spans.filter((s) => s.name.startsWith('chat '))
|
|
107
|
+
|
|
108
|
+
describe('the model call has a span of its own', () => {
|
|
109
|
+
it('opens one named for the model', async () => {
|
|
110
|
+
await runOnce([{ text: 'done' }])
|
|
111
|
+
|
|
112
|
+
// One per model call. A run makes at least one, and a forced-final
|
|
113
|
+
// turn makes another — both are model calls and both deserve a span.
|
|
114
|
+
expect(chatSpans().length).toBeGreaterThanOrEqual(1)
|
|
115
|
+
for (const s of chatSpans()) expect(s.name).toBe('chat mock-model')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('closes it', async () => {
|
|
119
|
+
await runOnce([{ text: 'done' }])
|
|
120
|
+
|
|
121
|
+
for (const s of chatSpans()) expect(s.ended).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('carries the request parameters that were actually used', async () => {
|
|
125
|
+
await runOnce([{ text: 'done' }])
|
|
126
|
+
|
|
127
|
+
const attrs = chatSpans()[0]?.attributes ?? {}
|
|
128
|
+
expect(attrs['gen_ai.operation.name']).toBe('chat')
|
|
129
|
+
expect(attrs['gen_ai.request.model']).toBe('mock-model')
|
|
130
|
+
expect(attrs['gen_ai.request.temperature']).toBe(0.3)
|
|
131
|
+
expect(attrs['gen_ai.request.max_tokens']).toBe(256)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('carries the usage on the call that produced it', async () => {
|
|
135
|
+
await runOnce([{ text: 'done' }])
|
|
136
|
+
|
|
137
|
+
const attrs = chatSpans()[0]?.attributes ?? {}
|
|
138
|
+
// These previously landed only on the iteration span — one level up
|
|
139
|
+
// from the operation that spent the tokens.
|
|
140
|
+
expect(attrs).toHaveProperty('gen_ai.usage.input_tokens')
|
|
141
|
+
expect(attrs).toHaveProperty('gen_ai.usage.output_tokens')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('records what the response said it was, not only what was asked for', async () => {
|
|
145
|
+
await runOnce([{ text: 'done' }])
|
|
146
|
+
|
|
147
|
+
const attrs = chatSpans()[0]?.attributes ?? {}
|
|
148
|
+
// A provider may answer on a different model than the alias asked
|
|
149
|
+
// for, and the response id is how a trace is matched to a provider's
|
|
150
|
+
// own logs. Both constants existed and neither was ever set.
|
|
151
|
+
expect(attrs['gen_ai.response.model']).toBe('mock-model')
|
|
152
|
+
expect(attrs['gen_ai.response.id']).toBeDefined()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('records the finish reason as an array, per the convention', async () => {
|
|
156
|
+
await runOnce([{ text: 'done' }])
|
|
157
|
+
|
|
158
|
+
// One call can finish several ways when a provider returns more than
|
|
159
|
+
// one choice, which is why the convention makes it a list.
|
|
160
|
+
expect(Array.isArray(chatSpans()[0]?.attributes['gen_ai.response.finish_reasons'])).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('reports cache tokens, which had constants and no producer', async () => {
|
|
164
|
+
await runOnce([{ text: 'done' }])
|
|
165
|
+
|
|
166
|
+
const attrs = chatSpans()[0]?.attributes ?? {}
|
|
167
|
+
expect(attrs).toHaveProperty('namzu.cache.read_tokens')
|
|
168
|
+
expect(attrs).toHaveProperty('namzu.cache.write_tokens')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('opens one per turn, not one per run', async () => {
|
|
172
|
+
await runOnce([{ text: 'first' }, { text: 'second' }])
|
|
173
|
+
|
|
174
|
+
// A run with several turns is exactly the case a trace is opened for.
|
|
175
|
+
expect(chatSpans().length).toBeGreaterThanOrEqual(1)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('nests under the iteration span rather than emitting as a root', async () => {
|
|
179
|
+
await runOnce([{ text: 'done' }])
|
|
180
|
+
|
|
181
|
+
// The iteration span is created first; the chat span is parented to it
|
|
182
|
+
// explicitly because this body is an async generator and the ambient
|
|
183
|
+
// context at resume time belongs to the consumer.
|
|
184
|
+
const iterationIndex = spans.findIndex((s) => s.name.includes('iteration'))
|
|
185
|
+
const chatIndex = spans.findIndex((s) => s.name.startsWith('chat '))
|
|
186
|
+
expect(iterationIndex).toBeGreaterThanOrEqual(0)
|
|
187
|
+
expect(chatIndex).toBeGreaterThan(iterationIndex)
|
|
188
|
+
})
|
|
189
|
+
})
|