Skip to content

refactor(llm): Improving syntax, prompt, types and bug fixes - #367

Open
szanata wants to merge 38 commits into
mainfrom
refactor/llm
Open

refactor(llm): Improving syntax, prompt, types and bug fixes#367
szanata wants to merge 38 commits into
mainfrom
refactor/llm

Conversation

@szanata

@szanata szanata commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Comprehensive refactor of the LLM module in order to fix many bugs and tight the types, signatures, validations, heuristics:

Skills

  • Fixed implicit and inconsistent skill loading by making prompt frontmatter the only skills definition:
    • Removed the skills argument from generateText(), streamText(), generateTextWithStreaming(), and Agent.
    • Removed the skills/ auto-discovery.
    • Removed the skill() helper and the Skill and SkillsArg types.
    • Fixed configured skill directories silently omitting Markdown files in nested folders. Directory loading is recursive and sorted.
    • Fixed skill discovery following symbolic links. Symbolic links are ignored.

Call signatures and validation

  • Fixed unrestricted native AI SDK call arguments bypassing validation and overriding prompt-owned model/configuration. Generation and Agent APIs now reject unsupported or misplaced arguments before provider I/O. Removed native AI SDK call arguments from generateText(), generateTextWithStreaming(), streamText(), generateImage(), and Agent, as well as skills and maxSteps. These are the supported arguments:

    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 -
    onFinish - - 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
    onFinish - - - optional
    onError - - - optional
    • Fixed callers being able to replace the prompt model while traces and cost used the prompt provider/model.
    • Fixed unsupported callbacks on completion APIs being silently overwritten by internal callbacks.
    • Fixed Agent using AI SDK's two automatic retries while other Output generation APIs disabled them. Agent now uses maxRetries: 0.
    • Added the public GenerateImageInput type for generateImage() images and mask values.
  • Fixed invalid Agent constructor and method arguments being forwarded for late failure by validating messages, callbacks, tool choices, and MessageStore implementations at the public boundary.

Tools and tool loops

  • Fixed tool handling:
    • Fixed call-argument tools overriding prompt tools. They now merge (caller wins on the same key; load_skill is last).
    • Fixed prompt-only native tools missing the tool-loop limit. They now use stopWhen: stepCountIs(maxSteps) from the prompt (default 10).
    • Fixed non-callable provider tool entries failing later with an opaque TypeError. They now fail provider-tool validation.

Prompt files

  • Fixed loaded prompt shape inconsistencies:
    • Renamed Prompt.promptFileDir to Prompt.fileDir.
    • Added PromptVariables (Record<string, unknown>, including nested objects and arrays) and Prompt.variables (default {}). LLM API variables arguments use the same type.
    • Fixed Prompt.config.skills varying between missing, a string, and a string array. It is always a string[] after load.
    • Added Prompt.config.maxSteps (positive integer, default 10). It replaces the old argument and is required on the public type.
    • Fixed Prompt.instructions allowing undefined in its public contract. It is always string | null after load (chat prompts are null).
    • Fixed per-message options remaining unresolved until generation. options="<name>" is resolved during loadPrompt against config.messageOptions into optional PromptMessage.providerOptions; PromptMessage.attributes is removed.
  • Fixed prompt body scanning silently dropping text, accepting malformed structure, or truncating messages by replacing it with explicit instruction and message modes:
    • Fixed mode detection so plain text as the first meaningful body token selects instruction mode and preserves the complete body, while a tag selects message mode.
    • Clarified that text generation APIs require message mode, while generateImage() requires instruction mode.
    • Restricted message mode to top-level system, user, and assistant blocks, with no root text between blocks.
    • Fixed prompt files accepting authored <tool> blocks as string messages even though AI SDK requires structured tool-result parts. They now fail at load; structured tool messages remain supported through Agent messages and messageStore.
    • Added explicit errors for invalid roles, root self-closing or unmatched closing tags, unclosed blocks, and malformed attributes.
    • Fixed nested non-self-closing tags with the same name closing the outer block early. They now throw with an &lt;tag&gt; escape hint; different-name tags remain message content.
    • Fixed closing tags inside HTML comments prematurely terminating and truncating messages.
    • Fixed role-tag casing and closing-tag whitespace being misclassified as instructions. Role tags are case-insensitive and allow whitespace inside closing tags.
    • Added support for spaces around attribute = and > inside quoted values. Bare options, unknown option names, and invalid quote pairs throw at load.
    • Fixed empty or whitespace-only options values invalidating templated prompts. They now behave as no per-message options.
  • Fixed prompt file config accepting ignored or invalid values:
    • Unknown top-level keys throw.
    • model must be a non-empty string.
    • maxTokens must be a positive integer.

Streaming and Agent message store

  • Fixed stream observer failures escaping or disappearing silently. streamText() and Agent.stream() onError and onFinish callbacks are fire-and-forget observers. Output maps and forwards provider errors, and logs and ignores observer exceptions and rejected promises.
  • Updated Agent message store:
    • Renamed conversationStore to messageStore and ConversationStore to MessageStore.
    • Removed createMemoryConversationStore(). The caller supplies a MessageStore (getMessages / addMessages).
    • Fixed Agent.stream() to persist to messageStore when finishReason is not 'error'.
    • Fixed stream message-store failures suppressing the user onFinish. Failures are logged and stream finalization continues.
    • Fixed Agent store failures producing success-then-error trace sequences. Store persistence completes before a successful trace end.

