Skip to content

Typescript sdk#100

Open
spichen wants to merge 24 commits intooracle:mainfrom
spichen:typescript-sdk
Open

Typescript sdk#100
spichen wants to merge 24 commits intooracle:mainfrom
spichen:typescript-sdk

Conversation

@spichen
Copy link

@spichen spichen commented Feb 13, 2026

Resolves #97

…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.
@oracle-contributor-agreement
Copy link

Thank you for your pull request and welcome to our community! To contribute, please sign the Oracle Contributor Agreement (OCA).
The following contributors of this PR have not signed the 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.

@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. label Feb 13, 2026
- 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.
@dhilloulinoracle
Copy link
Contributor

Thanks a lot @spichen for this big contribution!
We will start reviewing it.

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?

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

@spichen spichen marked this pull request as ready for review February 13, 2026 13:51
@spichen spichen requested a review from a team February 13, 2026 13:51
@oracle-contributor-agreement
Copy link

Thank you for signing the OCA.

@oracle-contributor-agreement oracle-contributor-agreement bot added OCA Verified All contributors have signed the Oracle Contributor Agreement. and removed OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. labels Feb 13, 2026

export type DataFlowEdge = z.infer<typeof DataFlowEdgeSchema>;

export function createDataFlowEdge(opts: {
Copy link
Member

Choose a reason for hiding this comment

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

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?

Copy link
Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Done

opts.inputs ??
getPlaceholderPropertiesFromJsonObject(opts.promptTemplate);

const outputs = opts.outputs ?? [
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Done

- 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
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(),
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

}

/** Base property Zod schema */
export const PropertySchema = z.object({
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

}

/** Load a single field value, handling component refs and nested structures */
loadField(value: unknown): unknown {
Copy link
Member

Choose a reason for hiding this comment

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

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").

Copy link
Author

Choose a reason for hiding this comment

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

Fixed. Refactored the serialization logic.

const mappedId =
this.componentsIdMapping.get(componentId) ?? componentId;

if (!this.resolvedComponents.has(mappedId)) {
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

}

/** Fields that should never be transformed between camelCase/snake_case */
const NEVER_TRANSFORM_FIELDS = new Set([
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed. Refactored the serialisation logic.


export type EndNode = z.infer<typeof EndNodeSchema>;

export function createEndNode(opts: {
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

// 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());
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeScript SDK for Agent Spec

4 participants