Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
80 changes: 80 additions & 0 deletions examples/integrations/vercel_ai_sdk_structured_output/README.md
Original file line number Diff line number Diff line change
@@ -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.
115 changes: 115 additions & 0 deletions examples/integrations/vercel_ai_sdk_structured_output/index.ts
Original file line number Diff line number Diff line change
@@ -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<TObject> = (request: GenerateObjectRequest) => Promise<{ object: TObject }>;

const SCHEMA_REGISTRY: Record<StructuredSchemaName, StructuredSchema> = {
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<TObject>(
state: EngineState,
prompt: string,
generateObject: GenerateObjectLike<TObject>
): 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;
});
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
10 changes: 10 additions & 0 deletions tests/examples-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"');
});
});
74 changes: 74 additions & 0 deletions tests/vercel_ai_sdk_structured_output_example.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) => 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);
});
9 changes: 8 additions & 1 deletion tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Loading