Repository navigation
Expand file tree
/
Copy pathdurable-parity.test-d.ts
More file actions
152 lines (141 loc) · 6.73 KB
/
Copy pathdurable-parity.test-d.ts
File metadata and controls
152 lines (141 loc) · 6.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import { describe, it } from 'vitest';
import type { AgentExecutionOptionsBase } from '../agent.types';
import type { SerializableDurableOptions, RunRegistryEntry } from './types';
/**
* Type-level parity gate: every key on AgentExecutionOptionsBase must be
* accounted for in the durable agent path — either serialized into
* SerializableDurableOptions, stored on RunRegistryEntry, or consumed during
* preparation (prepareForDurableExecution).
*
* When a new field is added to AgentExecutionOptionsBase and this type test
* fails, the developer must:
*
* 1. Decide whether the field is serializable or a closure/object reference.
* 2. Add it to SerializableDurableOptions (serialized path) and/or
* RunRegistryEntry (in-process registry path).
* 3. Handle it in prepareForDurableExecution().
* 4. Add the key to DurableHandledKeys below.
*
* This prevents silent drift where new Agent options are ignored by the
* durable execution path.
*/
// ---------------------------------------------------------------------------
// 1. Keys on SerializableDurableOptions (serialized into workflow input)
// ---------------------------------------------------------------------------
type SerializedKeys = keyof SerializableDurableOptions;
// ---------------------------------------------------------------------------
// 2. Keys on RunRegistryEntry (non-serializable, in-process registry)
// ---------------------------------------------------------------------------
type RegistryKeys = keyof RunRegistryEntry;
// ---------------------------------------------------------------------------
// 3. Keys consumed during prepareForDurableExecution (not forwarded, but used)
// These are either:
// - read and applied during preparation (instructions, system, context, memory)
// - explicitly not forwarded because they're internal (runId, requestContext)
// - handled by agent-level config (scorers resolved from agent, not options)
// - observability keys from Partial<ObservabilityContext>
// - callback keys that live on the registry under a different name or are
// forwarded to the workflow engine
// ---------------------------------------------------------------------------
type ConsumedDuringPreparation =
// Read and applied to MessageList during preparation
| 'instructions'
| 'system'
| 'context'
// Memory is resolved during preparation; thread/resource/memoryConfig are
// extracted and stored in workflow state + registry
| 'memory'
// savePerStep is extracted and stored in workflow state during preparation
| 'savePerStep'
// RunId is generated/used during preparation, not forwarded as an "option"
| 'runId'
// RequestContext is resolved during preparation and stored on registry
| 'requestContext'
// Version overrides are merged into requestContext during preparation
| 'versions'
// Scorers are resolved from agent.listScorers() during preparation
| 'scorers'
// Callbacks are stored on the registry via the workflow engine (onChunk,
// onStepFinish, onFinish are wired by DurableAgent.stream(), not serialized)
| 'onChunk'
| 'onStepFinish'
| 'onFinish'
| 'onError'
| 'onAbort'
| 'experimentalTransform'
// AbortSignal is managed via the registry's abortController/abortSignal
| 'abortSignal'
// Toolsets and clientTools are resolved into the `tools` record during
// getToolsForExecution() in preparation
| 'toolsets'
| 'clientTools'
// Hooks are passed to getToolsForExecution() during preparation
| 'hooks'
// Delegation config contains closures; stored on registry indirectly via
// getToolsForExecution() which wires delegation into tool definitions
| 'delegation'
// _skipBgTaskWait is serialized as `skipBgTaskWait` in SerializableDurableOptions
// (key name differs between the two interfaces)
| '_skipBgTaskWait'
// untilIdle is handled by DurableAgent.streamUntilIdle() before preparation
| 'untilIdle'
// Serverless waitUntil is call-site only for non-durable generate/stream.
// Durable finish already awaits title generation, so this is intentionally unused.
| 'serverless'
// Observability context keys from Partial<ObservabilityContext>
| 'tracing'
| 'loggerVNext'
| 'metrics'
| 'tracingContext';
// ---------------------------------------------------------------------------
// 4. Exhaustive union of all handled keys
// ---------------------------------------------------------------------------
type DurableHandledKeys = SerializedKeys | RegistryKeys | ConsumedDuringPreparation;
// ---------------------------------------------------------------------------
// 5. The parity assertion
//
// Every key on AgentExecutionOptionsBase<any> must appear in DurableHandledKeys.
// If this fails, a new key was added to AgentExecutionOptionsBase without being
// accounted for in the durable path.
// ---------------------------------------------------------------------------
/**
* Compile-time assertion: resolves to `true` when T is exactly `never`,
* and to a descriptive error type otherwise.
*/
type AssertNever<T> = [T] extends [never] ? true : { ERROR: 'Unhandled keys found'; keys: T };
/**
* Compute keys present in Base but missing from Handled.
* If this resolves to `never`, all keys are covered.
*/
type MissingKeys = Exclude<keyof AgentExecutionOptionsBase<any>, DurableHandledKeys>;
/**
* Compute keys on SerializableDurableOptions that don't correspond to any
* AgentExecutionOptionsBase field (after excluding known durable-internal keys).
*/
type PhantomSerializedKeys = Exclude<
SerializedKeys,
| keyof AgentExecutionOptionsBase<any>
// These are durable-internal representations that don't map 1:1 to a
// base option key but are derived from one:
| 'hasErrorProcessors' // derived from errorProcessors.length
| 'skipBgTaskWait' // derived from _skipBgTaskWait
| 'instructionsOverride' // derived from instructions
| 'systemMessage' // derived from system
| 'transform' // shadow of transform policy (targets only)
| 'isTaskComplete' // shadow of isTaskComplete (scorer names only)
| 'structuredOutput' // serialized form of structuredOutput
>;
describe('DurableAgent ↔ Agent parity gate', () => {
it('every AgentExecutionOptionsBase key is handled by the durable path', () => {
// If MissingKeys is not `never`, this assignment fails with a type error
// showing exactly which keys are unhandled, e.g.:
// Type '{ ERROR: "Unhandled keys found"; keys: "newField" | "anotherField" }'
// is not assignable to type 'true'.
const _check: AssertNever<MissingKeys> = true;
});
it('SerializableDurableOptions has no phantom keys', () => {
// Guard against adding keys to SerializableDurableOptions that don't
// correspond to any AgentExecutionOptionsBase field.
const _check: AssertNever<PhantomSerializedKeys> = true;
});
});