Skip to content

Agent code export

Status: Current | Last reviewed: 2026-07-08

TrueServe can export selected agents as credential-free code packages for external runtimes. Three code targets exist: Strands (Python), Genkit (TypeScript), and Google ADK (Python). Workflows have their own code target — see Workflow code export.

Entry point

GET /api/pipelines/[pipelineId]/export?format=<target> returns application/zip:

format Target Flag
strands Strands Python package ENABLE_STRANDS_EXPORT / NEXT_PUBLIC_ENABLE_STRANDS_EXPORT
genkit Firebase Genkit TypeScript package ENABLE_CODE_EXPORT / NEXT_PUBLIC_ENABLE_CODE_EXPORT
adk Google ADK Python package ENABLE_CODE_EXPORT / NEXT_PUBLIC_ENABLE_CODE_EXPORT

The route uses the same version-aware lookup as the existing pipeline export endpoint:

  • agent-id:draft resolves the draft version.
  • agent-id:vN resolves the requested published version.
  • agent-id falls back through the current export lookup behavior.

Translation and copilot agents return 422 for every code target. ADK additionally requires a Google-stack conversational stage (see below). The 422 body carries {error, issues[]}; the editor's export modal (components/ExportModal.tsx, opened from the agent editor's Export button) surfaces the issue messages in a toast. The modal presents every enabled format as a selectable card with a description and a "what's included" line, grouped into Configuration (portable / JSON / AgentCore — re-importable) and Code packages (Strands / Genkit / ADK — one-way) — the fetch-based code-package handlers keep the 422 issue list visible.

Deferred: AgentCore SDK. format=agentcore already exists and means raw-config JSON for the config-sync-service (DynamoDB sync path) — it is NOT a code package. A future Bedrock AgentCore SDK code export must use a new format name (e.g. agentcore-sdk); do not repurpose the existing branch.

Shared core

All agent code targets share apps/ai-manager/lib/agent-code-export/core/:

  • types.ts — target-agnostic analysis/report types + AgentExportUnsupportedError (status 422) base class every target error extends.
  • config.ts — pipeline traversal (iterateAgentStages recurses swimlanes + subagent inline pipelines + translation flows), agent-kind inference, model-stage selection, topology warnings, voice-session analysis (analyzeVoiceSession: voice/greeting/VAD params), subagent analysis (analyzeSubagents).
  • prompt.ts — prompt template extraction (pickPromptTemplateWithSource reports which config key supplied the prompt, so a greeting_message used as the prompt fallback is not also emitted as a greeting), mustache-variable + {{TOOL:}}/{{SUBAGENT:}}/{{COMPONENT:}} token collection, preview rendering.
  • tools.ts — saved-tool (MCP/knowledge) + inline-tool (API/Python) analysis with per-target labels. The MCP branch carries tool_filters, parameter_bindings, and cached_tool_schemas through to tool.config; mcpBindingFromTool() is the typed accessor.
  • secrets.ts — env-var registration, header scrubbing, secret_report.json assembly. Credential policy is always no_credentials_exported.
  • db.ts — tool_configs/integrations row loading scoped by the route's tenant context, plus loadComponentRows/collectComponentRowsForExport: A2UI catalog components (name, description, props_schema) for the agent's multimodality.catalogs[], matched by trailing slug (same rule as the runtime's token resolver — built-ins were rescoped shared→tenant, so a stored v1/<slug> URI must still match) and deduped preferring tenant rows. RLS scopes the query; a missing catalog degrades to a warning, never a failed export.
  • analysis.ts — analyzeAgentExport(), the shared analysis pipeline each target parameterizes (targetLabel, modelEnvName, modelLabel). When the caller passes componentRows, {{COMPONENT:}} handling switches to component-aware mode: one AnalyzedComponentTool per catalog component (real description + props schema, function names like show_appointmentsummary), tokens resolve to the tool name, and the generic send_ui_event tool is emitted only for tokens with no catalog row (a2ui_component_unresolved). Without componentRows (Genkit/ADK today) the legacy single-send_ui_event behavior is unchanged.
  • emit-python.ts — pyJsonLiteral/pyJsonLoads, plus readable emitters: pyDictLiteral (hand-editable Python dict literals), pyTripleQuoted (prompts), pyDocArgsFromSchema (schema → doc lines).

Genkit, ADK, the LangGraph workflow export, and Strands (since format v2) all consume the core directly.

Common package contract

Every target zip contains manifest.json (target id, source version, runtime mode, generated file list), secret_report.json, unsupported.json, .env.example, README.md, and the generated source tree. No credentials, tenant settings, conversation history, UI assets, telephony config, or runtime logs are ever exported — required credential surfaces are represented only as env-var names.

Prompt rendering preserves TrueServe token order in every target: {{TOOL:}} → {{SUBAGENT:}} → {{COMPONENT:}} → mustache merge fields, with system built-ins (current date/time, session id, agent id, tenant id) injected per render.

Strands (Python) — format v2

Format v2 (2026-07-08) rewrote the target for fidelity: purpose-built, hand-editable code instead of a generic JSON-interpreting runtime. The old tools/runtime.py interpreter (including its exec() path) and data/tool_specs.json are gone — everything is baked into readable modules.

  • Eligibility: chat + voice agents. Full voice is limited to stages that map to a Strands bidi provider (OpenAI Realtime → BidiOpenAIRealtimeModel, AWS Nova Sonic → BidiNovaSonicModel); cascaded STT→LLM→TTS exports a runnable chat core + a run_voice.py shell (degraded, warned).
  • Split runtimes. Voice agents get a BidiAgent core (build_bidi_agent()) and run_voice.py as the only entrypoint (--text swaps the microphone for keyboard I/O — the no-audio-hardware smoke test); the realtime model id and configured voice are baked in. Chat agents get a persistent multi-turn Agent (build_agent()), a REPL run_chat.py, and a per-session FastAPI server.py; the provider model (OpenAIModel/AnthropicModel/BedrockModel) is emitted with only that agent's branch, temperature included. Voice session params Strands cannot set (vad_threshold, silence_duration_ms, temperature, transcription language) are emitted as a documented comment block in agent.py with the source values.
  • Prompt (prompt.py): the system prompt is a triple-quoted constant with {{TOOL:}}/{{SUBAGENT:}}/{{COMPONENT:}} tokens pre-resolved at export time to the registered tool function names; only {{variable}} merge fields remain (render(variables), missing_variables() warns at startup). A greeting instruction rides the prompt as a final "Conversation opening" section.
  • UI components (tools/ui.py): one module-style TOOL_SPEC tool per catalog component with the component's real a2ui_components.description and verbatim props_schema (readable dict literal + prop-summary comments). All component tools funnel into emit_ui_event(component, props) — the single marked host-integration point (prints JSON envelopes by default; UI_EVENT_HANDLER hook for real UIs). post_send_instruction is appended to the tool result when configured.
  • MCP (tools/mcp_tools.py): when a saved MCP/Knowledge tool has cached_tool_schemas, each (filtered) method becomes a named wrapper tool — advertised schema minus the bound params, parameter_bindings merged server-side on every call, so {{TOOL:}} prompt tokens resolve to real registered names. Without cached schemas the MCPClient is attached directly (with tool_filters={"allowed": [...]}). Clients are lazy singletons with atexit cleanup.
  • Other tools: inline API → self-contained httpx module with url/method/headers/response_path baked as literals (secret headers scrubbed to env vars); inline Python → verbatim source + typed entry wrapper (inline_python_runtime_changed warning); builtin:end_session → functional (tools/session.py sets request_state["stop_event_loop"] = True, which both the chat event loop and the bidi voice loop honor); enabled inline subagents on chat agents → tools/subagents.py (one tool per subagent constructing a nested Agent with the inline prompt/model); disabled/ref/voice subagents → explicit unsupported.json entries; telephony built-ins → unsupported.json; unresolved {{COMPONENT:}} tokens → generic send_ui_event fallback.
  • Deps: voice pins strands-agents[bidi,bidi-io,bidi-openai]>=1.41.0 (PyAudio needs PortAudio — documented in the README); chat uses the provider extra; mcp/httpx/fastapi only when used.
  • Manifest extras: components[] (component → tool mapping) and mcp_tools[] (mode, filtered functions, bound params).

Genkit (TypeScript)

  • Eligibility: chat + voice; voice is always degraded (Genkit has no bidi live-audio API) — chat core + genkit_voice_degraded warning, never a 422.
  • Package: package.json (ESM, genkit + model plugin + @genkit-ai/mcp when needed), tsconfig.json, src/{agent,prompting,chat,server}.ts, src/tools/ (one ai.defineTool module per local tool).
  • Model mapping: google → @genkit-ai/googleai; google-vertex → @genkit-ai/vertexai; openai/anthropic/other → @genkit-ai/compat-oai (anthropic flagged with anthropic_via_compat_oai). Model id rides GENKIT_MODEL.
  • Tools: input schemas are converted to zod source text; inline Python tools cannot run in TS and export as stubs (inline_python_not_portable); MCP/knowledge tools become mcpClient plugin entries with env-var URL/auth.
  • Module: apps/ai-manager/lib/agent-code-export/genkit/.

Google ADK (Python)

  • Eligibility gate (422): the primary conversational stage must be Google-stack — provider google/google-vertex or service in {google-adk, google-adk-agent, google-gemini, google-vertex}. Everything else raises adk_requires_google_stack.
  • Package: pyproject.toml (google-adk), src/trueserve_exported_agent/{agent,prompting}.py, tools/, run_chat.py, run_voice.py. agent.py exposes root_agent, so adk run / adk web work out of the box.
  • Instruction is an InstructionProvider callable, never a plain string — ADK's canonical_instruction runs {var} session-state templating on string instructions, which collides with TrueServe's literal {{mustache}} placeholders (same trick as the runtime's adk.py integration). The callable wraps the shipped prompting.py renderer.
  • Tools: MCP/knowledge → McpToolset with env-var URL/auth; inline API → httpx function tools; inline Python → local code + warning; end_session/send_ui_event → plain function tools.
  • Voice: cascaded topologies degrade with adk_voice_degraded; the model id rides ADK_MODEL.
  • Module: apps/ai-manager/lib/agent-code-export/adk/.

Testing

Each target has focused tests appended to the ai-manager package.json test list (an explicit file list — a test file not listed never runs):

  • lib/agent-code-export/strands/index.test.ts — chat/voice mapping, 422s, token order, zip inventory, secret-free output, component/MCP-binding/voice-session assertions, plus refactor-safety pins: golden SHA-256 byte comparisons of the chat AND voice fixture zips, an iterateAgentStages traversal fixture, and a secret-scrub determinism check. Regenerate the golden maps after an intentional output change with REGENERATE_GOLDEN=1 npm test (prints the new map literals to paste in).
  • lib/agent-code-export/strands/route.test.ts — DI route tests (tenant scoping, flag-off 404, 422 passthrough, version fallback).
  • lib/agent-code-export/genkit/index.test.ts — package shape, provider plugin mapping, degraded voice, 422, zod generation, secret scrubbing, python-stub warnings.
  • lib/agent-code-export/adk/index.test.ts — package shape, Google-stack gate, InstructionProvider emission, vertex acceptance, degraded voice, secret scrubbing.