> ## Documentation Index
> Fetch the complete documentation index at: https://growthx-changeset-release-main.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# v0.11.0 → v0.12.0

> Upgrading Output.ai projects from v0.11.0 to v0.12.0: AI SDK 7, LLM call signatures, loaded prompt shape, aiSdk re-exports, and prompt-only skills in @outputai/llm.

This guide covers breaking changes in `@outputai/llm`. The package now uses AI SDK 7 and its matching provider majors. Generation APIs drop native AI SDK call arguments. Skills load only from prompt frontmatter. Call-argument tools merge with prompt YAML tools. Prompt file `config` is a strict key list. Loaded messages carry resolved `providerOptions` instead of tag `attributes`. LLM traces use a single loaded `prompt` on start, keep raw usage and merged sources on end, and store normalized usage and cost as attributes.

## Upgrade AI SDK and provider packages

`@outputai/llm` v0.12 requires these dependency majors:

* `ai`: 7.x
* `@ai-sdk/openai`, `@ai-sdk/anthropic`, `@ai-sdk/azure`, and `@ai-sdk/perplexity`: 4.x
* `@ai-sdk/amazon-bedrock` and `@ai-sdk/google-vertex`: 5.x

Update only the provider packages your application installs. The complete upstream changes are in the [AI SDK 7 migration guide](https://ai-sdk.dev/docs/migration-guides/migration-guide-7-0).

The separate `@perplexity-ai/ai-sdk` search-tool package is not the official `@ai-sdk/perplexity` provider. Its latest release, `0.1.3`, declares support for AI SDK 5 and 6 only. Projects using `perplexitySearch()` should replace it or verify its behavior with AI SDK 7 before upgrading; package managers may report an unmet peer dependency.

### Rename stream callbacks and properties

`streamText()` and `Agent.stream()` now use `onEnd` instead of `onFinish`:

```ts theme={null}
// Before
await streamText( {
  prompt: 'writer@v1',
  onFinish( event ) {
    console.log( event.result );
  }
} );

// After
await streamText( {
  prompt: 'writer@v1',
  onEnd( event ) {
    console.log( event.result );
  }
} );
```

Rename the corresponding exported types:

* `WrappedStreamTextOnFinishEvent` -> `WrappedStreamTextOnEndEvent`
* `WrappedStreamTextOnFinishCallback` -> `WrappedStreamTextOnEndCallback`

The wrapped event continues to include `result`, `cost`, and merged `sources`.

AI SDK 7 also renamed the full event stream:

```ts theme={null}
// Before
for await ( const part of result.fullStream ) {
  // ...
}

// After
for await ( const part of result.stream ) {
  // ...
}
```

`fullStream` remains available as a deprecated alias, but migrate to `stream`.

`textStream` is unchanged. Direct AI SDK `onStepFinish` usage becomes `onStepEnd`.

`onChunk` now receives every AI SDK 7 stream part, including start, finish, error, step boundaries, text boundaries, and reasoning boundaries. Update exhaustive handlers to ignore part types they do not use.

### Review native AI SDK response usage

Output returns proxied AI SDK 7 results. If your application reads native response fields beyond Output's `result`, `cost`, and merged `sources`, follow the official AI SDK 7 migration guide linked above instead of treating this guide as a replacement for it.

Output's merged `sources` now includes native sources accumulated across every AI SDK step as well as URLs extracted from tools.

### Normalized usage and cost metadata

Cost calculation uses AI SDK 7's detailed token counts when they reconcile with the aggregate input or output count. Incomplete breakdowns use the aggregate count. Missing cache pricing uses the regular input price, and missing reasoning pricing uses the regular output price.

Traces now store separate `llm:generation:usage` and `llm:generation:cost` attributes. Both include `providerId`, `modelId`, nullable aggregate `input`, `output`, and `total` fields, plus detailed `items`. Priced calls also retain the deprecated `llm:usage` attribute for existing trace consumers.

Usage items have an `amount`, a `group` (`input` or `output`), and one of these labels:

* Input: `no_cache`, `cache_read`, or `cache_write`
* Output: `text` or `reasoning`

When detailed counts do not reconcile with an aggregate count, that group uses `label: null`. `LLMGenerationUsage.status` is `complete` when both input and output usage are available; otherwise it is `incomplete`.

Cost items repeat the usage identity and amount, then add `ppm`, item `total`, and a status:

* `ok`: the matching price was available.
* `fallback`: cache or reasoning usage used the regular input or output price.
* `missing`: no applicable price was available; `ppm` and item `total` are `null`.

`LLMGenerationCost.status` is `precise`, `imprecise`, or `incomplete` based on those item statuses and usage completeness. Missing model or dimension pricing therefore produces a non-null, incomplete `LLMGenerationCost` whose `total` may be `null`. The complete cost object is `null` only when the pricing catalog itself could not be loaded.

The new `llm:generation:metering` event is emitted whenever raw AI SDK usage can be normalized. Its payload is `{ usage: LLMGenerationUsage, cost: LLMGenerationCost | null }`. Prefer it for new integrations because it preserves reported usage independently from pricing and records fallback and missing prices explicitly.

The existing `cost:llm:request` event remains supported with its legacy payload shape, so existing handlers do not need a structural migration for v0.12. Its deprecated `LLMUsageEvent` retains the `input`, `input_cached`, `output`, and `reasoning` line types. Values can differ where AI SDK 7 reports a different breakdown or where v0.12 corrects legacy reasoning double-counting.

Raw AI SDK usage remains available on the response and trace output.

### Update Agent assumptions

`Agent` no longer inherits from AI SDK's `ToolLoopAgent`. Its Output APIs retain their existing behavior, but code must not depend on `Agent instanceof ToolLoopAgent` or other inherited `ToolLoopAgent` members.

The intended `MessageStore` conversation behavior is unchanged. Output reads AI SDK 7's accumulated `responseMessages` so intermediate tool-call and tool-result messages continue to be persisted once.

Custom stores must preserve all AI SDK 7 `ModelMessage` content parts. File data may require binary-safe serialization, and assistant content can contain the new `reasoning-file` part.

### Review provider behavior

* OpenAI reasoning requests now return detailed reasoning summaries by default when reasoning is enabled. Set `providerOptions.openai.reasoningSummary` to `null` in prompt frontmatter to disable them.
* Function-valued tool descriptions are now accepted in addition to string descriptions.

Call-level `toolApproval`, `runtimeContext`, and `toolsContext` are not exposed by the v0.12 Output wrapper. AI SDK 7's deprecated tool-level `needsApproval` field remains accepted.

## Skills load only from the prompt file

`generateText`, `streamText`, `generateTextWithStreaming`, and `Agent` no longer accept a `skills` argument. Dynamic skill resolvers (sync or async functions) are gone with it. Passing `skills` throws:

```
skills must be set in the prompt file, not as a call argument
```

`skill()` is no longer exported. Colocated `skills/` auto-discovery is gone: a `skills/` folder next to the prompt is not loaded unless you list it in frontmatter.

### Move call-argument and inline skills into the prompt

#### Before

```ts theme={null}
import { generateText, skill } from '@outputai/llm';

const audienceSkill = skill( {
  name: 'audience',
  description: 'Audience voice',
  instructions: 'Write for operators, not executives.'
} );

await generateText( {
  prompt: 'writer@v1',
  skills: [ audienceSkill ]
} );
```

#### After

Put the instructions in a markdown file and list the path in frontmatter. Paths are relative to the prompt file.

```markdown prompts/skills/audience.md theme={null}
---
name: audience
description: Audience voice
---

Write for operators, not executives.
```

```yaml prompts/writer@v1.prompt theme={null}
---
provider: anthropic
model: claude-sonnet-4-6
skills:
  - ./skills/audience.md
---

<system>
You are a writer. Use load_skill before applying a skill.
</system>

<user>
{{ task }}
</user>
```

```ts theme={null}
import { generateText } from '@outputai/llm';

await generateText( { prompt: 'writer@v1' } );
```

A directory path loads every `.md` file under it (recursive):

```yaml theme={null}
skills:
  - ./skills
```

A single string is still valid YAML and is coerced to an array at load time:

```yaml theme={null}
skills: ./skills/audience.md
```

### Restore colocated skills that used auto-discovery

#### Before

```
prompts/
├── writer@v1.prompt
└── skills/
    └── audience.md
```

No `skills:` key in the prompt. Output discovered `./skills` automatically.

#### After

Keep the folder. Add an explicit path:

```yaml theme={null}
skills:
  - ./skills
```

## Prompt tools and call-argument tools merge

Call-argument `tools` no longer replace the whole prompt YAML tools map.

* Prompt YAML tools and call-argument tools are merged.
* The same key: the caller wins.
* When skills are present, `load_skill` is added last and cannot be overridden.

#### Before

```yaml theme={null}
tools:
  googleSearch: {}
```

```ts theme={null}
await generateText( {
  prompt: 'research@v1',
  tools: { lookup: lookupTool }
} );
```

`googleSearch` was dropped. Only `lookup` was sent.

#### After

Both are sent: `{ googleSearch, lookup }`. If you meant to disable YAML tools, remove them from the prompt (or override that key on the call).

## Loaded prompt shape

`loadPrompt` returns the parsed prompt object.

| v0.11.0                                                     | v0.12.0                                                                                           |                                |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------ |
| `prompt.promptFileDir`                                      | `prompt.fileDir` (always set)                                                                     |                                |
| -                                                           | `prompt.variables` (`Record<string, unknown>`, including nested objects and arrays; default `{}`) |                                |
| `prompt.config.skills` missing, a string, or a string array | Always a `string[]` (`null` / missing -> `[]`, a string -> `[string]`)                            |                                |
| `prompt.config.maxSteps` missing                            | Always a positive integer (default 10)                                                            |                                |
| `prompt.instructions` missing or omitted                    | Always \`string                                                                                   | null`(chat prompts are`null\`) |
| `message.attributes` (`{ options: 'cached' }` or `{}`)      | `message.providerOptions` (resolved set(s); omitted when the tag has no `options`)                |                                |

#### Before

```ts theme={null}
const prompt = loadPrompt( 'writer@v1' );
const dir = prompt.promptFileDir;
```

#### After

```ts theme={null}
const prompt = loadPrompt( 'writer@v1' );
const dir = prompt.fileDir;
```

The `SkillsArg` and `Skill` types are removed. Skills are now internal loaded values; declare file paths in prompt frontmatter instead of constructing or typing skill objects.

### Per-message options resolve at load

v0.11 kept the role tag's `attributes` on the loaded message and compiled `options="..."` into AI SDK `providerOptions` at generate time. v0.12 compiles that at `loadPrompt`. LLM traces (`input.prompt.messages`) use the same loaded shape.

#### Before

```ts theme={null}
const prompt = loadPrompt( 'writer@v1' );
prompt.messages[0].attributes; // { options: 'cached' }
```

#### After

```ts theme={null}
const prompt = loadPrompt( 'writer@v1' );
prompt.messages[0].providerOptions;
// { anthropic: { cacheControl: { type: 'ephemeral' } } }
```

The only supported role-tag attribute is `options`. Any other attribute throws at load (previously this could fail later, at generate):

```
Error parsing content on prompt "writer@v1": Message has unsupported attributes. The only supported attribute is "options".
```

Unknown `options` names, `options` without a value, and `options` set while `config.messageOptions` is missing or empty also throw at load.

### Prompt message roles are narrowed

`PromptMessage.role` is now typed as `'system' | 'user' | 'assistant'` instead of `string`, matching the authored role blocks accepted by `loadPrompt()`. Code that constructs a `PromptMessage` from a dynamic string must validate or narrow the value before assigning it.

### Prompt bodies use explicit parsing modes

v0.11 searched the rendered body for supported role blocks wherever they appeared. This could silently ignore text outside those blocks, unknown top-level tags, and malformed attributes. v0.12 selects one mode from the first meaningful token after leading whitespace and HTML comments:

* Plain text selects instruction mode. The whole trimmed body becomes `prompt.instructions`, including any tags that appear later.
* A tag selects message mode. The complete body is validated as top-level role blocks and `prompt.instructions` is `null`.

These modes describe `loadPrompt()` output; API requirements are unchanged. `generateText`, `generateTextWithStreaming`, `streamText`, and `Agent` require message mode, while `generateImage` requires instruction mode.

For example, v0.11 extracted the `<user>` block here and discarded the surrounding text:

```text theme={null}
Context for the prompt:
<user>Summarize {{ article }}.</user>
End of prompt.
```

In v0.12 this is one instruction string because it starts with text. To keep message mode, move the text inside the role block:

```text theme={null}
<user>
Context for the prompt:
Summarize {{ article }}.
End of prompt.
</user>
```

Message mode now enforces these rules:

* Top-level blocks must use `system`, `user`, or `assistant`.
* Only whitespace and HTML comments may appear between blocks.
* Root self-closing tags, unmatched closing tags, and unclosed blocks throw.
* Different-name tags inside a message remain literal message content.
* A nested non-self-closing tag with the same name as its message throws instead of prematurely closing the outer block. Escape literal examples, such as `&lt;user&gt;example&lt;/user&gt;`.
* Attribute names, separators, and quotes are validated. Spaces around `=` and `>` inside quoted values are supported; malformed fragments no longer pass silently.

Prompt files no longer accept authored `<tool>` blocks. Their string content never matched AI SDK's structured tool-result message contract, so text generation rejected them. AI SDK continues to create tool messages during execution, and Agent callers may supply structured tool messages through `messages` or `messageStore`.

## LLM trace details

Start-trace `input` on `generateText`, `streamText`, `generateTextWithStreaming`, `generateImage`, and `Agent` (`generate`, `generateWithStreaming`, `stream`) is the loaded prompt object only. Filename, interpolation values, and rendered config live on that object. Agent traces use the same shape as the text APIs (v0.11 recorded only the filename as `prompt`).

End-trace `output` keeps the raw AI SDK `usage` and replaces tool-only `sourcesFromTools` with merged `sources` (tool results plus native provider sources; always an array). Normalized usage and cost are stored as trace attributes.

### Start (`input`)

| v0.11.0                        | v0.12.0            |
| ------------------------------ | ------------------ |
| `prompt` (filename string)     | `prompt.name`      |
| `variables` (sibling)          | `prompt.variables` |
| `loadedPrompt` (loaded object) | `prompt`           |

#### Before

```json theme={null}
{
  "prompt": "generate_summary@v1",
  "variables": { "companyName": "Acme Corp" },
  "loadedPrompt": {
    "name": "generate_summary@v1",
    "config": { "provider": "anthropic", "model": "claude-sonnet-4-6" }
  }
}
```

#### After

```json theme={null}
{
  "prompt": {
    "name": "generate_summary@v1",
    "fileDir": "/prompts",
    "variables": { "companyName": "Acme Corp" },
    "config": { "provider": "anthropic", "model": "claude-sonnet-4-6" }
  }
}
```

If you read `input.prompt` as a filename, use `input.prompt.name`. If you read `input.loadedPrompt`, switch to `input.prompt`. If you read sibling `input.variables`, use `input.prompt.variables`.

### End (`output`)

| v0.11.0                               | v0.12.0                                    |
| ------------------------------------- | ------------------------------------------ |
| `result`, `usage`, `providerMetadata` | same                                       |
| `sourcesFromTools`                    | `sources` (merged tool + provider sources) |

#### Before

```json theme={null}
{
  "result": "Acme Corp is a B2B SaaS company...",
  "usage": { "inputTokens": 38, "outputTokens": 204, "totalTokens": 242 },
  "providerMetadata": { "anthropic": {} },
  "sourcesFromTools": []
}
```

#### After

```json theme={null}
{
  "result": "Acme Corp is a B2B SaaS company...",
  "usage": { "inputTokens": 38, "outputTokens": 204, "totalTokens": 242 },
  "providerMetadata": { "anthropic": {} },
  "sources": []
}
```

If you read `output.sourcesFromTools`, switch to `output.sources`.

### Attributes

The raw AI SDK `output.usage` shape remains on the trace output. Normalized usage and cost live under the LLM node's `attributes` object:

```json theme={null}
{
  "llm:generation:usage": {
    "type": "llm:generation:usage",
    "providerId": "anthropic",
    "modelId": "claude-sonnet-4-6",
    "status": "complete",
    "input": 38,
    "output": 204,
    "total": 242,
    "items": [
      { "group": "input", "label": null, "amount": 38 },
      { "group": "output", "label": null, "amount": 204 }
    ]
  },
  "llm:generation:cost": {
    "type": "llm:generation:cost",
    "providerId": "anthropic",
    "modelId": "claude-sonnet-4-6",
    "status": "precise",
    "input": 0.000114,
    "output": 0.00306,
    "total": 0.003174,
    "items": [
      { "group": "input", "label": null, "amount": 38, "ppm": 3, "total": 0.000114, "status": "ok" },
      { "group": "output", "label": null, "amount": 204, "ppm": 15, "total": 0.00306, "status": "ok" }
    ]
  },
  "llm:usage": {
    "type": "llm:usage",
    "modelId": "claude-sonnet-4-6",
    "usage": [
      { "type": "input", "ppm": 3, "amount": 38, "total": 0.000114 },
      { "type": "output", "ppm": 15, "amount": 204, "total": 0.00306 }
    ],
    "total": 0.003174,
    "tokensUsed": 242
  }
}
```

The legacy `attributes["llm:usage"]` payload remains available for compatibility, so existing trace readers do not need an immediate migration. New integrations should use `attributes["llm:generation:usage"]` and `attributes["llm:generation:cost"]`; these preserve normalized usage independently from pricing and represent detailed dimensions explicitly. The CLI accepts all three keys and prefers the normalized attributes.

### Response source, cost, and metering types

`ExtractedSource` now matches the AI SDK source union. A source can be a URL or a document, so narrow on `sourceType` before reading `url`:

```ts theme={null}
// Before
const urls = response.sources.map(source => source.url);

// After
const urls = response.sources
  .filter(source => source.sourceType === 'url')
  .map(source => source.url);
```

`response.cost` and direct-stream `onEnd` cost now use `LLMGenerationCost | null`. The old `LLMCallCost` type is removed.

`@outputai/core` no longer exports the LLM-specific `Attribute.LLMUsage` class or `Attribute.Usage` interface. Core now exposes only the generic `Attribute.BaseAttribute` for package-owned trace attributes. Import `LLMGenerationUsage`, `LLMGenerationCost`, and the deprecated legacy `LLMUsageEvent` type from `@outputai/llm`.

In v0.11, `response.cost` used the legacy `llm:usage` shape with priced lines under `usage`. In v0.12, read aggregate costs directly and detailed dimensions from `items`:

```ts theme={null}
// Before
const cachedInputCost = response.cost?.usage
  .find(item => item.type === 'input_cached')
  ?.total;

// After
const cachedInputCost = response.cost?.items
  .find(item => item.group === 'input' && item.label === 'cache_read')
  ?.total;
```

The new object has `type: "llm:generation:cost"`, `providerId`, `modelId`, nullable `input`, `output`, and `total`, a calculation `status`, and one cost item for each normalized usage item.

Do not use only `response.cost === null` to detect unavailable totals. Missing model or dimension pricing returns an incomplete `LLMGenerationCost`; check `response.cost?.total == null` or inspect its status and item statuses. `response.cost` itself is `null` when the pricing catalog could not be loaded.

`LLMUsageEvent` remains exported from `@outputai/llm` for `cost:llm:request`. Its legacy payload shape remains compatible, but the type is deprecated to guide new integrations toward the more complete metering event.

```ts theme={null}
import { on } from '@outputai/core/hooks';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';

on<LLMGenerationMeteringEvent>('llm:generation:metering', event => {
  if (!event.payload) {
    return;
  }

  console.log(event.payload.usage.total, event.payload.cost?.total);
});
```

Use `llm:generation:metering` for new integrations when you need normalized usage and cost data. It fires when usage can be normalized; `event.payload.cost` may be `null` or incomplete. Existing `cost:llm:request` handlers can remain until you choose to adopt the more detailed format.

## AI SDK helpers come from `aiSdk`

`@outputai/llm` no longer re-exports `tool`, `Output`, `smoothStream`, `stepCountIs`, `hasToolCall`, or `jsonSchema` as named exports. The namespace re-export `ai` is renamed to `aiSdk`.

#### Before

```ts theme={null}
import { generateText, Output, stepCountIs, tool } from '@outputai/llm';

await generateText( {
  prompt: 'writer@v1',
  output: Output.object( { schema } ),
  stopWhen: stepCountIs( 1 ),
  tools: { lookup: tool( { description: 'Lookup', parameters: schema, execute } ) }
} );
```

#### After

```ts theme={null}
import { generateText, aiSdk } from '@outputai/llm';

await generateText( {
  prompt: 'writer@v1',
  output: aiSdk.Output.object( { schema } ),
  stopWhen: aiSdk.isStepCount( 1 ),
  tools: { lookup: aiSdk.tool( { description: 'Lookup', inputSchema: schema, execute } ) }
} );
```

`import { ai } from '@outputai/llm'` becomes `import { aiSdk } from '@outputai/llm'`. Output APIs (`generateText`, `Agent`, `loadPrompt`, ...) stay named exports.

Cherry-picked AI SDK type re-exports (`ToolSet`, `FinishReason`, `ModelMessage`, `StreamTextOnChunkCallback`, ...) are also gone. Import those from `ai`, or as `aiSdk.ToolSet`.

### Replace removed Output option types

The Output-owned AI SDK option aliases were removed with the unrestricted native arguments. Use the corresponding public parameter type:

```ts theme={null}
// Before
import type {
  GenerateTextAiSdkOptions,
  StreamTextAiSdkOptions,
  GenerateImageAiSdkOptions
} from '@outputai/llm';

// After
import type {
  GenerateTextParameters,
  StreamTextParameters,
  GenerateImageParameters
} from '@outputai/llm';
```

`OutputAgentGenerateWithStreamingParameters` no longer accepts an output type argument. Remove the generic:

```ts theme={null}
// Before
type Options = OutputAgentGenerateWithStreamingParameters<MyOutput>;

// After
type Options = OutputAgentGenerateWithStreamingParameters;
```

## Call arguments are a fixed list

Dropped native AI SDK call arguments from `generateText()`, `generateTextWithStreaming()`, `streamText()`, `generateImage()`, and `Agent`. Calls no longer accept `temperature`, `maxOutputTokens`, `maxTokens`, `maxSteps`, `providerOptions`, image `n`/`size`/`seed`, `experimental_transform`, `onStepFinish`, and similar. Unknown keys throw. Set model and generation or image config (including `maxSteps`, default 10) on the prompt file; call-argument `stopWhen` still overrides it.

| Argument      | `generateText` | `generateTextWithStreaming` | `streamText` | `generateImage` |
| ------------- | -------------- | --------------------------- | ------------ | --------------- |
| `prompt`      | required       | required                    | required     | required        |
| `promptDir`   | optional       | optional                    | optional     | optional        |
| `variables`   | optional       | optional                    | optional     | optional        |
| `tools`       | optional       | optional                    | optional     | -               |
| `output`      | optional       | optional                    | optional     | -               |
| `toolChoice`  | optional       | optional                    | optional     | -               |
| `stopWhen`    | optional       | optional                    | optional     | -               |
| `abortSignal` | optional       | optional                    | optional     | optional        |
| `onChunk`     | -              | optional                    | optional     | -               |
| `onEnd`       | -              | -                           | optional     | -               |
| `onError`     | -              | -                           | optional     | -               |
| `images`      | -              | -                           | -            | optional        |
| `mask`        | -              | -                           | -            | optional        |

| Argument       | `new Agent` | `.generate` | `.generateWithStreaming` | `.stream` |
| -------------- | ----------- | ----------- | ------------------------ | --------- |
| `prompt`       | required    | -           | -                        | -         |
| `promptDir`    | optional    | -           | -                        | -         |
| `variables`    | optional    | -           | -                        | -         |
| `tools`        | optional    | -           | -                        | -         |
| `output`       | optional    | -           | -                        | -         |
| `stopWhen`     | optional    | -           | -                        | -         |
| `messageStore` | optional    | -           | -                        | -         |
| `messages`     | -           | optional    | optional                 | optional  |
| `abortSignal`  | -           | optional    | optional                 | optional  |
| `toolChoice`   | -           | optional    | optional                 | optional  |
| `onChunk`      | -           | -           | optional                 | optional  |
| `onEnd`        | -           | -           | -                        | optional  |
| `onError`      | -           | -           | -                        | optional  |

`generateImage` `mask` still requires `images`. `Agent.generate()`, `Agent.generateWithStreaming()`, and `Agent.stream()` append to `messageStore` only when `finishReason` is not `'error'`. Errored turns are not persisted.

`streamText()` and `Agent.stream()` now treat `onError` as a fire-and-forget observer. Output maps and forwards the provider error, but exceptions and rejected promises from the callback are ignored. To fail a workflow step with the original error, capture it in `onError` and throw it after consuming the stream.

## Agent message store

`conversationStore` is renamed to `messageStore`. The type is `MessageStore`. `createMemoryConversationStore()` is removed; implement the store yourself.

#### Before

```ts theme={null}
import { Agent, createMemoryConversationStore } from '@outputai/llm';

new Agent( {
  prompt: 'chatbot@v1',
  conversationStore: createMemoryConversationStore()
} );
```

#### After

```ts theme={null}
import { Agent } from '@outputai/llm';
import type { MessageStore } from '@outputai/llm';

const messages: Parameters<MessageStore['addMessages']>[0] = [];
const messageStore: MessageStore = {
  getMessages: () => messages,
  addMessages: incoming => {
    messages.push( ...incoming );
  }
};

new Agent( {
  prompt: 'chatbot@v1',
  messageStore
} );
```

### Move model config onto the prompt

#### Before

```ts theme={null}
await generateText( {
  prompt: 'writer@v1',
  temperature: 0.2,
  maxSteps: 5,
  maxRetries: 2
} );

await generateTextWithStreaming( {
  prompt: 'writer@v1',
  experimental_transform: aiSdk.smoothStream()
} );
```

#### After

```yaml prompts/writer@v1.prompt theme={null}
---
provider: anthropic
model: claude-sonnet-4-6
temperature: 0.2
maxSteps: 5
---
```

```ts theme={null}
await generateText( { prompt: 'writer@v1' } );
```

Any merged tools, including prompt-only Vertex `googleSearch` / `urlContext`, now get `stopWhen: isStepCount(maxSteps)` from that prompt value. Previously the ceiling applied only when call-argument tools or `load_skill` were present; YAML-only grounding stayed at the AI SDK default of one step.

If you need the old one-step grounding behavior, set `maxSteps: 1` on the prompt, or pass `stopWhen` on the call:

```ts theme={null}
import { generateText, aiSdk } from '@outputai/llm';

await generateText( {
  prompt: 'grounded@v1',
  stopWhen: aiSdk.isStepCount( 1 )
} );
```

## Agent constructor validation

`new Agent( {} )` no longer throws `Agent requires a prompt`. Invalid constructor args use the same schema as `generateText` and throw `Invalid Agent() arguments`.

That includes a missing/empty `prompt`, an empty `promptDir`, and call-argument `skills` or `maxSteps` fields.

## Prompt config is a strict key list

Unknown top-level keys on a `.prompt` file now throw `Invalid prompt file`. Previously they were kept on `config` and ignored. `provider` and `model` must be non-empty strings. When provided, `maxOutputTokens` and deprecated `maxTokens` must each be a positive integer. Nested `providerOptions` (including `thinking`) stays open.

Allowed top-level keys: `provider`, `model`, `temperature`, `maxOutputTokens`, deprecated `maxTokens`, `topP`, `topK`, `presencePenalty`, `frequencyPenalty`, `stopSequences`, `seed`, `maxSteps`, `skills`, `tools`, `providerOptions`, `messageOptions`, `n`, `maxImagesPerCall`, `size`, `aspectRatio`.

`maxTokens` remains available for backward compatibility and is retained on the loaded prompt config. When `maxOutputTokens` is absent, `loadPrompt` also copies the `maxTokens` value to `maxOutputTokens`. When both keys are set, `maxOutputTokens` takes precedence. Text generation APIs read the normalized `maxOutputTokens` value.

Snake\_case aliases of those keys fail with a suggestion:

```
Invalid prompt file "writer@v1": Unrecognized key: "max_images_per_call". "max_images_per_call" is not valid; use "maxImagesPerCall"
```

Move provider-specific fields under `providerOptions`. `effort` and `reasoningEffort` at the top level are unknown keys; they belong under `providerOptions.anthropic` and `providerOptions.openai`.

#### Before

```yaml theme={null}
---
provider: openai
model: gpt-5.4
reasoningEffort: medium
max_output_tokens: 16000
---
```

#### After

```yaml theme={null}
---
provider: openai
model: gpt-5.4
maxOutputTokens: 16000
providerOptions:
  openai:
    reasoningEffort: medium
---
```

## Checklist

* Delete `skills` from `generateText` / `streamText` / `generateTextWithStreaming` / `Agent` calls.
* Remove `skill()`, `Skill`, and `SkillsArg` imports; move inline skills into files listed under prompt `skills:`.
* Add `skills: ./skills` (or explicit file paths) to prompts that relied on colocated auto-discovery.
* Expect YAML tools and call-argument tools to merge; remove YAML tools if you previously relied on replacement.
* Strip dropped call arguments from `generateText` / `generateTextWithStreaming` / `streamText` / `generateImage` / `Agent` (`temperature`, `maxOutputTokens`, `maxTokens`, `maxSteps`, `providerOptions`, `maxRetries`, `experimental_transform`, `onStepFinish`, image `n` / `size` / `seed`, and any other AI SDK-only keys). Unknown keys throw. Put model and generation or image config on the prompt file; call-argument `stopWhen` still overrides `maxSteps`.
* Update installed AI SDK packages to `ai` 7.x, OpenAI / Anthropic / Azure / Perplexity 4.x, and Amazon Bedrock / Google Vertex 5.x.
* Rename `onFinish` to `onEnd` in `streamText()` and `Agent.stream()`. Rename the corresponding wrapped callback and event types.
* Replace `fullStream` with `stream`, and update exhaustive `onChunk` handlers for the expanded AI SDK 7 part union.
* Replace `stepCountIs()` with `isStepCount()`.
* Review direct AI SDK result-field access against the official AI SDK 7 migration guide.
* Use `group` and `label` when reading normalized `LLMGenerationUsage` and `LLMGenerationCost` items. Legacy event lines retain their existing type names.
* Remove assumptions that Output's `Agent` inherits from AI SDK's `ToolLoopAgent`.
* Preserve binary file and `reasoning-file` content parts in custom `MessageStore` serializers.
* Set `providerOptions.openai.reasoningSummary: null` on reasoning prompts that should not return summaries.
* Set `maxSteps: 1` on YAML-only grounding prompts that must stay one-shot, or pass `stopWhen: aiSdk.isStepCount(1)` on the call.
* Rename `promptFileDir` to `fileDir`. Read interpolation values from `prompt.variables`.
* Treat `config.skills` as always `string[]` after `loadPrompt`.
* Treat `config.maxSteps` as always a positive integer after `loadPrompt` (default 10).
* Treat `prompt.instructions` as always `string | null` after `loadPrompt` (chat prompts are `null`).
* Read `message.providerOptions` instead of `message.attributes` on `loadPrompt` results and LLM trace `input.prompt.messages`.
* Narrow dynamic role strings before assigning them to `PromptMessage.role`; the type now accepts only `'system'`, `'user'`, and `'assistant'`.
* Remove unknown attributes from role tags (`name`, `id`, `pinned`, ...). Only `options` is allowed; extras throw at `loadPrompt`.
* Remove authored `<tool>` blocks from prompt files; pass structured tool history through Agent `messages` or `messageStore`.
* Audit prompt bodies that put prose before the first role tag. They now load as instructions; move the prose inside a role block to keep message mode.
* Remove text between or after top-level role blocks. Only whitespace and HTML comments are allowed there.
* Escape literal same-name role tags inside messages (`&lt;user&gt;...&lt;/user&gt;`). Different-name semantic tags remain valid content.
* Give every `options` attribute a value and fix malformed attribute names or quotes; prompt markup now fails explicitly at load.
* Read LLM trace `input.prompt` as the loaded prompt object (`input.prompt.name`, `input.prompt.variables`). Do not treat `input.prompt` as a filename or read `input.loadedPrompt`.
* Read LLM trace `output.sources` instead of `output.sourcesFromTools`.
* Keep existing `llm:usage` trace readers for compatibility, but prefer `llm:generation:usage` and `llm:generation:cost` for normalized trace metadata.
* Narrow `ExtractedSource` on `sourceType` before reading `url`. Replace `LLMCallCost` with `LLMGenerationCost`, replace `response.cost.usage` reads with `response.cost.items`, and handle an incomplete cost whose `total` is `null`. Import LLM generation usage and cost types from `@outputai/llm`, not `@outputai/core`.
* Keep existing `cost:llm:request` handlers for structural compatibility, while allowing corrected reasoning values. Prefer `llm:generation:metering` with `LLMGenerationMeteringEvent` for new normalized usage/cost integrations.
* Import AI SDK helpers and types from `aiSdk` (`aiSdk.Output`, `aiSdk.tool`, `aiSdk.isStepCount`, `aiSdk.ToolSet`, ...). Replace `import { ai }` with `import { aiSdk }`. Do not import cherry-picked AI SDK types from `@outputai/llm`.
* Replace `GenerateTextAiSdkOptions`, `StreamTextAiSdkOptions`, and `GenerateImageAiSdkOptions` with their `*Parameters` equivalents. Remove the generic from `OutputAgentGenerateWithStreamingParameters`.
* Expect `Agent.generate()`, `Agent.generateWithStreaming()`, and `Agent.stream()` to persist message-store history only when `finishReason` is not `'error'`.
* Replace `conversationStore` with `messageStore`. Replace `ConversationStore` with `MessageStore`. Remove `createMemoryConversationStore()` and pass your own store.
* Update Agent tests and error matchers that expected `Agent requires a prompt`.
* Ensure prompt `provider` and `model` values are non-empty. If set, `maxOutputTokens` and deprecated `maxTokens` must each be positive integers.
* Review prompt frontmatter against the supported configuration and remove unsupported fields. Any remaining fields cause an Invalid prompt file error.
