Skip to content

Commit 9464e3e

Browse files
irl-danclaude
andcommitted
Observability Phase 3: Wire emit into rlm.ts
Add observer option to RlmOptions, create emit closure with runId, and place all 14 event types at existing code seams: lifecycle (run/invocation/ iteration), LLM calls (request/response/error), delegation (spawn/return/ error/unawaited), and sandbox snapshots. Add integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f922ea commit 9464e3e

3 files changed

Lines changed: 525 additions & 6 deletions

File tree

src/rlm.ts

Lines changed: 218 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { JsEnvironment } from "./environment.js";
2+
import type { RlmEvent, RlmEventSink } from "./events.js";
23
import { buildModelTable, buildSystemPrompt } from "./system-prompt.js";
34

45
export interface CallLLMResponse {
@@ -34,6 +35,7 @@ export interface RlmOptions {
3435
/** Keyed by name; looked up when a parent calls `rlm(query, ctx, { app: "name" })`. */
3536
childApps?: Record<string, string>;
3637
reasoningEffort?: string;
38+
observer?: RlmEventSink;
3739
}
3840

3941
export interface RlmResult {
@@ -99,6 +101,11 @@ export async function rlm(query: string, context: string | undefined, options: R
99101
reasoningEffort: options.reasoningEffort,
100102
};
101103

104+
const emit: ((event: RlmEvent) => void) | undefined = options.observer
105+
? (event) => options.observer!.emit(event)
106+
: undefined;
107+
const runId = globalThis.crypto.randomUUID();
108+
102109
const modelTable = buildModelTable(opts.models);
103110

104111
const env = new JsEnvironment();
@@ -230,6 +237,17 @@ export async function rlm(query: string, context: string | undefined, options: R
230237
modelTable,
231238
});
232239

240+
emit?.({
241+
type: "invocation:start",
242+
runId,
243+
timestamp: performance.now(),
244+
invocationId,
245+
parentId,
246+
depth,
247+
query,
248+
systemPrompt: effectiveSystemPrompt,
249+
});
250+
233251
if (!contextStore.locals.has(invocationId)) {
234252
contextStore.locals.set(invocationId, {});
235253
}
@@ -279,22 +297,83 @@ export async function rlm(query: string, context: string | undefined, options: R
279297

280298
const messages: Array<{ role: string; content: string; meta?: Record<string, unknown> }> = [{ role: "user", content: query }];
281299

300+
let invocationResult: RlmResult | undefined;
301+
let invocationError: unknown;
302+
try {
282303
for (let iteration = 0; iteration < effectiveMaxIterations; iteration++) {
304+
emit?.({
305+
type: "iteration:start",
306+
runId,
307+
timestamp: performance.now(),
308+
invocationId,
309+
parentId,
310+
depth,
311+
iteration,
312+
budgetRemaining: effectiveMaxIterations - iteration,
313+
});
314+
315+
let iterationReturned = false;
316+
let iterationCode: string | null = null;
317+
let iterationOutput = "";
318+
let iterationError: string | null = null;
319+
320+
try {
321+
322+
const llmStart = performance.now();
323+
emit?.({
324+
type: "llm:request",
325+
runId,
326+
timestamp: llmStart,
327+
invocationId,
328+
parentId,
329+
depth,
330+
iteration,
331+
messageCount: messages.length,
332+
systemPromptLength: effectiveSystemPrompt.length,
333+
});
334+
283335
let response: CallLLMResponse;
284336
try {
285337
response = await callLLM(messages, effectiveSystemPrompt, effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : undefined);
286338
} catch (err) {
287-
throw new RlmError(
288-
err instanceof Error ? err.message : String(err),
339+
const llmError = err instanceof Error ? err.message : String(err);
340+
emit?.({
341+
type: "llm:error",
342+
runId,
343+
timestamp: performance.now(),
344+
invocationId,
345+
parentId,
346+
depth,
289347
iteration,
290-
);
348+
error: llmError,
349+
duration: performance.now() - llmStart,
350+
});
351+
iterationError = llmError;
352+
throw new RlmError(llmError, iteration);
291353
}
292354

355+
emit?.({
356+
type: "llm:response",
357+
runId,
358+
timestamp: performance.now(),
359+
invocationId,
360+
parentId,
361+
depth,
362+
iteration,
363+
duration: performance.now() - llmStart,
364+
reasoning: response.reasoning,
365+
code: response.code,
366+
hasToolUse: !!response.toolUseId,
367+
usage: (response as unknown as Record<string, unknown>).usage as import("./events.js").TokenUsage | undefined,
368+
});
369+
293370
const reasoning = response.reasoning;
294371
const codeBlocks = response.code !== null ? [response.code] : [];
295372
const toolUseId = response.toolUseId ?? null;
296373
const reasoningDetails = response.reasoningDetails ?? null;
297374

375+
iterationCode = response.code;
376+
298377
let combinedOutput = "";
299378
let combinedError: string | null = null;
300379

@@ -334,6 +413,15 @@ export async function rlm(query: string, context: string | undefined, options: R
334413
await new Promise((r) => setTimeout(r, 0));
335414
if (pendingRlmCalls.size > 0) {
336415
const count = pendingRlmCalls.size;
416+
emit?.({
417+
type: "delegation:unawaited",
418+
runId,
419+
timestamp: performance.now(),
420+
invocationId,
421+
parentId,
422+
depth,
423+
count,
424+
});
337425
const warning =
338426
`[ERROR] ${count} rlm() call(s) were NOT awaited. Their results are LOST and the API calls were wasted. ` +
339427
`You MUST write: const result = await rlm("query", context). ` +
@@ -352,10 +440,16 @@ export async function rlm(query: string, context: string | undefined, options: R
352440
break;
353441
}
354442
const answer = typeof returnValue === "object" ? JSON.stringify(returnValue) : String(returnValue);
355-
return { answer, iterations: iteration + 1 };
443+
iterationReturned = true;
444+
iterationOutput = combinedOutput;
445+
invocationResult = { answer, iterations: iteration + 1 };
446+
return invocationResult;
356447
}
357448
}
358449

450+
iterationOutput = combinedOutput;
451+
iterationError = combinedError;
452+
359453
// Build iteration context for the next turn (if there will be one)
360454
const nextIterContext = (effectiveMaxIterations > 1 && iteration + 1 < effectiveMaxIterations)
361455
? buildIterationContext(iteration + 1) + "\n"
@@ -386,9 +480,57 @@ export async function rlm(query: string, context: string | undefined, options: R
386480
"[WARNING] No code was executed. Use the execute_code tool to run JavaScript and make progress.",
387481
});
388482
}
483+
484+
} finally {
485+
emit?.({
486+
type: "iteration:end",
487+
runId,
488+
timestamp: performance.now(),
489+
invocationId,
490+
parentId,
491+
depth,
492+
iteration,
493+
code: iterationCode,
494+
output: iterationOutput,
495+
error: iterationError,
496+
returned: iterationReturned,
497+
});
498+
499+
if (emit) {
500+
emit({
501+
type: "sandbox:snapshot",
502+
runId,
503+
timestamp: performance.now(),
504+
invocationId,
505+
parentId,
506+
depth,
507+
iteration,
508+
state: env.snapshot(snapshotExcludeKeys),
509+
});
510+
}
511+
}
389512
}
390513

391-
throw new RlmMaxIterationsError(effectiveMaxIterations);
514+
const maxIterErr = new RlmMaxIterationsError(effectiveMaxIterations);
515+
invocationError = maxIterErr;
516+
throw maxIterErr;
517+
518+
} catch (err) {
519+
invocationError = err;
520+
throw err;
521+
} finally {
522+
emit?.({
523+
type: "invocation:end",
524+
runId,
525+
timestamp: performance.now(),
526+
invocationId,
527+
parentId,
528+
depth,
529+
answer: invocationResult?.answer ?? null,
530+
error: invocationError instanceof Error ? invocationError.message : invocationError ? String(invocationError) : null,
531+
iterations: invocationResult?.iterations ?? (invocationError instanceof RlmError ? invocationError.iterations : 0),
532+
});
533+
}
392534
}
393535

394536
env.set("rlm", (q: string, c?: string, rlmOpts?: { systemPrompt?: string; model?: string; maxIterations?: number; app?: string; reasoning?: string }): Promise<string> => {
@@ -440,10 +582,48 @@ export async function rlm(query: string, context: string | undefined, options: R
440582
? childDepthLabel
441583
: `${callerInvocationId}.${childDepthLabel}`;
442584

585+
emit?.({
586+
type: "delegation:spawn",
587+
runId,
588+
timestamp: performance.now(),
589+
invocationId: callerInvocationId,
590+
parentId: (env.get("__rlm") as DelegationContext | undefined)?.parentId ?? null,
591+
depth: savedDepth,
592+
childId: childInvocationId,
593+
query: q,
594+
modelAlias: rlmOpts?.model,
595+
maxIterations: rlmOpts?.maxIterations,
596+
appName: rlmOpts?.app,
597+
});
598+
443599
const promise = (async () => {
444600
try {
445601
const result = await rlmInternal(q, c, savedDepth + 1, childLineage, childInvocationId, callerInvocationId, resolvedSystemPrompt, modelCallLLM, rlmOpts?.maxIterations, rlmOpts?.reasoning);
602+
emit?.({
603+
type: "delegation:return",
604+
runId,
605+
timestamp: performance.now(),
606+
invocationId: callerInvocationId,
607+
parentId: (env.get("__rlm") as DelegationContext | undefined)?.parentId ?? null,
608+
depth: savedDepth,
609+
childId: childInvocationId,
610+
answer: result.answer,
611+
iterations: result.iterations,
612+
});
446613
return result.answer;
614+
} catch (err) {
615+
emit?.({
616+
type: "delegation:error",
617+
runId,
618+
timestamp: performance.now(),
619+
invocationId: callerInvocationId,
620+
parentId: (env.get("__rlm") as DelegationContext | undefined)?.parentId ?? null,
621+
depth: savedDepth,
622+
childId: childInvocationId,
623+
error: err instanceof Error ? err.message : String(err),
624+
iterations: err instanceof RlmError ? err.iterations : 0,
625+
});
626+
throw err;
447627
} finally {
448628
activeDepth = savedDepth;
449629
}
@@ -455,5 +635,37 @@ export async function rlm(query: string, context: string | undefined, options: R
455635
return promise;
456636
});
457637

458-
return rlmInternal(query, context, 0, [query], "root", null);
638+
emit?.({
639+
type: "run:start",
640+
runId,
641+
timestamp: performance.now(),
642+
invocationId: "root",
643+
parentId: null,
644+
depth: 0,
645+
query,
646+
maxIterations: opts.maxIterations,
647+
maxDepth: opts.maxDepth,
648+
});
649+
650+
let runResult: RlmResult | undefined;
651+
let runError: unknown;
652+
try {
653+
runResult = await rlmInternal(query, context, 0, [query], "root", null);
654+
return runResult;
655+
} catch (err) {
656+
runError = err;
657+
throw err;
658+
} finally {
659+
emit?.({
660+
type: "run:end",
661+
runId,
662+
timestamp: performance.now(),
663+
invocationId: "root",
664+
parentId: null,
665+
depth: 0,
666+
answer: runResult?.answer ?? null,
667+
error: runError instanceof Error ? runError.message : runError ? String(runError) : null,
668+
iterations: runResult?.iterations ?? (runError instanceof RlmError ? runError.iterations : 0),
669+
});
670+
}
459671
}

0 commit comments

Comments
 (0)