blob: dccfef85c3e86c453d20813f1bc1d3d00f1da0d8 [file]
// Test-only reader for the model-facing observation text.
//
// Production has no parser for this format and should not grow one: the text
// exists to be read by a model, and the ids the model quotes back come from a
// tool call, not from re-parsing our own output. Tests are the exception —
// several of them drive a full model loop and need the `observation_id` and
// element ids that the loop just handed the model.
//
// Kept in one place because four suites need it and four copies of a parser is
// four chances to disagree about the format under test. One of those suites is
// in @maka/computer-use, which reaches it through the
// `@maka/runtime/test-only/observation-text-reader` entry point rather than by
// carrying its own copy — that suite went red on this PR precisely because it
// had its own `JSON.parse`.
export interface ParsedObservationElement {
element_id: string;
role: string;
label?: string;
value?: string;
}
export interface ParsedObservation {
observation_id: string;
elements: ParsedObservationElement[];
}
/** Parse one rendered observation, or undefined when the text is not one. */
export function parseObservationText(text: string): ParsedObservation | undefined {
const lines = text.split('\n');
const headerIndex = lines.findIndex((line) => line.startsWith('observation_id='));
if (headerIndex < 0) return undefined;
const observationId = /observation_id=(\S+)/.exec(lines[headerIndex] ?? '')?.[1];
if (!observationId) return undefined;
const elements: ParsedObservationElement[] = [];
for (const line of lines.slice(headerIndex + 1)) {
const body = line.replace(/^\t+/, '');
const match = /^(\S+) (\S+)(.*)$/.exec(body);
if (!match) break;
const [, elementId, role, rest = ''] = match;
if (!elementId || !role) break;
const label = /^ ("(?:[^"\\]|\\.)*")/.exec(rest)?.[1];
const value = / =("(?:[^"\\]|\\.)*")/.exec(rest)?.[1];
elements.push({
element_id: elementId,
role,
...(label ? { label: JSON.parse(label) as string } : {}),
...(value !== undefined ? { value: JSON.parse(value) as string } : {}),
});
}
return { observation_id: observationId, elements };
}
/**
* Find the most recent observation anywhere in a model prompt.
*
* An action result appends its fresh observation after a `Fresh observation:`
* marker, so a single string can carry both the action outcome and the next
* observation; the marker is where the second one starts.
*/
export function latestObservationIn(prompt: unknown): ParsedObservation | undefined {
const found = stringsIn(prompt).flatMap((text) => {
const marker = text.lastIndexOf('Fresh observation:\n');
const candidates = marker >= 0 ? [text.slice(marker + 'Fresh observation:\n'.length)] : [text];
return candidates.flatMap((candidate) => {
const parsed = parseObservationText(candidate);
return parsed ? [parsed] : [];
});
});
return found.at(-1);
}
/**
* Every string anywhere in a prompt structure, including the ones nested
* inside serialized JSON.
*
* A tool result reaches the provider as a JSON string holding a content array,
* so the observation arrives with its newlines escaped and is not a line-based
* document until that layer is undone.
*/
export function stringsIn(value: unknown): string[] {
if (typeof value === 'string') {
const trimmed = value.trimStart();
if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) return [value];
try {
return [value, ...stringsIn(JSON.parse(value))];
} catch {
return [value];
}
}
if (Array.isArray(value)) return value.flatMap(stringsIn);
if (!value || typeof value !== 'object') return [];
return Object.values(value).flatMap(stringsIn);
}