@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
|
@@ -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())
|
|
@@ -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
|
+
}
|