diff --git a/.github/workflows/fix-drift.yml b/.github/workflows/fix-drift.yml index 5e0e7b88..cfdbdce9 100644 --- a/.github/workflows/fix-drift.yml +++ b/.github/workflows/fix-drift.yml @@ -30,7 +30,11 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - # Step 0: Configure git identity and create fix branch + # Step 0a: Clone ag-ui repo for AG-UI schema drift detection + - name: Clone ag-ui repo + run: git clone --depth 1 https://github.com/ag-ui-protocol/ag-ui.git ../ag-ui + + # Step 0b: Configure git identity and create fix branch - name: Configure git run: | git config user.name "aimock-drift-bot" @@ -83,7 +87,9 @@ jobs: # Step 3: Invoke Claude Code to fix - name: Auto-fix drift + id: autofix if: steps.check.outputs.skip != 'true' + continue-on-error: true run: npx tsx scripts/fix-drift.ts env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} @@ -118,8 +124,8 @@ jobs: id: pr if: success() && steps.check.outputs.skip != 'true' run: | - npx tsx scripts/fix-drift.ts --create-pr 2>&1 | tee /tmp/pr-output.txt - PR_URL=$(grep -oE 'https://github.com/[^ ]+/pull/[0-9]+' /tmp/pr-output.txt | head -1) + npx tsx scripts/fix-drift.ts --create-pr + PR_URL=$(gh pr list --head "$(git branch --show-current)" --json url --jq '.[0].url') if [ -z "$PR_URL" ]; then echo "No PR URL found"; exit 1; fi echo "url=$PR_URL" >> $GITHUB_OUTPUT env: @@ -146,9 +152,11 @@ jobs: if: success() && steps.pr.outputs.url != '' run: | if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi - curl -s -X POST "$SLACK_WEBHOOK" \ + MSG="✅ *Drift auto-fix PR created with auto-merge enabled*\nPR: ${{ steps.pr.outputs.url }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + PAYLOAD=$(jq -n --arg text "$MSG" '{text: $text}') + curl -sf -X POST "$SLACK_WEBHOOK" \ -H "Content-Type: application/json" \ - -d "{\"text\":\"✅ *Drift auto-fixed and merged*\nPR: ${{ steps.pr.outputs.url }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" + -d "$PAYLOAD" env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -157,8 +165,10 @@ jobs: if: failure() && steps.check.outputs.skip != 'true' run: | if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi - curl -s -X POST "$SLACK_WEBHOOK" \ + MSG="❌ *Drift auto-fix failed* — issue created\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + PAYLOAD=$(jq -n --arg text "$MSG" '{text: $text}') + curl -sf -X POST "$SLACK_WEBHOOK" \ -H "Content-Type: application/json" \ - -d "{\"text\":\"❌ *Drift auto-fix failed* — issue created\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" + -d "$PAYLOAD" env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/test-drift.yml b/.github/workflows/test-drift.yml index 52ee7465..173fbc6c 100644 --- a/.github/workflows/test-drift.yml +++ b/.github/workflows/test-drift.yml @@ -67,47 +67,79 @@ jobs: if-no-files-found: warn retention-days: 30 + - name: Fail if critical drift detected + if: steps.drift.outputs.exit_code == '2' + run: exit 1 + + notify: + if: always() && github.event_name != 'pull_request' + needs: [drift, agui-schema-drift] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: - name: Check previous run status id: prev - if: always() run: | - PREV=$(gh run list --workflow="Drift Tests" --branch=main --limit=2 --json conclusion --jq '.[1].conclusion // "unknown"') + PREV=$(gh run list --workflow="Drift Tests" --repo="${{ github.repository }}" --branch=main --limit=2 --json conclusion --jq '.[1].conclusion // "unknown"') echo "conclusion=$PREV" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Notify Slack - if: always() run: | if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi PREV="${{ steps.prev.outputs.conclusion }}" - NOW="${{ job.status }}" - DRIFT="${{ steps.drift.outputs.exit_code }}" + DRIFT_RESULT="${{ needs.drift.result }}" + AGUI_RESULT="${{ needs.agui-schema-drift.result }}" - # Drift detected — always notify - if [ "$DRIFT" = "2" ]; then + HTTP_DRIFT=false + AGUI_DRIFT=false + INFRA_ERROR=false + + # Determine what happened in each job + if [ "$DRIFT_RESULT" = "failure" ]; then + HTTP_DRIFT=true + elif [ "$DRIFT_RESULT" != "success" ] && [ "$DRIFT_RESULT" != "skipped" ]; then + INFRA_ERROR=true + fi + + if [ "$AGUI_RESULT" = "failure" ]; then + AGUI_DRIFT=true + elif [ "$AGUI_RESULT" != "success" ] && [ "$AGUI_RESULT" != "skipped" ]; then + INFRA_ERROR=true + fi + + RUN_URL="" + + # Both types of drift + if [ "$HTTP_DRIFT" = "true" ] && [ "$AGUI_DRIFT" = "true" ]; then + EMOJI="🚨" + MSG="*Drift detected* in aimock — HTTP API drift + AG-UI schema drift. ${RUN_URL}" + # HTTP API drift only + elif [ "$HTTP_DRIFT" = "true" ]; then EMOJI="🚨" - MSG="*Drift detected* in aimock — providers changed response formats. " + MSG="*HTTP API drift detected* in aimock — providers changed response formats. ${RUN_URL}" + # AG-UI schema drift only + elif [ "$AGUI_DRIFT" = "true" ]; then + EMOJI="🚨" + MSG="*AG-UI schema drift detected* in aimock — canonical ag-ui types changed. ${RUN_URL}" # Infra failure — always notify - elif [ "$NOW" != "success" ]; then + elif [ "$INFRA_ERROR" = "true" ]; then EMOJI="❌" - MSG="*Drift tests failed* (infra error). " + MSG="*Drift tests failed* (infra error). ${RUN_URL}" # Recovery: previous was bad, now good — notify once elif [ "$PREV" = "failure" ]; then EMOJI="✅" - MSG="Drift tests passing again — all providers match." + MSG="Drift tests passing again — all providers and AG-UI schema match." # Good → good — stay quiet else exit 0 fi - curl -s -X POST "$SLACK_WEBHOOK" \ + PAYLOAD=$(jq -n --arg text "${EMOJI} ${MSG}" '{text: $text}') + curl -sf -X POST "$SLACK_WEBHOOK" \ -H "Content-Type: application/json" \ - -d "{\"text\": \"${EMOJI} ${MSG}\"}" + -d "$PAYLOAD" env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - - - name: Fail if critical drift detected - if: steps.drift.outputs.exit_code == '2' - run: exit 1 diff --git a/package.json b/package.json index ee6aad6c..465eb835 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,16 @@ "default": "./dist/vector-stub.cjs" } }, + "./agui": { + "import": { + "types": "./dist/agui-stub.d.ts", + "default": "./dist/agui-stub.js" + }, + "require": { + "types": "./dist/agui-stub.d.cts", + "default": "./dist/agui-stub.cjs" + } + }, "./vitest": { "import": { "types": "./dist/vitest.d.ts", @@ -124,6 +134,10 @@ "./dist/vector-stub.d.ts", "./dist/vector-stub.d.cts" ], + "agui": [ + "./dist/agui-stub.d.ts", + "./dist/agui-stub.d.cts" + ], "vitest": [ "./dist/vitest.d.ts", "./dist/vitest.d.cts" diff --git a/scripts/drift-report-collector.ts b/scripts/drift-report-collector.ts index 8d2e8c98..fca97435 100644 --- a/scripts/drift-report-collector.ts +++ b/scripts/drift-report-collector.ts @@ -17,7 +17,7 @@ */ import { execSync } from "node:child_process"; -import { writeFileSync } from "node:fs"; +import { existsSync, statSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import type { DriftEntry, DriftReport, DriftSeverity, ParsedDiff } from "./drift-types.js"; @@ -146,6 +146,13 @@ const PROVIDER_MAP: Record = { const SDK_SHAPES_FILE = "src/__tests__/drift/sdk-shapes.ts"; +// --------------------------------------------------------------------------- +// AG-UI schema drift constants +// --------------------------------------------------------------------------- + +const AGUI_TYPES_FILE = "src/agui-types.ts"; +const AGUI_DRIFT_TEST = "src/__tests__/drift/agui-schema.drift.ts"; + // --------------------------------------------------------------------------- // Parse the formatted drift report text from a vitest failure message // --------------------------------------------------------------------------- @@ -379,6 +386,189 @@ function collectDriftEntries(results: VitestJsonResult): DriftEntry[] { return entries; } +// --------------------------------------------------------------------------- +// AG-UI schema drift: run and collect +// --------------------------------------------------------------------------- + +/** + * Attempt to run the AG-UI schema drift test and collect results. + * + * The ag-ui schema drift test requires the canonical ag-ui repo to be + * cloned at `../ag-ui` relative to the project root. If it isn't present, + * we clone it (shallow, depth=1) before running the test. + * + * Returns drift entries in the same DriftEntry format as HTTP API drift, + * or an empty array if the canonical repo is unavailable or tests pass. + */ +function ensureAgUiRepo(): boolean { + const agUiPath = resolve("..", "ag-ui"); + try { + if (existsSync(agUiPath) && statSync(agUiPath).isDirectory()) { + return true; + } + } catch (statErr: unknown) { + const msg = statErr instanceof Error ? statErr.message : String(statErr); + console.warn(`Could not stat AG-UI repo path: ${msg}`); + } + { + // Not present — try to clone + console.log("AG-UI canonical repo not found. Cloning..."); + try { + execSync("git clone --depth 1 https://github.com/ag-ui-protocol/ag-ui.git ../ag-ui", { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + timeout: 60_000, + }); + console.log("AG-UI repo cloned successfully."); + return true; + } catch (cloneErr: unknown) { + const msg = cloneErr instanceof Error ? cloneErr.message : String(cloneErr); + console.warn(`Could not clone AG-UI repo: ${msg}`); + console.warn("AG-UI schema drift detection will be skipped."); + return false; + } + } +} + +function runAgUiDriftTests(): VitestJsonResult | null { + if (!ensureAgUiRepo()) return null; + + try { + const stdout = execSync( + `npx vitest run ${AGUI_DRIFT_TEST} --config vitest.config.drift.ts --reporter=json`, + { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + maxBuffer: 50 * 1024 * 1024, + }, + ); + const result = parseVitestOutput(stdout, "AG-UI drift JSON parse of successful run failed"); + if (result) return result; + // Tests passed, no failures — return empty result + return { testResults: [] }; + } catch (err: unknown) { + if (hasStdout(err)) { + const result = parseVitestOutput(err.stdout, "AG-UI drift JSON parse of failed run"); + if (result) return result; + } + const msg = err instanceof Error ? err.message : String(err); + console.warn(`AG-UI schema drift tests failed to run: ${msg}`); + return null; + } +} + +/** + * Parse AG-UI schema drift failures into DriftEntry objects. + * + * The ag-ui schema drift test produces failure messages like: + * - `[CRITICAL] Event type "X" exists in canonical but missing from aimock` + * - `[CRITICAL] EventType: field "fieldName" (...) exists in canonical but missing from aimock` + * - `[WARNING] EventType: field "fieldName" optionality mismatch` + * + * These are converted to DriftEntry objects that point at `src/agui-types.ts` + * as the builder file (the file that needs fixing). + */ +function collectAgUiDriftEntries(results: VitestJsonResult): DriftEntry[] { + const entries: DriftEntry[] = []; + + // Accumulate all diffs across assertions into a single entry per scenario + const missingTypesDiffs: ParsedDiff[] = []; + const fieldDriftDiffs: ParsedDiff[] = []; + + for (const file of results.testResults) { + for (const assertion of file.assertionResults) { + if (assertion.status !== "failed") continue; + if (assertion.failureMessages.length === 0) continue; + + const fullMessage = assertion.failureMessages.join("\n"); + const testName = assertion.title || assertion.ancestorTitles.join(" > "); + + // Track whether THIS assertion extracted any structured data + const missingTypesBefore = missingTypesDiffs.length; + const fieldDriftBefore = fieldDriftDiffs.length; + + // Parse missing event types: [CRITICAL] Event type "X" exists in canonical... + const missingTypePattern = + /\[CRITICAL\]\s*Event type "(\w+)" exists in canonical @ag-ui\/core but is missing from aimock/g; + let match: RegExpExecArray | null; + while ((match = missingTypePattern.exec(fullMessage)) !== null) { + missingTypesDiffs.push({ + severity: "critical", + issue: `AG-UI event type missing from aimock AGUIEventType union`, + path: `AGUIEventType.${match[1]}`, + expected: match[1], + real: match[1], + mock: "", + }); + } + + // Parse missing fields: [CRITICAL] EventType: field "fieldName" (...) exists in canonical but missing + const missingFieldPattern = + /\[CRITICAL\]\s*(\w+):\s*field "(\w+)"\s*\(([^)]*)\)\s*exists in canonical but missing from aimock/g; + while ((match = missingFieldPattern.exec(fullMessage)) !== null) { + fieldDriftDiffs.push({ + severity: "critical", + issue: `AG-UI event field missing from aimock interface`, + path: `AGUI${match[1]}Event.${match[2]}`, + expected: `${match[2]} (${match[3]})`, + real: `${match[2]} (${match[3]})`, + mock: "", + }); + } + + // TODO: Optionality drift is not currently collected because the drift + // test only emits optionality mismatches via console.warn(), not via + // failing assertions. If the drift test is updated to include + // optionality in assertion failure messages, add parsing here. + + // If THIS assertion did not extract any structured data, try a generic fallback + const thisAssertionExtracted = + missingTypesDiffs.length > missingTypesBefore || fieldDriftDiffs.length > fieldDriftBefore; + if ( + !thisAssertionExtracted && + (fullMessage.includes("Missing event types") || + fullMessage.includes("Critical field drift")) + ) { + // Generic critical failure from the ag-ui schema drift test + missingTypesDiffs.push({ + severity: "critical", + issue: `AG-UI schema drift detected in test: ${testName}`, + path: "AGUIEventType", + expected: "(see test output)", + real: "(see test output)", + mock: "(see test output)", + }); + } + } + } + + if (missingTypesDiffs.length > 0) { + entries.push({ + provider: "AG-UI", + scenario: "missing event types", + builderFile: AGUI_TYPES_FILE, + builderFunctions: ["AGUIEventType"], + typesFile: AGUI_TYPES_FILE, + sdkShapesFile: AGUI_DRIFT_TEST, + diffs: missingTypesDiffs, + }); + } + + if (fieldDriftDiffs.length > 0) { + entries.push({ + provider: "AG-UI", + scenario: "event field shapes", + builderFile: AGUI_TYPES_FILE, + builderFunctions: ["AGUI*Event interfaces"], + typesFile: AGUI_TYPES_FILE, + sdkShapesFile: AGUI_DRIFT_TEST, + diffs: fieldDriftDiffs, + }); + } + + return entries; +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -390,11 +580,25 @@ function main(): void { outIndex !== -1 && args[outIndex + 1] ? args[outIndex + 1] : "drift-report.json", ); - console.log("Running drift tests..."); - const results = runDriftTests(); + // Collect HTTP API drift entries + console.log("Running HTTP API drift tests..."); + const httpResults = runDriftTests(); + console.log("Collecting HTTP API drift entries..."); + const httpEntries = collectDriftEntries(httpResults); + + // Collect AG-UI schema drift entries + console.log("Running AG-UI schema drift tests..."); + const agUiResults = runAgUiDriftTests(); + const agUiSkipped = agUiResults === null; + let agUiEntries: DriftEntry[] = []; + if (agUiResults) { + console.log("Collecting AG-UI schema drift entries..."); + agUiEntries = collectAgUiDriftEntries(agUiResults); + } else { + console.warn("WARNING: AG-UI schema drift tests could not run — results will be incomplete."); + } - console.log("Collecting drift entries..."); - const entries = collectDriftEntries(results); + const entries = [...httpEntries, ...agUiEntries]; const report: DriftReport = { timestamp: new Date().toISOString(), @@ -409,7 +613,13 @@ function main(): void { process.exit(1); } console.log(`Drift report written to ${outPath}`); - console.log(` Entries: ${entries.length}`); + console.log(` HTTP API entries: ${httpEntries.length}`); + if (agUiSkipped) { + console.log(` AG-UI schema entries: SKIPPED (could not run tests)`); + } else { + console.log(` AG-UI schema entries: ${agUiEntries.length}`); + } + console.log(` Total entries: ${entries.length}`); const criticalCount = entries.reduce( (sum, e) => sum + e.diffs.filter((d) => d.severity === "critical").length, @@ -422,6 +632,11 @@ function main(): void { process.exit(2); } + if (agUiSkipped) { + console.warn("Exiting with code 1 (AG-UI drift detection was skipped — infra failure)."); + process.exit(1); + } + console.log("No critical diffs found."); } diff --git a/scripts/fix-drift.ts b/scripts/fix-drift.ts index 950a86cf..4d47a621 100644 --- a/scripts/fix-drift.ts +++ b/scripts/fix-drift.ts @@ -14,7 +14,8 @@ * Exit codes: * 0 — success (or issue created successfully in --create-issue mode) * 1 — failure - * 2 — no source files changed (--create-pr mode, nothing to commit) + * 2 — critical drift found (drift collector) + * 4 — no source files changed (--create-pr mode, nothing to commit) * 3 — unhandled error (e.g. bad arguments, missing report, git/gh command failure) * 124 — Claude Code timed out (default mode) * In default mode, the exit code is passed through from Claude Code. @@ -60,6 +61,8 @@ export const BUILDER_TO_SKILL_SECTION: Record = { "src/ws-gemini-live.ts": "Gemini Live WebSocket", "src/helpers.ts": "OpenAI Chat Completions", "src/gemini-interactions.ts": "Gemini Interactions", + "src/agui-types.ts": "AG-UI Events", + "src/agui-handler.ts": "AG-UI Events", }; // --------------------------------------------------------------------------- @@ -268,8 +271,9 @@ export function buildPrompt(report: DriftReport): string { for (const diff of entry.diffs) { lines.push(` - [${diff.severity}] ${diff.issue}`); lines.push(` Path: ${diff.path}`); + lines.push(` SDK type: ${diff.expected}`); lines.push(` Real API: ${diff.real}`); - lines.push(` Mock: ${diff.mock}`); + lines.push(` Mock: ${diff.mock}`); } lines.push(""); } @@ -282,6 +286,25 @@ export function buildPrompt(report: DriftReport): string { lines.push("Only update the Response Types and API Endpoints sections that correspond to the"); lines.push("changed builders. Do not rewrite unrelated sections."); lines.push(""); + // Add AG-UI specific guidance if any AG-UI entries exist + const hasAgUiDrift = report.entries.some((e) => e.provider === "AG-UI"); + if (hasAgUiDrift) { + lines.push("## AG-UI Schema Drift"); + lines.push(""); + lines.push("For AG-UI drift entries, the fix target is `src/agui-types.ts`."); + lines.push( + "Compare against the canonical source at `../ag-ui/sdks/typescript/packages/core/src/events.ts`.", + ); + lines.push(""); + lines.push("- Add missing event types to the `AGUIEventType` union type."); + lines.push("- Add missing fields to the corresponding `AGUI*Event` interfaces."); + lines.push("- Fix optionality mismatches (required vs optional) to match canonical schemas."); + lines.push( + "- Also update any builder functions in `src/agui-handler.ts` that construct these events.", + ); + lines.push(""); + } + lines.push("## After all fixes"); lines.push(""); lines.push("1. Run the full test suite: pnpm test"); @@ -428,10 +451,10 @@ function syncDescriptionFromReadme(pkg: { description?: string; [key: string]: u if ( !trimmed || trimmed.startsWith("#") || - trimmed.startsWith("[") || - trimmed.startsWith("http") || + trimmed.startsWith("[![") || trimmed.startsWith("![") || - trimmed.startsWith("[![") + trimmed.startsWith("[") || + trimmed.startsWith("http") ) { continue; } @@ -442,8 +465,10 @@ function syncDescriptionFromReadme(pkg: { description?: string; [key: string]: u } break; } - } catch { - // README not found — skip + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { + console.warn("Could not sync description from README:", err); + } } } @@ -466,9 +491,10 @@ export function addChangelogEntry(report: DriftReport, version: string): void { "", ].join("\n"); - // Insert after the first line (the title) - const titleLine = "# @copilotkit/aimock\n"; - if (existing.startsWith(titleLine)) { + // Insert after the title line (any line starting with "# ") + const titleMatch = existing.match(/^# .+\n/); + if (titleMatch) { + const titleLine = titleMatch[0]; const rest = existing.slice(titleLine.length); writeFileSync(changelogPath, titleLine + "\n" + newEntry + rest, "utf-8"); } else { @@ -591,7 +617,7 @@ function createPr(report: DriftReport): void { "ERROR: No source files changed. Claude Code may not have made any fixes, " + "or all changes were reverted during verification. Aborting PR creation.", ); - process.exit(2); + process.exit(4); // no source files changed (distinct from exit 2 = critical drift) } if (builderFiles.length > 0) { @@ -613,15 +639,20 @@ function createPr(report: DriftReport): void { ]); } - const newVersion = patchBumpVersion(); - console.log(`Bumped version to ${newVersion}`); - - addChangelogEntry(report, newVersion); - console.log("Added CHANGELOG.md entry"); - - // Always commit version bump + changelog - execFileSafe("git", ["add", "package.json", "CHANGELOG.md"]); - execFileSafe("git", ["commit", "-m", `chore: bump version to ${newVersion}`, "--allow-empty"]); + try { + const newVersion = patchBumpVersion(); + console.log(`Bumped version to ${newVersion}`); + + addChangelogEntry(report, newVersion); + console.log("Added CHANGELOG.md entry"); + + // Always commit version bump + changelog + execFileSafe("git", ["add", "package.json", "CHANGELOG.md"]); + execFileSafe("git", ["commit", "-m", `chore: bump version to ${newVersion}`, "--allow-empty"]); + } catch (err) { + console.warn("Version bump failed, skipping:", err); + // Continue with PR creation without version bump + } // Catch any remaining files const remaining = getChangedFiles(); diff --git a/src/__tests__/drift/agui-schema.drift.ts b/src/__tests__/drift/agui-schema.drift.ts index fd64f965..cc3fee79 100644 --- a/src/__tests__/drift/agui-schema.drift.ts +++ b/src/__tests__/drift/agui-schema.drift.ts @@ -48,11 +48,12 @@ function parseCanonicalEventTypes(source: string): string[] { * Extract field definitions from a Zod `.extend({...})` block body. */ function extractExtendFields(extendBody: string): FieldInfo[] { + // Strip comment lines so they don't match as field definitions + const cleanBody = extendBody.replace(/^\s*\/\/.*$/gm, ""); const fields: FieldInfo[] = []; - for (const fieldMatch of extendBody.matchAll(/(\w+)\s*:\s*([^\n,]+)/g)) { + for (const fieldMatch of cleanBody.matchAll(/(\w+)\s*:\s*(.+)/g)) { const fieldName = fieldMatch[1]; - const fieldDef = fieldMatch[2].trim(); - if (fieldDef.startsWith("//")) continue; + const fieldDef = fieldMatch[2].replace(/,\s*$/, "").trim(); const optional = fieldDef.includes(".optional()") || fieldDef.includes(".default("); fields.push({ name: fieldName, optional }); } @@ -70,15 +71,30 @@ function extractExtendFields(extendBody: string): FieldInfo[] { * TextMessageContentEventSchema.omit({...}).extend({...}) * where ThinkingTextMessageContentEventSchema inherits delta from TextMessageContent. */ + +/** + * Parse base fields from `BaseEventSchema = z.object({...})` in canonical source. + * Falls back to hardcoded defaults if parsing fails. + */ +function parseCanonicalBaseFields(source: string): FieldInfo[] { + const baseMatch = source.match( + /export const BaseEventSchema\s*=\s*z\s*\.\s*object\(\{([\s\S]*?)\}\)/, + ); + if (!baseMatch) { + return [ + { name: "type", optional: false }, + { name: "timestamp", optional: true }, + { name: "rawEvent", optional: true }, + ]; + } + return extractExtendFields(baseMatch[1]); +} + function parseCanonicalSchemas(source: string): Map { const schemas = new Map(); - // Base event fields (always inherited) - const baseFields: FieldInfo[] = [ - { name: "type", optional: false }, - { name: "timestamp", optional: true }, - { name: "rawEvent", optional: true }, - ]; + // Parse base event fields dynamically from BaseEventSchema + const baseFields = parseCanonicalBaseFields(source); // Pass 1: collect raw schema definitions keyed by schema name interface RawSchema { @@ -123,6 +139,14 @@ function parseCanonicalSchemas(source: string): Map { fieldsBySchemaName.set(schemaName, ownFields); } + // Recursive parent field resolver for multi-level inheritance chains + function resolveParentFields(schemaName: string): FieldInfo[] { + const entry = rawSchemas.get(schemaName); + if (!entry) return []; + const parentFields = entry.parentSchemaName ? resolveParentFields(entry.parentSchemaName) : []; + return [...parentFields, ...(fieldsBySchemaName.get(schemaName) || [])]; + } + // Pass 2: resolve full field sets with parent inheritance for (const [, raw] of rawSchemas) { const fields = new Map(); @@ -132,13 +156,10 @@ function parseCanonicalSchemas(source: string): Map { fields.set(f.name, { ...f }); } - // If there's a parent schema (not BaseEventSchema), inherit its extend fields + // Resolve full parent chain (handles multi-level inheritance) if (raw.parentSchemaName) { - const parentFields = fieldsBySchemaName.get(raw.parentSchemaName); - if (parentFields) { - for (const f of parentFields) { - fields.set(f.name, { ...f }); - } + for (const f of resolveParentFields(raw.parentSchemaName)) { + fields.set(f.name, { ...f }); } } @@ -179,9 +200,34 @@ function parseAimockEventTypes(source: string): string[] { return members; } +/** + * Parse base fields from `AGUIBaseEvent` interface in aimock source. + * Falls back to hardcoded defaults if parsing fails. + */ +function parseAimockBaseFields(source: string): FieldInfo[] { + const baseMatch = source.match(/export interface AGUIBaseEvent\s*\{([\s\S]*?)\}/); + if (!baseMatch) { + return [ + { name: "type", optional: false }, + { name: "timestamp", optional: true }, + { name: "rawEvent", optional: true }, + ]; + } + const fields: FieldInfo[] = []; + for (const fieldMatch of baseMatch[1].matchAll(/(\w+)(\??)\s*:\s*([^;]+);/g)) { + const fieldName = fieldMatch[1]; + const optional = fieldMatch[2] === "?"; + fields.push({ name: fieldName, optional }); + } + return fields; +} + function parseAimockInterfaces(source: string): Map { const interfaces = new Map(); + // Parse base fields dynamically from AGUIBaseEvent interface + const baseFields = parseAimockBaseFields(source); + // Match interface blocks const interfacePattern = /export interface AGUI(\w+Event)\s+extends\s+\w+\s*\{([\s\S]*?)\}/g; @@ -193,12 +239,8 @@ function parseAimockInterfaces(source: string): Map { if (!typeMatch) continue; const eventType = typeMatch[1]; - // Start with base fields (all extend AGUIBaseEvent) - const fields: FieldInfo[] = [ - { name: "type", optional: false }, - { name: "timestamp", optional: true }, - { name: "rawEvent", optional: true }, - ]; + // Start with dynamically-parsed base fields + const fields: FieldInfo[] = baseFields.map((f) => ({ ...f })); // Parse fields from the interface body for (const fieldMatch of body.matchAll(/(\w+)(\??)\s*:\s*([^;]+);/g)) { @@ -235,7 +277,7 @@ interface DriftItem { const canonicalExists = fs.existsSync(CANONICAL_EVENTS_PATH); const aimockExists = fs.existsSync(AIMOCK_TYPES_PATH); -describe.skipIf(!canonicalExists)("AG-UI schema drift", () => { +describe.skipIf(!canonicalExists || !aimockExists)("AG-UI schema drift", () => { let canonicalSource: string; let aimockSource: string; let canonicalTypes: string[]; diff --git a/src/__tests__/fix-drift.test.ts b/src/__tests__/fix-drift.test.ts index e4a75e6f..5cf3c157 100644 --- a/src/__tests__/fix-drift.test.ts +++ b/src/__tests__/fix-drift.test.ts @@ -398,8 +398,9 @@ describe("buildPrompt", () => { const prompt = buildPrompt(report); expect(prompt).toContain("Path: body.model"); + expect(prompt).toContain("SDK type: string"); expect(prompt).toContain('Real API: "gpt-4o"'); - expect(prompt).toContain('Mock: "gpt-4"'); + expect(prompt).toContain('Mock: "gpt-4"'); }); }); @@ -794,6 +795,9 @@ describe("BUILDER_TO_SKILL_SECTION", () => { "src/ws-realtime.ts", "src/ws-responses.ts", "src/ws-gemini-live.ts", + "src/helpers.ts", + "src/agui-types.ts", + "src/agui-handler.ts", ]; for (const file of expectedFiles) { expect(BUILDER_TO_SKILL_SECTION).toHaveProperty(file); diff --git a/src/agui-handler.ts b/src/agui-handler.ts index 84e9e68d..5f82223d 100644 --- a/src/agui-handler.ts +++ b/src/agui-handler.ts @@ -13,6 +13,7 @@ import type { AGUIMessage, AGUIRunStartedEvent, AGUIRunFinishedEvent, + AGUIRunFinishedOutcome, AGUIRunErrorEvent, AGUITextMessageStartEvent, AGUITextMessageContentEvent, @@ -21,6 +22,7 @@ import type { AGUIToolCallStartEvent, AGUIToolCallArgsEvent, AGUIToolCallEndEvent, + AGUIToolCallChunkEvent, AGUIToolCallResultEvent, AGUIStateSnapshotEvent, AGUIStateDeltaEvent, @@ -31,8 +33,14 @@ import type { AGUIReasoningMessageStartEvent, AGUIReasoningMessageContentEvent, AGUIReasoningMessageEndEvent, + AGUIReasoningMessageChunkEvent, AGUIReasoningEndEvent, + AGUIReasoningEncryptedValueEvent, + AGUIReasoningEncryptedValueSubtype, AGUIActivitySnapshotEvent, + AGUIActivityDeltaEvent, + AGUIRawEvent, + AGUICustomEvent, } from "./agui-types.js"; // ─── Matching functions ────────────────────────────────────────────────────── @@ -61,6 +69,7 @@ export function matchesFixture(input: AGUIRunAgentInput, match: AGUIFixtureMatch if (typeof match.message === "string") { if (!text.includes(match.message)) return false; } else { + match.message.lastIndex = 0; if (!match.message.test(text)) return false; } } @@ -121,11 +130,16 @@ function makeRunStarted(opts?: AGUIBuildOpts): AGUIRunStartedEvent { }; } -function makeRunFinished(started: AGUIRunStartedEvent): AGUIRunFinishedEvent { +function makeRunFinished( + started: AGUIRunStartedEvent, + finishOpts?: { outcome?: AGUIRunFinishedOutcome; result?: unknown }, +): AGUIRunFinishedEvent { return { type: "RUN_FINISHED", threadId: started.threadId, runId: started.runId, + ...(finishOpts?.result !== undefined ? { result: finishOpts.result } : {}), + ...(finishOpts?.outcome !== undefined ? { outcome: finishOpts.outcome } : {}), }; } @@ -405,7 +419,136 @@ export function buildCompositeResponse( } } - return [started, ...inner, makeRunFinished(started)]; + const hasError = inner.some((e) => e.type === "RUN_ERROR"); + return [started, ...inner, ...(hasError ? [] : [makeRunFinished(started)])]; +} + +// ─── Convenience event builders ───────────────────────────────────────────── + +/** + * Build an activity delta response (JSON Patch on an activity). + * [RUN_STARTED, ACTIVITY_DELTA, RUN_FINISHED] + */ +export function buildActivityDelta( + messageId: string, + activityType: string, + patch: unknown[], + opts?: AGUIBuildOpts, +): AGUIEvent[] { + const started = makeRunStarted(opts); + return [ + started, + { + type: "ACTIVITY_DELTA", + messageId, + activityType, + patch, + } as AGUIActivityDeltaEvent, + makeRunFinished(started), + ]; +} + +/** + * Build a tool call chunk response (single chunk, no start/end envelope). + * [RUN_STARTED, TOOL_CALL_CHUNK, RUN_FINISHED] + */ +export function buildToolCallChunk( + delta: string, + opts?: AGUIBuildOpts & { + toolCallId?: string; + toolCallName?: string; + parentMessageId?: string; + }, +): AGUIEvent[] { + const started = makeRunStarted(opts); + return [ + started, + { + type: "TOOL_CALL_CHUNK", + ...(opts?.toolCallId !== undefined ? { toolCallId: opts.toolCallId } : {}), + ...(opts?.toolCallName !== undefined ? { toolCallName: opts.toolCallName } : {}), + ...(opts?.parentMessageId !== undefined ? { parentMessageId: opts.parentMessageId } : {}), + delta, + } as AGUIToolCallChunkEvent, + makeRunFinished(started), + ]; +} + +/** + * Build a raw event response. + * [RUN_STARTED, RAW, RUN_FINISHED] + */ +export function buildRawEvent(event: unknown, source?: string, opts?: AGUIBuildOpts): AGUIEvent[] { + const started = makeRunStarted(opts); + return [ + started, + { + type: "RAW", + event, + ...(source !== undefined ? { source } : {}), + } as AGUIRawEvent, + makeRunFinished(started), + ]; +} + +/** + * Build a custom event response. + * [RUN_STARTED, CUSTOM, RUN_FINISHED] + */ +export function buildCustomEvent(name: string, value: unknown, opts?: AGUIBuildOpts): AGUIEvent[] { + const started = makeRunStarted(opts); + return [ + started, + { + type: "CUSTOM", + name, + value, + } as AGUICustomEvent, + makeRunFinished(started), + ]; +} + +/** + * Build a reasoning message chunk response (single chunk, no start/end envelope). + * [RUN_STARTED, REASONING_MESSAGE_CHUNK, RUN_FINISHED] + */ +export function buildReasoningChunk( + delta: string, + opts?: AGUIBuildOpts & { messageId?: string }, +): AGUIEvent[] { + const started = makeRunStarted(opts); + return [ + started, + { + type: "REASONING_MESSAGE_CHUNK", + ...(opts?.messageId !== undefined ? { messageId: opts.messageId } : {}), + delta, + } as AGUIReasoningMessageChunkEvent, + makeRunFinished(started), + ]; +} + +/** + * Build a reasoning encrypted value event response. + * [RUN_STARTED, REASONING_ENCRYPTED_VALUE, RUN_FINISHED] + */ +export function buildReasoningEncryptedValue( + subtype: AGUIReasoningEncryptedValueSubtype, + entityId: string, + encryptedValue: string, + opts?: AGUIBuildOpts, +): AGUIEvent[] { + const started = makeRunStarted(opts); + return [ + started, + { + type: "REASONING_ENCRYPTED_VALUE", + subtype, + entityId, + encryptedValue, + } as AGUIReasoningEncryptedValueEvent, + makeRunFinished(started), + ]; } // ─── SSE writer ────────────────────────────────────────────────────────────── @@ -432,11 +575,16 @@ export async function writeAGUIEventStream( if (opts?.signal?.aborted) break; if (res.socket?.destroyed) break; - const stamped = { ...event, timestamp: Date.now() }; + const stamped = { ...event, timestamp: event.timestamp ?? Date.now() }; try { res.write(`data: ${JSON.stringify(stamped)}\n\n`); - } catch { - break; // client disconnected or stream error — stop writing + } catch (err) { + if (err instanceof TypeError || err instanceof RangeError) { + console.warn("AG-UI SSE write failed (serialization):", (err as Error).message); + } else if (err instanceof Error) { + console.warn("AG-UI SSE write failed:", err.message); + } + break; } if (delayMs > 0) { diff --git a/src/agui-mock.ts b/src/agui-mock.ts index cbdf6c8b..6ca1d9d1 100644 --- a/src/agui-mock.ts +++ b/src/agui-mock.ts @@ -143,9 +143,10 @@ export class AGUIMock implements Mountable { let input: AGUIRunAgentInput; try { input = JSON.parse(body) as AGUIRunAgentInput; - } catch { + } catch (err) { res.writeHead(400, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Invalid JSON body" })); + const detail = err instanceof Error ? err.message : "unknown parse error"; + res.end(JSON.stringify({ error: `Invalid JSON body: ${detail}` })); this.journalRequest(req, pathname, 400); return true; } @@ -160,7 +161,7 @@ export class AGUIMock implements Mountable { // No match — if recording is enabled, proxy to upstream if (this.recordConfig) { - const proxied = await proxyAndRecordAGUI( + const result = await proxyAndRecordAGUI( req, res, input, @@ -168,8 +169,8 @@ export class AGUIMock implements Mountable { this.recordConfig, this.logger, ); - if (proxied) { - this.journalRequest(req, pathname, 200); + if (result !== false) { + this.journalRequest(req, pathname, result); return true; } } diff --git a/src/agui-recorder.ts b/src/agui-recorder.ts index 12c0d2d7..1e2df0f5 100644 --- a/src/agui-recorder.ts +++ b/src/agui-recorder.ts @@ -12,7 +12,8 @@ import type { Logger } from "./logger.js"; * SSE event stream as a fixture on disk and in memory, and relay the * response back to the original client in real time. * - * Returns `true` if the request was proxied, `false` if no upstream is configured. + * Returns the HTTP status code written to the client if the request was proxied, + * or `false` if no upstream is configured. */ export async function proxyAndRecordAGUI( req: http.IncomingMessage, @@ -21,7 +22,7 @@ export async function proxyAndRecordAGUI( fixtures: AGUIFixture[], config: AGUIRecordConfig, logger: Logger, -): Promise { +): Promise { if (!config.upstream) { logger.warn("No upstream URL configured for AG-UI recording — cannot proxy"); return false; @@ -34,7 +35,7 @@ export async function proxyAndRecordAGUI( logger.error(`Invalid upstream AG-UI URL: ${config.upstream}`); res.writeHead(502, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Invalid upstream AG-UI URL" })); - return true; + return 502; } logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`); @@ -58,8 +59,9 @@ export async function proxyAndRecordAGUI( const requestBody = JSON.stringify(input); + let status: number; try { - await teeUpstreamStream( + status = await teeUpstreamStream( target, forwardHeaders, requestBody, @@ -76,9 +78,10 @@ export async function proxyAndRecordAGUI( res.writeHead(502, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Upstream AG-UI agent unreachable" })); } + status = 502; } - return true; + return status; } // --------------------------------------------------------------------------- @@ -94,7 +97,7 @@ function teeUpstreamStream( fixtures: AGUIFixture[], config: AGUIRecordConfig, logger: Logger, -): Promise { +): Promise { return new Promise((resolve, reject) => { const transport = target.protocol === "https:" ? https : http; const UPSTREAM_TIMEOUT_MS = 30_000; @@ -110,9 +113,11 @@ function teeUpstreamStream( }, }, (upstreamRes) => { + const upstreamStatus = upstreamRes.statusCode ?? 200; + // Set SSE headers on the client response if (!clientRes.headersSent) { - clientRes.writeHead(upstreamRes.statusCode ?? 200, { + clientRes.writeHead(upstreamStatus, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", @@ -151,19 +156,33 @@ function teeUpstreamStream( // Build fixture const message = extractLastUserMessage(input); - if (!message) { - logger.warn("Recorded AG-UI fixture has no message match — it will match ALL requests"); - } const fixture: AGUIFixture = { - match: { message: message || undefined }, + match: message + ? { message } + : { + predicate: (inp: AGUIRunAgentInput) => + !inp.messages?.length || !inp.messages.some((m) => m.role === "user"), + }, events, }; + if (!message) { + logger.warn( + "Recorded AG-UI fixture has no user message — will use __NO_USER_MESSAGE__ sentinel on disk", + ); + } if (!config.proxyOnly) { // Register in memory first (always available even if disk write fails) fixtures.push(fixture); - // Write to disk + // Write to disk — predicate functions are not serializable, + // so replace with a sentinel string that won't match real user messages. + const serializableFixture = { + match: fixture.match.predicate ? { message: "__NO_USER_MESSAGE__" } : fixture.match, + events: fixture.events, + ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}), + }; + const fixturePath = config.fixturePath ?? "./fixtures/agui-recorded"; const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`; @@ -173,11 +192,7 @@ function teeUpstreamStream( fs.mkdirSync(fixturePath, { recursive: true }); fs.writeFileSync( filepath, - JSON.stringify( - { fixtures: [{ match: fixture.match, events: fixture.events }] }, - null, - 2, - ), + JSON.stringify({ fixtures: [serializableFixture] }, null, 2), "utf-8", ); logger.warn(`AG-UI response recorded → ${filepath}`); @@ -191,7 +206,7 @@ function teeUpstreamStream( logger.info("Proxied AG-UI request (proxy-only mode)"); } - resolve(); + resolve(upstreamStatus); }); }, ); @@ -227,12 +242,13 @@ function parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] { for (const block of blocks) { const lines = block.split("\n"); for (const line of lines) { - if (line.startsWith("data: ")) { + if (line.startsWith("data:")) { + const payload = line.startsWith("data: ") ? line.slice(6) : line.slice(5); try { - const parsed = JSON.parse(line.slice(6)) as AGUIEvent; + const parsed = JSON.parse(payload) as AGUIEvent; events.push(parsed); } catch { - logger?.warn(`Skipping unparseable SSE data line: ${line.slice(0, 200)}`); + logger?.warn(`Skipping unparseable SSE data line: ${payload.slice(0, 200)}`); } } } diff --git a/src/agui-stub.ts b/src/agui-stub.ts index b304451b..d3b95544 100644 --- a/src/agui-stub.ts +++ b/src/agui-stub.ts @@ -37,11 +37,15 @@ export type { AGUIThinkingTextMessageEndEvent, AGUIEvent, AGUITextMessageRole, + AGUIMessageRole, AGUIReasoningEncryptedValueSubtype, AGUIRunAgentInput, AGUIToolCall, AGUIMessage, AGUIToolDefinition, + AGUIInterrupt, + AGUIResumeEntry, + AGUIRunFinishedOutcome, AGUIFixtureMatch, AGUIFixture, AGUIMockOptions, @@ -62,6 +66,12 @@ export { buildErrorResponse, buildStepWithText, buildCompositeResponse, + buildActivityDelta, + buildToolCallChunk, + buildRawEvent, + buildCustomEvent, + buildReasoningChunk, + buildReasoningEncryptedValue, writeAGUIEventStream, } from "./agui-handler.js"; export type { AGUIBuildOpts } from "./agui-handler.js"; diff --git a/src/agui-types.ts b/src/agui-types.ts index 0d1cb11b..a17ba048 100644 --- a/src/agui-types.ts +++ b/src/agui-types.ts @@ -73,6 +73,7 @@ export interface AGUIRunFinishedEvent extends AGUIBaseEvent { threadId: string; runId: string; result?: unknown; + outcome?: AGUIRunFinishedOutcome; } export interface AGUIRunErrorEvent extends AGUIBaseEvent { @@ -95,6 +96,15 @@ export interface AGUIStepFinishedEvent extends AGUIBaseEvent { export type AGUITextMessageRole = "developer" | "system" | "assistant" | "user"; +export type AGUIMessageRole = + | "developer" + | "system" + | "assistant" + | "user" + | "tool" + | "activity" + | "reasoning"; + export interface AGUITextMessageStartEvent extends AGUIBaseEvent { type: "TEXT_MESSAGE_START"; messageId: string; @@ -310,17 +320,40 @@ export type AGUIEvent = | AGUIThinkingTextMessageContentEvent | AGUIThinkingTextMessageEndEvent; +// ─── Interrupt / Resume types ──────────────────────────────────────────────── + +export interface AGUIInterrupt { + id: string; + reason: string; + message?: string; + toolCallId?: string; + responseSchema?: Record; + expiresAt?: string; + metadata?: Record; +} + +export interface AGUIResumeEntry { + interruptId: string; + status: "resolved" | "cancelled"; + payload?: unknown; +} + +export type AGUIRunFinishedOutcome = + | { type: "success" } + | { type: "interrupt"; interrupts: AGUIInterrupt[] }; + // ─── Request types ─────────────────────────────────────────────────────────── export interface AGUIRunAgentInput { - threadId?: string; - runId?: string; + threadId: string; + runId: string; parentRunId?: string; state?: unknown; messages?: AGUIMessage[]; tools?: AGUIToolDefinition[]; context?: Array<{ description: string; value: string }>; forwardedProps?: unknown; + resume?: AGUIResumeEntry[]; } export interface AGUIToolCall { @@ -331,18 +364,21 @@ export interface AGUIToolCall { } export interface AGUIMessage { - id?: string; - role: string; + id: string; + role: AGUIMessageRole; content?: string; name?: string; + encryptedValue?: string; + error?: string; toolCallId?: string; toolCalls?: AGUIToolCall[]; } export interface AGUIToolDefinition { name: string; - description?: string; + description: string; parameters?: unknown; // JSON Schema + metadata?: Record; } // ─── Fixture types ─────────────────────────────────────────────────────────── diff --git a/src/index.ts b/src/index.ts index b01dba77..6db57152 100644 --- a/src/index.ts +++ b/src/index.ts @@ -199,6 +199,7 @@ export type { AGUIMockOptions, AGUIRunAgentInput, AGUIMessage, + AGUIMessageRole, AGUIToolDefinition, AGUIToolCall, AGUIEvent, @@ -206,24 +207,58 @@ export type { AGUIFixture, AGUIFixtureMatch, AGUIRecordConfig, - // Key individual event types + // Base event + AGUIBaseEvent, + // Lifecycle events AGUIRunStartedEvent, AGUIRunFinishedEvent, AGUIRunErrorEvent, + AGUIStepStartedEvent, + AGUIStepFinishedEvent, + // Text message events AGUITextMessageStartEvent, AGUITextMessageContentEvent, AGUITextMessageEndEvent, AGUITextMessageChunkEvent, + AGUITextMessageRole, + // Tool call events AGUIToolCallStartEvent, AGUIToolCallArgsEvent, AGUIToolCallEndEvent, + AGUIToolCallChunkEvent, AGUIToolCallResultEvent, + // State events AGUIStateSnapshotEvent, AGUIStateDeltaEvent, AGUIMessagesSnapshotEvent, + // Activity events AGUIActivitySnapshotEvent, AGUIActivityDeltaEvent, + // Reasoning events + AGUIReasoningStartEvent, + AGUIReasoningMessageStartEvent, + AGUIReasoningMessageContentEvent, + AGUIReasoningMessageEndEvent, + AGUIReasoningMessageChunkEvent, + AGUIReasoningEndEvent, + AGUIReasoningEncryptedValueEvent, + AGUIReasoningEncryptedValueSubtype, + // Special events + AGUIRawEvent, + AGUICustomEvent, + // Deprecated thinking events + AGUIThinkingStartEvent, + AGUIThinkingEndEvent, + AGUIThinkingTextMessageStartEvent, + AGUIThinkingTextMessageContentEvent, + AGUIThinkingTextMessageEndEvent, + // Interrupt/Resume + AGUIInterrupt, + AGUIResumeEntry, + AGUIRunFinishedOutcome, } from "./agui-types.js"; +export type { AGUIBuildOpts } from "./agui-handler.js"; +export { matchesFixture as matchesAGUIFixture } from "./agui-handler.js"; export { buildTextResponse as buildAGUITextResponse, buildTextChunkResponse as buildAGUITextChunkResponse, @@ -236,6 +271,12 @@ export { buildErrorResponse as buildAGUIErrorResponse, buildStepWithText as buildAGUIStepWithText, buildCompositeResponse as buildAGUICompositeResponse, + buildActivityDelta as buildAGUIActivityDelta, + buildToolCallChunk as buildAGUIToolCallChunk, + buildRawEvent as buildAGUIRawEvent, + buildCustomEvent as buildAGUICustomEvent, + buildReasoningChunk as buildAGUIReasoningChunk, + buildReasoningEncryptedValue as buildAGUIReasoningEncryptedValue, extractLastUserMessage as extractAGUILastUserMessage, findFixture as findAGUIFixture, writeAGUIEventStream, diff --git a/tsdown.config.ts b/tsdown.config.ts index 712829d8..46467b42 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ "src/mcp-stub.ts", "src/a2a-stub.ts", "src/vector-stub.ts", + "src/agui-stub.ts", "src/vitest.ts", "src/jest.ts", ],