Skip to content

Commit 021ff55

Browse files
dmealingclaude
andcommitted
fix(ai): thread redaction through generated record<Entity>/call<Entity> + doc nits
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f41c8fa commit 021ff55

4 files changed

Lines changed: 33 additions & 5 deletions

File tree

server/typescript/packages/codegen-ts/src/generator-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export const generatorRegistry: Record<string, GeneratorRegistryEntry> = {
164164

165165
"trace-helper": {
166166
name: "trace-helper",
167-
description: "Per-entity typed record<Entity> helper wrapping recordLlmCall (LlmCallBase-derived entities only).",
167+
description: "Per-entity typed record<Entity>/call<Entity> trace helpers (extract + buildLlmCallRow + persist; LlmCallBase-derived entities only).",
168168
tier: "native",
169169
factory: () => traceHelperFile(),
170170
options: "outDir?, target?",

server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export interface TraceHelperOpts {
4545
target?: string;
4646
}
4747

48-
/** Walk the super chain looking for a node whose name matches `baseName`. */
48+
/** Walk the super chain looking for a node named LLM_CALL_BASE. */
4949
function extendsBase(obj: MetaObject): boolean {
5050
let cur = obj.superResolved;
5151
while (cur !== undefined) {
@@ -124,6 +124,9 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
124124
// that renders the prompt text, calls the LLM, then parses + persists a trace row.
125125
const textRef = prompt.ownAttr(TEMPLATE_ATTR_TEXT_REF);
126126
const renderable = typeof textRef === "string";
127+
// Same @format attr, two intentionally different shapes: extract() takes the
128+
// Format enum (formatLiteral, above → Format.XML/Format.JSON), render() takes the
129+
// raw format string (renderFormat, here → e.g. "json"/"xml", default "text").
127130
const renderFormat = typeof promptFormat === "string" ? promptFormat : "text";
128131

129132
// Build the import block — all imports MUST stay at the top of the emitted file.
@@ -139,6 +142,7 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
139142
` extractSchemaFor,`,
140143
` Format,`,
141144
` type LlmCallInput,`,
145+
` type LlmCallRow,`,
142146
`} from "@metaobjectsdev/runtime-ts";`,
143147
renderable
144148
? `import { extract, render, type Provider } from "@metaobjectsdev/render";`
@@ -191,11 +195,15 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
191195
` * Passed explicitly because ObjectManager does not expose`,
192196
` * its loaded metadata root.`,
193197
` * @param input - LLM call inputs; type \`llmRequest\` as \`${requestType}\`.`,
198+
` * @param opts - Optional \`redact\` hook applied to the row before persist`,
199+
` * (scrub PII/secrets on the typed path, same as the generic`,
200+
` * recordLlmCall/callLlm helpers).`,
194201
` */`,
195202
`export async function ${fnName}(`,
196203
` om: ObjectManager,`,
197204
` responseMo: MetaObject,`,
198205
` input: Omit<LlmCallInput, ${recordInputOmit}> & { llmRequest: ${requestType} },`,
206+
` opts?: { redact?: (row: LlmCallRow) => LlmCallRow },`,
199207
`): Promise<${entityName}TraceResult> {`,
200208
` const schema = extractSchemaFor(responseMo, ${formatLiteral});`,
201209
` const outcome = extract(input.llmResponseText, schema);`,
@@ -204,7 +212,7 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
204212
' const errorDetail = failed ? `lost required: ${outcome.report.lostRequired().join(", ")}` : null;',
205213
` const base = buildLlmCallRow(${recordBuildArg});`,
206214
` const row = { ...base, voRequest: input.llmRequest, voResponse: failed ? null : outcome.data };`,
207-
` await persistLlmCallRow(new LlmCallDbRecorder(om, "${entityName}"), row);`,
215+
` await persistLlmCallRow(new LlmCallDbRecorder(om, "${entityName}"), row, opts?.redact ? { redact: opts.redact } : undefined);`,
208216
` return { status, errorDetail, voResponse: failed ? null : (outcome.data as ${responseRef}) };`,
209217
`}`,
210218
``,
@@ -231,6 +239,8 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
231239
` traceId?: string;`,
232240
` parentSpanId?: string;`,
233241
` sessionId?: string;`,
242+
` /** Optional row-redaction hook applied before persist (scrub PII/secrets). */`,
243+
` redact?: (row: LlmCallRow) => LlmCallRow;`,
234244
`}`,
235245
``,
236246
`/**`,
@@ -270,7 +280,7 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
270280
` }`,
271281
` }`,
272282
` const row = { ...buildLlmCallRow({ ...recInput, status, errorDetail }), voRequest: payload, voResponse };`,
273-
` await persistLlmCallRow(new LlmCallDbRecorder(deps.om, ${JSON.stringify(entityName)}), row);`,
283+
` await persistLlmCallRow(new LlmCallDbRecorder(deps.om, ${JSON.stringify(entityName)}), row, deps.redact ? { redact: deps.redact } : undefined);`,
274284
` return { status, errorDetail, voResponse };`,
275285
`}`,
276286
);

server/typescript/packages/codegen-ts/test/ai-trace-sti.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ describe("ai-trace #1c — STI callType stamping", () => {
6666
});
6767
});
6868

69+
describe("ai-trace — redaction threaded through the generated typed path", () => {
70+
test("record<Entity> exposes an opts.redact param and threads it to persist", async () => {
71+
const { classify } = await genTrace();
72+
// The emitted file must import the LlmCallRow type used by the redact callback.
73+
expect(classify).toContain("type LlmCallRow,");
74+
// Optional 4th param exposing redact.
75+
expect(classify).toContain("opts?: { redact?: (row: LlmCallRow) => LlmCallRow }");
76+
// Threaded into persistLlmCallRow (exactOptionalPropertyTypes-safe guard).
77+
expect(classify).toContain("opts?.redact ? { redact: opts.redact } : undefined");
78+
});
79+
80+
test("call<Entity> exposes redact on CallDeps and threads it to persist", async () => {
81+
const { summarize } = await genTrace();
82+
expect(summarize).toContain("redact?: (row: LlmCallRow) => LlmCallRow;");
83+
expect(summarize).toContain("deps.redact ? { redact: deps.redact } : undefined");
84+
});
85+
});
86+
6987
describe("ai-trace #1c — STI table collapse", () => {
7088
test("N trace subtypes collapse to one prompt_llm_call table", async () => {
7189
const dir = mkdtempSync(join(tmpdir(), "ai1c-schema-"));

server/typescript/packages/codegen-ts/test/derive-trace-fields.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe("derive trace fields", () => {
6565
expect(h).toContain("buildLlmCallRow(");
6666
expect(h).toContain("voRequest: input.llmRequest");
6767
expect(h).toContain("voResponse:");
68-
expect(h).toContain('persistLlmCallRow(new LlmCallDbRecorder(om, "ClassifyCall"), row)');
68+
expect(h).toContain('persistLlmCallRow(new LlmCallDbRecorder(om, "ClassifyCall"), row, opts?.redact ? { redact: opts.redact } : undefined)');
6969
// stale generic-path entry point must be gone.
7070
expect(h).not.toContain("recordLlmCall(");
7171
// non-STI invariant: a trace entity that is NOT a TPH subtype keeps callType

0 commit comments

Comments
 (0)