Skip to content

Agent Spec — Compact OAS Flow (`oas.json`)

See also: packages/agents/AGENTS.md — implementation details

Canonical filename: Phase 200 (vendor-namespaced agent layout) renamed the on-disk file from agent.json to oas.json — the new path is agents/<vendor>/<slug>/cinatra/oas.json. The legacy filenames (agents/<slug>/cinatra/agent.json and agents/<slug>/agent.json) are still recognised by the loader/handlers as a transitional fallback, but all new agents MUST be written as oas.json in the vendor-namespaced path. Both the live MCP schemas and the runtime resolver (resolveAgentJsonPath) now treat oas.json as the only canonical name. The historical references to agent.json below are retained for context on the v2 → OAS Flow content migration; the file CONTENT format is covered, not the on-disk filename.

Phase 157 (commit 0025ceab, 2026-04-24) migrated the file content from the nested v2 shape to the compact OAS Flow 26.1.0 format. Root-level fields are now agentspec_version: "26.1.0" and component_type: "Flow"; all Cinatra-specific metadata lives under metadata.cinatra. This document is the canonical reference. The filename agent-spec-v2.md is retained for link stability — the current format is NOT v2; it is OAS Flow. See the ## What Changed table below for the full legacy → current mapping.

Related docs:

  • docs/ai/agent-packaging.md — layout conventions, templates, install paths
  • docs/ai/ensure-agent-package.md — startup scan + ZIP install mechanics
  • packages/agents/src/oas-compiler.ts — live compiler (source of truth for derivation)
  • packages/agents/src/validate-agent-json.ts — live validator (source of truth for rejection rules)
  • packages/agents/AGENTS.md §OAS compiler (Phase 157) — package-scoped reference

v2 (nested)OAS Flow (compact)
componentType: "Agent" at rootcomponent_type: "Flow" at root
metadata.cinatra.formatVersion: 2agentspec_version: "26.1.0" at root
metadata.cinatra.inputSchema (authored)derived by compiler from StartNode.inputs
metadata.cinatra.outputSchema (authored)derived by compiler from EndNode.outputs
metadata.cinatra.prompt (authored)sourced from Agent.system_prompt component
metadata.cinatra.taskSpec (authored)rejected — moved to SKILL.md / Agent.system_prompt
metadata.cinatra.approvalPolicy (authored)derived from AgentNodes by compiler
metadata.cinatra.compiledPlan (authored)rejected — always [], produced at compile time
validateAgentJsonV2()validateOasAgentJson()
AgentJsonV2 typeDELETED

