Skip to content

[tsagentspec] Custom components nested inside builtin components can't be deserialized (closed Zod unions) #219

Description

@spichen

Deserialization plugins work for custom components at the top level, but not when the custom component sits inside a builtin component's field. An agent whose transforms or toolboxes contain a plugin component fails to deserialize even when a plugin for that component type is registered.

What happens:

  • Without a plugin: No plugin to deserialize component type "FunctionTransform".
  • With a plugin registered: the child deserializes fine, but the builtin Agent plugin then calls createAgent, and AgentSchema validates transforms/toolboxes against closed discriminated unions (MessageTransformUnion, ToolBoxUnion). The unknown discriminator fails the whole parse: Invalid discriminator value. Expected 'MessageSummarizationTransform' | ...
  • Registering your own Agent plugin to work around it isn't possible either: buildComponentTypesToPlugins throws on duplicate component types.

So the serializer can produce documents (via ComponentSerializationPlugin) that no deserializer configuration can read back. Round-trip is broken for custom components. This is what broke agent export/import in AgentFabric (OA-1801): exported agents with a FunctionTransform or ConnectorToolBox attached couldn't be re-imported.

Repro sketch:

const agent = {
  ...createAgent({ name: 'a', llmConfig, systemPrompt: 'x' }),
  transforms: [myFunctionTransform], // componentType: 'FunctionTransform'
};
const doc = new AgentSpecSerializer([new FunctionTransformSerializationPlugin()])
  .toJson(agent);

new AgentSpecDeserializer([new FunctionTransformDeserializationPlugin()]).fromJson(doc);
// ZodError: Invalid discriminator value ...

Two things worth noting. The construction API has the same restriction: createAgent rejects custom toolboxes/transforms, so integrators already strip them before calling the factory and reattach after. And pyagentspec doesn't have this problem, its plugin system composes with nested custom components, so the two implementations currently disagree on what round-trips.

Possible fixes:

  1. Let component-container fields accept a plugin-deserialized ComponentBase when the component_type isn't in the builtin union.
  2. Relax transforms/toolboxes (and similar fields) to accept any ComponentBase, keeping strict validation for builtin types only.
  3. Allow overriding builtin plugins (last registration wins), so integrators can wrap Agent deserialization themselves. Least SDK work, but every integrator ends up writing their own strip/reattach.

Whichever shape, the factories should get the same treatment so construction and deserialization stay symmetric.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions