55// template.prompt with @payloadRef and/or @responseRef.
66//
77// The emitted helper exports an async function `record<Entity>(om, responseMo, input)`
8- // that calls `recordLlmCall` from @metaobjectsdev/runtime-ts. The helper is typed
9- // against the generated payload interfaces (request + response VOs) so call-sites
10- // get compile-time checks.
8+ // that EXTRACTS the typed response VO itself and persists ONE row = base envelope +
9+ // raw I/O (via buildLlmCallRow) PLUS the typed voRequest/voResponse columns. The
10+ // helper is typed against the generated payload interfaces (request + response VOs)
11+ // so call-sites get compile-time checks.
1112//
1213// NOTE: ObjectManager does not expose its loaded metadata root, so the caller must
1314// pass the resolved `responseMo: MetaObject` explicitly. The generated helper
@@ -90,10 +91,17 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
9091 const sti = tphPin !== undefined ;
9192 const callTypeValue = sti ? tphPin . value : entityName ;
9293
93- // Emitted record<Entity> fragments: the keys Omit'd from the caller input,
94- // and the first argument passed to recordLlmCall.
95- const recordInputOmit = sti ? `"llmRequest" | "callType"` : `"llmRequest"` ;
96- const recordArg = sti ? `{ ...input, callType: ${ JSON . stringify ( callTypeValue ) } }` : `input` ;
94+ // Emitted record<Entity> fragments. The caller never supplies the derived
95+ // status/errorDetail (the helper computes them from extraction), and an STI
96+ // subtype additionally drops the framework-managed callType discriminator.
97+ const recordInputOmit = sti
98+ ? `"llmRequest" | "status" | "errorDetail" | "callType"`
99+ : `"llmRequest" | "status" | "errorDetail"` ;
100+ // The object spread passed to buildLlmCallRow: STI subtypes stamp their
101+ // discriminator value, all helpers fold in the derived status/errorDetail.
102+ const recordBuildArg = sti
103+ ? `{ ...input, callType: ${ JSON . stringify ( callTypeValue ) } , status, errorDetail }`
104+ : `{ ...input, status, errorDetail }` ;
97105
98106 // Derive the parse format from the prompt's @format attr.
99107 // "xml" → Format.XML; absent or any other value → Format.JSON.
@@ -119,26 +127,32 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
119127 const renderFormat = typeof promptFormat === "string" ? promptFormat : "text" ;
120128
121129 // Build the import block — all imports MUST stay at the top of the emitted file.
130+ // `extract` + `render` both live in @metaobjectsdev/render: import them together
131+ // (one de-duplicated statement) when the prompt is renderable, else import only
132+ // `extract`.
122133 const importLines : string [ ] = [
123134 `import type { ObjectManager } from "@metaobjectsdev/runtime-ts";` ,
124135 `import {` ,
125136 ` LlmCallDbRecorder,` ,
126- ` recordLlmCall,` ,
137+ ` buildLlmCallRow,` ,
138+ ` persistLlmCallRow,` ,
139+ ` extractSchemaFor,` ,
140+ ` Format,` ,
127141 ` type LlmCallInput,` ,
128- ` type RecordLlmCallResult,` ,
129142 `} from "@metaobjectsdev/runtime-ts";` ,
130- `import { Format } from "@metaobjectsdev/runtime-ts";` ,
143+ renderable
144+ ? `import { extract, render, type Provider } from "@metaobjectsdev/render";`
145+ : `import { extract } from "@metaobjectsdev/render";` ,
131146 `import type { MetaObject } from "@metaobjectsdev/metadata";` ,
132147 ] ;
133148 if ( renderable ) {
134149 importLines . push (
135- `import { render, type Provider } from "@metaobjectsdev/render";` ,
136150 `import {` ,
137- ` callLlm,` ,
151+ ` runLlmCall,` ,
152+ ` type RunLlmCallInput,` ,
153+ ` type RunLlmCallDeps,` ,
138154 ` type LlmClient,` ,
139155 ` type LlmRequest,` ,
140- ` type CallLlmInput,` ,
141- ` type CallLlmDeps,` ,
142156 ` type CostFn,` ,
143157 ` type Clock,` ,
144158 ` type IdGen,` ,
@@ -157,7 +171,9 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
157171 `` ,
158172 `// ---- Typed result -----------------------------------------------------------` ,
159173 `` ,
160- `export interface ${ entityName } TraceResult extends RecordLlmCallResult {` ,
174+ `export interface ${ entityName } TraceResult {` ,
175+ ` status: "ok" | "error";` ,
176+ ` errorDetail: string | null;` ,
161177 ` /** Parsed response VO, or null when extraction reported a lost-required field. */` ,
162178 ` /** Note: voResponse is the plain extracted record typed as the response shape (structural, not an instance). */` ,
163179 ` voResponse: ${ responseRef } | null;` ,
@@ -181,12 +197,15 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
181197 ` responseMo: MetaObject,` ,
182198 ` input: Omit<LlmCallInput, ${ recordInputOmit } > & { llmRequest: ${ requestType } },` ,
183199 `): Promise<${ entityName } TraceResult> {` ,
184- ` const result = await recordLlmCall(${ recordArg } , {` ,
185- ` recorder: new LlmCallDbRecorder(om, "${ entityName } "),` ,
186- ` responseMo,` ,
187- ` format: ${ formatLiteral } ,` ,
188- ` });` ,
189- ` return result as ${ entityName } TraceResult;` ,
200+ ` const schema = extractSchemaFor(responseMo, ${ formatLiteral } );` ,
201+ ` const outcome = extract(input.llmResponseText, schema);` ,
202+ ` const failed = outcome.report.hasLostRequired();` ,
203+ ` const status = failed ? ("error" as const) : ("ok" as const);` ,
204+ ' const errorDetail = failed ? `lost required: ${outcome.report.lostRequired().join(", ")}` : null;' ,
205+ ` const base = buildLlmCallRow(${ recordBuildArg } );` ,
206+ ` const row = { ...base, voRequest: input.llmRequest, voResponse: failed ? null : outcome.data };` ,
207+ ` await persistLlmCallRow(new LlmCallDbRecorder(om, "${ entityName } "), row);` ,
208+ ` return { status, errorDetail, voResponse: failed ? null : (outcome.data as ${ responseRef } ) };` ,
190209 `}` ,
191210 `` ,
192211 ] ;
@@ -229,21 +248,30 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
229248 ` const request: LlmRequest = { prompt, model: deps.model };` ,
230249 ` if (deps.system !== undefined) request.system = deps.system;` ,
231250 ` if (deps.params !== undefined) request.params = deps.params;` ,
232- ` const callInput: CallLlmInput = { callType: ${ JSON . stringify ( callTypeValue ) } , payload, request };` ,
233- ` if (deps.traceId !== undefined) callInput.traceId = deps.traceId;` ,
234- ` if (deps.parentSpanId !== undefined) callInput.parentSpanId = deps.parentSpanId;` ,
235- ` if (deps.sessionId !== undefined) callInput.sessionId = deps.sessionId;` ,
236- ` const callDeps: CallLlmDeps = {` ,
237- ` client: deps.client,` ,
238- ` recorder: new LlmCallDbRecorder(deps.om, ${ JSON . stringify ( entityName ) } ),` ,
239- ` responseMo: deps.responseMo,` ,
240- ` format: ${ formatLiteral } ,` ,
241- ` };` ,
242- ` if (deps.cost !== undefined) callDeps.cost = deps.cost;` ,
243- ` if (deps.clock !== undefined) callDeps.clock = deps.clock;` ,
244- ` if (deps.ids !== undefined) callDeps.ids = deps.ids;` ,
245- ` const result = await callLlm(callInput, callDeps);` ,
246- ` return result as ${ entityName } TraceResult;` ,
251+ ` const runInput: RunLlmCallInput = { callType: ${ JSON . stringify ( callTypeValue ) } , request };` ,
252+ ` if (deps.traceId !== undefined) runInput.traceId = deps.traceId;` ,
253+ ` if (deps.parentSpanId !== undefined) runInput.parentSpanId = deps.parentSpanId;` ,
254+ ` if (deps.sessionId !== undefined) runInput.sessionId = deps.sessionId;` ,
255+ ` const runDeps: RunLlmCallDeps = { client: deps.client };` ,
256+ ` if (deps.cost !== undefined) runDeps.cost = deps.cost;` ,
257+ ` if (deps.clock !== undefined) runDeps.clock = deps.clock;` ,
258+ ` if (deps.ids !== undefined) runDeps.ids = deps.ids;` ,
259+ ` const { input: recInput, completion } = await runLlmCall(runInput, runDeps);` ,
260+ ` let voResponse: ${ responseRef } | null = null;` ,
261+ ` let status = recInput.status;` ,
262+ ` let errorDetail = recInput.errorDetail;` ,
263+ ` if (completion !== undefined) {` ,
264+ ` const outcome = extract(completion.body, extractSchemaFor(deps.responseMo, ${ formatLiteral } ));` ,
265+ ` if (outcome.report.hasLostRequired()) {` ,
266+ ` status = "error";` ,
267+ ' errorDetail = `lost required: ${outcome.report.lostRequired().join(", ")}`;' ,
268+ ` } else {` ,
269+ ` voResponse = outcome.data as ${ responseRef } ;` ,
270+ ` }` ,
271+ ` }` ,
272+ ` const row = { ...buildLlmCallRow({ ...recInput, status, errorDetail }), voRequest: payload, voResponse };` ,
273+ ` await persistLlmCallRow(new LlmCallDbRecorder(deps.om, ${ JSON . stringify ( entityName ) } ), row);` ,
274+ ` return { status, errorDetail, voResponse };` ,
247275 `}` ,
248276 ) ;
249277 }
0 commit comments