From 883ccb06cb9d1727562323b8d71a5539de60f12e Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Fri, 29 May 2026 01:58:26 -0400 Subject: [PATCH 1/2] docs: add Vercel AI SDK structured-output integration example with host-side schema selection tests --- README.md | 1 + examples/README.md | 4 + .../vercel_ai_sdk_structured_output/README.md | 80 ++++++++++++ .../vercel_ai_sdk_structured_output/index.ts | 115 ++++++++++++++++++ tests/examples-smoke.test.ts | 10 ++ ...l_ai_sdk_structured_output_example.test.ts | 74 +++++++++++ tsconfig.build.json | 9 +- 7 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 examples/integrations/vercel_ai_sdk_structured_output/README.md create mode 100644 examples/integrations/vercel_ai_sdk_structured_output/index.ts create mode 100644 tests/vercel_ai_sdk_structured_output_example.test.ts diff --git a/README.md b/README.md index f21c999..e7eaae6 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ npm install @rlippmann/context-compiler - `clarify` blocks LLM calls - per-session compiler state via checkpoint export/import so sessions can resume safely - `examples/node-basic/` — minimal Node HTTP server integration +- `examples/integrations/vercel_ai_sdk_structured_output/` — host-side schema selection for Vercel AI SDK structured output ## Quick Start diff --git a/examples/README.md b/examples/README.md index a2bcb3f..72058a5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -41,3 +41,7 @@ Demonstrates explicit single-policy correction without `reset policies`: Shows controller-layer auditability with `preview(engine, input)` and `state_diff(before, after)`. Shows that preview does not mutate live engine state, then applies the same input with `step(engine, input)`. + +## Integrations + +- [`examples/integrations/vercel_ai_sdk_structured_output/README.md`](/Users/rlippmann/Source/context-compiler-ts/examples/integrations/vercel_ai_sdk_structured_output/README.md) diff --git a/examples/integrations/vercel_ai_sdk_structured_output/README.md b/examples/integrations/vercel_ai_sdk_structured_output/README.md new file mode 100644 index 0000000..53e4d94 --- /dev/null +++ b/examples/integrations/vercel_ai_sdk_structured_output/README.md @@ -0,0 +1,80 @@ +# Vercel AI SDK Structured Output Integration + +This example shows a minimal host integration pattern: + +compiler state +-> host selects Zod schema +-> Vercel AI SDK `generateObject` call +-> model generates structured object + +## Scope and Guarantees + +- Context Compiler does not select schemas. +- The host selects schemas using compiler state. +- This example uses compiler state from: + - `use python_script` + - `prohibit shell_command` +- Host behavior in this integration: + - `python_script` schema is offered. + - `shell_command` schema is not offered. + +This is different from prompt reinjection. Schema availability and `generateObject` request configuration are explicit host decisions that are observable and testable before any model call. + +## Important Boundaries + +- Zod validates object shape and fields, not semantic policy compliance. +- Structured generation behavior may vary by provider and SDK behavior. +- `prohibit shell_command` means this host will not offer a shell-command schema. +- It does not mean the model can never discuss shell commands in freeform text. +- The generated object remains model-dependent. + +## Minimal Integration + +```ts +import { createEngine } from '@rlippmann/context-compiler'; +import { selectStructuredSchemasFromState } from './index.js'; +import { generateObject } from 'ai'; +import { createOpenAI } from '@ai-sdk/openai'; +import { z } from 'zod'; + +const engine = createEngine(); +engine.step('use python_script'); +engine.step('prohibit shell_command'); + +// Host-side selection using compiler state. +const availableSchemas = selectStructuredSchemasFromState(engine.state); + +if (availableSchemas[0]?.name === 'python_script') { + const result = await generateObject({ + model: createOpenAI({ apiKey: process.env.OPENAI_API_KEY })('gpt-4.1-mini'), + prompt: 'Write a short Python script that prints hello.', + schema: z.object({ code: z.string() }) + }); + + console.log(result.object); +} +``` + +## Local Example Module + +See [`index.ts`](./index.ts) for the deterministic host-side selection logic used by tests. + +Exported testable functions: +- `selectStructuredSchemasFromState(state)` +- `buildGenerateObjectRequest(state, prompt)` +- `generateStructuredObject(state, prompt, generateObject)` + +## Tests + +Primary tests do not call a model. +They assert: +- state -> selected schema +- selected schema -> `generateObject` request configuration + +Optional smoke test (real model call) is env-gated: + +```bash +CONTEXT_COMPILER_RUN_VERCEL_AI_SMOKE=1 OPENAI_API_KEY=... npm test +``` + +When the smoke flag is off, deterministic tests still validate host wiring. diff --git a/examples/integrations/vercel_ai_sdk_structured_output/index.ts b/examples/integrations/vercel_ai_sdk_structured_output/index.ts new file mode 100644 index 0000000..a4b48ef --- /dev/null +++ b/examples/integrations/vercel_ai_sdk_structured_output/index.ts @@ -0,0 +1,115 @@ +import { POLICY_PROHIBIT, POLICY_USE, createEngine, type EngineState, getPolicyItems } from '../../../src/index.js'; + +declare const process: { argv: string[]; exitCode?: number }; + +export type StructuredSchemaName = 'python_script' | 'shell_command'; + +export type StructuredSchema = { + name: StructuredSchemaName; + description: string; + fields: readonly string[]; +}; + +export type GenerateObjectRequest = { + prompt: string; + schemaName: StructuredSchemaName; + schema: StructuredSchema; +}; + +export type GenerateObjectLike = (request: GenerateObjectRequest) => Promise<{ object: TObject }>; + +const SCHEMA_REGISTRY: Record = { + python_script: { + name: 'python_script', + description: 'Generate a Python script request object.', + fields: ['code'] + }, + shell_command: { + name: 'shell_command', + description: 'Generate a shell command request object.', + fields: ['command'] + } +}; + +const KNOWN_SCHEMAS: readonly StructuredSchemaName[] = ['python_script', 'shell_command']; + +export function selectStructuredSchemasFromState(state: EngineState): StructuredSchema[] { + const prohibited = new Set(getPolicyItems(state, POLICY_PROHIBIT)); + const preferred = getPolicyItems(state, POLICY_USE).filter((item): item is StructuredSchemaName => { + return KNOWN_SCHEMAS.includes(item as StructuredSchemaName); + }); + + if (preferred.length > 0) { + return preferred + .filter((name) => !prohibited.has(name)) + .map((name) => SCHEMA_REGISTRY[name]); + } + + return KNOWN_SCHEMAS.filter((name) => !prohibited.has(name)).map((name) => SCHEMA_REGISTRY[name]); +} + +export function buildGenerateObjectRequest(state: EngineState, prompt: string): GenerateObjectRequest { + const available = selectStructuredSchemasFromState(state); + if (available.length === 0) { + throw new Error('No structured schemas are available for this compiler state.'); + } + const selected = available[0]; + return { + prompt, + schemaName: selected.name, + schema: selected + }; +} + +export async function generateStructuredObject( + state: EngineState, + prompt: string, + generateObject: GenerateObjectLike +): Promise<{ request: GenerateObjectRequest; object: TObject }> { + const request = buildGenerateObjectRequest(state, prompt); + const result = await generateObject(request); + return { + request, + object: result.object + }; +} + +export async function runIntegrationExample(): Promise<{ + availableSchemaNames: StructuredSchemaName[]; + request: GenerateObjectRequest; + object: { code: string }; +}> { + const engine = createEngine(); + engine.step('use python_script'); + engine.step('prohibit shell_command'); + const state = engine.state; + + const available = selectStructuredSchemasFromState(state); + const generated = await generateStructuredObject<{ code: string }>( + state, + 'Write a short Python script that prints Fibonacci numbers up to 21.', + async (request) => ({ + object: { + code: `# schema=${request.schemaName}\nprint([0, 1, 1, 2, 3, 5, 8, 13, 21])` + } + }) + ); + + return { + availableSchemaNames: available.map((schema) => schema.name), + request: generated.request, + object: generated.object + }; +} + +if (typeof process !== 'undefined' && process.argv[1] && import.meta.url === new URL(process.argv[1], 'file://').href) { + runIntegrationExample() + .then((result) => { + console.log('integration example: vercel ai sdk structured output (host-side schema selection)'); + console.log(JSON.stringify(result, null, 2)); + }) + .catch((error: unknown) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/tests/examples-smoke.test.ts b/tests/examples-smoke.test.ts index 1d78017..ed4f61b 100644 --- a/tests/examples-smoke.test.ts +++ b/tests/examples-smoke.test.ts @@ -85,4 +85,14 @@ describe('examples smoke', () => { expect(run.stdout).toContain('example 07: single policy correction'); expect(run.stdout).toContain('"finalPolicy": "use"'); }); + + it('integration: vercel ai sdk structured output', () => { + const run = runExampleScript('integrations/vercel_ai_sdk_structured_output/index.js'); + expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); + expect(run.stdout).toContain('integration example: vercel ai sdk structured output (host-side schema selection)'); + expect(run.stdout).toContain('"availableSchemaNames": ['); + expect(run.stdout).toContain('"python_script"'); + expect(run.stdout).toContain('"schemaName": "python_script"'); + }); }); diff --git a/tests/vercel_ai_sdk_structured_output_example.test.ts b/tests/vercel_ai_sdk_structured_output_example.test.ts new file mode 100644 index 0000000..9147d8b --- /dev/null +++ b/tests/vercel_ai_sdk_structured_output_example.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; + +import { createEngine } from '../src/index.js'; +import { + buildGenerateObjectRequest, + selectStructuredSchemasFromState +} from '../examples/integrations/vercel_ai_sdk_structured_output/index.js'; + +describe('vercel ai sdk structured output example', () => { + it('selects python_script and excludes shell_command from compiler state', () => { + const engine = createEngine(); + engine.step('use python_script'); + engine.step('prohibit shell_command'); + + const schemas = selectStructuredSchemasFromState(engine.state); + expect(schemas.map((schema) => schema.name)).toEqual(['python_script']); + }); + + it('builds generateObject request configuration from selected schema', () => { + const engine = createEngine(); + engine.step('use python_script'); + engine.step('prohibit shell_command'); + + const request = buildGenerateObjectRequest(engine.state, 'Write a hello-world program.'); + + expect(request.schemaName).toBe('python_script'); + expect(request.schema.name).toBe('python_script'); + expect(request.schema.fields).toEqual(['code']); + expect(request.prompt).toBe('Write a hello-world program.'); + }); + + const RUN_SMOKE = process.env.CONTEXT_COMPILER_RUN_VERCEL_AI_SMOKE === '1'; + + it.skipIf(!RUN_SMOKE)('optional smoke: can call Vercel AI SDK generateObject with selected schema', async () => { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey || apiKey.trim() === '') { + throw new Error('OPENAI_API_KEY is required when CONTEXT_COMPILER_RUN_VERCEL_AI_SMOKE=1.'); + } + + const { generateObject } = (await import('ai')) as { + generateObject: (request: { + model: unknown; + prompt: string; + schema: unknown; + }) => Promise<{ object: unknown }>; + }; + const { createOpenAI } = (await import('@ai-sdk/openai')) as { + createOpenAI: (config: { apiKey: string }) => (model: string) => unknown; + }; + const { z } = (await import('zod')) as { + z: { + object: (shape: Record) => unknown; + string: () => unknown; + }; + }; + + const engine = createEngine(); + engine.step('use python_script'); + engine.step('prohibit shell_command'); + + const request = buildGenerateObjectRequest(engine.state, 'Write a tiny Python hello-world script.'); + + const openai = createOpenAI({ apiKey }); + const schema = z.object({ code: z.string() }); + const result = await generateObject({ + model: openai('gpt-4.1-mini'), + prompt: request.prompt, + schema + }); + + expect(request.schemaName).toBe('python_script'); + expect(result.object).toBeTruthy(); + }, 60_000); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json index c50b696..986970b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -7,6 +7,13 @@ "declarationMap": false, "types": [] }, - "include": ["index.ts", "src/**/*.ts", "examples/*.ts", "demos/**/*.ts", "demos/**/*.d.ts"], + "include": [ + "index.ts", + "src/**/*.ts", + "examples/*.ts", + "examples/integrations/**/*.ts", + "demos/**/*.ts", + "demos/**/*.d.ts" + ], "exclude": ["tests", "dist", "node_modules"] } From 5327860144b247f1829c8c21d93f1140f8f0da22 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Fri, 29 May 2026 02:05:23 -0400 Subject: [PATCH 2/2] chore: bump package version to 0.7.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e53f67b..ad2f604 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rlippmann/context-compiler", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rlippmann/context-compiler", - "version": "0.7.0", + "version": "0.7.1", "license": "Apache-2.0", "devDependencies": { "typescript": "^5.9.3", diff --git a/package.json b/package.json index 3a71bbf..e77c398 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rlippmann/context-compiler", - "version": "0.7.0", + "version": "0.7.1", "description": "Store AI rules and corrections separately from chat history so they stay consistent across turns.", "keywords": [ "llm",