Conversation
…ation
Implements a complete TypeScript SDK providing the same core capabilities
as the Python SDK (pyagentspec) for authoring, validating, and
serializing/deserializing Agent Spec configurations.
Core features:
- All component types: Agent, Swarm, ManagerWorkers, Flow, 14 node types,
4 tool types, 6 LLM config types, edges, datastores, transforms
- Property system with JSON Schema mapping (9 typed property factories)
- Full serialization/deserialization with JSON and YAML support
- $component_ref referencing for shared components
- Template variable extraction from {{placeholder}} syntax
- Plugin architecture for custom component types
- Version-gated field serialization (agentspec_version 25.4.1+)
- camelCase/snake_case field name conversion for Python SDK interop
- FlowBuilder with ergonomic builder pattern API
Technical details:
- Zod schemas + factory functions (no class hierarchies)
- Discriminated unions on componentType for type-safe polymorphism
- Strict TypeScript with zero `any` types in public API
- ESM + CJS dual build via tsup
- 384 tests across 29 test files (vitest)
Signed-off-by: Salah <nh.salah@gmail.com>
…ComponentUnion Several component types had Zod schemas registered in BUILTIN_SCHEMA_MAP but no corresponding factory functions in BUILTIN_FACTORY_MAP, causing deserialization to crash at runtime. Added create* factories for: RemoteAgent, A2AConnectionConfig, all transport types, Oracle/Postgres datastores, and their connection configs. Also added RemoteAgent to the AgenticComponentUnion discriminated union so it is accepted where AgenticComponent is expected.
…mponent
versionLt now uses Math.max instead of Math.min when comparing version
segments, treating missing segments as 0. This fixes the latent bug where
versionLt("1.0", "1.0.1") incorrectly returned false.
Extracted isComponent to a shared utility in component.ts with a stronger
UUID check on the id field, eliminating the duplicate definitions in
serialization-context.ts and referencing.ts.
- Add input size and nesting depth limits to AgentSpecDeserializer to prevent resource exhaustion from untrusted input (backwards-compatible constructor accepts both legacy plugin array and new options object) - Add license, repository, homepage, keywords, and engines to package.json - Add 77 new tests (384 -> 461) covering previously untested components: A2AAgent, SpecializedAgent, MCP tools/transports, datastores, transforms, version-gated serialization, sensitive field exclusion, and negative deserialization paths (malformed data, size/depth limits)
…ation
Address critical and important issues found during code review:
Critical fixes:
- Add depth check to fromDict() to prevent resource exhaustion bypass
- Filter __proto__/constructor/prototype keys in deserialization to
prevent prototype pollution
- Use safe YAML schema ({ schema: "core" }) to restrict type resolution
- Move FlowBuilder.addConditional validation before state mutation
- Always increment conditional edge counter for unique data edge names
- Use user-provided outputs in InputMessageNode instead of discarding them
Important fixes:
- Extract duplicated map helpers (inferMapInputs, getDefaultReducers,
inferMapOutputs) into shared map-helpers.ts
- Extract duplicated getEndNodeBranches into shared node-helpers.ts
- Remove redundant .optional() before .default() in transform schemas
- Add minNumMessages <= maxNumMessages validation
- Fix AgentNode operator precedence with explicit parentheses
- Remove redundant Buffer.isBuffer check (Uint8Array covers Buffer)
- Recurse priority key values in makeOrderedDict for consistency
- Document camelToSnake/snakeToCamel scope limitations
- Document circular-dep workarounds in Swarm/ManagerWorkers/Flow schemas
- Add explanatory comment on FactoryFn `any` type necessity
…ml/fromJson/fromYaml Remove Python-ism toDict/fromDict public methods and replace with private _toDict/_fromDict. The snake_case conversion is now controlled via a camelCase option on toJson/toYaml/fromJson/fromYaml instead of being exposed as a separate API surface. - Add camelCase flag to SerializationContext and DeserializationContext - Remove ComponentAsDict and DisaggregatedComponentsConfig from public exports - Update all tests to use toJson+JSON.parse / fromJson+JSON.stringify
Eight examples covering basic agents, tools, multi-agent patterns, flows, MCP tools, serialization, A2A agents, and datastores.
|
Thank you for your pull request and welcome to our community! To contribute, please sign the Oracle Contributor Agreement (OCA).
To sign the OCA, please create an Oracle account and sign the OCA in Oracle's Contributor Agreement Application. When signing the OCA, please provide your GitHub username. After signing the OCA and getting an OCA approval from Oracle, this PR will be automatically updated. If you are an Oracle employee, please make sure that you are a member of the main Oracle GitHub organization, and your membership in this organization is public. |
- Add depth limits to serialization (dumpField, makeOrderedDict) to prevent stack overflow from deeply nested plain objects - Rewrite deserializer checkDepth() iteratively to avoid stack overflow in the depth checker itself - Filter __proto__/constructor/prototype keys in serialization (dumpField, dumpModelObject) to prevent prototype pollution via round-tripping through external systems - Deduplicate DANGEROUS_KEYS into shared types.ts module - Fix Uint8Array.toString() in templating returning byte values instead of meaningful content; now returns empty array - Add tests for prototype pollution prevention, circular reference detection, serialization depth limits, and dangerous key filtering
…d more Add comprehensive tests targeting low-coverage areas, bringing overall coverage from ~93% to 99%+ statements. Key additions include property comparison helpers (type equality, castability, union/object/dict), serialization context (version gating, camelCase mode, dangerous keys), deserialization context (loadField, loadReference, camelCase round-trip), flow builder edge cases, map node reducers, and factory functions for remote agents and OCI configs.
|
Thanks a lot @spichen for this big contribution! To be able to merge it, we would need you to sign the CLA. Would it be possible for you to have a look at that? |
There was a problem hiding this comment.
Pull request overview
This pull request introduces a comprehensive TypeScript SDK for the Oracle Open Agent Specification. The SDK provides a strongly-typed, developer-friendly API for defining AI agents, multi-agent systems, workflows, tools, and related components with full serialization/deserialization support.
Changes:
- Complete TypeScript SDK implementation with Zod schemas for validation
- Comprehensive test suite with 30+ test files covering all major features
- Full serialization/deserialization system supporting JSON/YAML with version gating
- 8 detailed examples demonstrating SDK usage patterns
Reviewed changes
Copilot reviewed 135 out of 136 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Package configuration with dependencies (zod, yaml) and dev tooling |
| tsconfig.json | Strict TypeScript configuration with modern ES2022 target |
| tsup.config.ts | Build configuration for dual CJS/ESM output with type definitions |
| vitest.config.ts | Test configuration with v8 coverage |
| src/component.ts | Base component schemas and type system |
| src/agents/*.ts | Agent types: Agent, Swarm, ManagerWorkers, RemoteAgent, A2AAgent, SpecializedAgent |
| src/tools/*.ts | Tool types: ServerTool, ClientTool, RemoteTool, BuiltinTool, MCPTool, ToolBox |
| src/llms/*.ts | LLM configs: OpenAI, vLLM, Ollama, OCI GenAI |
| src/flows/*.ts | Flow system with nodes, edges, and FlowBuilder |
| src/mcp/*.ts | MCP tool and transport support |
| src/serialization/*.ts | Complete serialization system with plugins, version gating, and sensitive field exclusion |
| src/versioning.ts | Version tracking and comparison utilities |
| src/templating.ts | Template placeholder extraction from strings/objects |
| src/sensitive-field.ts | Security: marks fields for exclusion from serialization |
| tests/**/*.test.ts | Comprehensive test coverage for all components |
| examples/*.ts | 8 working examples demonstrating SDK usage |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tsagentspec/src/serialization/builtin-deserialization-plugin.ts
Outdated
Show resolved
Hide resolved
tsagentspec/src/serialization/builtin-deserialization-plugin.ts
Outdated
Show resolved
Hide resolved
|
Thank you for signing the OCA. |
|
|
||
| export type DataFlowEdge = z.infer<typeof DataFlowEdgeSchema>; | ||
|
|
||
| export function createDataFlowEdge(opts: { |
There was a problem hiding this comment.
createDataFlowEdge never checks that sourceOutput exists on sourceNode or that destinationInput exists on destinationNode. The Python SDK enforces this with Pydantic validators, preventing misspelled property connections. Can you add equivalent checks (and tests) to keep flow graphs valid at construction time?
There was a problem hiding this comment.
The suite covers scalar properties comprehensively but does not exercise objectProperty with nested schemas or dictProperty with complex value types. A couple of extra assertions would confirm that validateSchemaTitle recurses correctly.
| opts.inputs ?? | ||
| getPlaceholderPropertiesFromJsonObject(opts.promptTemplate); | ||
|
|
||
| const outputs = opts.outputs ?? [ |
There was a problem hiding this comment.
Default output property is hand-constructed as a wrapper { jsonSchema, title, type, ... } rather than using the property constructors. This increases the chance of drift (e.g., missing title validation, missing schema normalization). Consider reusing a stringProperty(...) constructor.
- Validate sourceOutput/destinationInput exist on nodes in createDataFlowEdge - Add type castability check between connected properties - Use stringProperty constructor for LLM node default output - Add tests for nested objectProperty and complex dictProperty value types
9865d00 to
24ce2ca
Compare
Replace Record<string, unknown> with ComponentWithIO for sourceNode and destinationNode parameters, eliminating unknown type casts.
| name: z.string(), | ||
| description: z.string().optional(), | ||
| metadata: z.record(z.unknown()).default({}), | ||
| componentType: z.string(), |
There was a problem hiding this comment.
Internal field name is componentType, but serialization uses component_type. That’s workable, but it increases drift risk and already causes bugs in deserialization (nested component detection doesn’t respect camelCase). Consider aligning internal field to the canonical serialized discriminator or ensuring a single, consistent mapping layer.
| } | ||
|
|
||
| /** Base property Zod schema */ | ||
| export const PropertySchema = z.object({ |
There was a problem hiding this comment.
PropertySchema defaults jsonSchema to {} and title to "property", and type is optional. This allows constructing a Property that serializes to JSON Schema missing required spec annotations (title, type). Recommend enforcing jsonSchema.title and jsonSchema.type invariants at construction time.
| } | ||
|
|
||
| /** Load a single field value, handling component refs and nested structures */ | ||
| loadField(value: unknown): unknown { |
There was a problem hiding this comment.
loadField() only treats nested component dicts as components when "component_type" in dict. If camelCase mode is enabled (or user-provided nested dicts use componentType), nested components will be treated as plain objects and won’t deserialize properly. Suggest using the same key selection logic as getComponentType() (camelCase ? "componentType" : "component_type").
There was a problem hiding this comment.
Fixed. Refactored the serialization logic.
| const mappedId = | ||
| this.componentsIdMapping.get(componentId) ?? componentId; | ||
|
|
||
| if (!this.resolvedComponents.has(mappedId)) { |
There was a problem hiding this comment.
Potential cache key inconsistency for disaggregation: checks !this.resolvedComponents.has(mappedId) but later stores this.resolvedComponents.set(componentId, componentDump). If mappedId !== componentId (disaggregated or remapped IDs), lookups can fail and $component_ref may point at an ID not present in $referenced_components.
| } | ||
|
|
||
| /** Fields that should never be transformed between camelCase/snake_case */ | ||
| const NEVER_TRANSFORM_FIELDS = new Set([ |
There was a problem hiding this comment.
NEVER_TRANSFORM_FIELDS includes component_type and agentspec_version but not componentType / agentspecVersion. If camelCase mode is intended as a full alternate representation, this needs a clearer contract: right now it risks partially-transformed output.
There was a problem hiding this comment.
Fixed. Refactored the serialisation logic.
|
|
||
| export type EndNode = z.infer<typeof EndNodeSchema>; | ||
|
|
||
| export function createEndNode(opts: { |
There was a problem hiding this comment.
createEndNode() uses the same input/output mirroring pattern as createStartNode() (inputs = opts.inputs ?? opts.outputs, outputs = opts.outputs ?? opts.inputs). This can produce serialized configs where EndNode inputs incorrectly equal EndNode outputs (or vice versa) when only one is provided. That changes the IO surface exposed by the node and can cause Flow output inference / validation to disagree with pyagentspec, where EndNode outputs are the authoritative flow outputs.
tsagentspec/src/flows/flow.ts
Outdated
| // z.record(z.unknown()) is used instead of NodeUnion to break a circular dependency | ||
| // (Flow -> FlowNode -> Flow). Validation of individual nodes happens via their own | ||
| // schemas when constructed through factory functions. | ||
| const NodeRef = z.record(z.unknown()); |
There was a problem hiding this comment.
NodeRef = z.record(z.unknown()) is used to avoid circular deps, but it means FlowSchema itself doesn’t validate that nodes are valid Node components. This can allow invalid configs to pass TS parsing and only fail later at serialization/deserialization boundaries.
There was a problem hiding this comment.
Fixed. node and subflow refs now use z.lazy() with late-binding registration to break the circular dep while validating against NodeUnion/FlowSchema at parse time.
…erfaces Replace the untyped Record<string, unknown> alias with discriminated interfaces (SerializedComponent, CamelCaseSerializedComponent, ComponentRef) and centralize protocol field management via PROTOCOL_KEYS, getProtocolKeys(), and ALL_PROTOCOL_FIELDS. This eliminates duplicate field sets across 4+ files, fixes camelCase-mode bugs in nested component detection and key ordering, and moves type guards to a single source of truth in types.ts.
…ction
PropertySchema no longer defaults jsonSchema to {} or title to "property".
The jsonSchema field now requires a non-empty title and either a type or
anyOf field. propertyFromJsonSchema applies the same validation instead of
silently falling back. ApiNode default output now explicitly declares
type "string".
…ration Replace z.record(z.unknown()) workarounds with LazyNodeRef/LazyFlowRef backed by z.lazy() and late-binding registration, breaking the circular dependency (Flow -> NodeUnion -> FlowNodeSchema -> FlowSchema) while enabling proper schema validation for nodes, subflows, and edge refs.
Resolves #97