{
"agentspec_version": "26.1.0",
"component_type": "Flow",
"id": "email-recipients-flow",
"name": "Recipient Generator",
"metadata": {
"cinatra": {
"type": "leaf",
"hitlScreens": [
"@cinatra-agents/email-outreach:contact-source-selector",
"@cinatra-agents/email-recipients:output"
]
}
},
"inputs": [
{ "title": "campaignId", "type": "string", "format": "uuid" },
{ "title": "accountScope", "type": "object" }
],
"outputs": [
{ "title": "campaignId", "type": "string", "format": "uuid" },
{ "title": "recipientCount", "type": "integer" },
{ "title": "confirmedRecipients", "type": "array", "items": { "type": "object" } }
],
"start_node": { "$component_ref": "start" },
"nodes": [
{ "$component_ref": "start" },
{ "$component_ref": "generate" },
{ "$component_ref": "end" }
],
"control_flow_connections": [
{
"component_type": "ControlFlowEdge",
"name": "start_to_generate",
"from_node": { "$component_ref": "start" },
"to_node": { "$component_ref": "generate" }
},
{
"component_type": "ControlFlowEdge",
"name": "generate_to_end",
"from_node": { "$component_ref": "generate" },
"to_node": { "$component_ref": "end" }
}
],
"data_flow_connections": [
{
"component_type": "DataFlowEdge",
"name": "start_to_generate_campaignId",
"source_node": { "$component_ref": "start" },
"source_output": "campaignId",
"destination_node": { "$component_ref": "generate" },
"destination_input": "campaignId"
}
],
"$referenced_components": {
"start": {
"component_type": "StartNode",
"id": "start",
"name": "Inputs",
"metadata": {
"cinatra": {
"required": ["campaignId", "accountScope"],
"hidden": ["campaignId"],
"inputRenderers": {
"accountScope": "@cinatra-agents/email-outreach:contact-source-selector"
}
}
},
"inputs": [
{ "title": "campaignId", "type": "string", "format": "uuid" },
{ "title": "accountScope", "type": "object" }
]
},
"generate": {
"component_type": "AgentNode",
"id": "generate",
"name": "Generate recipient list",
"metadata": {
"cinatra": {
"riskClass": "read_only",
"requiresApproval": true,
"renderer": "@cinatra-agents/email-recipients:output",
"a2uiSurfaceId": "email-recipients:step-1:output"
}
},
"agent": { "$component_ref": "agent-recipients" }
},
"end": {
"component_type": "EndNode",
"id": "end",
"name": "End",
"outputs": [
{ "title": "campaignId", "type": "string", "format": "uuid" },
{ "title": "recipientCount", "type": "integer" },
{ "title": "confirmedRecipients", "type": "array", "items": { "type": "object" } }
]
},
"agent-recipients": {
"component_type": "Agent",
"id": "agent-recipients",
"name": "Recipient generator agent",
"llm_config": { "$component_ref": "shared-llm-config" },
"toolboxes": [{ "$component_ref": "cinatra-mcp-toolbox" }],
"system_prompt": "You are a campaign recipient selection agent...",
"metadata": {
"cinatra": {
"packageName": "@cinatra-agents/email-recipients"
}
}
}
}
}

FieldTypeRequiredNotes
agentspec_version"26.1.0" (literal)YesAnything else is rejected by validateOasFlowStructural.
component_type"Flow" (literal)YesIdentifies the compact OAS envelope.
idstringYesStable human-readable id (convention: <slug>-flow).
namestringYesDisplay name.
metadata.cinatra.type"leaf" | "orchestrator"YesDetermines compile topology. Other classifications (proxy, parallel, supervisor, iterative) are runtime concerns of the LangGraph type graphs, not the compiler.
metadata.cinatra.hitlScreensstring[]NoNamespaced @cinatra-agents/<slug>:<renderer-id> ids of HITL renderers this agent may emit.
inputsPropertySchema[]YesFlat JSON Schema property list — compiler derives inputSchema from this + StartNode.
outputsPropertySchema[]YesFlat JSON Schema property list — compiler derives outputSchema from EndNode.
start_node{ $component_ref: string }YesMust resolve to a StartNode inside $referenced_components.
nodes{ $component_ref: string }[]YesAll flow nodes (Start + all AgentNodes + End). Array order is NOT execution order — see BFS topology.
control_flow_connectionsControlFlowEdge[]YesCompiler traverses these to derive step order.
data_flow_connectionsDataFlowEdge[]NoCompiler uses these to derive per-step inputMapping.
$referenced_componentsRecord<string, unknown>YesLocal component registry. Must contain exactly one StartNode and exactly one EndNode (validator cases 40, 41).
FieldTypeRequiredNotes
requiredstring[]NoInput ids that must be present in agent_runs.inputParams before the setup interrupt loop falls through to LangGraph dispatch.
hiddenstring[]NoInput ids suppressed from the setup form UI. Compiler still threads them into inputSchema.
inputRenderersRecord<string, string>NoMaps input id → namespaced x-renderer id (e.g. @cinatra-agents/email-outreach:cta).
inputTitlesRecord<string, string>NoHuman-readable label overrides per input id, shown in the setup UI.
FieldTypeRequiredNotes
riskClassstringNoSurfaced as step.riskClass for UI risk badges (e.g. "read_only", "send_external_message").
requiresApprovalbooleanNoDrives HITL gate. Leaf defaults may apply via mergeParentLeafCinatra.
rendererstringNoNamespaced x-renderer id; emitted as step.xRenderer in compiled output.
a2uiSurfaceIdstringNoA2UI catalog surface id. Emitted as step.a2uiSurfaceId.
a2uiSurfaceIdOverridestringNoConsumed by mergeParentLeafCinatra; never re-emitted in compiled output. Only valid when hitlOwnedBy === "childAgent".
hitlOwnedBy"childAgent" | "self"NoIndicates which actor gates HITL. "childAgent" = defer to sub-agent’s own approval policy; "self" = this step gates.
descriptionstringNoProse shown in admin views.

