Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c3cc795
chore(core): add @standard-schema/spec dependency
AlemTuzlak May 6, 2026
18df213
feat(core): add AgentValidator interface and defaultEventValidator
AlemTuzlak May 6, 2026
2a124f4
fix(core): tighten AgentValidator guards and add fromStandardSchema t…
AlemTuzlak May 6, 2026
6cd0583
feat(core): add hand-written static types for messages, tools, run input
AlemTuzlak May 6, 2026
2d870b8
refactor(core): extract BaseMessageFields, document types-static.ts p…
AlemTuzlak May 6, 2026
0c1dbfc
feat(core): add hand-written static types for events
AlemTuzlak May 6, 2026
827d65b
fix(core): correct RunFinishedInterruptOutcome.interrupts to Interrupt[]
AlemTuzlak May 6, 2026
201eb74
feat(core): add hand-written static types for capabilities
AlemTuzlak May 6, 2026
0106c81
docs(core): port JSDoc from capabilities.ts to capabilities-static.ts
AlemTuzlak May 6, 2026
d64cc65
refactor(core): drop schema.parse from event factories, inline defaults
AlemTuzlak May 6, 2026
394bc69
refactor(core): use AGUIError in createRunFinishedInterruptEvent guard
AlemTuzlak May 6, 2026
87e75e4
refactor(client,proto): route runtime event validation through AgentV…
AlemTuzlak May 6, 2026
ca58179
refactor(client,proto): include path in validator error messages, doc…
AlemTuzlak May 6, 2026
908f9d5
refactor(core)!: remove zod schemas, expose only types and validator …
AlemTuzlak May 6, 2026
e3dffed
test(core): assert defaultEventValidator preserves forward-compat tol…
AlemTuzlak May 6, 2026
83510a4
docs(core): update stale comments in *-static.ts now that zod is removed
AlemTuzlak May 6, 2026
bf99721
chore(core)!: remove zod runtime dependency
AlemTuzlak May 6, 2026
a065541
chore: migrate external schema consumers off @ag-ui/core schemas
AlemTuzlak May 6, 2026
21dd0e9
docs(core): update event-validation guidance to reflect AgentValidato…
AlemTuzlak May 6, 2026
b715285
chore(core): bump to 0.1.0 with zod removal changelog entry
AlemTuzlak May 6, 2026
1006a5f
test(core): tighten validator.test.ts casts to satisfy strict tsc
AlemTuzlak May 6, 2026
53d7de4
refactor(core): collapse *-static.ts scaffolding back into surface files
AlemTuzlak May 6, 2026
b243a15
refactor(core): restore @ag-ui/core/schemas subpath; drop defaultEven…
AlemTuzlak May 6, 2026
2618efe
chore(core): tighten zod peer range to ^3.24.0 and add types conditions
AlemTuzlak May 6, 2026
7c7d0a7
test(core): restore schema tests, redirected to @ag-ui/core/schemas s…
AlemTuzlak May 6, 2026
cd1d2aa
chore(core): widen zod peer range to ^3.24.0 || ^4.0.0
AlemTuzlak May 6, 2026
95e7b24
refactor: revert client/proto to EventSchemas.parse, drop AgentValidator
AlemTuzlak May 6, 2026
3b4a06f
docs: 0.1.0 migration guide + jscodeshift codemod for *Schema imports
AlemTuzlak May 6, 2026
4988162
fix(codemods): use jscodeshift paths() API for the schemas merge site
AlemTuzlak May 6, 2026
07f33b3
fix(core): resolve typecheck errors in schema-type-equality and modul…
AlemTuzlak May 6, 2026
320496f
fix(codemod,packages): address load-bearing CR findings
AlemTuzlak May 6, 2026
1026d89
fix(codemod): preserve aliases and separate type/value declarations
AlemTuzlak May 6, 2026
eede4f7
fix(codemods): use shell:true and mkdtempSync for cross-platform robu…
AlemTuzlak May 6, 2026
742661b
chore: revert tsconfig changes and move codemods to repo root
AlemTuzlak May 6, 2026
1634966
chore: sync pnpm-lock.yaml after removing @standard-schema/spec dep
AlemTuzlak May 6, 2026
db8f1aa
fix(core): add passthrough index signature to BaseEvent
AlemTuzlak May 6, 2026
b6b7ccf
refactor(core): drop defensive casts, align Tool/RunAgentInput types …
AlemTuzlak May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 316 additions & 0 deletions codemods/0.1.0-schemas-to-subpath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
/**
* jscodeshift codemod: @ag-ui/core 0.0.x → 0.1.0
*
* Moves every `*Schema` import (and `EventSchemas`) from `@ag-ui/core` to the
* new `@ag-ui/core/schemas` subpath. Type-only imports are preserved. Idempotent.
*
* Usage:
* npx jscodeshift -t 0.1.0-schemas-to-subpath.ts --parser=tsx --extensions=ts,tsx src/
*/
import type { Transform, ImportSpecifier, ImportNamespaceSpecifier } from "jscodeshift";

// ---------------------------------------------------------------------------
// Curated list — the public *Schema exports from 0.0.x.
// The transform also matches anything whose imported name ends with "Schema"
// as a fallback, so this list does not need to be exhaustive. It exists to
// catch `EventSchemas` (the only "Schemas" plural export) and to make the
// intent of the transform explicit.
// Keep in sync with sdks/typescript/packages/core/src/schemas.ts
// ---------------------------------------------------------------------------
const SCHEMA_NAMES = new Set([
// Discriminated event union
"EventSchemas",
// EventType enum schema
"EventTypeSchema",
// Base type schemas
"FunctionCallSchema",
"ToolCallSchema",
"TextInputContentSchema",
"InputContentDataSourceSchema",
"InputContentUrlSourceSchema",
"InputContentSourceSchema",
"ImageInputContentSchema",
"AudioInputContentSchema",
"VideoInputContentSchema",
"DocumentInputContentSchema",
"ImageInputPartSchema",
"AudioInputPartSchema",
"VideoInputPartSchema",
"DocumentInputPartSchema",
"BinaryInputContentSchema",
"InputContentSchema",
"InputContentPartSchema",
"DeveloperMessageSchema",
"SystemMessageSchema",
"AssistantMessageSchema",
"UserMessageSchema",
"ToolMessageSchema",
"ActivityMessageSchema",
"ReasoningMessageSchema",
"MessageSchema",
"RoleSchema",
"ContextSchema",
"ToolSchema",
"InterruptSchema",
"ResumeEntrySchema",
"RunAgentInputSchema",
"StateSchema",
// Event schemas
"BaseEventSchema",
"TextMessageStartEventSchema",
"TextMessageContentEventSchema",
"TextMessageEndEventSchema",
"TextMessageChunkEventSchema",
"ThinkingTextMessageStartEventSchema",
"ThinkingTextMessageContentEventSchema",
"ThinkingTextMessageEndEventSchema",
"ToolCallStartEventSchema",
"ToolCallArgsEventSchema",
"ToolCallEndEventSchema",
"ToolCallResultEventSchema",
"ToolCallChunkEventSchema",
"ThinkingStartEventSchema",
"ThinkingEndEventSchema",
"StateSnapshotEventSchema",
"StateDeltaEventSchema",
"MessagesSnapshotEventSchema",
"ActivitySnapshotEventSchema",
"ActivityDeltaEventSchema",
"RawEventSchema",
"CustomEventSchema",
"RunStartedEventSchema",
"RunFinishedSuccessOutcomeSchema",
"RunFinishedInterruptOutcomeSchema",
"RunFinishedOutcomeSchema",
"RunFinishedEventSchema",
"RunErrorEventSchema",
"StepStartedEventSchema",
"StepFinishedEventSchema",
"ReasoningEncryptedValueSubtypeSchema",
"ReasoningStartEventSchema",
"ReasoningMessageStartEventSchema",
"ReasoningMessageContentEventSchema",
"ReasoningMessageEndEventSchema",
"ReasoningMessageChunkEventSchema",
"ReasoningEndEventSchema",
"ReasoningEncryptedValueEventSchema",
// Capability schemas
"SubAgentInfoSchema",
"IdentityCapabilitiesSchema",
"TransportCapabilitiesSchema",
"ToolsCapabilitiesSchema",
"OutputCapabilitiesSchema",
"StateCapabilitiesSchema",
"MultiAgentCapabilitiesSchema",
"ReasoningCapabilitiesSchema",
"MultimodalInputCapabilitiesSchema",
"MultimodalOutputCapabilitiesSchema",
"MultimodalCapabilitiesSchema",
"ExecutionCapabilitiesSchema",
"HumanInTheLoopCapabilitiesSchema",
"AgentCapabilitiesSchema",
]);

/** Returns true if this imported name should move to @ag-ui/core/schemas. */
const isSchemaSpecifier = (importedName: string): boolean =>
importedName.endsWith("Schema") || importedName === "EventSchemas" || SCHEMA_NAMES.has(importedName);

const CORE_SOURCE = "@ag-ui/core";
const SCHEMAS_SOURCE = "@ag-ui/core/schemas";

const transform: Transform = (file, api) => {
const j = api.jscodeshift;
const root = j(file.source);

let dirty = false;

// Collect all import declarations from @ag-ui/core
const coreImports = root.find(j.ImportDeclaration, {
source: { value: CORE_SOURCE },
});

if (coreImports.length === 0) {
return file.source;
}

// Collect any existing import from @ag-ui/core/schemas so we can merge into it
const existingSchemasImports = root.find(j.ImportDeclaration, {
source: { value: SCHEMAS_SOURCE },
});

// We may need to merge schema specifiers into an existing schemas import.
// Build a set of (imported::local) pairs already imported from @ag-ui/core/schemas
// so we don't create duplicates. Keying by both imported name AND local alias ensures
// that `Foo` and `Foo as Bar` are treated as distinct specifiers and both preserved.
const alreadyInSchemas = new Set<string>();
existingSchemasImports.forEach((path) => {
(path.node.specifiers ?? []).forEach((spec) => {
if (spec.type === "ImportSpecifier") {
const s = spec as ImportSpecifier;
alreadyInSchemas.add(`${s.imported.name}::${s.local.name}`);
}
});
});

// Specifiers to move to @ag-ui/core/schemas, accumulated across all @ag-ui/core imports.
// We separate value specs from type-only specs so we can emit correctly typed declarations.
const valueSpecsToMove: ImportSpecifier[] = [];
const typeSpecsToMove: ImportSpecifier[] = [];

coreImports.forEach((path) => {
const specifiers = path.node.specifiers ?? [];
// A whole-declaration `import type { ... }` makes all specifiers type-only.
const declIsTypeOnly = path.node.importKind === "type";

// Partition: stay vs. move
const staySpecs: typeof specifiers = [];
const moveSpecs: ImportSpecifier[] = [];

for (const spec of specifiers) {
if (spec.type !== "ImportSpecifier") {
// Default or namespace imports — always stay on @ag-ui/core
if (spec.type === "ImportNamespaceSpecifier") {
console.warn(
`[codemod 0.1.0-schemas-to-subpath] ${file.path}: namespace import "import * as ${(spec as ImportNamespaceSpecifier).local.name} from "@ag-ui/core"" cannot be automatically migrated. Schema references via ${(spec as ImportNamespaceSpecifier).local.name}.<SchemaName> must be updated manually.`
);
}
staySpecs.push(spec);
continue;
}
const named = spec as ImportSpecifier;
const importedName = named.imported.name;

if (isSchemaSpecifier(importedName)) {
moveSpecs.push(named);
} else {
staySpecs.push(named);
}
}

if (moveSpecs.length === 0) {
// Nothing to move in this declaration — leave it untouched
return;
}

dirty = true;

// Only add to specsToMove if not already present in @ag-ui/core/schemas.
// Preserve type-only intent: a specifier is type-only if the whole declaration
// is `import type { ... }` OR if the individual specifier has importKind "type".
// Uniqueness key is `${imported.name}::${local.name}` so that the same schema
// imported under two different local aliases (e.g. `Foo` and `Foo as Bar`)
// are both preserved rather than the second being silently dropped.
for (const spec of moveSpecs) {
const importedName = spec.imported.name;
const localName = spec.local.name;
const dedupKey = `${importedName}::${localName}`;
if (!alreadyInSchemas.has(dedupKey)) {
const specIsTypeOnly = declIsTypeOnly || spec.importKind === "type";
if (specIsTypeOnly) {
// Clone spec without per-specifier importKind — the declaration itself
// will be emitted as `import type { ... }`, so the per-specifier marker
// is redundant and would produce `import type { type Foo }`.
const cloned = j.importSpecifier(
j.identifier(importedName),
j.identifier(localName),
);
typeSpecsToMove.push(cloned);
} else {
valueSpecsToMove.push(spec);
}
alreadyInSchemas.add(dedupKey);
}
}

// Update or remove the original @ag-ui/core declaration
if (staySpecs.length === 0) {
// Nothing left on @ag-ui/core — remove the declaration entirely
j(path).remove();
} else {
// Mutate the specifier list in-place
path.node.specifiers = staySpecs;
// If the declaration was `import type` but we stripped all type-only specs
// and only non-schema (value) specifiers remain, the importKind stays "type"
// which is still correct since all remaining specifiers are type imports.
}
});

const specsToMove = [...valueSpecsToMove, ...typeSpecsToMove];

if (!dirty || specsToMove.length === 0) {
return dirty ? root.toSource({ quote: "double" }) : file.source;
}

// Helper to insert a new import declaration after the last remaining import.
const insertAfterLastImport = (newImport: ReturnType<typeof j.importDeclaration>) => {
const allImports = root.find(j.ImportDeclaration);
if (allImports.length > 0) {
allImports.at(allImports.length - 1).insertAfter(newImport);
} else {
const body = root.find(j.Program).get("body");
body.value.unshift(newImport);
}
};

if (existingSchemasImports.length > 0) {
// Merge specifiers only into a declaration of the matching kind to avoid
// accidentally making value imports type-only (erased at runtime) or
// making type imports lose their type-only status.
//
// Strategy:
// - typeSpecsToMove → merge into an existing `import type` declaration,
// or create a new one if none exists.
// - valueSpecsToMove → merge into an existing value import declaration,
// or create a new one if none exists.
//
// We never mix kinds within a single declaration.

let mergedValue = false;
let mergedType = false;

existingSchemasImports.forEach((path) => {
if (path.node.importKind === "type") {
// Type-only declaration: only merge type specs here.
if (typeSpecsToMove.length > 0 && !mergedType) {
path.node.specifiers = [...(path.node.specifiers ?? []), ...typeSpecsToMove];
mergedType = true;
}
// Do NOT merge value specs — they would become type-only and be erased at runtime.
} else {
// Value import declaration: only merge value specs here.
if (valueSpecsToMove.length > 0 && !mergedValue) {
path.node.specifiers = [...(path.node.specifiers ?? []), ...valueSpecsToMove];
mergedValue = true;
}
// Do NOT merge type specs into a value import — emit a separate `import type` below.
}
});

// Anything not yet merged needs a fresh declaration.
if (!mergedValue && valueSpecsToMove.length > 0) {
const newImport = j.importDeclaration(valueSpecsToMove, j.stringLiteral(SCHEMAS_SOURCE));
insertAfterLastImport(newImport);
}
if (!mergedType && typeSpecsToMove.length > 0) {
const typeImport = j.importDeclaration(typeSpecsToMove, j.stringLiteral(SCHEMAS_SOURCE));
typeImport.importKind = "type";
insertAfterLastImport(typeImport);
}
} else {
// No existing @ag-ui/core/schemas import. Emit value and type declarations separately.
if (valueSpecsToMove.length > 0) {
const newImport = j.importDeclaration(valueSpecsToMove, j.stringLiteral(SCHEMAS_SOURCE));
insertAfterLastImport(newImport);
}
if (typeSpecsToMove.length > 0) {
const typeImport = j.importDeclaration(typeSpecsToMove, j.stringLiteral(SCHEMAS_SOURCE));
typeImport.importKind = "type";
insertAfterLastImport(typeImport);
}
}

return root.toSource({ quote: "double" });
};

export default transform;
export const parser = "tsx";
56 changes: 56 additions & 0 deletions codemods/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# @ag-ui/core codemods

Automated transforms for upgrading code that depends on `@ag-ui/core`.

---

## 0.1.0-schemas-to-subpath

**What it does.** Moves every `*Schema` import (and `EventSchemas`, the only "Schemas" plural export) from `@ag-ui/core` to the new `@ag-ui/core/schemas` subpath introduced in 0.1.0. If a file already has an import from `@ag-ui/core/schemas`, the moved specifiers are merged into it rather than creating a duplicate declaration. Type-only imports (`import type { ... }`) are preserved on the appropriate side. The transform is idempotent — running it twice produces the same output.

**How to run it.**

```bash
npx jscodeshift -t https://raw.githubusercontent.com/ag-ui-protocol/ag-ui/main/codemods/0.1.0-schemas-to-subpath.ts \
--parser=tsx \
--extensions=ts,tsx \
src/
```

To do a dry run (print changes without writing):

```bash
npx jscodeshift --dry --print \
-t https://raw.githubusercontent.com/ag-ui-protocol/ag-ui/main/codemods/0.1.0-schemas-to-subpath.ts \
--parser=tsx \
--extensions=ts,tsx \
src/
```

**What it does NOT do.**

- It does not add `zod` to your `package.json`. After running the codemod, run `npm install zod` (or `pnpm add zod` / `yarn add zod`) if any file imports from `@ag-ui/core/schemas`.
- It does not update the `@ag-ui/core` version constraint in `package.json`. Update it to `^0.1.0` manually.

**Recognized schema names.**

The transform uses two complementary heuristics:

1. Any imported name that ends with `"Schema"` is moved (e.g. `UserMessageSchema`, `AgentCapabilitiesSchema`).
2. The name `EventSchemas` is explicitly matched (the only "Schemas" plural export).

The curated list in `SCHEMA_NAMES` inside the transform source mirrors the full public schema surface of `@ag-ui/core/schemas`. Both heuristics are applied, so unknown future schema additions (if they follow the naming convention) are also covered.

**Aliasing behavior.**

**Aliased imports work correctly.** Detection uses the *imported* name (the name on the `@ag-ui/core` side), so `import { UserMessageSchema as Foo } from "@ag-ui/core"` is recognized regardless of the local alias. The alias is preserved in the moved declaration: `import { UserMessageSchema as Foo } from "@ag-ui/core/schemas"`.

**Known limitations.**

- **Namespace imports** — `import * as core from "@ag-ui/core"` cannot be automatically migrated. The codemod will emit a warning to stderr and leave the import untouched. You must manually split `core.<SchemaName>` references into a named import from `@ag-ui/core/schemas` and update all usages.
- **Re-exports** — `export { UserMessageSchema } from "@ag-ui/core"` is not handled; only `import` declarations are transformed. Re-export-from syntax will need to be updated manually.
- **Dynamic imports** — `import("@ag-ui/core")` and `require("@ag-ui/core")` calls are not transformed; only static `import` declarations are handled.

---

For more context, see the [0.1.0 migration guide](https://docs.ag-ui.com/sdk/js/core/migration-0-1-0).
Loading
Loading