Skip to content

Agent payloads use schema allowlists, not blocklist sanitization

Define client and persistence payloads with explicit contracts — never strip internal AI response fields with a deny list.

nestjs-realtime-stream-tool-output-schema-required

Why it matters

Failure modes if this rule is ignored
StakeIf ignored
Data leak
  • Blocklists miss new provider fields in the AI response — internals leak into Mongo and API payloads.
Unclear failure
  • New fields in the AI response leak until someone updates the deny list.
API drift
  • Raw AI response parts or unvalidated tool output have no stable contract.

How to fix

Every tool declares outputSchema and returns only schema-validated output from execute. Project AI response stream events into your RunEvent contract (related structured-events rule). Provider-only metadata stays in the LLM adapter. No deny lists, no destructuring internal fields off a raw response object. Code blocks below are illustrative, not prescriptive.

Examples

Bad
ts
const INTERNAL_KEYS = ['providerMetadata', 'thoughtSignature'];
export function sanitizeRunResult(msgs: AgentMessage[]) {
  return msgs.map((m) => stripKeys(m, INTERNAL_KEYS));
}
await this.runs.save({ parts: aiResponse.parts });
export const searchSiteTool = {
  execute: async (input) => this.searchService.find(input.query),
};
Good
ts
export const searchSiteTool = defineTool({
  outputSchema: z.object({ matches: z.array(z.object({ id: z.string(), title: z.string() })) }),
  execute: async ({ query }) =>
    searchSiteTool.outputSchema.parse(await this.searchService.find(query)),
});
function toRunEvent(chunk: AgentStreamEvent, out: Map<string, unknown>): RunEvent | null {
  if (chunk.type === 'text-delta') return { type: 'text-delta', delta: chunk.delta };
  if (chunk.type === 'tool-completed') {
    return { type: 'tool-call-completed', toolCallId: chunk.id, result: out.get(chunk.id)! };
  }
  return null;
}

Contribute

Released under the MIT License.

esc