@graphorin/agent 0.6.0 → 0.7.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 +96 -0
- package/README.md +31 -11
- package/dist/errors/index.d.ts +17 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +19 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +153 -1930
- package/dist/factory.js.map +1 -1
- package/dist/fanout/index.d.ts +13 -1
- package/dist/fanout/index.d.ts.map +1 -1
- package/dist/fanout/index.js +13 -4
- package/dist/fanout/index.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/lateral-leak/index.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/run-state/index.d.ts +32 -6
- package/dist/run-state/index.d.ts.map +1 -1
- package/dist/run-state/index.js +46 -22
- package/dist/run-state/index.js.map +1 -1
- package/dist/runtime/agent-surface.js +122 -0
- package/dist/runtime/agent-surface.js.map +1 -0
- package/dist/runtime/agent-to-tool.d.ts +51 -0
- package/dist/runtime/agent-to-tool.d.ts.map +1 -0
- package/dist/runtime/agent-to-tool.js +145 -0
- package/dist/runtime/agent-to-tool.js.map +1 -0
- package/dist/runtime/approvals.js +0 -0
- package/dist/runtime/approvals.js.map +1 -0
- package/dist/runtime/dispatch.js +108 -0
- package/dist/runtime/dispatch.js.map +1 -0
- package/dist/runtime/executor-wiring.js +128 -0
- package/dist/runtime/executor-wiring.js.map +1 -0
- package/dist/runtime/fallback-chain.js +139 -0
- package/dist/runtime/fallback-chain.js.map +1 -0
- package/dist/runtime/handoff.js +307 -0
- package/dist/runtime/handoff.js.map +1 -0
- package/dist/runtime/messages.d.ts +22 -0
- package/dist/runtime/messages.d.ts.map +1 -0
- package/dist/runtime/messages.js +204 -0
- package/dist/runtime/messages.js.map +1 -0
- package/dist/runtime/provider-events.js +117 -0
- package/dist/runtime/provider-events.js.map +1 -0
- package/dist/runtime/run-compaction.js +210 -0
- package/dist/runtime/run-compaction.js.map +1 -0
- package/dist/runtime/run-finish.js +48 -0
- package/dist/runtime/run-finish.js.map +1 -0
- package/dist/runtime/run-gates.js +336 -0
- package/dist/runtime/run-gates.js.map +1 -0
- package/dist/runtime/run-init.js +81 -0
- package/dist/runtime/run-init.js.map +1 -0
- package/dist/runtime/run-input.js +46 -0
- package/dist/runtime/run-input.js.map +1 -0
- package/dist/runtime/step-catalogue.js +173 -0
- package/dist/runtime/step-catalogue.js.map +1 -0
- package/dist/runtime/tool-call-walk.js +189 -0
- package/dist/runtime/tool-call-walk.js.map +1 -0
- package/dist/runtime/tool-wiring.js +159 -0
- package/dist/runtime/tool-wiring.js.map +1 -0
- package/dist/tooling/adapters.js +1 -1
- package/dist/tooling/dataflow.js +1 -1
- package/dist/tooling/policy.js +2 -0
- package/dist/tooling/policy.js.map +1 -1
- package/dist/types.d.ts +63 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/errors/index.ts +320 -0
- package/src/evaluator-optimizer/index.ts +212 -0
- package/src/factory.ts +957 -0
- package/src/fallback/index.ts +108 -0
- package/src/fanout/index.ts +523 -0
- package/src/filters/index.ts +347 -0
- package/src/index.ts +180 -0
- package/src/internal/ids.ts +46 -0
- package/src/internal/usage-accumulator.ts +90 -0
- package/src/lateral-leak/causality-monitor.ts +221 -0
- package/src/lateral-leak/index.ts +35 -0
- package/src/lateral-leak/merge-guard.ts +151 -0
- package/src/lateral-leak/protocol-guard.ts +222 -0
- package/src/preferred-model/index.ts +210 -0
- package/src/progress/index.ts +238 -0
- package/src/run-state/index.ts +607 -0
- package/src/runtime/agent-surface.ts +218 -0
- package/src/runtime/agent-to-tool.ts +323 -0
- package/src/runtime/approvals.ts +0 -0
- package/src/runtime/dispatch.ts +183 -0
- package/src/runtime/executor-wiring.ts +331 -0
- package/src/runtime/fallback-chain.ts +250 -0
- package/src/runtime/handoff.ts +428 -0
- package/src/runtime/messages.ts +309 -0
- package/src/runtime/provider-events.ts +175 -0
- package/src/runtime/run-compaction.ts +288 -0
- package/src/runtime/run-finish.ts +93 -0
- package/src/runtime/run-gates.ts +419 -0
- package/src/runtime/run-init.ts +169 -0
- package/src/runtime/run-input.ts +102 -0
- package/src/runtime/step-catalogue.ts +338 -0
- package/src/runtime/tool-call-walk.ts +301 -0
- package/src/runtime/tool-wiring.ts +218 -0
- package/src/testing/replay-provider.ts +121 -0
- package/src/tooling/adapters.ts +403 -0
- package/src/tooling/catalogue.ts +36 -0
- package/src/tooling/dataflow.ts +171 -0
- package/src/tooling/plan.ts +123 -0
- package/src/tooling/policy.ts +67 -0
- package/src/tooling/registry-build.ts +191 -0
- package/src/types.ts +696 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction lifecycle for the agent run loop (WI-09 / P1-1, CE-3 /
|
|
3
|
+
* AG-13, context-engine-06): the auto-compaction trigger evaluated
|
|
4
|
+
* before each provider request, the last-resort emergency compaction at
|
|
5
|
+
* hard context overflow, the servicing of `agent.compact()` manual
|
|
6
|
+
* requests, and the prefix-pinned splice shared by all three paths.
|
|
7
|
+
* Extracted verbatim from `factory.ts` (issue #23); the former run-loop
|
|
8
|
+
* closures now take an explicit {@link CompactionRunEnv}.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AgentEvent, Message, RunState, Sensitivity } from '@graphorin/core';
|
|
14
|
+
import type { Memory } from '@graphorin/memory';
|
|
15
|
+
import type { CompactionApiResult, CompactOptions } from '../types.js';
|
|
16
|
+
import type { MutableRunState } from './run-input.js';
|
|
17
|
+
|
|
18
|
+
/** Return envelope of `ContextEngine.compactNow` (shared splice paths, CE-3). */
|
|
19
|
+
export type CompactionEnvelope = Awaited<ReturnType<Memory['contextEngine']['compactNow']>>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Manual-compaction requests enqueued by `agent.compact()` (CE-3 /
|
|
23
|
+
* AG-13). The run loop owns the live message buffer, so the splice
|
|
24
|
+
* must happen inside the loop: `maybeAutoCompact` services the queue
|
|
25
|
+
* at the next step boundary, and `finishRun` settles leftovers as
|
|
26
|
+
* explicit no-ops.
|
|
27
|
+
*/
|
|
28
|
+
export interface PendingManualCompact {
|
|
29
|
+
readonly options: CompactOptions | undefined;
|
|
30
|
+
readonly resolve: (result: CompactionApiResult) => void;
|
|
31
|
+
readonly reject: (cause: unknown) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const noopCompactionResult = (
|
|
35
|
+
skippedReason: NonNullable<CompactionApiResult['skippedReason']>,
|
|
36
|
+
): CompactionApiResult => ({
|
|
37
|
+
beforeTokens: 0,
|
|
38
|
+
afterTokens: 0,
|
|
39
|
+
summaryTokens: 0,
|
|
40
|
+
durationMs: 0,
|
|
41
|
+
hooksFiredCount: 0,
|
|
42
|
+
summary: '',
|
|
43
|
+
applied: false,
|
|
44
|
+
skippedReason,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The run-scoped context the compaction paths operate on. Field names
|
|
49
|
+
* mirror the run-loop locals the former closures captured; the live
|
|
50
|
+
* message buffer and the pending-manual queue are shared references.
|
|
51
|
+
*/
|
|
52
|
+
export interface CompactionRunEnv {
|
|
53
|
+
readonly config: { readonly sensitivity?: Sensitivity };
|
|
54
|
+
readonly memory: Memory | undefined;
|
|
55
|
+
readonly state: MutableRunState & RunState;
|
|
56
|
+
readonly messages: Message[];
|
|
57
|
+
readonly sessionId: string;
|
|
58
|
+
readonly agentId: string;
|
|
59
|
+
readonly systemPrefixLength: number;
|
|
60
|
+
readonly pendingManualCompacts: PendingManualCompact[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Auto-compaction trigger (WI-09 / P1-1). Before assembling each
|
|
65
|
+
* provider request, ask the memory {@link ContextEngine} whether the
|
|
66
|
+
* in-flight buffer has crossed its per-provider threshold
|
|
67
|
+
* (`shouldCompact`); when it has, summarise the older turns
|
|
68
|
+
* (`compactNow`, `source: 'auto-trigger'`), splice the result back in
|
|
69
|
+
* - preserving the byte-stable system prefix and the most-recent
|
|
70
|
+
* turns verbatim - and emit `context.compacted`. The compaction is
|
|
71
|
+
* configured on the memory facade (`createMemory({ contextEngine })`,
|
|
72
|
+
* RB-46); there is no parallel agent-level knob.
|
|
73
|
+
*
|
|
74
|
+
* No-op when no memory is wired, when compaction is disabled or below
|
|
75
|
+
* threshold (the engine returns `false`), or for `secret`-tier runs
|
|
76
|
+
* (secret history is never shipped to the summarizer - a less-trusted
|
|
77
|
+
* external sink; per-result handle references land in WI-10). Best
|
|
78
|
+
* effort: a misconfigured engine (e.g. no summarizer) is swallowed and
|
|
79
|
+
* the run proceeds uncompacted rather than aborting mid-flight.
|
|
80
|
+
*
|
|
81
|
+
* Operator-requested compactions (`agent.compact()`, CE-3/AG-13) are
|
|
82
|
+
* serviced here too, FIRST - the queue carries the `compact()` promise
|
|
83
|
+
* resolvers, and manual requests bypass the trigger evaluation.
|
|
84
|
+
*/
|
|
85
|
+
export async function* maybeAutoCompact<TOutput>(
|
|
86
|
+
env: CompactionRunEnv,
|
|
87
|
+
): AsyncGenerator<AgentEvent<TOutput>, void, void> {
|
|
88
|
+
const { config, memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
|
|
89
|
+
const { pendingManualCompacts } = env;
|
|
90
|
+
while (pendingManualCompacts.length > 0) {
|
|
91
|
+
const pending = pendingManualCompacts.shift();
|
|
92
|
+
if (pending !== undefined) yield* serviceManualCompact<TOutput>(env, pending);
|
|
93
|
+
}
|
|
94
|
+
const mem = memory;
|
|
95
|
+
if (mem === undefined) return;
|
|
96
|
+
// Sensitivity gate (WI-09 step 2): drop, never re-route, secret-tier
|
|
97
|
+
// content. Auto-compaction is an LLM summarizer call, so a secret
|
|
98
|
+
// run is left un-compacted here.
|
|
99
|
+
if (config.sensitivity === 'secret') return;
|
|
100
|
+
const engine = mem.contextEngine;
|
|
101
|
+
// context-engine-04: trigger, reclaim floor, and anti-thrash guard
|
|
102
|
+
// share one basis - the engine sees the full buffer for the trigger
|
|
103
|
+
// total, learns where the pinned (uncompactable) prefix ends, and
|
|
104
|
+
// receives the prefix messages so the guard arms against the FULL
|
|
105
|
+
// post-splice context instead of the sliced body.
|
|
106
|
+
const triggered = await engine
|
|
107
|
+
.shouldCompact(messages, { compactableFromIndex: systemPrefixLength })
|
|
108
|
+
.catch(() => false);
|
|
109
|
+
if (!triggered) return;
|
|
110
|
+
|
|
111
|
+
const startedAt = Date.now();
|
|
112
|
+
const envelope = await engine
|
|
113
|
+
.compactNow({
|
|
114
|
+
scope: { userId: state.userId ?? agentId, sessionId, agentId },
|
|
115
|
+
runId: state.id,
|
|
116
|
+
sessionId,
|
|
117
|
+
agentId,
|
|
118
|
+
source: 'auto-trigger',
|
|
119
|
+
messages: messages.slice(systemPrefixLength),
|
|
120
|
+
prefixMessages: messages.slice(0, systemPrefixLength),
|
|
121
|
+
memory: mem,
|
|
122
|
+
})
|
|
123
|
+
// No summarizer configured (or the strategy threw) - proceed with
|
|
124
|
+
// the un-compacted buffer rather than failing a live run.
|
|
125
|
+
.catch(() => undefined);
|
|
126
|
+
if (envelope === undefined) return;
|
|
127
|
+
// Nothing was old enough to trim (body ≤ preserve-recent) - skip the
|
|
128
|
+
// splice + event so `context.compacted` only fires on real work.
|
|
129
|
+
if (envelope.result.droppedMessageIndices.length === 0) return;
|
|
130
|
+
|
|
131
|
+
spliceCompacted(env, envelope);
|
|
132
|
+
yield {
|
|
133
|
+
type: 'context.compacted',
|
|
134
|
+
runId: state.id,
|
|
135
|
+
sessionId,
|
|
136
|
+
agentId,
|
|
137
|
+
beforeTokens: envelope.result.beforeTokens,
|
|
138
|
+
afterTokens: envelope.result.afterTokens,
|
|
139
|
+
summaryTokens: envelope.result.summaryTokens,
|
|
140
|
+
durationMs: Date.now() - startedAt,
|
|
141
|
+
source: 'auto-trigger',
|
|
142
|
+
hooksFiredCount: envelope.result.hooksFiredCount,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* context-engine-06: last-resort tier at hard context overflow. When
|
|
148
|
+
* a provider rejects the request as over-window, force ONE aggressive
|
|
149
|
+
* compaction (`preserveRecentTurns: 2`, trigger evaluation bypassed)
|
|
150
|
+
* and let the caller retry the same provider - the fallback chain's
|
|
151
|
+
* members usually share the same window, so without this the run just
|
|
152
|
+
* dies. Returns `true` when the buffer actually shrank (retry is
|
|
153
|
+
* worthwhile); `false` when memory is not wired, the run is
|
|
154
|
+
* secret-tier, compaction trimmed nothing, or the engine threw.
|
|
155
|
+
*/
|
|
156
|
+
export async function* tryEmergencyCompact<TOutput>(
|
|
157
|
+
env: CompactionRunEnv,
|
|
158
|
+
): AsyncGenerator<AgentEvent<TOutput>, boolean, void> {
|
|
159
|
+
const { config, memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
|
|
160
|
+
const mem = memory;
|
|
161
|
+
if (mem === undefined || config.sensitivity === 'secret') return false;
|
|
162
|
+
const startedAt = Date.now();
|
|
163
|
+
const envelope = await mem.contextEngine
|
|
164
|
+
.compactNow({
|
|
165
|
+
scope: { userId: state.userId ?? agentId, sessionId, agentId },
|
|
166
|
+
runId: state.id,
|
|
167
|
+
sessionId,
|
|
168
|
+
agentId,
|
|
169
|
+
source: 'auto-trigger',
|
|
170
|
+
messages: messages.slice(systemPrefixLength),
|
|
171
|
+
prefixMessages: messages.slice(0, systemPrefixLength),
|
|
172
|
+
memory: mem,
|
|
173
|
+
preserveRecentTurns: 2,
|
|
174
|
+
})
|
|
175
|
+
.catch(() => undefined);
|
|
176
|
+
if (envelope === undefined || envelope.result.droppedMessageIndices.length === 0) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
spliceCompacted(env, envelope);
|
|
180
|
+
yield {
|
|
181
|
+
type: 'context.compacted',
|
|
182
|
+
runId: state.id,
|
|
183
|
+
sessionId,
|
|
184
|
+
agentId,
|
|
185
|
+
beforeTokens: envelope.result.beforeTokens,
|
|
186
|
+
afterTokens: envelope.result.afterTokens,
|
|
187
|
+
summaryTokens: envelope.result.summaryTokens,
|
|
188
|
+
durationMs: Date.now() - startedAt,
|
|
189
|
+
source: 'auto-trigger',
|
|
190
|
+
hooksFiredCount: envelope.result.hooksFiredCount,
|
|
191
|
+
};
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Prefix-pinned splice shared by the auto + manual compaction paths
|
|
197
|
+
* (CE-3): stable system prefix + [summary, ...recent turns], with the
|
|
198
|
+
* post-compaction hooks' text Context Essentials re-anchored as a
|
|
199
|
+
* trailing system message so they survive the trim (RB-46). Mutates
|
|
200
|
+
* BOTH the live loop buffer and `state.messages`.
|
|
201
|
+
*/
|
|
202
|
+
function spliceCompacted(env: CompactionRunEnv, envelope: CompactionEnvelope): void {
|
|
203
|
+
const { state, messages, systemPrefixLength } = env;
|
|
204
|
+
const prefix = messages.slice(0, systemPrefixLength);
|
|
205
|
+
const rebuilt: Message[] = [...prefix, ...envelope.result.trimmedMessages];
|
|
206
|
+
const essentials = envelope.extraContent
|
|
207
|
+
.map((part) =>
|
|
208
|
+
typeof part === 'object' && part !== null && 'text' in part
|
|
209
|
+
? String((part as { readonly text: unknown }).text)
|
|
210
|
+
: '',
|
|
211
|
+
)
|
|
212
|
+
.filter((text) => text.length > 0)
|
|
213
|
+
.join('\n\n');
|
|
214
|
+
if (essentials.length > 0) {
|
|
215
|
+
rebuilt.push({ role: 'system', content: essentials });
|
|
216
|
+
}
|
|
217
|
+
messages.splice(0, messages.length, ...rebuilt);
|
|
218
|
+
state.messages.splice(0, state.messages.length, ...rebuilt);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Service one `agent.compact()` request inside the loop (CE-3/AG-13):
|
|
223
|
+
* same prefix-pinned splice as the auto path, `source: 'manual'` (or
|
|
224
|
+
* the caller's `'pre-step'`), `preserveRecentTurns` forwarded as a
|
|
225
|
+
* per-call strategy override. An engine failure rejects the caller's
|
|
226
|
+
* promise but never aborts the live run; a summarize that trims
|
|
227
|
+
* nothing resolves `applied: false` without an event.
|
|
228
|
+
*/
|
|
229
|
+
async function* serviceManualCompact<TOutput>(
|
|
230
|
+
env: CompactionRunEnv,
|
|
231
|
+
pending: PendingManualCompact,
|
|
232
|
+
): AsyncGenerator<AgentEvent<TOutput>, void, void> {
|
|
233
|
+
const { memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
|
|
234
|
+
const mem = memory;
|
|
235
|
+
if (mem === undefined) {
|
|
236
|
+
pending.resolve(noopCompactionResult('no-memory'));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const source = pending.options?.source ?? 'manual';
|
|
240
|
+
const startedAt = Date.now();
|
|
241
|
+
let envelope: CompactionEnvelope;
|
|
242
|
+
try {
|
|
243
|
+
envelope = await mem.contextEngine.compactNow({
|
|
244
|
+
scope: { userId: state.userId ?? agentId, sessionId, agentId },
|
|
245
|
+
runId: state.id,
|
|
246
|
+
sessionId,
|
|
247
|
+
agentId,
|
|
248
|
+
source,
|
|
249
|
+
messages: messages.slice(systemPrefixLength),
|
|
250
|
+
// context-engine-04: same accounting basis as the auto path.
|
|
251
|
+
prefixMessages: messages.slice(0, systemPrefixLength),
|
|
252
|
+
memory: mem,
|
|
253
|
+
...(pending.options?.preserveRecentTurns !== undefined
|
|
254
|
+
? { preserveRecentTurns: pending.options.preserveRecentTurns }
|
|
255
|
+
: {}),
|
|
256
|
+
});
|
|
257
|
+
} catch (cause) {
|
|
258
|
+
pending.reject(cause);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const { result } = envelope;
|
|
262
|
+
const applied = result.droppedMessageIndices.length > 0;
|
|
263
|
+
if (applied) {
|
|
264
|
+
spliceCompacted(env, envelope);
|
|
265
|
+
yield {
|
|
266
|
+
type: 'context.compacted',
|
|
267
|
+
runId: state.id,
|
|
268
|
+
sessionId,
|
|
269
|
+
agentId,
|
|
270
|
+
beforeTokens: result.beforeTokens,
|
|
271
|
+
afterTokens: result.afterTokens,
|
|
272
|
+
summaryTokens: result.summaryTokens,
|
|
273
|
+
durationMs: Date.now() - startedAt,
|
|
274
|
+
source,
|
|
275
|
+
hooksFiredCount: result.hooksFiredCount,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
pending.resolve({
|
|
279
|
+
beforeTokens: result.beforeTokens,
|
|
280
|
+
afterTokens: result.afterTokens,
|
|
281
|
+
summaryTokens: result.summaryTokens,
|
|
282
|
+
durationMs: Date.now() - startedAt,
|
|
283
|
+
hooksFiredCount: result.hooksFiredCount,
|
|
284
|
+
summary: result.summary ?? '',
|
|
285
|
+
applied,
|
|
286
|
+
...(applied ? {} : { skippedReason: 'nothing-to-trim' as const }),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal path of every agent run: settle leftover manual-compact
|
|
3
|
+
* requests (CE-3 / AG-13), assemble the final {@link AgentResult}
|
|
4
|
+
* (AG-9), clear terminal-run spill artifacts (TL-10), and end the
|
|
5
|
+
* stream with `agent.end` (AG-20). Extracted verbatim from
|
|
6
|
+
* `factory.ts` (issue #23); the former factory closures now hang off
|
|
7
|
+
* {@link createRunFinisher}'s deps.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AgentEvent, AgentResult, CheckpointStore, RunState } from '@graphorin/core';
|
|
13
|
+
import type { createDefaultSpillWriter } from '@graphorin/tools/result';
|
|
14
|
+
import { noopCompactionResult, type PendingManualCompact } from './run-compaction.js';
|
|
15
|
+
import type { InternalRunSnapshot, MutableRunState } from './run-input.js';
|
|
16
|
+
|
|
17
|
+
/** What the terminal path needs from the agent factory scope. */
|
|
18
|
+
export interface RunFinisherDeps {
|
|
19
|
+
readonly pendingManualCompacts: PendingManualCompact[];
|
|
20
|
+
readonly spillWriter: ReturnType<typeof createDefaultSpillWriter>;
|
|
21
|
+
/** W-005: thread erasure on terminal runs (`'delete-on-terminal'`). */
|
|
22
|
+
readonly checkpointStore?: CheckpointStore | undefined;
|
|
23
|
+
readonly checkpointPolicy?: 'keep' | 'delete-on-terminal' | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the factory's `finishRunBase` over the shared manual-compact
|
|
28
|
+
* queue and spill writer.
|
|
29
|
+
*/
|
|
30
|
+
export function createRunFinisher<TOutput>(
|
|
31
|
+
deps: RunFinisherDeps,
|
|
32
|
+
): (
|
|
33
|
+
state: MutableRunState & RunState,
|
|
34
|
+
snapshot: InternalRunSnapshot<TOutput>,
|
|
35
|
+
) => AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {
|
|
36
|
+
const { pendingManualCompacts, spillWriter, checkpointStore, checkpointPolicy } = deps;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Terminal wrapper around {@link finalize}: every exit path of the run
|
|
40
|
+
* loop - completed, failed, aborted, suspended - ends the stream with
|
|
41
|
+
* an `agent.end` event carrying the final {@link AgentResult} (AG-20).
|
|
42
|
+
*
|
|
43
|
+
* The parameter is `MutableRunState & RunState`: the single caller
|
|
44
|
+
* (factory `finishRun`) is typed exactly so, and the intersection lets
|
|
45
|
+
* {@link finalize} assemble `AgentResult.state` without a cast (W-047).
|
|
46
|
+
*/
|
|
47
|
+
async function* finishRunBase(
|
|
48
|
+
state: MutableRunState & RunState,
|
|
49
|
+
snapshot: InternalRunSnapshot<TOutput>,
|
|
50
|
+
): AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {
|
|
51
|
+
// CE-3/AG-13: settle manual-compact requests the loop never serviced -
|
|
52
|
+
// the run is over, there is no live buffer left to splice.
|
|
53
|
+
while (pendingManualCompacts.length > 0) {
|
|
54
|
+
pendingManualCompacts.shift()?.resolve(noopCompactionResult('no-active-run'));
|
|
55
|
+
}
|
|
56
|
+
const result = finalize(state, snapshot);
|
|
57
|
+
// TL-10: spill artifacts are run-scoped scratch - drop them once the
|
|
58
|
+
// run is terminal. `awaiting_approval` and `aborted` runs keep
|
|
59
|
+
// theirs (handles must survive resume); orphans fall to the writer's
|
|
60
|
+
// startup TTL sweep.
|
|
61
|
+
if (result.status === 'completed' || result.status === 'failed') {
|
|
62
|
+
await spillWriter.clear?.(result.state.id).catch(() => {});
|
|
63
|
+
// W-005: opt-in checkpoint hygiene mirrors the spill lifecycle -
|
|
64
|
+
// same statuses, best-effort, and NEVER for awaiting_approval /
|
|
65
|
+
// aborted runs (their thread is the resume state). Default 'keep'
|
|
66
|
+
// preserves post-hoc debugging and process-restart resume.
|
|
67
|
+
if (checkpointPolicy === 'delete-on-terminal' && checkpointStore !== undefined) {
|
|
68
|
+
await checkpointStore.deleteThread(result.state.id).catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
yield { type: 'agent.end', runId: result.state.id, result };
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function finalize(
|
|
76
|
+
state: MutableRunState & RunState,
|
|
77
|
+
snapshot: InternalRunSnapshot<TOutput>,
|
|
78
|
+
): AgentResult<TOutput> {
|
|
79
|
+
state.finishedAt = state.finishedAt ?? new Date().toISOString();
|
|
80
|
+
// AG-9: the result carries the terminal status, the failure (when
|
|
81
|
+
// any), and the final RunState - a suspended run is resumable from
|
|
82
|
+
// the result alone, no checkpointStore required.
|
|
83
|
+
return {
|
|
84
|
+
output: snapshot.output,
|
|
85
|
+
usage: state.usage,
|
|
86
|
+
status: state.status,
|
|
87
|
+
...(state.error !== undefined ? { error: state.error } : {}),
|
|
88
|
+
state,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return finishRunBase;
|
|
93
|
+
}
|