@namzu/sdk 3.1.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 +177 -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 +4 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +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__/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/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 +96 -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/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 +13 -1
- 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/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 +8 -0
- 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__/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/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +103 -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/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +14 -1
- 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/toolset/index.ts +22 -0
package/package.json
CHANGED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { AdvisorDefinition } from '../../types/advisory/index.js'
|
|
4
|
+
import type { Message } from '../../types/message/index.js'
|
|
5
|
+
import { AdvisoryContext } from '../context.js'
|
|
6
|
+
import { TriggerEvaluator } from '../evaluator.js'
|
|
7
|
+
import { AdvisoryExecutor } from '../executor.js'
|
|
8
|
+
import { ADVISORY_RESPONSE_CONTRACT } from '../parse.js'
|
|
9
|
+
import { AdvisorRegistry } from '../registry.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An advisor consulted BY THE MODEL saw the question and nothing else.
|
|
13
|
+
*
|
|
14
|
+
* Two paths reach `AdvisoryExecutor.consult`. The trigger path
|
|
15
|
+
* (`iteration/phases/advisory.ts`) has always passed the live messages, the
|
|
16
|
+
* working state and the tool catalogue. The tool path passed
|
|
17
|
+
* `{ messages: [], iteration: 0 }` — a literal empty context — so the
|
|
18
|
+
* model's own `include_context: true` had nothing to include, and the
|
|
19
|
+
* advisor answered a question about a situation it could not see.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function recordingProvider(): { provider: AdvisorDefinition['provider']; calls: Message[][] } {
|
|
23
|
+
const calls: Message[][] = []
|
|
24
|
+
const provider = {
|
|
25
|
+
chatStream: async function* (params: { messages: Message[] }) {
|
|
26
|
+
calls.push(params.messages)
|
|
27
|
+
yield { id: 'a1', delta: { content: 'ADVICE: do the thing' } }
|
|
28
|
+
yield {
|
|
29
|
+
id: 'a1',
|
|
30
|
+
delta: {},
|
|
31
|
+
finishReason: 'stop',
|
|
32
|
+
usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
} as unknown as AdvisorDefinition['provider']
|
|
36
|
+
return { provider, calls }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function advisor(over: Partial<AdvisorDefinition> = {}): AdvisorDefinition {
|
|
40
|
+
return {
|
|
41
|
+
id: 'adv_1',
|
|
42
|
+
name: 'Reviewer',
|
|
43
|
+
provider: recordingProvider().provider,
|
|
44
|
+
model: 'mock-model',
|
|
45
|
+
...over,
|
|
46
|
+
} as AdvisorDefinition
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const log = {
|
|
50
|
+
debug: vi.fn(),
|
|
51
|
+
info: vi.fn(),
|
|
52
|
+
warn: vi.fn(),
|
|
53
|
+
error: vi.fn(),
|
|
54
|
+
child: () => log,
|
|
55
|
+
} as never
|
|
56
|
+
|
|
57
|
+
function contextFor(a: AdvisorDefinition): AdvisoryContext {
|
|
58
|
+
return new AdvisoryContext(
|
|
59
|
+
new AdvisorRegistry([a]),
|
|
60
|
+
new AdvisoryExecutor(log),
|
|
61
|
+
new TriggerEvaluator([]),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('the context a tool-initiated consultation is given', () => {
|
|
66
|
+
it('is empty until the runtime supplies one', () => {
|
|
67
|
+
const ctx = contextFor(advisor())
|
|
68
|
+
|
|
69
|
+
// The fallback exists for a context built without a runtime, and it is
|
|
70
|
+
// what every tool-initiated call used to get.
|
|
71
|
+
expect(ctx.callContext()).toEqual({ messages: [], iteration: 0 })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('is the live run once the runtime wires it', () => {
|
|
75
|
+
const ctx = contextFor(advisor())
|
|
76
|
+
const messages: Message[] = [{ role: 'user', content: 'the situation', timestamp: 1 }]
|
|
77
|
+
|
|
78
|
+
ctx.setCallContextProvider(() => ({ messages, iteration: 7 }))
|
|
79
|
+
|
|
80
|
+
expect(ctx.callContext().messages).toBe(messages)
|
|
81
|
+
expect(ctx.callContext().iteration).toBe(7)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('is read at call time, not at construction', () => {
|
|
85
|
+
const ctx = contextFor(advisor())
|
|
86
|
+
let iteration = 1
|
|
87
|
+
ctx.setCallContextProvider(() => ({ messages: [], iteration }))
|
|
88
|
+
|
|
89
|
+
iteration = 4
|
|
90
|
+
|
|
91
|
+
// The tool is built once per run and called at an unknown later point.
|
|
92
|
+
// A snapshot would hand every advisor the state the run started with.
|
|
93
|
+
expect(ctx.callContext().iteration).toBe(4)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('what the advisor actually receives', () => {
|
|
98
|
+
it('sees the conversation when context is included', async () => {
|
|
99
|
+
const { provider, calls } = recordingProvider()
|
|
100
|
+
const executor = new AdvisoryExecutor(log)
|
|
101
|
+
|
|
102
|
+
await executor.consult(
|
|
103
|
+
advisor({ provider }),
|
|
104
|
+
{ advisorId: 'adv_1', question: 'what next?', includeContext: true },
|
|
105
|
+
{ messages: [{ role: 'user', content: 'deploy is failing', timestamp: 1 }], iteration: 2 },
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const sent = JSON.stringify(calls[0])
|
|
109
|
+
expect(sent).toContain('deploy is failing')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('sees none of it when the caller says not to', async () => {
|
|
113
|
+
const { provider, calls } = recordingProvider()
|
|
114
|
+
const executor = new AdvisoryExecutor(log)
|
|
115
|
+
|
|
116
|
+
await executor.consult(
|
|
117
|
+
advisor({ provider }),
|
|
118
|
+
{ advisorId: 'adv_1', question: 'what next?', includeContext: false },
|
|
119
|
+
{ messages: [{ role: 'user', content: 'deploy is failing', timestamp: 1 }], iteration: 2 },
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
expect(JSON.stringify(calls[0])).not.toContain('deploy is failing')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('is told when the caller marked the request urgent', async () => {
|
|
126
|
+
const { provider, calls } = recordingProvider()
|
|
127
|
+
const executor = new AdvisoryExecutor(log)
|
|
128
|
+
|
|
129
|
+
await executor.consult(
|
|
130
|
+
advisor({ provider }),
|
|
131
|
+
{ advisorId: 'adv_1', question: 'what next?', urgency: 'high' },
|
|
132
|
+
{ messages: [], iteration: 1 },
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// The value used to reach exactly one debug log line, so 'high' and
|
|
136
|
+
// 'low' produced byte-identical requests.
|
|
137
|
+
expect(String(calls[0]?.[0]?.content)).toContain('URGENT')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('is told when there is room to consider alternatives', async () => {
|
|
141
|
+
const { provider, calls } = recordingProvider()
|
|
142
|
+
const executor = new AdvisoryExecutor(log)
|
|
143
|
+
|
|
144
|
+
await executor.consult(
|
|
145
|
+
advisor({ provider }),
|
|
146
|
+
{ advisorId: 'adv_1', question: 'what next?', urgency: 'low' },
|
|
147
|
+
{ messages: [], iteration: 1 },
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
expect(String(calls[0]?.[0]?.content)).toContain('low urgency')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("appends nothing at all for 'normal', which is the point of not stating it", async () => {
|
|
154
|
+
const { provider, calls } = recordingProvider()
|
|
155
|
+
const executor = new AdvisoryExecutor(log)
|
|
156
|
+
const a = advisor({ provider })
|
|
157
|
+
|
|
158
|
+
await executor.consult(
|
|
159
|
+
a,
|
|
160
|
+
{ advisorId: 'adv_1', question: 'what next?', urgency: 'normal' },
|
|
161
|
+
{ messages: [], iteration: 1 },
|
|
162
|
+
)
|
|
163
|
+
await executor.consult(
|
|
164
|
+
a,
|
|
165
|
+
{ advisorId: 'adv_1', question: 'what next?', urgency: 'high' },
|
|
166
|
+
{ messages: [], iteration: 1 },
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Asserting "does not contain the other two phrases" is too weak — it
|
|
170
|
+
// would let ANY new sentence in. The response contract is the last
|
|
171
|
+
// thing the prompt says when urgency contributes nothing, so ending on
|
|
172
|
+
// it is what "appended nothing" actually means.
|
|
173
|
+
expect(String(calls[0]?.[0]?.content).trimEnd().endsWith(ADVISORY_RESPONSE_CONTRACT)).toBe(true)
|
|
174
|
+
expect(String(calls[1]?.[0]?.content).trimEnd().endsWith(ADVISORY_RESPONSE_CONTRACT)).toBe(
|
|
175
|
+
false,
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('says nothing about urgency when the caller did not', async () => {
|
|
180
|
+
const { provider, calls } = recordingProvider()
|
|
181
|
+
const executor = new AdvisoryExecutor(log)
|
|
182
|
+
|
|
183
|
+
await executor.consult(
|
|
184
|
+
advisor({ provider }),
|
|
185
|
+
{ advisorId: 'adv_1', question: 'what next?' },
|
|
186
|
+
{ messages: [], iteration: 1 },
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
expect(String(calls[0]?.[0]?.content).trimEnd().endsWith(ADVISORY_RESPONSE_CONTRACT)).toBe(true)
|
|
190
|
+
})
|
|
191
|
+
})
|
package/src/advisory/context.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import type { AdvisoryBudget, AdvisoryCallRecord } from '../types/advisory/index.js'
|
|
2
2
|
import type { TriggerEvaluator } from './evaluator.js'
|
|
3
|
+
import type { AdvisoryCallContext } from './executor.js'
|
|
3
4
|
import type { AdvisoryExecutor } from './executor.js'
|
|
4
5
|
import type { AdvisorRegistry } from './registry.js'
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* What the run looks like right now, for an advisory call that did not come
|
|
9
|
+
* from the iteration loop.
|
|
10
|
+
*
|
|
11
|
+
* A function rather than a snapshot because the tool is built once per run
|
|
12
|
+
* and called at an unknown later point: capturing the context at
|
|
13
|
+
* construction would hand every advisor the state the run had before it
|
|
14
|
+
* started.
|
|
15
|
+
*/
|
|
16
|
+
export type AdvisoryCallContextProvider = () => AdvisoryCallContext
|
|
17
|
+
|
|
6
18
|
export class AdvisoryContext {
|
|
7
19
|
readonly registry: AdvisorRegistry
|
|
8
20
|
readonly executor: AdvisoryExecutor
|
|
@@ -10,6 +22,7 @@ export class AdvisoryContext {
|
|
|
10
22
|
readonly callHistory: AdvisoryCallRecord[] = []
|
|
11
23
|
|
|
12
24
|
private readonly budget: AdvisoryBudget | undefined
|
|
25
|
+
private callContextProvider: AdvisoryCallContextProvider | undefined
|
|
13
26
|
|
|
14
27
|
constructor(
|
|
15
28
|
registry: AdvisorRegistry,
|
|
@@ -23,6 +36,25 @@ export class AdvisoryContext {
|
|
|
23
36
|
this.budget = budget
|
|
24
37
|
}
|
|
25
38
|
|
|
39
|
+
/** Wired by the runtime once the run exists. */
|
|
40
|
+
setCallContextProvider(provider: AdvisoryCallContextProvider): void {
|
|
41
|
+
this.callContextProvider = provider
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The call context for a tool-initiated consultation.
|
|
46
|
+
*
|
|
47
|
+
* The trigger path (`iteration/phases/advisory.ts`) has always passed the
|
|
48
|
+
* live messages, working state and tool catalogue. The TOOL path passed
|
|
49
|
+
* `{ messages: [], iteration: 0 }` — a literal empty context — so an
|
|
50
|
+
* advisor consulted by the model saw the question and nothing else, and
|
|
51
|
+
* the model's `include_context` had nothing to include either way. The
|
|
52
|
+
* empty fallback survives only for a context built without a runtime.
|
|
53
|
+
*/
|
|
54
|
+
callContext(): AdvisoryCallContext {
|
|
55
|
+
return this.callContextProvider?.() ?? { messages: [], iteration: 0 }
|
|
56
|
+
}
|
|
57
|
+
|
|
26
58
|
recordCall(record: AdvisoryCallRecord): void {
|
|
27
59
|
this.callHistory.push(record)
|
|
28
60
|
}
|
package/src/advisory/executor.ts
CHANGED
|
@@ -11,6 +11,19 @@ import { calculateCost } from '../utils/cost.js'
|
|
|
11
11
|
import { type Logger, getRootLogger } from '../utils/logger.js'
|
|
12
12
|
import { ADVISORY_RESPONSE_CONTRACT, parseAdvisoryResponse } from './parse.js'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* What the advisor is told about how urgent the caller said this is.
|
|
16
|
+
*
|
|
17
|
+
* `'normal'` says nothing on purpose. A sentence asserting the ordinary case
|
|
18
|
+
* is prompt weight that changes no answer, and stating it on every call
|
|
19
|
+
* would make the two that matter harder to notice.
|
|
20
|
+
*/
|
|
21
|
+
const URGENCY_DIRECTION: Record<'low' | 'normal' | 'high', string | undefined> = {
|
|
22
|
+
high: 'This request is marked URGENT. Lead with the single most important action and keep the reasoning to what is needed to justify it.',
|
|
23
|
+
normal: undefined,
|
|
24
|
+
low: 'This request is marked low urgency. There is room to note secondary considerations and alternatives worth weighing.',
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
export interface AdvisoryCallContext {
|
|
15
28
|
readonly messages: Message[]
|
|
16
29
|
readonly workingStateSummary?: string
|
|
@@ -53,7 +66,7 @@ export class AdvisoryExecutor {
|
|
|
53
66
|
): Promise<AdvisoryExecutionResult> {
|
|
54
67
|
const startMs = Date.now()
|
|
55
68
|
|
|
56
|
-
const systemPrompt = this.buildSystemPrompt(advisor)
|
|
69
|
+
const systemPrompt = this.buildSystemPrompt(advisor, request.urgency)
|
|
57
70
|
const contextMessages = this.buildContext(advisor, request, callCtx)
|
|
58
71
|
|
|
59
72
|
const messages: Message[] = [
|
|
@@ -100,11 +113,25 @@ export class AdvisoryExecutor {
|
|
|
100
113
|
}
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
private buildSystemPrompt(
|
|
116
|
+
private buildSystemPrompt(
|
|
117
|
+
advisor: AdvisorDefinition,
|
|
118
|
+
urgency?: AdvisoryRequest['urgency'],
|
|
119
|
+
): string {
|
|
104
120
|
// The contract is appended to every branch, not folded into the
|
|
105
121
|
// default: an advisor with its own prompt or a persona is still read
|
|
106
122
|
// back by the same parser, and used to be the one never told so.
|
|
107
|
-
|
|
123
|
+
const parts = [this.describeAdvisor(advisor), ADVISORY_RESPONSE_CONTRACT]
|
|
124
|
+
|
|
125
|
+
// The caller is invited to say how urgent this is, and the value used
|
|
126
|
+
// to reach exactly one debug log line — `urgency: 'high'` and
|
|
127
|
+
// `urgency: 'low'` produced byte-identical requests. Telling the
|
|
128
|
+
// ADVISOR is the honest minimum: it is the party that can act on the
|
|
129
|
+
// answer, and it costs one sentence rather than a routing policy this
|
|
130
|
+
// kernel has no business inventing.
|
|
131
|
+
const direction = URGENCY_DIRECTION[urgency ?? 'normal']
|
|
132
|
+
if (direction) parts.push(direction)
|
|
133
|
+
|
|
134
|
+
return parts.join('\n\n')
|
|
108
135
|
}
|
|
109
136
|
|
|
110
137
|
private describeAdvisor(advisor: AdvisorDefinition): string {
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { AssistantMessage, Message, ToolMessage } from '../../types/message/index.js'
|
|
4
|
+
import { createSlidingWindowReducer } from '../reducer.js'
|
|
5
|
+
import type { ContextReduction } from '../reducer.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `strategy: 'sliding-window'` was accepted by the config schema and then
|
|
9
|
+
* ignored — the runtime asked only whether the strategy was `'disabled'`, so
|
|
10
|
+
* a host who chose the cheap non-LLM path silently got the expensive LLM one,
|
|
11
|
+
* summarization calls and all. These pin the behaviour the name always
|
|
12
|
+
* claimed, and the three invariants that make a shorter history usable rather
|
|
13
|
+
* than merely shorter.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const sys = (content: string): Message => ({ role: 'system', content, timestamp: 1 })
|
|
17
|
+
const user = (content: string): Message => ({ role: 'user', content, timestamp: 1 })
|
|
18
|
+
const asst = (content: string): Message => ({ role: 'assistant', content, timestamp: 1 })
|
|
19
|
+
|
|
20
|
+
const callsTool = (id: string): AssistantMessage => ({
|
|
21
|
+
role: 'assistant',
|
|
22
|
+
content: null,
|
|
23
|
+
timestamp: 1,
|
|
24
|
+
toolCalls: [{ id, type: 'function', function: { name: 'echo', arguments: '{}' } }],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const toolResult = (id: string): ToolMessage => ({
|
|
28
|
+
role: 'tool',
|
|
29
|
+
content: 'ok',
|
|
30
|
+
timestamp: 1,
|
|
31
|
+
toolCallId: id,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function reduction(
|
|
35
|
+
messages: readonly Message[],
|
|
36
|
+
over?: Partial<ContextReduction>,
|
|
37
|
+
): ContextReduction {
|
|
38
|
+
return {
|
|
39
|
+
messages,
|
|
40
|
+
reason: 'threshold',
|
|
41
|
+
estimatedTokens: 1_000,
|
|
42
|
+
contextWindowTokens: 2_000,
|
|
43
|
+
model: 'mock-model',
|
|
44
|
+
keepRecentMessages: 4,
|
|
45
|
+
...over,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('the sliding window keeps recent turns and drops the rest', () => {
|
|
50
|
+
it('keeps the configured tail', async () => {
|
|
51
|
+
const reduce = createSlidingWindowReducer()
|
|
52
|
+
const messages = [sys('prompt'), ...Array.from({ length: 10 }, (_, i) => user(`m${i}`))]
|
|
53
|
+
|
|
54
|
+
const next = await reduce(reduction(messages))
|
|
55
|
+
|
|
56
|
+
expect(next).toBeDefined()
|
|
57
|
+
expect(next?.length).toBe(5) // the system floor plus keepRecentMessages
|
|
58
|
+
expect(next?.at(-1)).toBe(messages.at(-1))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('cuts harder when the provider already rejected the prompt', async () => {
|
|
62
|
+
const reduce = createSlidingWindowReducer()
|
|
63
|
+
const messages = [sys('prompt'), ...Array.from({ length: 10 }, (_, i) => user(`m${i}`))]
|
|
64
|
+
|
|
65
|
+
const threshold = await reduce(reduction(messages))
|
|
66
|
+
const overflow = await reduce(reduction(messages, { reason: 'overflow' }))
|
|
67
|
+
|
|
68
|
+
// An overflow means the ordinary window was ALREADY too big. Cutting to
|
|
69
|
+
// the same size would shed nothing and the caller would retry an
|
|
70
|
+
// identical prompt.
|
|
71
|
+
expect(overflow?.length).toBeLessThan(threshold?.length ?? 0)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('summarizes nothing — every survivor is the original object', async () => {
|
|
75
|
+
const reduce = createSlidingWindowReducer()
|
|
76
|
+
const messages = [sys('prompt'), ...Array.from({ length: 8 }, (_, i) => user(`m${i}`))]
|
|
77
|
+
|
|
78
|
+
const next = await reduce(reduction(messages))
|
|
79
|
+
|
|
80
|
+
for (const message of next ?? []) expect(messages).toContain(message)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('the three invariants a shorter history has to keep', () => {
|
|
85
|
+
it('never drops the leading system floor', async () => {
|
|
86
|
+
const reduce = createSlidingWindowReducer()
|
|
87
|
+
const messages = [
|
|
88
|
+
sys('prompt'),
|
|
89
|
+
sys('working memory'),
|
|
90
|
+
...Array.from({ length: 8 }, (_, i) => user(`m${i}`)),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
const next = await reduce(reduction(messages))
|
|
94
|
+
|
|
95
|
+
expect(next?.[0]).toBe(messages[0])
|
|
96
|
+
expect(next?.[1]).toBe(messages[1])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('does not split a tool_use from its tool_result', async () => {
|
|
100
|
+
const reduce = createSlidingWindowReducer()
|
|
101
|
+
// The naive cut at length-3 lands between the assistant call and its
|
|
102
|
+
// result, which is a provider 400 on the next turn.
|
|
103
|
+
const messages = [
|
|
104
|
+
sys('prompt'),
|
|
105
|
+
user('go'),
|
|
106
|
+
asst('thinking'),
|
|
107
|
+
callsTool('c1'),
|
|
108
|
+
toolResult('c1'),
|
|
109
|
+
user('again'),
|
|
110
|
+
asst('done'),
|
|
111
|
+
user('more'),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const next = await reduce(reduction(messages, { keepRecentMessages: 5 }))
|
|
115
|
+
|
|
116
|
+
const kept = new Set(next ?? [])
|
|
117
|
+
// Cutting at 3 would keep the call and its result together; cutting at
|
|
118
|
+
// 4 would orphan the result. The pair is never split either way.
|
|
119
|
+
expect(kept.has(messages[4] as Message)).toBe(kept.has(messages[3] as Message))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('takes a cut above the requested window when nothing below is safe', async () => {
|
|
123
|
+
const reduce = createSlidingWindowReducer()
|
|
124
|
+
// A multi-step turn: the user is silent while the agent works, so every
|
|
125
|
+
// boundary inside the recent window opens on an assistant or tool
|
|
126
|
+
// message. A backwards-only search would decline here — exactly when
|
|
127
|
+
// the history is longest.
|
|
128
|
+
const messages = [
|
|
129
|
+
sys('prompt'),
|
|
130
|
+
user('go'),
|
|
131
|
+
callsTool('c1'),
|
|
132
|
+
toolResult('c1'),
|
|
133
|
+
asst('working'),
|
|
134
|
+
callsTool('c2'),
|
|
135
|
+
toolResult('c2'),
|
|
136
|
+
// The only user turn after the opening one, and it sits ABOVE the
|
|
137
|
+
// requested cut — so every candidate below opens on an assistant or
|
|
138
|
+
// tool message and the backwards search comes back empty.
|
|
139
|
+
user('next'),
|
|
140
|
+
callsTool('c3'),
|
|
141
|
+
toolResult('c3'),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
const next = await reduce(reduction(messages, { keepRecentMessages: 5 }))
|
|
145
|
+
|
|
146
|
+
expect(next).toBeDefined()
|
|
147
|
+
expect(next?.length).toBeLessThan(messages.length)
|
|
148
|
+
// It kept LESS than asked rather than declining, and what it kept opens
|
|
149
|
+
// on a user turn.
|
|
150
|
+
expect(next?.[1]?.role).toBe('user')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('keeps a pinned turn from the middle of the conversation', async () => {
|
|
154
|
+
const reduce = createSlidingWindowReducer()
|
|
155
|
+
const pinned: Message = { ...user('the account id is X'), retain: true }
|
|
156
|
+
const messages = [
|
|
157
|
+
sys('prompt'),
|
|
158
|
+
user('m0'),
|
|
159
|
+
pinned,
|
|
160
|
+
...Array.from({ length: 8 }, (_, i) => user(`m${i + 1}`)),
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
const next = await reduce(reduction(messages))
|
|
164
|
+
|
|
165
|
+
// Recency is not the only thing that matters, which is what `retain`
|
|
166
|
+
// exists to say.
|
|
167
|
+
expect(next).toContain(pinned)
|
|
168
|
+
expect(next).not.toContain(messages[1])
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('refusing beats a reduction that would break the next turn', () => {
|
|
173
|
+
it('declines when there is nothing but the system floor to cut', async () => {
|
|
174
|
+
const reduce = createSlidingWindowReducer()
|
|
175
|
+
|
|
176
|
+
expect(await reduce(reduction([sys('prompt'), user('go')]))).toBeUndefined()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('declines when every candidate cut splits one assistant fan-out', async () => {
|
|
180
|
+
const reduce = createSlidingWindowReducer()
|
|
181
|
+
// Six results answering one call: no cut inside the block is safe, and
|
|
182
|
+
// the condition clears itself on the next assistant message.
|
|
183
|
+
const messages = [
|
|
184
|
+
sys('prompt'),
|
|
185
|
+
user('go'),
|
|
186
|
+
{
|
|
187
|
+
role: 'assistant',
|
|
188
|
+
content: null,
|
|
189
|
+
timestamp: 1,
|
|
190
|
+
toolCalls: Array.from({ length: 6 }, (_, i) => ({
|
|
191
|
+
id: `c${i}`,
|
|
192
|
+
type: 'function' as const,
|
|
193
|
+
function: { name: 'echo', arguments: '{}' },
|
|
194
|
+
})),
|
|
195
|
+
} as AssistantMessage,
|
|
196
|
+
...Array.from({ length: 6 }, (_, i) => toolResult(`c${i}`)),
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
expect(await reduce(reduction(messages, { keepRecentMessages: 2 }))).toBeUndefined()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('declines rather than reporting a reduction that removed nothing', async () => {
|
|
203
|
+
const reduce = createSlidingWindowReducer()
|
|
204
|
+
// Every non-system message is pinned, so the survivor set is the input.
|
|
205
|
+
const messages = [
|
|
206
|
+
sys('prompt'),
|
|
207
|
+
...Array.from({ length: 8 }, (_, i) => ({ ...user(`m${i}`), retain: true })),
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
// Saying "reduced" here would have the overflow path retry an identical
|
|
211
|
+
// prompt and burn a model call to be told the same thing.
|
|
212
|
+
expect(await reduce(reduction(messages))).toBeUndefined()
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('the knob a caller reaches for', () => {
|
|
217
|
+
it('lets an explicit window override the run config', async () => {
|
|
218
|
+
const reduce = createSlidingWindowReducer({ keepRecentMessages: 2 })
|
|
219
|
+
const messages = [sys('prompt'), ...Array.from({ length: 10 }, (_, i) => user(`m${i}`))]
|
|
220
|
+
|
|
221
|
+
const next = await reduce(reduction(messages, { keepRecentMessages: 8 }))
|
|
222
|
+
|
|
223
|
+
expect(next?.length).toBe(3)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('is a plain function, so a host can wrap one', async () => {
|
|
227
|
+
const inner = createSlidingWindowReducer()
|
|
228
|
+
const seen = vi.fn()
|
|
229
|
+
const reduce = async (input: ContextReduction) => {
|
|
230
|
+
seen(input.reason)
|
|
231
|
+
return inner(input)
|
|
232
|
+
}
|
|
233
|
+
const messages = [sys('prompt'), ...Array.from({ length: 8 }, (_, i) => user(`m${i}`))]
|
|
234
|
+
|
|
235
|
+
await reduce(reduction(messages, { reason: 'overflow' }))
|
|
236
|
+
|
|
237
|
+
expect(seen).toHaveBeenCalledWith('overflow')
|
|
238
|
+
})
|
|
239
|
+
})
|
|
@@ -20,6 +20,13 @@ import type { CompactionStrategy } from './types.js'
|
|
|
20
20
|
* const trimmed = manager.applyManagement(messages)
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Nothing calls this. Select a strategy with
|
|
25
|
+
* `compactionConfig.strategy`, or pass a `contextReducer` to `query()`.
|
|
26
|
+
*
|
|
27
|
+
* The factory produced managers the runtime had no seam to drive, so its
|
|
28
|
+
* return value was always dropped on the floor. See {@link ConversationManager}.
|
|
29
|
+
*/
|
|
23
30
|
export function createConversationManager(
|
|
24
31
|
strategy: CompactionStrategy,
|
|
25
32
|
config: CompactionConfig,
|
package/src/compaction/index.ts
CHANGED
|
@@ -11,6 +11,14 @@ export type { DanglingResult } from './dangling.js'
|
|
|
11
11
|
|
|
12
12
|
export type { ConversationManager } from './interface.js'
|
|
13
13
|
|
|
14
|
+
export { createSlidingWindowReducer } from './reducer.js'
|
|
15
|
+
export type {
|
|
16
|
+
ContextReducer,
|
|
17
|
+
ContextReduction,
|
|
18
|
+
ContextReductionReason,
|
|
19
|
+
SlidingWindowOptions,
|
|
20
|
+
} from './reducer.js'
|
|
21
|
+
|
|
14
22
|
export { WorkingStateManager } from './manager.js'
|
|
15
23
|
|
|
16
24
|
export { serializeState } from './serializer.js'
|
|
@@ -7,6 +7,19 @@ import type { Message } from '../types/message/index.js'
|
|
|
7
7
|
* A manager applies two strategies:
|
|
8
8
|
* 1. **Routine management** (applyManagement): Called after each iteration to proactively optimize context.
|
|
9
9
|
* 2. **Overflow reduction** (reduceContext): Called when the LLM reports context window exceeded.
|
|
10
|
+
*
|
|
11
|
+
* @deprecated Use {@link ContextReducer}, which the runtime actually drives.
|
|
12
|
+
*
|
|
13
|
+
* This interface cannot be implemented correctly. `reduceContext` is
|
|
14
|
+
* documented as reducing the history, but it takes `Message[]` and returns
|
|
15
|
+
* `boolean` — the only way to honour the contract is to mutate the argument
|
|
16
|
+
* in place, and neither shipped implementation does. Both build a shorter
|
|
17
|
+
* array locally, discard it, and return `true`. Nothing in the runtime ever
|
|
18
|
+
* called any of it, which is why an unfulfillable contract survived.
|
|
19
|
+
*
|
|
20
|
+
* `ContextReducer` returns the new history, may be async so a reducer can
|
|
21
|
+
* call a model, and is told whether it was asked speculatively or after the
|
|
22
|
+
* provider rejected the prompt. Kept exported until the next major.
|
|
10
23
|
*/
|
|
11
24
|
export interface ConversationManager {
|
|
12
25
|
/** Unique name for this manager (e.g., 'structured', 'sliding-window', 'disabled') */
|
|
@@ -5,6 +5,9 @@ import type { ConversationManager } from '../interface.js'
|
|
|
5
5
|
* No-op conversation manager implementation.
|
|
6
6
|
* Never modifies messages, useful for testing or when context management is disabled.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Use `strategy: 'disabled'`, which the runtime reads.
|
|
10
|
+
*/
|
|
8
11
|
export class NullManager implements ConversationManager {
|
|
9
12
|
readonly name = 'null'
|
|
10
13
|
|
|
@@ -20,6 +20,12 @@ export interface SlidingWindowManagerConfig {
|
|
|
20
20
|
* 2. Use findSafeTrimIndex to ensure tool call/result pairs remain atomic
|
|
21
21
|
* 3. applyManagement runs proactively each iteration; reduceContext on overflow
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Use `createSlidingWindowReducer()`, or `strategy: 'sliding-window'`.
|
|
25
|
+
*
|
|
26
|
+
* Never reachable through the runtime, and `reduceContext` here computes a
|
|
27
|
+
* trim index and returns a boolean without trimming anything.
|
|
28
|
+
*/
|
|
23
29
|
export class SlidingWindowManager implements ConversationManager {
|
|
24
30
|
readonly name = 'sliding-window'
|
|
25
31
|
private readonly keepRecentMessages: number
|
|
@@ -22,6 +22,16 @@ import { serializeState } from '../serializer.js'
|
|
|
22
22
|
*
|
|
23
23
|
* Provides maximum context preservation but more computational overhead.
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated Not the structured strategy. `strategy: 'structured'` runs the
|
|
27
|
+
* live path in `runtime/query/iteration/phases/compaction.ts`, which this
|
|
28
|
+
* class predates and does not share code with.
|
|
29
|
+
*
|
|
30
|
+
* What is here is a weaker parallel copy: no LLM-verified summary, no stale
|
|
31
|
+
* tool-result clearing, no retention pins, no events — and a `reduceContext`
|
|
32
|
+
* that builds a shorter history into a local array and then returns a
|
|
33
|
+
* boolean, discarding it.
|
|
34
|
+
*/
|
|
25
35
|
export class StructuredCompactionManager implements ConversationManager {
|
|
26
36
|
readonly name = 'structured'
|
|
27
37
|
private readonly config: CompactionConfig
|