Cinatra agents may declare the object types they produce as output. The declaration is placed inline on the output port that carries the object’s id, as a cinatra custom keyword directly on the JSON Schema property. This follows OAS 26.1.0 §6.3 which defines output ports as JSON Schema Property objects and permits custom keyword annotations inline.

Schema (snake_case in JSON, all fields except object_type are optional):

outputs[*].cinatra?: {
object_type: string; // @scope/package:local-id (validated at compile time)
display_name?: string; // Human-readable label (defaults to port title)
category?: "profile" | "content" | "project" | "idea" | "report"; // defaults to "report"
canonical_keys?: string[]; // Identity-resolution keys for dedup
identity_key?: string; // Data field used as Graphiti dedup key (e.g. "cinatra_agent_run_id")
};

Compile-time validation:

  1. Each object_type is validated against the namespace regex /^@[\w-]+\/[\w-]+:[\w-]+$/. Failed entries return { ok: false, error: ... }.
  2. Type IDs already registered statically by @cinatra/* packages are accepted; the install hook skips creating a duplicate dynamic row. A category mismatch with the static registration emits console.warn but does not fail compile.
  3. Type IDs not statically registered are inserted into cinatra.dynamic_object_types at install time with source: "install", status: "active".

Example (orchestrator outputting a campaign id):

{
"metadata": {
"cinatra": { "type": "orchestrator" }
},
"outputs": [
{
"title": "campaignId",
"type": "string",
"format": "uuid",
"cinatra": {
"object_type": "@cinatra/campaigns:campaign",
"display_name": "Email Outreach Campaign",
"category": "project",
"canonical_keys": ["name", "offeringCompanyWebsite", "cinatraAgentRunId"],
"identity_key": "cinatra_agent_run_id"
}
}
]
}

See also: 167-DESIGN.md for the full lifecycle (classifier proposes → admin approves → MCP register → agent install registers).


Implemented in packages/agents/src/validate-agent-json.ts (validateOasAgentJson):

  1. validateOasFlowStructural — Zod parse of flowSchema (agentspec_version=26.1.0, component_type=Flow, required arrays).
  2. Root-level legacy field rejection: componentType (leftover from v2).
  3. metadata.cinatra legacy-field rejection: formatVersion, executionMode, approvalPolicy, compiledPlan, inputSchema, outputSchema, prompt, taskSpec.
  4. Per-AgentNode semantic checks: hitlOwnedBy ∈ {childAgent, self}, a2uiSurfaceIdOverride only valid when hitlOwnedBy === "childAgent", requiresApproval boolean.
  5. Case 38 — every $component_ref resolves locally OR against the known global registry ids (shared-llm-config, cinatra-mcp-toolbox, a2a-default-connection).
  6. Case 39 — no duplicate node ids in the nodes array.
  7. Case 40 — exactly one StartNode in $referenced_components.
  8. Case 41 — exactly one EndNode in $referenced_components.

Component references ({ "$component_ref": "<id>" }) resolve through a two-tier lookup: first against the flow’s local $referenced_components map, then against the global registry at agents/_shared/cinatra/components.json. An unresolved id throws; cycles throw too (the walker tracks visited per resolution pass).

TierSourceBehavior on miss
1 — Localparsed.$referenced_componentsFall through to tier 2
2 — Globalagents/_shared/cinatra/components.jsonThrow unresolved component ref: {id}
(cycle)visited: Set<string> per walkThrow cycle detected at component id {id}
// From packages/agents/src/oas-compiler.ts:223-238
function resolveComponentRef(
id: string,
localRefs: Record<string, unknown>,
globalRegistry: Record<string, unknown>,
visited: Set<string>,
): Record<string, unknown> {
if (visited.has(id)) throw new Error(`cycle detected at component id ${id}`);
visited.add(id);
const local = localRefs[id];
if (local && typeof local === "object") return local as Record<string, unknown>;
const global = globalRegistry[id];
if (global && typeof global === "object") return global as Record<string, unknown>;
throw new Error(
`unresolved component ref: ${id} (looked in local $referenced_components and global registry)`,
);
}
{
"$schema": "/schemas/cinatra/oas-component-registry.schema.json",
"agentspec_version": "26.1.0",
"components": {
"shared-llm-config": {
"component_type": "OpenAiConfig",
"id": "shared-llm-config",
"name": "Shared LLM config",
"model_id": "gpt-4o",
"api_type": "chat_completions",
"metadata": { "cinatra": { "resolvedAtRuntime": true } }
},
"cinatra-mcp-toolbox": {
"component_type": "MCPToolBox",
"id": "cinatra-mcp-toolbox",
"name": "Cinatra MCP Server",
"metadata": { "cinatra": { "resolvedAtRuntime": true } },
"client_transport": {
"component_type": "StreamableHTTPTransport",
"id": "cinatra-mcp-transport",
"name": "Cinatra MCP transport",
"url": "/api/mcp"
}
},
"a2a-default-connection": {
"component_type": "A2AConnectionConfig",
"id": "a2a-default-connection",
"name": "Default A2A connection",
"timeout": 600,
"verify": true
}
}
}

Do not add per-package components.json files. Package-specific Agent / A2AAgent / ServerTool components live under each agent.json’s $referenced_components map. Only truly shared infrastructure (LLM config, MCP toolbox, A2A connection) lives in the global registry (Phase 157 Assumption A8).


Step order follows breadth-first search (BFS) from start_node via control_flow_connections, NOT the parsed.nodes array index (peer-review fix #2). The compiler:

  • Builds an adjacency map from control_flow_connections (from_node.$component_refto_node.$component_ref).
  • Deduplicates identical (from, to) edges inline (oas-compiler.ts:495) so queue operations are not wasted and step ordering is not muddled.
  • Seeds the BFS queue from parsed.start_node.$component_ref.
  • Appends any disconnected nodes in array order as a safety net (oas-compiler.ts:511); the structural validator should reject the upstream condition before compile is reached.
  • Assigns stepNumber 1-indexed across the BFS-ordered AgentNodes only — StartNode and EndNode are never steps.

The BFS topology guarantees that adding a new linear leaf or re-ordering the nodes array does not change emitted step order: only the control_flow graph shape determines step sequence.


compileOasAgentJson returns a discriminated union (peer-review fix #1) — callers branch on .ok instead of the legacy null-or-throw mix:

// From packages/agents/src/oas-compiler.ts:386-397
export type CompiledAgentOas = {
approvalPolicy: { steps: CompiledAgentOasStep[] };
inputSchema: Record<string, unknown>;
outputSchema: Record<string, unknown> | null;
prompt: string | null;
packageName: string | null;
packageVersion: string | null;
agentDependencies: Record<string, string>;
type: "leaf" | "orchestrator";
compiledPlan: [];
hitlScreens: string[];
};
// From packages/agents/src/oas-compiler.ts:371-384
export type CompiledAgentOasStep = {
stepNumber: number;
riskClass?: string;
requiresApproval: boolean;
xRenderer?: string;
description?: string;
name?: string;
a2uiSurfaceId?: string;
hitlOwnedBy?: "childAgent" | "self";
childAgent?: {
packageName: string;
inputMapping: Record<string, string>;
};
};

The CompileOasResult union has two branches:

// From packages/agents/src/oas-compiler.ts:400-402
export type CompileOasResult =
| { ok: true; value: CompiledAgentOas }
| { ok: false; error: string };

RolePath
Orchestratoragents/email-outreach/cinatra/agent.json
Leafagents/email-recipients/cinatra/agent.json
Leafagents/email-drafts/cinatra/agent.json
Leafagents/email-reviewer/cinatra/agent.json
Leafagents/email-sender/cinatra/agent.json
Global registryagents/_shared/cinatra/components.json
Compiler sourcepackages/agents/src/oas-compiler.ts
Validator sourcepackages/agents/src/validate-agent-json.ts