Tracing, sources, and response types

  • Fixed inconsistent and incomplete LLM trace payloads across generateText(), streamText(), generateTextWithStreaming(), generateImage(), and Agent:
    • Updated start input to { prompt } (the loaded object). Removed the v0.11 filename prompt, sibling variables, and loadedPrompt.
    • Added cost on end output (also still a trace attribute and cost:llm:request when present).
    • Renamed sourcesFromTools to sources (merged tool + provider sources).
    • Fixed source merge dropping provider document sources that have id but no url.
    • Fixed blank or untrimmed search URLs producing invalid citations or unstable source IDs. URLs are trimmed and blank values are dropped.
    • Fixed wrapped text responses not consistently exposing sources as an array.
    • Updated ExtractedSource to the AI SDK generateText sources item type (url and document).
  • Fixed public cost, source, and stream callback types not matching wrapped runtime values. LLMCallCost and LLMUsageEvent are the Tracing.Attribute.LLMUsage instance type from @outputai/core; response.cost and stream onFinish cost are that instance, or null when pricing is missing. Stream onFinish types also include wrapped result and merged sources.

AI SDK exports and public parameter types

  • Renamed the AI SDK namespace re-export from ai to aiSdk (both code and types).
  • Removed named AI SDK re-exports (tool, Output, smoothStream, stepCountIs, hasToolCall, jsonSchema) and AI SDK type re-exports (ToolSet, FinishReason, ModelMessage, and others). Use the aiSdk namespace (or import from ai) for those.
  • Removed the Output-owned GenerateTextAiSdkOptions, StreamTextAiSdkOptions, and GenerateImageAiSdkOptions aliases. Use GenerateTextParameters, StreamTextParameters, and GenerateImageParameters.
  • Updated OutputAgentGenerateWithStreamingParameters to no longer accept an output type argument.

@mintlify

mintlify Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
outputai 🟢 Ready View Preview Aug 20, 2026, 10:10 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Comment thread sdk/llm/src/utils/sources.js Outdated
const { steps, sources: sourcesFromResponse } = response;
const sourcesFromTools = extractSourcesFromSteps( steps );
const allSources = sourcesFromTools.concat( asArray( sourcesFromResponse ) );
return new Map( allSources.map( s => [ s.url, s ] ) ).values().toArray();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must-fix (Correctness): dedup key is s.url, but AI SDK sources can contain { type: 'source', sourceType: 'document', id, mediaType, title, filename } entries with no url. Every document source maps to key undefined, so only the last one survives.

This is newly reachable: wrapGeneration/wrapStream now call extractSources unconditionally, whereas v0.11 only overrode sources when sourcesFromTools.length > 0 and otherwise passed response.sources through untouched. Consider keying on s.url ?? s.id.

@github-actions

This comment was marked as outdated.

@szanata szanata changed the title Refactor/llm refactor(llm): Improving syntax, prompt, types and bug fixes Aug 21, 2026
* @param {string[]} paths
* @returns {Skills[]}
*/
export const recursiveLoadSkillFile = paths => recursiveLoadSkillPaths( paths, false );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice-to-have (Quality): recursiveLoadSkillFile is exported but its only production caller is loadSkills two lines below. Inline the recursiveLoadSkillPaths( paths, false ) call and drop the export.

Comment thread sdk/llm/src/index.d.ts Outdated
export type PromptMessage = {
/** The role of the message. Examples include 'system', 'user', and 'assistant'. */
/** The message role. Authored prompt blocks support 'system', 'user', and 'assistant'. */
role: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice-to-have (Quality): after this PR parseContent guarantees the role is one of system / user / assistant (promptRoleSet throws otherwise), so role: string is looser than the runtime contract. role: 'system' | 'user' | 'assistant' would match the tightening this PR is about.

@github-actions

This comment was marked as outdated.

const stats = lstatSync( path );
if ( stats.isSymbolicLink() ) {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice-to-have (Correctness): the symlink skip also applies to paths listed explicitly in config.skills, so a symlinked skill file is dropped silently — no error, no log, and existsSync above already passed. Consider only skipping symlinks during directory recursion, or logging when an explicitly listed path is skipped.

@github-actions

Copy link
Copy Markdown
Contributor

PR review

Verdict

✅ PASS

Findings

  1. Nice-to-have (Documentation): The root README.md example (line 147) still calls bare Output.object({...}), which this PR removes from @outputai/llm's named exports. Every other example in the repo was updated to aiSdk.Output.*. .claude/skills/llm-output-schema-constraints/SKILL.md has the same stale Output.object() references.

  2. Nice-to-have (Correctness): recursiveLoadSkillPaths (sdk/llm/src/utils/skills.js:28) skips symlinks for any path, including files listed explicitly in config.skills — the skill vanishes with no error or log even though existsSync passed. Consider restricting the symlink skip to directory recursion, or logging when an explicit path is dropped.

  3. Nice-to-have (Changeset): No changeset names @outputai/cli, but the PR changes CLI-generated scaffolding (templates/workflow/evaluators.ts.template, templates/project/.../evaluators.ts.template, README.md.template now emit aiSdk.Output.*) and sdk/cli/src/types/cost.ts. The fixed version group still bumps the CLI, so this is only a changelog gap, not a release blocker.

Categories

  • Design: ✅ PASS
  • Quality: ✅ PASS
  • Correctness: ✅ PASS
  • Documentation: ✅ PASS
  • Changeset: ✅ PASS
  • Tests: ✅ PASS
  • Security: ✅ PASS
  • Compatibility: ✅ PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant