@intx/inference 0.1.2 → 0.3.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/LICENSE +176 -0
- package/dist/actions.d.ts +16 -0
- package/dist/actions.js +200 -0
- package/dist/adapter.d.ts +40 -0
- package/dist/adapter.js +31 -0
- package/dist/assembly.d.ts +75 -0
- package/dist/assembly.js +133 -0
- package/dist/audit-collector.d.ts +10 -0
- package/dist/audit-collector.js +139 -0
- package/dist/auth.d.ts +24 -0
- package/{src/auth.ts → dist/auth.js} +13 -19
- package/dist/authz-extension.d.ts +46 -0
- package/dist/authz-extension.js +184 -0
- package/dist/correlation.d.ts +26 -0
- package/dist/correlation.js +39 -0
- package/dist/default-director.d.ts +111 -0
- package/dist/default-director.js +228 -0
- package/dist/director.d.ts +6 -0
- package/dist/director.js +56 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +83 -0
- package/dist/gates.d.ts +28 -0
- package/dist/gates.js +103 -0
- package/dist/harness.d.ts +147 -0
- package/dist/harness.js +1407 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +21 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.js +44 -0
- package/dist/providers/anthropic.d.ts +37 -0
- package/dist/providers/anthropic.js +917 -0
- package/dist/providers/google-genai-files.d.ts +48 -0
- package/dist/providers/google-genai-files.js +205 -0
- package/dist/providers/google-genai.d.ts +5 -0
- package/dist/providers/google-genai.js +1205 -0
- package/dist/providers/index.d.ts +38 -0
- package/dist/providers/index.js +56 -0
- package/dist/providers/openai.d.ts +9 -0
- package/dist/providers/openai.js +903 -0
- package/dist/reactor.d.ts +50 -0
- package/dist/reactor.js +1233 -0
- package/dist/retry-policy.d.ts +31 -0
- package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
- package/dist/sse.d.ts +1 -0
- package/dist/sse.js +63 -0
- package/dist/state.d.ts +23 -0
- package/dist/state.js +100 -0
- package/dist/tool-name.d.ts +6 -0
- package/dist/tool-name.js +110 -0
- package/dist/transform.d.ts +11 -0
- package/dist/transform.js +132 -0
- package/dist/transforms/index.d.ts +2 -0
- package/dist/transforms/index.js +1 -0
- package/dist/transforms/size-cap.d.ts +12 -0
- package/dist/transforms/size-cap.js +80 -0
- package/dist/turns.d.ts +21 -0
- package/dist/turns.js +135 -0
- package/package.json +22 -6
- package/src/actions.ts +0 -245
- package/src/adapter.ts +0 -57
- package/src/assembly.test.ts +0 -728
- package/src/assembly.ts +0 -250
- package/src/audit-collector.test.ts +0 -332
- package/src/audit-collector.ts +0 -172
- package/src/auth.test.ts +0 -117
- package/src/authz-extension.test.ts +0 -269
- package/src/authz-extension.ts +0 -145
- package/src/correlation.ts +0 -61
- package/src/default-director.test.ts +0 -314
- package/src/default-director.ts +0 -344
- package/src/director.ts +0 -87
- package/src/errors.test.ts +0 -133
- package/src/errors.ts +0 -115
- package/src/gates.ts +0 -128
- package/src/harness.test.ts +0 -655
- package/src/harness.ts +0 -1571
- package/src/index.ts +0 -76
- package/src/providers/anthropic.test.ts +0 -771
- package/src/providers/anthropic.ts +0 -810
- package/src/providers/google-genai-files.ts +0 -289
- package/src/providers/google-genai.ts +0 -1518
- package/src/providers/openai.ts +0 -719
- package/src/providers/registry.ts +0 -33
- package/src/reactor.test.ts +0 -3660
- package/src/reactor.ts +0 -1058
- package/src/scheduler.test.ts +0 -41
- package/src/sse.test.ts +0 -133
- package/src/sse.ts +0 -76
- package/src/state.ts +0 -135
- package/src/transform.test.ts +0 -207
- package/src/transform.ts +0 -159
- package/src/transforms/index.ts +0 -2
- package/src/transforms/size-cap.test.ts +0 -172
- package/src/transforms/size-cap.ts +0 -110
- package/src/turns.ts +0 -54
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
package/src/reactor.ts
DELETED
|
@@ -1,1058 +0,0 @@
|
|
|
1
|
-
// Agent reactor: the event-driven dispatch loop.
|
|
2
|
-
//
|
|
3
|
-
// The reactor processes one event at a time, asks the director for the next
|
|
4
|
-
// action, validates the action set, and executes. It manages the streaming
|
|
5
|
-
// harness for inference, dispatches tool calls, handles gates and correlation,
|
|
6
|
-
// and emits all session events with monotonic sequence numbers.
|
|
7
|
-
//
|
|
8
|
-
// Suspension semantics: when the director returns a suspend action, the reactor
|
|
9
|
-
// registers the gate and continues processing events. Inbound messages during
|
|
10
|
-
// suspension reach the director as message.received events (director decides:
|
|
11
|
-
// queue, fork, or ignore). When the gate clears, a reactor.gate.cleared event
|
|
12
|
-
// is enqueued and the director gets to decide next steps.
|
|
13
|
-
//
|
|
14
|
-
// (INFERENCE.md § Agent Reactor)
|
|
15
|
-
|
|
16
|
-
import type {
|
|
17
|
-
InboundMessage,
|
|
18
|
-
InferenceEvent,
|
|
19
|
-
InferenceOptions,
|
|
20
|
-
InferenceSource,
|
|
21
|
-
ReactorDirector,
|
|
22
|
-
ReactorInboundEvent,
|
|
23
|
-
ContextStore,
|
|
24
|
-
ToolRunner,
|
|
25
|
-
TokenUsage,
|
|
26
|
-
ConversationTurn,
|
|
27
|
-
ToolResult,
|
|
28
|
-
ToolCall,
|
|
29
|
-
AbortReason,
|
|
30
|
-
BeforeToolExtension,
|
|
31
|
-
ReactorAction,
|
|
32
|
-
ToolResultTransform,
|
|
33
|
-
ContextTransform,
|
|
34
|
-
Compactor,
|
|
35
|
-
TransformRecord,
|
|
36
|
-
StrategyContext,
|
|
37
|
-
StrategyResult,
|
|
38
|
-
} from "@intx/types/runtime";
|
|
39
|
-
|
|
40
|
-
import { getLogger } from "@intx/log";
|
|
41
|
-
import { runInference } from "./harness";
|
|
42
|
-
import type { Dependencies, InferenceHarnessOptions } from "./harness";
|
|
43
|
-
import { createCapabilities } from "./director";
|
|
44
|
-
import { createGateManager } from "./gates";
|
|
45
|
-
import { createCorrelationRegistry } from "./correlation";
|
|
46
|
-
import { createStateManager } from "./state";
|
|
47
|
-
import { validateActions } from "./actions";
|
|
48
|
-
import { createToolResultTurn, createInboundTurn } from "./turns";
|
|
49
|
-
import type { CorrelationValidator } from "./correlation";
|
|
50
|
-
|
|
51
|
-
const logger = getLogger(["interchange", "reactor"]);
|
|
52
|
-
|
|
53
|
-
function buildHarnessOpts(
|
|
54
|
-
turns: ConversationTurn[],
|
|
55
|
-
source: InferenceSource,
|
|
56
|
-
options: InferenceOptions | undefined,
|
|
57
|
-
signal: AbortSignal,
|
|
58
|
-
nextSeq: () => number,
|
|
59
|
-
deps: Dependencies,
|
|
60
|
-
): InferenceHarnessOptions {
|
|
61
|
-
if (options !== undefined) {
|
|
62
|
-
return {
|
|
63
|
-
turns,
|
|
64
|
-
source,
|
|
65
|
-
inferenceOptions: options,
|
|
66
|
-
signal,
|
|
67
|
-
nextSeq,
|
|
68
|
-
deps,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return { turns, source, signal, nextSeq, deps };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export type ReactorEmittedEvent =
|
|
75
|
-
| InferenceEvent
|
|
76
|
-
| {
|
|
77
|
-
type: "message.received";
|
|
78
|
-
seq: number;
|
|
79
|
-
data: { message: InboundMessage };
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export type ReactorConfig = {
|
|
83
|
-
sessionId: string;
|
|
84
|
-
director: ReactorDirector;
|
|
85
|
-
source: InferenceSource;
|
|
86
|
-
toolRunner: ToolRunner;
|
|
87
|
-
contextStore: ContextStore;
|
|
88
|
-
correlationValidator?: CorrelationValidator;
|
|
89
|
-
onEvent: (event: ReactorEmittedEvent) => void;
|
|
90
|
-
deps: Dependencies;
|
|
91
|
-
inferenceRunner?: (
|
|
92
|
-
opts: InferenceHarnessOptions,
|
|
93
|
-
) => AsyncGenerator<InferenceEvent>;
|
|
94
|
-
beforeToolExtensions?: BeforeToolExtension[];
|
|
95
|
-
toolResultTransforms?: ToolResultTransform[];
|
|
96
|
-
contextTransforms?: ContextTransform[];
|
|
97
|
-
compactors?: Record<string, Compactor>;
|
|
98
|
-
afterCheckpoint?: () => Promise<void>;
|
|
99
|
-
onShutdown?: () => Promise<void>;
|
|
100
|
-
gateTimeout?: number;
|
|
101
|
-
shutdownTimeoutMs?: number;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export type Reactor = {
|
|
105
|
-
/** Begin processing. Emits reactor.start. Must be called exactly once. */
|
|
106
|
-
start(): void;
|
|
107
|
-
/** Inject an inbound message into the reactor. */
|
|
108
|
-
deliver(message: InboundMessage): void;
|
|
109
|
-
/** Initiate graceful shutdown with a reason. */
|
|
110
|
-
abort(reason: AbortReason): void;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const DEFAULT_GATE_TIMEOUT_MS = 3_600_000;
|
|
114
|
-
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Creates a reactor instance bound to the given configuration.
|
|
118
|
-
* Call `start()` to begin the event loop.
|
|
119
|
-
*/
|
|
120
|
-
export function createReactor(config: ReactorConfig): Reactor {
|
|
121
|
-
const {
|
|
122
|
-
sessionId,
|
|
123
|
-
director,
|
|
124
|
-
toolRunner,
|
|
125
|
-
contextStore,
|
|
126
|
-
correlationValidator,
|
|
127
|
-
onEvent,
|
|
128
|
-
deps,
|
|
129
|
-
inferenceRunner = runInference,
|
|
130
|
-
beforeToolExtensions = [],
|
|
131
|
-
toolResultTransforms = [],
|
|
132
|
-
contextTransforms = [],
|
|
133
|
-
compactors = {},
|
|
134
|
-
afterCheckpoint,
|
|
135
|
-
onShutdown,
|
|
136
|
-
gateTimeout = DEFAULT_GATE_TIMEOUT_MS,
|
|
137
|
-
shutdownTimeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
138
|
-
} = config;
|
|
139
|
-
|
|
140
|
-
// Monotonic sequence counter, scoped to this session.
|
|
141
|
-
let seq = 0;
|
|
142
|
-
function nextSeq(): number {
|
|
143
|
-
return ++seq;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function emit(event: ReactorEmittedEvent): void {
|
|
147
|
-
onEvent(event);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Inbound event queue. Events are pushed here and drained by the loop.
|
|
151
|
-
const queue: ReactorInboundEvent[] = [];
|
|
152
|
-
let queueResolve: (() => void) | null = null;
|
|
153
|
-
|
|
154
|
-
function enqueue(event: ReactorInboundEvent): void {
|
|
155
|
-
queue.push(event);
|
|
156
|
-
if (queueResolve !== null) {
|
|
157
|
-
const resolve = queueResolve;
|
|
158
|
-
queueResolve = null;
|
|
159
|
-
resolve();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function waitForEvent(): Promise<void> {
|
|
164
|
-
if (queue.length > 0) return;
|
|
165
|
-
await new Promise<void>((resolve) => {
|
|
166
|
-
queueResolve = resolve;
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// When the last message in history is an assistant message with tool_calls
|
|
171
|
-
// whose results haven't been appended yet, the conversation is in a
|
|
172
|
-
// transient state. Inserting a user text message (from message.received)
|
|
173
|
-
// at this point violates the provider's protocol: an assistant tool_call
|
|
174
|
-
// must be immediately followed by the corresponding tool result messages.
|
|
175
|
-
//
|
|
176
|
-
// To prevent this, we check whether the history has pending tool_calls
|
|
177
|
-
// and, if so, prioritize inference-cycle events (inference.done,
|
|
178
|
-
// inference.error, tool.done) so the cycle completes before inbound
|
|
179
|
-
// messages are interleaved. Once the tool results are in history, we
|
|
180
|
-
// revert to FIFO so inbound messages are processed promptly.
|
|
181
|
-
const CYCLE_EVENT_TYPES = new Set<ReactorInboundEvent["type"]>([
|
|
182
|
-
"inference.done",
|
|
183
|
-
"inference.error",
|
|
184
|
-
"tool.done",
|
|
185
|
-
]);
|
|
186
|
-
|
|
187
|
-
function historyHasPendingToolCalls(): boolean {
|
|
188
|
-
if (stateManager === null) return false;
|
|
189
|
-
const turns = stateManager.getTurns();
|
|
190
|
-
if (turns.length === 0) return false;
|
|
191
|
-
|
|
192
|
-
const last = turns.at(-1);
|
|
193
|
-
if (last === undefined || last.role !== "assistant") return false;
|
|
194
|
-
|
|
195
|
-
return last.content.some((b) => b.type === "tool_call");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function dequeueNext(): ReactorInboundEvent | undefined {
|
|
199
|
-
if (queue.length === 0) return undefined;
|
|
200
|
-
|
|
201
|
-
// Always process abort immediately.
|
|
202
|
-
const abortIdx = queue.findIndex((e) => e.type === "abort");
|
|
203
|
-
if (abortIdx !== -1) {
|
|
204
|
-
return queue.splice(abortIdx, 1)[0];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// When mid-cycle (assistant tool_calls without tool results), drain
|
|
208
|
-
// inference-cycle events before anything else.
|
|
209
|
-
if (historyHasPendingToolCalls()) {
|
|
210
|
-
const idx = queue.findIndex((e) => CYCLE_EVENT_TYPES.has(e.type));
|
|
211
|
-
if (idx !== -1) {
|
|
212
|
-
return queue.splice(idx, 1)[0];
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return queue.shift();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const gates = createGateManager();
|
|
220
|
-
const correlations = createCorrelationRegistry();
|
|
221
|
-
const capabilities = createCapabilities();
|
|
222
|
-
|
|
223
|
-
let stateManager: ReturnType<typeof createStateManager> | null = null;
|
|
224
|
-
let running = false;
|
|
225
|
-
let done = false;
|
|
226
|
-
let shutdownStarted = false;
|
|
227
|
-
|
|
228
|
-
// Per-cycle accumulator of TransformRecord entries produced by every
|
|
229
|
-
// transform invocation (tool result, context, compactor). Flushed via
|
|
230
|
-
// contextStore.writeManifest at cycle boundaries.
|
|
231
|
-
let manifestBuffer: TransformRecord[] = [];
|
|
232
|
-
|
|
233
|
-
// Tracks how the current cycle should be summarized in the commit message.
|
|
234
|
-
let cycleInferred = false;
|
|
235
|
-
let cycleToolCallsExecuted = 0;
|
|
236
|
-
let cycleCompactorName: string | null = null;
|
|
237
|
-
|
|
238
|
-
// Director-supplied checkpoint message override; consumed exactly once.
|
|
239
|
-
let pendingMessage: string | null = null;
|
|
240
|
-
|
|
241
|
-
// AbortController for in-flight inference/tool operations.
|
|
242
|
-
let operationController = new AbortController();
|
|
243
|
-
|
|
244
|
-
function abortOperations(): void {
|
|
245
|
-
operationController.abort();
|
|
246
|
-
operationController = new AbortController();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Track in-flight inference and tool promises for shutdown cleanup.
|
|
250
|
-
const inFlight = new Set<Promise<unknown>>();
|
|
251
|
-
|
|
252
|
-
function track<T>(p: Promise<T>): Promise<T> {
|
|
253
|
-
inFlight.add(p);
|
|
254
|
-
p.then(
|
|
255
|
-
() => inFlight.delete(p),
|
|
256
|
-
() => inFlight.delete(p),
|
|
257
|
-
);
|
|
258
|
-
return p;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// -------------------------------------------------------------------------
|
|
262
|
-
// Correlation helper
|
|
263
|
-
// -------------------------------------------------------------------------
|
|
264
|
-
|
|
265
|
-
// Guard against concurrent tryCorrelate calls for the same correlationId.
|
|
266
|
-
// deliver() is fire-and-forget async, so two rapid delivers can interleave
|
|
267
|
-
// across an await boundary in the validator, causing double-correlation.
|
|
268
|
-
const correlatingIds = new Set<string>();
|
|
269
|
-
|
|
270
|
-
async function tryCorrelate(message: InboundMessage): Promise<boolean> {
|
|
271
|
-
const correlationId = message.headers.interchangeCorrelationId;
|
|
272
|
-
if (correlationId === undefined) return false;
|
|
273
|
-
|
|
274
|
-
if (correlatingIds.has(correlationId)) return false;
|
|
275
|
-
const pending = correlations.lookup(correlationId);
|
|
276
|
-
if (pending === undefined) return false;
|
|
277
|
-
|
|
278
|
-
correlatingIds.add(correlationId);
|
|
279
|
-
|
|
280
|
-
if (correlationValidator !== undefined) {
|
|
281
|
-
let valid: boolean;
|
|
282
|
-
try {
|
|
283
|
-
valid = await correlationValidator.validate(pending, message);
|
|
284
|
-
} catch (cause) {
|
|
285
|
-
logger.warn`Correlation validator threw for ${correlationId}: ${cause}`;
|
|
286
|
-
correlatingIds.delete(correlationId);
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
if (!valid) {
|
|
290
|
-
correlatingIds.delete(correlationId);
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Clear the gate associated with this correlation, if any.
|
|
296
|
-
const gate = gates.findByCorrelationId(correlationId);
|
|
297
|
-
if (gate !== undefined) {
|
|
298
|
-
gates.clear(gate.gateId);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
correlations.remove(correlationId);
|
|
302
|
-
|
|
303
|
-
if (stateManager !== null) {
|
|
304
|
-
stateManager.removePendingOperation(correlationId);
|
|
305
|
-
|
|
306
|
-
// Append the correlated message to conversation history so the model
|
|
307
|
-
// sees the response content when it re-infers after the gate clears.
|
|
308
|
-
const msg = createInboundTurn(message);
|
|
309
|
-
if (msg !== null) {
|
|
310
|
-
stateManager.appendTurn(msg);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
emit({
|
|
315
|
-
type: "message.correlated",
|
|
316
|
-
seq: nextSeq(),
|
|
317
|
-
data: { message, correlationId },
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// -------------------------------------------------------------------------
|
|
324
|
-
// Action execution
|
|
325
|
-
// -------------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
let pendingPacingDelayMs = 0;
|
|
328
|
-
|
|
329
|
-
function buildStrategyContext(trigger: string): StrategyContext {
|
|
330
|
-
if (stateManager === null) {
|
|
331
|
-
throw new Error("State manager not initialized");
|
|
332
|
-
}
|
|
333
|
-
return { state: stateManager.snapshot(), trigger };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function persistBlobs(
|
|
337
|
-
blobs: StrategyResult<unknown>["blobs"],
|
|
338
|
-
): Promise<void> {
|
|
339
|
-
if (blobs === undefined) return;
|
|
340
|
-
for (const blob of blobs) {
|
|
341
|
-
await contextStore.writeBlob(blob.key, blob.bytes, blob.contentType);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async function executeInfer(
|
|
346
|
-
options: InferenceOptions | undefined,
|
|
347
|
-
): Promise<void> {
|
|
348
|
-
if (stateManager === null) return;
|
|
349
|
-
|
|
350
|
-
const signal = operationController.signal;
|
|
351
|
-
|
|
352
|
-
// Proactive pacing: if the previous inference response indicated we are
|
|
353
|
-
// at the rate limit, wait before sending the next request.
|
|
354
|
-
if (pendingPacingDelayMs > 0 && !signal.aborted) {
|
|
355
|
-
const delayMs = pendingPacingDelayMs;
|
|
356
|
-
pendingPacingDelayMs = 0;
|
|
357
|
-
logger.info`Pacing: waiting ${String(delayMs)}ms before next inference request`;
|
|
358
|
-
await new Promise<void>((resolve) => {
|
|
359
|
-
const timer = setTimeout(resolve, delayMs);
|
|
360
|
-
const onAbort = () => {
|
|
361
|
-
clearTimeout(timer);
|
|
362
|
-
resolve();
|
|
363
|
-
};
|
|
364
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
365
|
-
});
|
|
366
|
-
if (signal.aborted) return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Run the context transform chain to produce the materialized prompt.
|
|
370
|
-
let prompt: ConversationTurn[] = stateManager.getTurns();
|
|
371
|
-
for (const transform of contextTransforms) {
|
|
372
|
-
const ctx = buildStrategyContext("pre-inference");
|
|
373
|
-
const result = await transform.apply(prompt, ctx);
|
|
374
|
-
prompt = result.output;
|
|
375
|
-
manifestBuffer.push(result.record);
|
|
376
|
-
await persistBlobs(result.blobs);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
await contextStore.writePrompt(prompt);
|
|
381
|
-
} catch (cause) {
|
|
382
|
-
logger.error`writePrompt failed: ${cause}`;
|
|
383
|
-
emitError(
|
|
384
|
-
`writePrompt failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
385
|
-
false,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const p = (async () => {
|
|
390
|
-
const maxRetries = 3;
|
|
391
|
-
const defaultRetryMs = 60_000;
|
|
392
|
-
|
|
393
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
394
|
-
const harnessOpts = buildHarnessOpts(
|
|
395
|
-
prompt,
|
|
396
|
-
config.source,
|
|
397
|
-
options,
|
|
398
|
-
signal,
|
|
399
|
-
nextSeq,
|
|
400
|
-
deps,
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
let lastDone:
|
|
404
|
-
| Extract<InferenceEvent, { type: "inference.done" }>
|
|
405
|
-
| undefined;
|
|
406
|
-
let lastError:
|
|
407
|
-
| Extract<InferenceEvent, { type: "inference.error" }>
|
|
408
|
-
| undefined;
|
|
409
|
-
|
|
410
|
-
for await (const event of inferenceRunner(harnessOpts)) {
|
|
411
|
-
emit(event);
|
|
412
|
-
if (event.type === "inference.done") lastDone = event;
|
|
413
|
-
else if (event.type === "inference.error") lastError = event;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (lastDone !== undefined) {
|
|
417
|
-
if (stateManager !== null) {
|
|
418
|
-
stateManager.appendTurn(lastDone.data.turn);
|
|
419
|
-
stateManager.accumUsage(lastDone.data.usage);
|
|
420
|
-
stateManager.setLastCycleUsage(lastDone.data.usage);
|
|
421
|
-
stateManager.setLastCycleSource(lastDone.data.source);
|
|
422
|
-
}
|
|
423
|
-
cycleInferred = true;
|
|
424
|
-
try {
|
|
425
|
-
await contextStore.writeResponse(lastDone.data.turn);
|
|
426
|
-
} catch (cause) {
|
|
427
|
-
logger.error`writeResponse failed: ${cause}`;
|
|
428
|
-
emitError(
|
|
429
|
-
`writeResponse failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
430
|
-
false,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
if (lastDone.data.pacingDelayMs !== undefined) {
|
|
434
|
-
pendingPacingDelayMs = lastDone.data.pacingDelayMs;
|
|
435
|
-
}
|
|
436
|
-
const u = lastDone.data.usage;
|
|
437
|
-
logger.info`Inference usage: input=${String(u.input)} output=${String(u.output)} cacheRead=${String(u.cacheRead)} cacheWrite=${String(u.cacheWrite)}${lastDone.data.pacingDelayMs !== undefined ? ` pacing=${String(lastDone.data.pacingDelayMs)}ms` : ""}`;
|
|
438
|
-
enqueue({
|
|
439
|
-
type: "inference.done",
|
|
440
|
-
turn: lastDone.data.turn,
|
|
441
|
-
usage: lastDone.data.usage,
|
|
442
|
-
source: lastDone.data.source,
|
|
443
|
-
});
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (lastError !== undefined) {
|
|
448
|
-
const err = lastError.data.error;
|
|
449
|
-
if (
|
|
450
|
-
err.category === "quota_exhausted" &&
|
|
451
|
-
attempt < maxRetries &&
|
|
452
|
-
!signal.aborted
|
|
453
|
-
) {
|
|
454
|
-
const delayMs = err.retryAfterMs ?? defaultRetryMs;
|
|
455
|
-
logger.warn`Rate limited (attempt ${String(attempt + 1)}/${String(maxRetries)}), retrying after ${String(delayMs)}ms`;
|
|
456
|
-
await new Promise<void>((resolve) => {
|
|
457
|
-
const timer = setTimeout(resolve, delayMs);
|
|
458
|
-
const onAbort = () => {
|
|
459
|
-
clearTimeout(timer);
|
|
460
|
-
resolve();
|
|
461
|
-
};
|
|
462
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
463
|
-
});
|
|
464
|
-
if (signal.aborted) {
|
|
465
|
-
enqueue({
|
|
466
|
-
type: "inference.error",
|
|
467
|
-
error: {
|
|
468
|
-
category: "aborted",
|
|
469
|
-
message: "inference aborted during rate limit backoff",
|
|
470
|
-
},
|
|
471
|
-
partial: lastError.data.partial,
|
|
472
|
-
});
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
enqueue({
|
|
479
|
-
type: "inference.error",
|
|
480
|
-
error: err,
|
|
481
|
-
partial: lastError.data.partial,
|
|
482
|
-
});
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
emitError("Inference runner returned without a terminal event", true);
|
|
487
|
-
enqueue({
|
|
488
|
-
type: "inference.error",
|
|
489
|
-
error: {
|
|
490
|
-
category: "fatal",
|
|
491
|
-
message: "Inference runner returned without a terminal event",
|
|
492
|
-
},
|
|
493
|
-
partial: { text: "" },
|
|
494
|
-
});
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
})();
|
|
498
|
-
|
|
499
|
-
track(p);
|
|
500
|
-
await p;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
async function executeTools(
|
|
504
|
-
calls: ToolCall[],
|
|
505
|
-
parallel: boolean,
|
|
506
|
-
addToHistory = true,
|
|
507
|
-
): Promise<void> {
|
|
508
|
-
if (stateManager === null) return;
|
|
509
|
-
const state = stateManager;
|
|
510
|
-
|
|
511
|
-
const signal = operationController.signal;
|
|
512
|
-
|
|
513
|
-
const runOne = async (call: ToolCall): Promise<ToolResult> => {
|
|
514
|
-
// Run before-tool extensions. First block or throw terminates the chain.
|
|
515
|
-
for (const ext of beforeToolExtensions) {
|
|
516
|
-
let blockReason: string | undefined;
|
|
517
|
-
try {
|
|
518
|
-
blockReason = await ext.beforeTool(call, state.snapshot(), signal);
|
|
519
|
-
} catch (cause) {
|
|
520
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
521
|
-
emitError(
|
|
522
|
-
`BeforeToolExtension threw for ${call.name}: ${msg}`,
|
|
523
|
-
false,
|
|
524
|
-
);
|
|
525
|
-
blockReason = msg;
|
|
526
|
-
}
|
|
527
|
-
if (blockReason !== undefined) {
|
|
528
|
-
const blocked: ToolResult = {
|
|
529
|
-
callId: call.id,
|
|
530
|
-
content: blockReason,
|
|
531
|
-
isError: true,
|
|
532
|
-
};
|
|
533
|
-
emit({
|
|
534
|
-
type: "tool.done",
|
|
535
|
-
seq: nextSeq(),
|
|
536
|
-
data: { result: blocked },
|
|
537
|
-
});
|
|
538
|
-
return blocked;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
emit({ type: "tool.start", seq: nextSeq(), data: { call } });
|
|
543
|
-
const rawResult = await toolRunner.run(call, signal);
|
|
544
|
-
emit({ type: "tool.done", seq: nextSeq(), data: { result: rawResult } });
|
|
545
|
-
|
|
546
|
-
if (rawResult.pendingMarker !== undefined && stateManager !== null) {
|
|
547
|
-
const marker = rawResult.pendingMarker;
|
|
548
|
-
const gateId = `pending-${marker.correlationId}`;
|
|
549
|
-
const op: import("@intx/types/runtime").PendingOperation = {
|
|
550
|
-
correlationId: marker.correlationId,
|
|
551
|
-
registeredAt: Date.now(),
|
|
552
|
-
gateId,
|
|
553
|
-
...(marker.expectedFrom !== undefined
|
|
554
|
-
? { expectedFrom: marker.expectedFrom }
|
|
555
|
-
: {}),
|
|
556
|
-
};
|
|
557
|
-
correlations.register(op);
|
|
558
|
-
stateManager.addPendingOperation(op);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Apply the tool-result transform chain. Each transform's output is fed
|
|
562
|
-
// into the next; emitted blobs are persisted immediately so downstream
|
|
563
|
-
// transforms can rely on the spill being available.
|
|
564
|
-
let current = rawResult;
|
|
565
|
-
for (const transform of toolResultTransforms) {
|
|
566
|
-
const ctx = buildStrategyContext("tool-result-ingest");
|
|
567
|
-
const tr = await transform.apply({ call, result: current }, ctx);
|
|
568
|
-
manifestBuffer.push(tr.record);
|
|
569
|
-
await persistBlobs(tr.blobs);
|
|
570
|
-
current = tr.output;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
return current;
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
let results: ToolResult[];
|
|
577
|
-
if (parallel) {
|
|
578
|
-
const p = Promise.all(calls.map((c) => runOne(c)));
|
|
579
|
-
track(p);
|
|
580
|
-
results = await p;
|
|
581
|
-
} else {
|
|
582
|
-
results = [];
|
|
583
|
-
for (const call of calls) {
|
|
584
|
-
const p = runOne(call);
|
|
585
|
-
track(p);
|
|
586
|
-
results.push(await p);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
cycleToolCallsExecuted += results.length;
|
|
591
|
-
|
|
592
|
-
if (addToHistory && stateManager !== null) {
|
|
593
|
-
stateManager.appendTurn(createToolResultTurn(results));
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
for (const result of results) {
|
|
597
|
-
enqueue({ type: "tool.done", result });
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async function executeCompact(
|
|
602
|
-
compactorName: string,
|
|
603
|
-
reason: string,
|
|
604
|
-
): Promise<void> {
|
|
605
|
-
if (stateManager === null) return;
|
|
606
|
-
const compactor = compactors[compactorName];
|
|
607
|
-
if (compactor === undefined) {
|
|
608
|
-
throw new Error(
|
|
609
|
-
`executeCompact: no compactor registered for name ${JSON.stringify(compactorName)}`,
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const ctx: StrategyContext = {
|
|
614
|
-
state: stateManager.snapshot(),
|
|
615
|
-
trigger: `director:${reason}`,
|
|
616
|
-
};
|
|
617
|
-
const result = await compactor.apply(stateManager.getTurns(), ctx);
|
|
618
|
-
|
|
619
|
-
stateManager.replaceTurns(result.output);
|
|
620
|
-
await contextStore.writeTurns(result.output);
|
|
621
|
-
await persistBlobs(result.blobs);
|
|
622
|
-
manifestBuffer.push(result.record);
|
|
623
|
-
cycleCompactorName = compactor.name;
|
|
624
|
-
|
|
625
|
-
logger.info`Compaction by ${compactor.name} reduced history (reason: ${reason})`;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// -------------------------------------------------------------------------
|
|
629
|
-
// Cycle boundary commit
|
|
630
|
-
// -------------------------------------------------------------------------
|
|
631
|
-
|
|
632
|
-
function buildCycleMessage(): string {
|
|
633
|
-
if (pendingMessage !== null) {
|
|
634
|
-
const msg = pendingMessage;
|
|
635
|
-
pendingMessage = null;
|
|
636
|
-
return msg;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (cycleCompactorName !== null) {
|
|
640
|
-
return `Cycle: compaction by ${cycleCompactorName}`;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const parts: string[] = [];
|
|
644
|
-
if (cycleInferred) parts.push("inferred");
|
|
645
|
-
if (cycleToolCallsExecuted > 0) {
|
|
646
|
-
const noun = cycleToolCallsExecuted === 1 ? "tool call" : "tool calls";
|
|
647
|
-
parts.push(`${String(cycleToolCallsExecuted)} ${noun}`);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (parts.length === 0) return "Cycle: no-op";
|
|
651
|
-
return `Cycle: ${parts.join(" + ")}`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function resetCycleAccumulators(): void {
|
|
655
|
-
manifestBuffer = [];
|
|
656
|
-
cycleInferred = false;
|
|
657
|
-
cycleToolCallsExecuted = 0;
|
|
658
|
-
cycleCompactorName = null;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
async function commitCycle(): Promise<void> {
|
|
662
|
-
if (stateManager === null) return;
|
|
663
|
-
|
|
664
|
-
// Only commit when the cycle did real work or the director set an
|
|
665
|
-
// override message. An empty cycle (no inference, no tools, no compact,
|
|
666
|
-
// no override) commits nothing.
|
|
667
|
-
const hasWork =
|
|
668
|
-
cycleInferred ||
|
|
669
|
-
cycleToolCallsExecuted > 0 ||
|
|
670
|
-
cycleCompactorName !== null;
|
|
671
|
-
const hasOverride = pendingMessage !== null;
|
|
672
|
-
if (!hasWork && !hasOverride) {
|
|
673
|
-
resetCycleAccumulators();
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const message = buildCycleMessage();
|
|
678
|
-
|
|
679
|
-
try {
|
|
680
|
-
await contextStore.writeTurns(stateManager.getTurns());
|
|
681
|
-
await contextStore.writeManifest(manifestBuffer);
|
|
682
|
-
await writeMetadata();
|
|
683
|
-
const commit = await contextStore.commit({ message });
|
|
684
|
-
lastCheckpointHash = commit.hash;
|
|
685
|
-
} catch (cause) {
|
|
686
|
-
logger.error`Cycle commit failed: ${cause}`;
|
|
687
|
-
emitError(
|
|
688
|
-
`Cycle commit failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
689
|
-
false,
|
|
690
|
-
);
|
|
691
|
-
resetCycleAccumulators();
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
resetCycleAccumulators();
|
|
696
|
-
|
|
697
|
-
if (afterCheckpoint !== undefined) {
|
|
698
|
-
try {
|
|
699
|
-
await afterCheckpoint();
|
|
700
|
-
} catch (cause) {
|
|
701
|
-
logger.error`afterCheckpoint failed: ${cause}`;
|
|
702
|
-
emitError(
|
|
703
|
-
`afterCheckpoint failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
704
|
-
false,
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
async function writeMetadata(): Promise<void> {
|
|
711
|
-
if (stateManager === null) return;
|
|
712
|
-
await contextStore.writeMetadata({
|
|
713
|
-
pendingOperations: stateManager.getPendingOperations(),
|
|
714
|
-
tokenUsage: stateManager.getTokenUsage(),
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// -------------------------------------------------------------------------
|
|
719
|
-
// Main loop
|
|
720
|
-
// -------------------------------------------------------------------------
|
|
721
|
-
|
|
722
|
-
async function loop(): Promise<void> {
|
|
723
|
-
if (stateManager === null) {
|
|
724
|
-
throw new Error("State manager not initialized before loop");
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
while (!done) {
|
|
728
|
-
await waitForEvent();
|
|
729
|
-
|
|
730
|
-
if (done) break;
|
|
731
|
-
|
|
732
|
-
const event = dequeueNext();
|
|
733
|
-
if (event === undefined) continue;
|
|
734
|
-
|
|
735
|
-
// Handle abort events: initiate shutdown regardless of director.
|
|
736
|
-
if (event.type === "abort") {
|
|
737
|
-
if (!shutdownStarted) {
|
|
738
|
-
done = true;
|
|
739
|
-
await initiateShutdown();
|
|
740
|
-
}
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Append inbound messages to conversation history so the provider sees them.
|
|
745
|
-
if (event.type === "message.received" && stateManager !== null) {
|
|
746
|
-
const msg = createInboundTurn(event.message);
|
|
747
|
-
if (msg !== null) {
|
|
748
|
-
stateManager.appendTurn(msg);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
let actions;
|
|
753
|
-
try {
|
|
754
|
-
actions = await director.decide(
|
|
755
|
-
event,
|
|
756
|
-
stateManager.snapshot(),
|
|
757
|
-
capabilities,
|
|
758
|
-
);
|
|
759
|
-
} catch (cause) {
|
|
760
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
761
|
-
|
|
762
|
-
logger.error`Director threw during decide: ${cause}`;
|
|
763
|
-
emitError(`Director exception: ${msg}`, true);
|
|
764
|
-
done = true;
|
|
765
|
-
await initiateShutdown();
|
|
766
|
-
break;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const validation = validateActions(actions);
|
|
770
|
-
if (!validation.ok) {
|
|
771
|
-
emitError(`Invalid action set: ${validation.error}`, true);
|
|
772
|
-
done = true;
|
|
773
|
-
await initiateShutdown();
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const normalized = validation.normalized;
|
|
778
|
-
|
|
779
|
-
// Checkpoint sets the next cycle's commit message.
|
|
780
|
-
const checkpointAction = normalized.find(
|
|
781
|
-
(a): a is Extract<ReactorAction, { type: "checkpoint" }> =>
|
|
782
|
-
a.type === "checkpoint",
|
|
783
|
-
);
|
|
784
|
-
if (checkpointAction !== undefined) {
|
|
785
|
-
pendingMessage = checkpointAction.message;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Emit custom events (validated type namespace).
|
|
789
|
-
for (const action of normalized) {
|
|
790
|
-
if (action.type === "emit") {
|
|
791
|
-
const reserved = ["inference.", "tool.", "reactor.", "fork."];
|
|
792
|
-
const blocked = reserved.some((p) => action.eventType.startsWith(p));
|
|
793
|
-
if (blocked) {
|
|
794
|
-
emitError(
|
|
795
|
-
`Director tried to emit reserved event type: ${action.eventType}`,
|
|
796
|
-
false,
|
|
797
|
-
);
|
|
798
|
-
continue;
|
|
799
|
-
}
|
|
800
|
-
emit({ type: action.eventType, seq: nextSeq(), data: action.data });
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Fork is excluded in this build.
|
|
805
|
-
for (const action of normalized) {
|
|
806
|
-
if (action.type === "fork") {
|
|
807
|
-
emitError("Fork action is not supported in this build", false);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Handle done.
|
|
812
|
-
if (normalized.some((a) => a.type === "done")) {
|
|
813
|
-
// Flush the cycle (in case the director paired done with checkpoint
|
|
814
|
-
// or other work) before shutting down.
|
|
815
|
-
await commitCycle();
|
|
816
|
-
done = true;
|
|
817
|
-
await initiateShutdown();
|
|
818
|
-
break;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Handle wait: commit the cycle (if work happened) and return to the
|
|
822
|
-
// event loop without shutting down.
|
|
823
|
-
if (normalized.some((a) => a.type === "wait")) {
|
|
824
|
-
await commitCycle();
|
|
825
|
-
continue;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// Handle suspend: register gate and continue the loop (don't block).
|
|
829
|
-
const suspendAction = normalized.find((a) => a.type === "suspend");
|
|
830
|
-
if (suspendAction !== undefined && suspendAction.type === "suspend") {
|
|
831
|
-
const { gate } = suspendAction;
|
|
832
|
-
const effectiveTimeout =
|
|
833
|
-
gate.timeoutMs > 0 ? gate.timeoutMs : gateTimeout;
|
|
834
|
-
|
|
835
|
-
emit({
|
|
836
|
-
type: "reactor.gate.blocked",
|
|
837
|
-
seq: nextSeq(),
|
|
838
|
-
data: { reason: gate.type, gateId: gate.gateId },
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
if (stateManager !== null) {
|
|
842
|
-
stateManager.setGatesSnapshot(gates.snapshot());
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// Register the gate. The onCleared callback enqueues the cleared event
|
|
846
|
-
// so the loop processes it normally without blocking here.
|
|
847
|
-
void gates.register(
|
|
848
|
-
gate.gateId,
|
|
849
|
-
gate.type,
|
|
850
|
-
effectiveTimeout,
|
|
851
|
-
gate.correlationId,
|
|
852
|
-
(gateId, reason) => {
|
|
853
|
-
if (stateManager !== null) {
|
|
854
|
-
stateManager.setGatesSnapshot(gates.snapshot());
|
|
855
|
-
}
|
|
856
|
-
emit({
|
|
857
|
-
type: "reactor.gate.cleared",
|
|
858
|
-
seq: nextSeq(),
|
|
859
|
-
data: { gateId, reason },
|
|
860
|
-
});
|
|
861
|
-
enqueue({ type: "reactor.gate.cleared", gateId, reason });
|
|
862
|
-
},
|
|
863
|
-
);
|
|
864
|
-
|
|
865
|
-
if (stateManager !== null) {
|
|
866
|
-
stateManager.setGatesSnapshot(gates.snapshot());
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// Commit before the loop continues so the suspended-state turns are
|
|
870
|
-
// durable across restart.
|
|
871
|
-
await commitCycle();
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// Handle reply — emit the content for the harness/supervisor to send.
|
|
876
|
-
const replyAction = normalized.find((a) => a.type === "reply");
|
|
877
|
-
if (replyAction !== undefined && replyAction.type === "reply") {
|
|
878
|
-
// Flush any pending cycle work before signaling the reply so the
|
|
879
|
-
// emitted checkpointHash matches the visible state.
|
|
880
|
-
await commitCycle();
|
|
881
|
-
emit({
|
|
882
|
-
type: "connector.reply",
|
|
883
|
-
seq: nextSeq(),
|
|
884
|
-
data: {
|
|
885
|
-
content: replyAction.content,
|
|
886
|
-
...(lastCheckpointHash !== undefined
|
|
887
|
-
? { checkpointHash: lastCheckpointHash }
|
|
888
|
-
: {}),
|
|
889
|
-
},
|
|
890
|
-
});
|
|
891
|
-
// After replying, wait for the next inbound message.
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Handle compact (its own cycle; runs before any infer can be requested
|
|
896
|
-
// in the same director invocation — validation forbids that pairing).
|
|
897
|
-
const compactAction = normalized.find((a) => a.type === "compact");
|
|
898
|
-
if (compactAction !== undefined && compactAction.type === "compact") {
|
|
899
|
-
try {
|
|
900
|
-
await executeCompact(compactAction.compactor, compactAction.reason);
|
|
901
|
-
} catch (cause) {
|
|
902
|
-
logger.error`Compaction failed: ${cause}`;
|
|
903
|
-
emitError(
|
|
904
|
-
`Compaction failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
905
|
-
true,
|
|
906
|
-
);
|
|
907
|
-
done = true;
|
|
908
|
-
await initiateShutdown();
|
|
909
|
-
break;
|
|
910
|
-
}
|
|
911
|
-
await commitCycle();
|
|
912
|
-
continue;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Handle infer.
|
|
916
|
-
const inferAction = normalized.find((a) => a.type === "infer");
|
|
917
|
-
if (inferAction !== undefined && inferAction.type === "infer") {
|
|
918
|
-
await executeInfer(inferAction.options);
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// Handle execute_tools.
|
|
923
|
-
const toolsAction = normalized.find((a) => a.type === "execute_tools");
|
|
924
|
-
if (toolsAction !== undefined && toolsAction.type === "execute_tools") {
|
|
925
|
-
const parallel = toolsAction.parallel !== false;
|
|
926
|
-
const addToHistory = toolsAction.addToHistory !== false;
|
|
927
|
-
await executeTools(toolsAction.calls, parallel, addToHistory);
|
|
928
|
-
continue;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// No infer/tools/reply/suspend/wait/compact action — if a checkpoint
|
|
932
|
-
// override was set on its own (or alongside emit/fork), the next event
|
|
933
|
-
// will pick it up. Nothing to flush here.
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function emitError(message: string, fatal: boolean): void {
|
|
938
|
-
emit({
|
|
939
|
-
type: "reactor.error",
|
|
940
|
-
seq: nextSeq(),
|
|
941
|
-
data: { error: message, fatal },
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
let lastCheckpointHash: string | undefined;
|
|
946
|
-
|
|
947
|
-
async function initiateShutdown(): Promise<void> {
|
|
948
|
-
if (shutdownStarted) return;
|
|
949
|
-
shutdownStarted = true;
|
|
950
|
-
|
|
951
|
-
abortOperations();
|
|
952
|
-
gates.shutdown();
|
|
953
|
-
|
|
954
|
-
if (stateManager !== null) {
|
|
955
|
-
stateManager.setGatesSnapshot([]);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (inFlight.size > 0) {
|
|
959
|
-
const deadline = new Promise<void>((resolve) =>
|
|
960
|
-
setTimeout(resolve, shutdownTimeoutMs),
|
|
961
|
-
);
|
|
962
|
-
await Promise.race([Promise.allSettled([...inFlight]), deadline]);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
if (onShutdown !== undefined) {
|
|
966
|
-
try {
|
|
967
|
-
await onShutdown();
|
|
968
|
-
} catch (cause) {
|
|
969
|
-
logger.error`onShutdown failed: ${cause}`;
|
|
970
|
-
emitError(
|
|
971
|
-
`onShutdown failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
972
|
-
false,
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
emit({
|
|
978
|
-
type: "reactor.done",
|
|
979
|
-
seq: nextSeq(),
|
|
980
|
-
data: {},
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// -------------------------------------------------------------------------
|
|
985
|
-
// Public API
|
|
986
|
-
// -------------------------------------------------------------------------
|
|
987
|
-
|
|
988
|
-
function start(): void {
|
|
989
|
-
if (running) {
|
|
990
|
-
throw new Error("Reactor is already running");
|
|
991
|
-
}
|
|
992
|
-
running = true;
|
|
993
|
-
|
|
994
|
-
void (async () => {
|
|
995
|
-
let initialTurns: ConversationTurn[];
|
|
996
|
-
let initialOps;
|
|
997
|
-
let initialUsage: TokenUsage;
|
|
998
|
-
try {
|
|
999
|
-
const loaded = await contextStore.load();
|
|
1000
|
-
initialTurns = loaded.turns;
|
|
1001
|
-
initialOps = loaded.pendingOperations;
|
|
1002
|
-
initialUsage = loaded.tokenUsage;
|
|
1003
|
-
} catch (cause) {
|
|
1004
|
-
logger.error`Context store load failed: ${cause}`;
|
|
1005
|
-
emitError(
|
|
1006
|
-
`Context store load failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1007
|
-
true,
|
|
1008
|
-
);
|
|
1009
|
-
emit({ type: "reactor.done", seq: nextSeq(), data: {} });
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
stateManager = createStateManager(
|
|
1014
|
-
sessionId,
|
|
1015
|
-
initialTurns,
|
|
1016
|
-
initialOps,
|
|
1017
|
-
initialUsage,
|
|
1018
|
-
);
|
|
1019
|
-
stateManager.setGatesSnapshot(gates.snapshot());
|
|
1020
|
-
|
|
1021
|
-
emit({ type: "reactor.start", seq: nextSeq(), data: {} });
|
|
1022
|
-
|
|
1023
|
-
try {
|
|
1024
|
-
await loop();
|
|
1025
|
-
} catch (cause) {
|
|
1026
|
-
logger.error`Reactor loop threw unexpectedly: ${cause}`;
|
|
1027
|
-
emitError(
|
|
1028
|
-
`Internal reactor error: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1029
|
-
true,
|
|
1030
|
-
);
|
|
1031
|
-
if (!shutdownStarted) {
|
|
1032
|
-
await initiateShutdown();
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
})();
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function deliver(message: InboundMessage): void {
|
|
1039
|
-
if (done) return;
|
|
1040
|
-
void (async () => {
|
|
1041
|
-
const correlated = await tryCorrelate(message);
|
|
1042
|
-
if (!correlated) {
|
|
1043
|
-
emit({
|
|
1044
|
-
type: "message.received",
|
|
1045
|
-
seq: nextSeq(),
|
|
1046
|
-
data: { message },
|
|
1047
|
-
});
|
|
1048
|
-
enqueue({ type: "message.received", message });
|
|
1049
|
-
}
|
|
1050
|
-
})();
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
function abort(reason: AbortReason): void {
|
|
1054
|
-
enqueue({ type: "abort", reason });
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
return { start, deliver, abort };
|
|
1058
|
-
}
|