Implementation Plan: OpenAI Agents SDK Backend — Full Parity (post-MVP)¶
Current contract: The reconciled specification and acceptance matrix supersede conflicting historical assumptions below.
Execution handoff: Use the completion execution plan for current task order, acceptance gates, and progress. This record retains the reconciled design and historical task evidence.
Spec: docs/product-specs/035-openai-agents-backend/spec.md
Builds on: plan-mvp.md (shipped — function tools, real streaming, routing flip, SK agent-path carve)
Scope of this plan: Everything in spec 035 that the MVP deliberately left out, except surfaces
deferred to their own specs by decision below: the P3 hardened/Envoy profile, US8 sandbox mode, and
computer-use (ComputerTool).
Status: Partially implemented. Reconciled on 2026-09-06 against main at b585e80.
Implementation landed in 3805e80 (#338). The task register below supersedes the original
2026-06-10 planning status and the earlier blanket completion claims in todo-full.md.
SDK feasibility notes retain their original 2026-06-10 / 0.17.4 context; this reconciliation
inspected repository source, test definitions, and Git history, not current upstream SDK behavior.
This document audit did not execute tests, live provider calls, samples, or deployments.
Fresh checks run for the wider reconciliation are recorded in reconciliation.md.
Reconciled task register (2026-09-06)¶
Checked acceptance items below mean implementation is supported by inspected source and test coverage, not that tests were rerun. Unchecked checkpoints retain live, build, or broader suite acceptance. Proposed behavior in pending tasks is a requirement, not a shipped capability. Repository paths in this register are relative to the repository root.
| Tasks | Current state | Evidence and remaining work |
|---|---|---|
| A1–A3 | Implemented | models/openai_config.py, models/agent.py, schemas/agent.schema.json, backend _max_turns / _build_run_config, and validators.validate_openai_agents; covered in test_openai_config.py, test_openai_agents_backend.py, test_validators.py. Serve sizing remains I1. |
| B1 | Implemented; live acceptance open | openai_agents_tool_adapters.py and backend RAG initialization; adapter/backend unit tests cover searches, prompt warning, and embedding validation. Grounded live RAG and tool-init endpoint acceptance remain open. |
| C1 | Implemented | openai_agents_mcp.py and test_openai_agents_mcp.py cover transport construction, substitution, WebSocket warning, and static filtering; backend tests cover connection/cleanup. |
| D1–D3 | Implemented (T3, 2026-09-06) | models/openai_config.py (agents), models/tool.py (SkillTool), lib/skills.py, openai_agents_subagents.py, openai_agents_events.py, session tool_events; schema regenerated. Fixture-level handoff acceptance through the real Runner in test_openai_agents_events.py; live handoff remains T10. |
| E1–E2 | Pending | OpenAIConfig has no hooks; no OpenAI YAML hook or guardrail module exists. Budget hooks are implemented, but are not YAML hooks. |
| F1–F3 | Implemented | Model settings, permission filtering, openai_agents_cost.py, and their unit tests exist. Backend catches budget exceptions into error results with partial output. Hosted filtering remains dependent on G1. |
| F4 | Partial; acceptance reopened | openai_agents_fallback.py and its unit tests cover one fallback on 429/5xx and no stream restart after the first event. Tests call the wrapper directly: SDK Runner retry exhaustion and both-attempt trace acceptance are not demonstrated. The wrapper catches primary errors internally, so the claimed Runner-retries-first ordering requires validation and potentially a fix. |
| F5 | Implemented for configured reasoning effort | openai_agents_output.py and backend result extraction have unit coverage. summary="auto" is requested only when openai.effort is set; no-effort reasoning requests do not request a summary. Live structured/thinking acceptance remains K2. |
| G1–G2 | Pending | ToolUnion has no HostedTool; the unsafe opt-in field is defined ahead of runtime enforcement. |
| H1 | Partial; acceptance reopened | openai_agents_tracing.py, _install_tracing_mirror, and tracing tests exist; the Azure tracing-disable call is removed. Processor selection runs only when observability tracing is enabled and is guarded by a process-global installed flag. Validate disabled-observability and mixed-provider/config initialization before claiming unconditional Azure/override upload suppression. |
| I1 | Pending; shared readiness exists | serve/server.py still restricts active-turn caps and startup credential preflight to Anthropic. OpenAI sizing fields have no serve enforcement. |
| I2 | Shared implementation exists; acceptance open | Deploy CLI calls agent_needs_nodejs; Docker template conditionally includes Node and protects corpus/scratch paths. test_dockerfile_hardening.py covers these generic branches. Add OpenAI-configured coverage and verify generated image behavior; the proposed dedicated test file does not exist. |
| I3 | Pending | models/deployment.py retains 1 CPU / 2 GiB defaults; ACA resolved-cap echo still uses floor(cpu * 2). |
| J1 | Pending | No default OpenAI tool-output guardrail exists. The opt-out configuration alone provides no model-visible redaction. |
| J2 | Partial | test_openai_agents_tracing.py::test_credential_tool_output_is_redacted covers OTel redaction. openai_agents_mcp.py substitutes configured env values but does not implement the proposed credential scrub; function-child coverage and opt-out enforcement also remain open. |
| K1 | Unverified / pending | /sample is ignored; no tracked financial-assistant sample or reproducible acceptance record establishes completion. |
| K2 | Partial | tests/integration/test_openai_agents_chat_e2e.py provides credential-gated Azure function-call and text-stream smokes. RAG/tool-init, hosted, handoff, structured-output, and redaction acceptance are not covered by that file. |
| K3 | Partial | docs/guides/openai-backend.md, docs/api/backends.md, and docs/guides/observability.md describe shipped configuration, reasoning, and tracing. The final hooks/guardrails coverage matrix and full-parity documentation depend on pending features. |
These remaining parity tasks are separate from final Semantic Kernel removal: see SK decoupling and LiteLLM migration.
Overview¶
The MVP proved one vertical slice: a provider: openai / azure_openai agent runs the OpenAI
Agents SDK Runner loop, calls Python function tools, and streams under holodeck chat, with
default routing flipped and the SK agent path removed. This plan delivers the rest of the
backend's parity surface:
- the remaining HoloDeck tool types: vectorstore, hierarchical_document, MCP — plus skill tools,
whose
SkillToolmodel is net-new (the spec presumes it exists; it does not — see reconciliations); - subagents (handoffs) and YAML hooks;
- the spec-026 config mappings (
effort,max_budget_usd,fallback_model,disallowed_tools); - hosted tools (
type: hosted); - structured output +
thinkingparity (FR-004); RunConfigplumbing (trace sensitivity, workflow naming);- tracing dual-write;
- serve/deploy parity and the spec-034 hardening phases P1a/P1b/P2a/P2b.
It is organised as vertical, feature-complete slices (model + adapter + backend wiring +
tests per slice) so each phase leaves holodeck chat/test/serve working.
Decisions¶
Confirmed with user 2026-06-09 (1–4) and 2026-06-10 (3 revised; 5–7 added after the SDK review):
- Namespacing → a new top-level
openai:block. Hooks, subagents, permissions, and all backend knobs for this backend live underopenai:(sibling toclaude:).claude.hooks/claude.agentsstay Claude-only — they are NOT read by this backend. Cross-backend portability is achieved by declaring the relevant block per backend, not by sharingclaude.*. (This supersedes spec Open Question 1 — neither option (a) "reuse claude.*" nor theopenai_agents:name; the block is literallyopenai:.) - P3 hardened profile (Envoy sidecar,
deployment.security_profile, egress allowlist, base-URL rewriting) → deferred to a separate cross-backend spec. It exists for no backend today (Claude included); building it here would be net-new shared infra with backend-specific divergence. FR-090…FR-093 are out of this plan. P1a/P1b/P2a/P2b stay in. - US8 sandbox mode (
agent_mode: sandbox) → deferred to a follow-up spec, as a scoping decision. (Rationale corrected 2026-06-10: the first draft claimedUnixLocalSandboxClientdoes not exist in 0.17.4 — that is false; it exists atagents.sandbox.sandboxes.unix_localand imports on non-Windows platforms.) The honest reason for deferral: sandbox mode is a large, security-sensitive surface (manifests, sandbox runtimes, lifecycle management) that deserves its own spec and threat model rather than riding along in a parity plan. FR-094…FR-099 are out of this plan. Theopenai.i_understand_this_is_unsafegate still ships (it gatesCodeInterpreterTool). - Hosted tools allowed on Azure, runtime-gated. The MVP put Azure on the Responses API
(
/openai/v1), so the spec's blanket config-load block (US5 scenario 3) is relaxed: hosted tools are accepted onazure_openai; if a specific tool isn't available on the resource, the SDK surfaces a runtime error that HoloDeck propagates verbatim with a clear hint. (The spec's risk table cites FR-091 for this block — a mis-citation; FR-091 is the P3 egress-allowlist FR, deferred under Decision 2.) reject+ credential redaction → native per-tool guardrails, scoped to HoloDeck-built tools. The SDK'sFunctionTool.tool_input_guardrails/tool_output_guardrails(ToolGuardrailFunctionOutput.reject_content(message)) are the implementation for hookrejectactions and for default credential redaction — synchronous, message-bearing, run continues. Coverage is function/vectorstore/hier-doc/skill tools only: hosted tools execute server-side, and MCP-server tools become SDK-builtFunctionTools HoloDeck never touches (see reconciliations). The first draft'sneeds_approvalsynthesis is dropped:needs_approvalinterrupts the run (RunState.approve()/.reject()+ re-run), which is a human-in-the-loop feature, not an auto-reject — deferred to a follow-up spec if wanted.modifyhook action stays inert in v1 (load warning, no runtime effect) to keep hook semantics symmetric with the Claude backend. Note for later: SDK output guardrails can replace the model-visible tool output, so modify-on-output is implementable when both backends are ready to support it.ComputerToolrejected at config load, computer-use deferred.ComputerToolrequires a live PythonComputer/AsyncComputerimplementation (tool.py— requiredcomputer:field); it cannot be constructed from YAML params.type: hosted, name: ComputerToolfails load with a clear "requires a computer harness — not yet supported" error. Computer-use gets its own follow-up spec (likely alongside sandbox).
Spec ↔ reality reconciliations (verified against installed 0.17.4 source + this repo)¶
These differ from the spec text and/or earlier drafts; the plan follows reality:
- No
Backendenum / nobackend:override / noagent_frameworkbackend exist. Routing is purely bymodel.providerinBackendSelector(src/holodeck/lib/backends/selector.py), and theopenai/azure_openai → openai_agentsflip already shipped in the MVP. → FR-001 and FR-007 are already satisfied; US1 scenario 4 (explicit AF opt-out) is dropped (no AF backend in this repo). SkillTooldoes not exist in the codebase. The spec (line 37, FR-070, SC-001) presumes an "existing HoloDeckSkillTool(spec 023)", butToolUnion(models/tool.py) isVectorstoreTool | FunctionTool | MCPTool | PromptTool | HierarchicalDocumentToolConfig. D2 therefore creates theSkillToolmodel (+ schema regen) before translating it.PromptTool(type: prompt) has no runtime adapter in any backend — it is defined inmodels/tool.pyand referenced nowhere else insrc/. The spec's tool list excludes it. This plan keeps it out of scope but downgrades the openai adapter's unsupported-typeConfigErrorforpromptto a skip-with-warning (B1), so switching providers on an agent that declares one matches the de-facto status quo instead of hard-failing.- MCP-server tools are SDK-built
FunctionTools HoloDeck never sees.agents/mcp/util.py(to_function_tool) converts each MCP tool to aFunctionToolinside the SDK, per run, withtool_input_guardrails=None/tool_output_guardrails=None. HoloDeck only passesAgent(mcp_servers=[...]). Consequence: guardrail-basedreject(E2), credential redaction (J1), and adapter-wrapper failure detection (E1) cannot cover MCP tools in v1 — documented per task and in the K3 matrix. - Directly constructed
FunctionTools have no default failure handling. The SDK's error-string-to-model behavior lives in thefunction_toolfactory's failure-handling invoker (tool.py_build_wrapped_function_tool); HoloDeck constructsFunctionTooldirectly, so an uncaught tool exception propagates and fails the whole run (run_internal/tool_execution.py→UserError). E1's adapter wrapper must catch exceptions itself, firePostToolUseFailure, and return an error string to the model (mirroring the factory's default). - Run-level
InputGuardrailis tripwire-only.GuardrailFunctionOutput(tripwire_triggered)halts execution viaInputGuardrailTripwireTriggered(guardrail.py,run_internal/guardrails.py) — there is no message-and-continue option at the run-input level. Input-matchedrejecthooks therefore abort the turn (HoloDeck catches the exception and surfaces the configured message); only tool-matched rejects continue the run. - The shipped MVP already calls
set_tracing_disabled(True)on the Azure path (openai_agents_backend.py_build_model) — which makes the trace provider returnNoOpTrace/NoOpSpan(tracing/provider.py) and would starve the H1 OTel mirror. H1 removes that call; A2's preflight refactor keeps_build_model's credential checks side-effect-free (noset_tracing_disabled/set_default_openai_keyduring validation). MCPServerStdio/MCPServerSse/MCPServerStreamableHttplive underagents.mcp.*, not at theagentstop level. Confirmed present in 0.17.4, pluscreate_static_tool_filter.needs_approvalis an interrupt, not a synchronous reject (tool.py: "the run will be interrupted and the tool call will need to be approved using RunState.approve() or rejected using RunState.reject()"). Auto-reject maps totool_input_guardrailsinstead (Decision 5). This also covers the MCP server classes'require_approval(it resolves into the generated tool'sneeds_approval) — so MCPrejectcannot be implemented that way without HITL.on_tool_start/on_tool_endfire for local tools only (lifecycle docstrings; invocation sites are inrun_internal/tool_execution.py/tool_actions.py). Hosted tools execute server-side and never trigger AgentHooks/RunHooks tool events or guardrails.ReasoningEffortaccepts"none" | "minimal" | "low" | "medium" | "high" | "xhigh"in the installed client. Spec FR-030/031's "clampmaxtohigh" was written against an older value set; this plan mapsmax → "xhigh"(documented deviation).- Reasoning summaries must be requested.
ReasoningItems carry summaries only whenReasoning(summary=...)is set on the request; effort alone yields empty summaries. F5 setssummary="auto"for reasoning models sothinkinghas content. AgentOutputSchemaBaseis abstract and the SDK's only concrete implementation (AgentOutputSchema) requires a Python type. JSON-schema-dict-driven output (HoloDeck'sresponse_format) needs a small HoloDeck subclass (F5 writes it).RunConfig.trace_include_sensitive_datadefaults to True (env-derived), uploading raw tool inputs/outputs with provider traces. A security-focused plan must set it explicitly (Task A3 ties it toobservability.traces.capture_content).CodeInterpreterTool/ImageGenerationTool/HostedMCPToolrequire structuredtool_configobjects (openai-typesCodeInterpreter/ImageGeneration/Mcp), not flat kwargs — G1 factories build the nested objects from YAMLparams.HostedMCPToolapproval requiresrequire_approvalin itsMcptool_config — theon_approval_requestcallback only fires for tools the provider was told need approval. E2's hosted-MCP reject mapping must set both halves.- The SDK has runner-managed retry (
ModelSettings.retry: ModelRetrySettings(max_retries, backoff, policy)) that fires on the same 429/5xx class as F4's fallback — F4 must define the ordering (retries exhaust first, then fallback). - FR-084 reinterpreted. The FR says function tools'
needs_approvalgates are never weakened; v1 ships noneeds_approvalat all (Decision 5), so the FR is satisfied vacuously, and the enforced invariant becomes: default guardrail gates (J1 redaction, E2 rejects) are never weakened by hosted-tool or permission config. RedactingSpanProcessoris already backend-agnostic (otel_redaction.py) — P2b OTel redaction (FR-088) needs verification only, no new code.security_profile/ Envoy infra does not exist → see Decision 2.
Architecture decisions¶
- One
openai:config model (src/holodeck/models/openai_config.py,OpenAIConfig) mirrors the roleClaudeConfigplays for Claude. It carries sizing (max_concurrent_sessions,session_memory_estimate_mib),max_turns, safety (i_understand_this_is_unsafe), hook/redaction opt-outs,permissions, the spec-026 fields (effort,max_budget_usd,fallback_model,disallowed_tools), plushooksandagentssub-blocks. - Lazy import gate preserved. Every
import agents/import openaistays inside functions/methods of theopenai_agents_*modules (SC-005). Pydantic models inmodels/openai_config.pynever import the SDK. - Tool adapters extend the existing
build_sdk_toolsinopenai_agents_tool_adapters.py. At the MVP baseline it raisedConfigErrorfor every non-function type; each phase below removes one type from that error path and returns the right SDK object. Returns become a tuple(tools, mcp_servers, handoffs)so MCP servers and skill/subagent handoffs flow intoAgent(...). - Reuse the live RAG factories. Vectorstore / hierarchical_document adapters wrap the existing
tool
.search()callables (same objects the Claude adapter wraps inlib/backends/tool_adapters.py), reusingtool_initializer.create_embedding_service. No new embedding code. - One guardrail module (
openai_agents_guardrails.py) owns both hook-rejectinput guardrails (E2) and the default credential-redaction output guardrail (J1) — same mechanism, one attachment point at FunctionTool build time, for HoloDeck-built tools only (see reconciliations for the MCP gap). Observation hooks (log/notify/script) useAgentHooks/RunHooks; failure detection lives in the adapter wrapper. - Cost accountant price table = bundled versioned constant + unknown-model warning (spec Open
Question 3 → v1). Lives in
openai_agents_cost.py. - Unit tests mock the SDK (no network, no key); creds-gated integration smokes exercise live Azure.
Task List¶
Phase A — openai: config block foundation¶
Task A1: OpenAIConfig model + openai: block on Agent¶
Reconciled status: Implemented (source/test evidence; verification not rerun).
Description: New src/holodeck/models/openai_config.py with OpenAIConfig and
OpenAIPermissionsConfig. Add openai: OpenAIConfig | None to the Agent model (sibling to
claude). Fields: max_concurrent_sessions, session_memory_estimate_mib (default 100),
max_turns (default 20), i_understand_this_is_unsafe (default False),
disable_default_hooks, disable_subprocess_env_scrub, permissions (allowed_tools /
disallowed_tools), effort (low|medium|high|max), max_budget_usd, fallback_model,
disallowed_tools. (hooks and agents sub-blocks are added in Phases E and D.)
Acceptance criteria:
- [x] agent.yaml with an openai: block validates; unknown keys rejected (extra="forbid").
- [x] Defaults match the spec (session_memory_estimate_mib=100, max_turns=20).
- [x] schemas/agent.schema.json regenerated to include the openai block; schema still validates.
Verification: tests/unit/models/test_openai_config.py (field defaults, validation);
make type-check; sample loads.
Dependencies: None
Files: src/holodeck/models/openai_config.py, src/holodeck/models/agent.py,
schemas/agent.schema.json
Scope: M
Task A2: Backend consumes OpenAIConfig; side-effect-free validation entrypoint¶
Reconciled status: Implemented (source/test evidence; verification not rerun).
Description: Thread agent.openai into OpenAIAgentsBackend. Pass max_turns to
Runner.run(..., max_turns=...). Add validate_openai_agents(agent) to
lib/backends/validators.py that runs credential preflight + config consistency
(allowed ∩ disallowed empty, safety-gate references) in a single pass, surfacing all errors
together (FR-110). The preflight extracts _build_model's credential checks into a
side-effect-free helper — validation must not trigger _build_model's global mutations
(set_tracing_disabled, set_default_openai_key).
Acceptance criteria:
- [x] max_turns from openai.max_turns reaches Runner.run; default 20 when unset.
- [x] Missing-credential and conflicting-tool errors are collected and raised together.
- [x] Running validation leaves SDK global state untouched (no tracing/key side effects).
- [x] No SDK import occurs at module import time (SC-005 preserved).
Verification: tests/unit/lib/backends/test_openai_agents_backend.py (max_turns wiring);
test_validators.py (collect-all-errors, no-side-effect). make test-unit.
Dependencies: A1
Files: src/holodeck/lib/backends/openai_agents_backend.py,
src/holodeck/lib/backends/validators.py
Scope: S
Task A3: RunConfig plumbing (trace sensitivity, workflow identity)¶
Reconciled status: Implemented (source/test evidence; verification not rerun).
Description: Build a RunConfig for every Runner.run(...) call:
workflow_name=agent.name, group_id=<session id> for session-based runs (invoke_once has no
session and carries workflow_name only), trace_metadata carrying the HoloDeck run context,
and — security-critical — trace_include_sensitive_data = observability.traces.capture_content
(the SDK default is True via env, which would upload raw tool inputs/outputs to
platform.openai.com even while J1 redacts what the model sees). handoff_input_filter /
nest_handoff_history are left at SDK defaults and explicitly documented as unmapped in v1 (see
Out of scope).
Acceptance criteria:
- [x] Every run carries workflow_name; session runs additionally carry group_id.
- [x] capture_content: false (default) → trace_include_sensitive_data=False regardless of env.
- [x] capture_content: true → sensitive payloads included.
Verification: tests/unit/lib/backends/test_openai_agents_backend.py (RunConfig build).
Dependencies: A1
Files: src/holodeck/lib/backends/openai_agents_backend.py
Scope: S
Checkpoint A — Config foundation¶
-
openai:block validates; backend reads sizing/turns; RunConfig wired; validation side-effect-free; full unit suite green; schema valid.
Phase B — Native tool adapters (vectorstore + hierarchical_document)¶
Task B1: Vectorstore + hierarchical_document adapters; prompt downgraded to warning¶
Reconciled status: Partial; see reconciled register for remaining acceptance.
Description: In openai_agents_tool_adapters.py, translate VectorstoreTool and
HierarchicalDocumentToolConfig into SDK FunctionTools that wrap the same .search() callables
the Claude adapter uses (lib/backends/tool_adapters.py), reusing
tool_initializer.create_embedding_service. Remove these two from the "unsupported type"
ConfigError path. Tool name + {query: str} schema match the Claude adapter
({name}_search). Additionally, downgrade type: prompt from ConfigError to skip-with-warning
(no backend has a runtime adapter for it — see reconciliations).
Acceptance criteria:
- [ ] A type: vectorstore tool loads, initialises its store, and is invocable by the agent loop;
results reach the model (mocked run + a creds-gated live check).
- [ ] A type: hierarchical_document tool behaves identically (results joined \n---\n).
- [ ] A type: prompt tool is skipped with a warning; load does not fail.
- [ ] Embedding provider validation (validate_embedding_provider) still fires when these tools
are present.
Verification: tests/unit/lib/backends/test_openai_agents_tool_adapters.py (both types,
mocked search; prompt warning). RAG init path unaffected.
Dependencies: A1
Files: src/holodeck/lib/backends/openai_agents_tool_adapters.py
Scope: M
Checkpoint B — RAG tools work¶
- Vectorstore + hier-doc tools answer a grounded query on an
openai_agentsagent (creds-gated); tool-init endpoints (already backend-agnostic) smoke green.
Phase C — MCP transports (spec 027)¶
Task C1: MCP adapter (stdio / sse / http) → agents.mcp.*¶
Reconciled status: Implemented (source/test evidence; verification not rerun).
Description: Translate MCPTool configs into agents.mcp.MCPServerStdio /
MCPServerSse(params={url, headers}) / MCPServerStreamableHttp(params={url, headers}). Reuse the
env/header ${VAR} substitution + relative-arg resolution from mcp_bridge.py. transport:
websocket → skip with the spec-027 warning. allowed_tools subset →
agents.mcp.create_static_tool_filter(allowed_tool_names=[...]). Return MCP servers as a separate
list so build_sdk_tools hands them to Agent(mcp_servers=[...]). Node.js detection already
exists (validators.agent_needs_nodejs); no change. Note: the SDK converts MCP tools to
FunctionTools internally per run — HoloDeck cannot attach guardrails or failure wrappers to them
(reconciliations; affects E1/E2/J1 coverage).
Acceptance criteria:
- [x] stdio/sse/http each produce the right SDK class with substituted url/headers/env (mocked).
- [x] transport: websocket is skipped with a warning; load does not fail.
- [x] An allowed_tools subset yields a static tool filter.
Verification: tests/unit/lib/backends/test_openai_agents_mcp.py (per-transport).
make type-check.
Dependencies: A1
Files: src/holodeck/lib/backends/openai_agents_tool_adapters.py (may extract
openai_agents_mcp.py if it grows past ~150 lines)
Scope: M
Phase D — Subagents (handoffs) + skill tools¶
Task D1: openai.agents → SDK sub-Agents + parent handoffs¶
Reconciled status: Pending / acceptance not established.
Description: Add agents: dict[str, OpenAISubagentSpec] | None to OpenAIConfig
(OpenAISubagentSpec: description, prompt/prompt_file, tools, model). Build each as an
SDK Agent(name, instructions, handoff_description, tools, model) and set the parent's
handoffs=[...]. Auto-prepend agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX unless
skip_recommended_prefix: true. Validate model: inherit → parent model; reject Claude literals
(sonnet|opus|haiku); allow any other string. tools: null → inherit parent tools.
Handoff-history shaping (handoff_input_filter, nest_handoff_history) stays at SDK defaults in
v1 (documented).
Acceptance criteria:
- [x] Three subagents become three Agents on handoffs; prefix prepended.
- [x] model: sonnet|opus|haiku fails load; inherit uses the parent model; arbitrary id allowed.
- [x] A subagent with no tools inherits the parent's resolved tools.
Verification: tests/unit/lib/backends/test_openai_agents_subagents.py.
Dependencies: A1, B1, C1 (tool resolution must exist to inherit)
Files: src/holodeck/models/openai_config.py,
src/holodeck/lib/backends/openai_agents_subagents.py,
src/holodeck/lib/backends/openai_agents_backend.py
Scope: M
Task D2: SkillTool model (net-new) + skill → handoff target¶
Reconciled status: Pending / acceptance not established.
Description: The spec (FR-070, SC-001) presumes an existing SkillTool; none exists (see
reconciliations). First add SkillTool to models/tool.py + ToolUnion (+ schema regen):
type: skill, inline form (instructions, description, allowed_tools) or file-based form
(path to a directory whose SKILL.md body + frontmatter supply them), per spec-023 FR-022…026 as
restated in spec-035 FR-070. Then translate it into a handoff-target Agent (same machinery as
D1). allowed_tools restricts the skill agent's tool scope.
Acceptance criteria:
- [x] SkillTool validates in YAML (inline + file-based); schema regenerated.
- [x] Inline and file-based skills each become a handoff Agent with matching instructions.
- [x] allowed_tools scopes the skill agent's tools.
Verification: tests/unit/models/test_tool.py (SkillTool validation);
tests/unit/lib/backends/test_openai_agents_tool_adapters.py (skill happy paths).
Dependencies: D1
Files: src/holodeck/models/tool.py, schemas/agent.schema.json,
src/holodeck/lib/backends/openai_agents_tool_adapters.py,
src/holodeck/lib/backends/openai_agents_subagents.py
Scope: M
Task D3: Handoff ToolEvents for AG-UI (FR-006)¶
Reconciled status: Pending / acceptance not established.
Description: During streaming, map AgentUpdatedStreamEvent to subagent_message /
parent_link ToolEvents so the AG-UI panel renders handoffs identically to Claude. Push events
onto the serve tool_event_queue when present (real-time path); fall back to post-hoc otherwise.
Acceptance criteria:
- [x] A handoff run emits subagent_message + parent_link events (scripted-model Runner stream and post-hoc).
- [x] agui.py consumes them with no protocol change (same ToolEvent kinds; HTTP-level check is T7).
Verification: tests/unit/serve/ or test_openai_agents_subagents.py event-shape test.
Dependencies: D1
Files: src/holodeck/lib/backends/openai_agents_backend.py
Scope: S
Checkpoint D — Handoffs + skills¶
- A multi-agent handoff scenario runs (scripted fixture); skill tool routes; subagent events emitted. Suite green. Live handoff: T10.
Phase E — YAML hooks (spec 028)¶
Task E1: openai.hooks model + observation actions + failure path¶
Reconciled status: Pending / acceptance not established.
Description: Add hooks to OpenAIConfig (event + matcher + action shape mirroring spec 028).
New openai_agents_hooks.py builds AgentHooks/RunHooks from the config: PreToolUse /
PostToolUse with log|notify|script → on_tool_start/on_tool_end (observation-only — local
tools only, and MCP-server tools are observable here but not wrappable); PostToolUseFailure →
HoloDeck's adapter wrapper around the on_invoke callables it builds catches tool exceptions
itself, fires the configured action, and returns an error string to the model (directly
constructed FunctionTools have no SDK default failure handling — an uncaught exception fails the
whole run; see reconciliations). PostToolUseFailure does not fire for MCP-server tools (SDK
builds their invokers) — load warning when a failure matcher targets MCP tool names. Stop →
on_end; Notification → on_llm_start/on_llm_end; SessionStart → invoked at
create_session().
Acceptance criteria:
- [ ] Each event fires its log/notify/script action at the mapped lifecycle point (mocked).
- [ ] A raising function tool fires PostToolUseFailure and the model receives an error string;
the run does not abort.
- [ ] Hook chain ordering: internal hooks (tool tracking, redaction) before user hooks; declaration
order preserved (FR-051).
- [ ] A hook matcher that can only match hosted tools (or a failure matcher on MCP tool names)
loads with a "will never fire" warning.
Verification: tests/unit/lib/backends/test_openai_agents_hooks.py.
Dependencies: A1
Files: src/holodeck/models/openai_config.py,
src/holodeck/lib/backends/openai_agents_hooks.py,
src/holodeck/lib/backends/openai_agents_backend.py
Scope: M
Task E2: reject (→ tool guardrails) + modify (inert)¶
Reconciled status: Pending / acceptance not established.
Description: New openai_agents_guardrails.py. Reject mapping by target:
- HoloDeck-built function/local tool (function, vectorstore, hier-doc, skill) → attach a
tool_input_guardrail returning ToolGuardrailFunctionOutput.reject_content(<configured
message>) — synchronous, the run continues, the model sees the rejection message. (The
guardrail context exposes tool_name/tool_arguments — sufficient for spec-028 matchers.)
- HostedMCPTool → set require_approval for the matched tool names inside the Mcp
tool_config and wire on_approval_request to return a rejection with the configured
message (both halves required — the callback only fires for tools flagged as needing approval).
- MCP-server tools (agents.mcp.*) → fail load with a clear error: HoloDeck cannot attach
guardrails (SDK builds those FunctionTools internally), and the server classes'
require_approval resolves into needs_approval interrupts — i.e. HITL, excluded by
Decision 5.
- Other hosted tools (WebSearch/FileSearch/CodeInterpreter/ImageGeneration — server-side, no
guardrail path) → fail load with a clear error.
- Input-matched rejects → entry-agent InputGuardrail. This is tripwire-only: the turn
aborts via InputGuardrailTripwireTriggered; the backend catches it and surfaces the configured
message as the turn's error result. Distinct semantics from tool rejects (run does NOT
continue) — documented in the K3 matrix.
action: modify → load succeeds with a not yet supported on openai_agents warning (inert,
Decision 6). Phase ordering note: the function-tool and input paths land with E1/B1; the
hosted-tool paths require G1 and are verified at Checkpoint G.
Acceptance criteria:
- [ ] A reject on a function tool blocks the call via input guardrail; the model receives the
configured message; the run continues (no interrupt).
- [ ] An input-matched reject aborts the turn with the configured message as the error result.
- [ ] A reject on an MCP-server tool fails load with a clear error.
- [ ] (After G1) A reject on a HostedMCPTool sets require_approval + rejects via
on_approval_request; a reject on WebSearchTool (et al.) fails load.
- [ ] A modify action loads with a warning and has no runtime effect.
Verification: tests/unit/lib/backends/test_openai_agents_guardrails.py (reject paths),
test_openai_agents_hooks.py (modify warning).
Dependencies: E1 (function/input/MCP-error paths); G1 (hosted-tool paths only)
Files: src/holodeck/lib/backends/openai_agents_guardrails.py,
src/holodeck/lib/backends/openai_agents_hooks.py
Scope: M
Checkpoint E — Hooks (function-tool + input scope; hosted paths at Checkpoint G)¶
- log/notify/script observation works; failure path fires + returns error string; reject gates a function tool without interrupting the run; input reject aborts with message; MCP reject fails load clearly; modify warns; ordering correct.
Phase F — Spec-026 config mappings (US4) + parity gaps¶
Task F1: effort → ModelSettings(reasoning=...)¶
Reconciled status: Implemented (source/test evidence; verification not rerun).
Description: Map openai.effort to ModelSettings(reasoning=Reasoning(effort=<value>)) for
reasoning models. max → "xhigh" (supported by the installed client's ReasoningEffort
literal; documented deviation from FR-030/031's stale clamp-to-high wording). Fold into
_build_model_settings.
Acceptance criteria:
- [x] effort: high + gpt-5 → reasoning.effort="high".
- [x] effort: max → reasoning.effort="xhigh" (no warning needed; documented mapping).
Verification: tests/unit/lib/backends/test_openai_agents_backend.py (settings build).
Dependencies: A1
Files: src/holodeck/lib/backends/openai_agents_backend.py
Scope: S
Task F2: disallowed_tools config-time filter¶
Reconciled status: Implemented (source/test evidence; verification not rerun).
Description: Remove named tools from the resolved Agent.tools + mcp_servers at build time;
allowed ∩ disallowed non-empty → load fail (FR-034). For hosted tools, refuse to construct a
disallowed one.
Acceptance criteria:
- [x] A disallowed tool is absent from the built agent.
- [x] A name in both allow + disallow fails load with the spec message.
Verification: tests/unit/lib/backends/test_openai_agents_permissions.py.
Dependencies: A1, B1, C1
Files: src/holodeck/lib/backends/openai_agents_tool_adapters.py,
src/holodeck/lib/backends/openai_agents_backend.py
Scope: S
Task F3: max_budget_usd → cost-accountant RunHooks¶
Reconciled status: Implemented (source/test evidence; verification not rerun).
Description: New openai_agents_cost.py: a RunHooks that accumulates spend from run usage ×
bundled per-model price table on each LLM end (Usage.request_usage_entries supports per-request
math); on exhaustion raise BackendBudgetExceededError (new error) carrying partial response +
accumulated cost (FR-032). Unknown model → warning + no-op (degrade, don't crash).
Acceptance criteria:
- [x] Cost hooks raise BackendBudgetExceededError with partial response; the backend converts
this into an error ExecutionResult (streaming surfaces the error after emitted deltas).
- [x] An unknown model logs a warning and does not enforce.
Verification: tests/unit/lib/backends/test_openai_agents_cost.py.
Dependencies: A1
Files: src/holodeck/lib/backends/openai_agents_cost.py,
src/holodeck/lib/errors.py, openai_agents_backend.py
Scope: M
Task F4: fallback_model → wrapping Model provider¶
Reconciled status: Partial; see reconciled register for remaining acceptance.
Description: New wrapping Model (or ModelProvider) that catches the retryable set (429,
5xx) and re-issues against fallback_model; both attempts visible in the trace (FR-033). Define
the ordering against the SDK's runner-managed retry: ModelSettings.retry
(ModelRetrySettings(max_retries, backoff, policy)) exhausts on the primary model first; the
fallback wrapper engages only on the final failure (no double-fallback, no fallback mid-retry).
Pinned to function/MCP tools; documented caveat for hosted tools that need Responses on the
fallback.
Acceptance criteria:
- [ ] A primary 429 routes the retry to the fallback model after primary retries exhaust; a
non-retryable error propagates.
- [ ] Both attempts appear in the trace.
- [ ] Retry/fallback ordering is unit-tested (retry exhaust → one fallback attempt).
Verification: tests/unit/lib/backends/test_openai_agents_fallback.py (wrapper behavior) and
test_openai_agents_backend.py (wiring); Runner retry-order and trace-attempt acceptance remain open.
Dependencies: A1, F1 (shares model-build)
Files: src/holodeck/lib/backends/openai_agents_backend.py (or openai_agents_fallback.py)
Scope: M
Task F5: Structured output + thinking parity (FR-004)¶
Reconciled status: Implemented (source/test evidence; verification not rerun).
Description: Wire the existing Agent.response_format (dict JSON schema | str path | None,
models/agent.py) to SDK Agent(output_type=...). AgentOutputSchemaBase is abstract and the
SDK's concrete AgentOutputSchema requires a Python type, so write a small HoloDeck subclass
(JSONSchemaOutputSchema(AgentOutputSchemaBase): name, json_schema, strict flag, validate_json
via jsonschema). Populate ExecutionResult.structured_output from result.final_output when an
output type is set. Populate ExecutionResult.thinking from ReasoningItems in the run output
(the MVP backend hardcoded thinking=""; extraction now exists) — and set
Reasoning(summary="auto") for reasoning models (coupled with F1's effort mapping), since
summaries are only emitted when requested. Cover both invoke_once and streaming.
Acceptance criteria:
- [x] A response_format JSON schema yields structured_output as a parsed dict; absent →
None (current behavior).
- [x] A reasoning-model run (with summary requested) populates thinking; non-reasoning models
leave it empty.
Verification: tests/unit/lib/backends/test_openai_agents_backend.py (output schema +
thinking extraction, mocked run); creds-gated live check in K2.
Dependencies: A1, F1 (shares _build_model_settings)
Files: src/holodeck/lib/backends/openai_agents_backend.py
Scope: M
Checkpoint F — Config additions + parity gaps¶
- effort/disallowed/budget/fallback each behave per their FR; structured output + thinking populated; suite green.
Phase G — Hosted tools (US5)¶
Task G1: HostedTool model + 5 tool factories¶
Reconciled status: Pending / acceptance not established.
Description: Add HostedTool (type: hosted, name selecting the SDK class, params) to
ToolUnion in models/tool.py. Factory builds WebSearchTool / FileSearchTool /
CodeInterpreterTool / ImageGenerationTool / HostedMCPTool. CodeInterpreterTool,
ImageGenerationTool, and HostedMCPTool require structured tool_config objects
(openai-types CodeInterpreter / ImageGeneration / Mcp) — the factory constructs them from
the YAML params mapping (e.g. container spec for code interpreter), with clear errors for
missing required sub-fields. name: ComputerTool → config-load error: "requires a computer
harness — not yet supported" (Decision 7). Allowed on both providers (Decision 4); a tool
unavailable on the live Azure resource surfaces a runtime error with a clear hint (not a
config-load block). Unblocks E2's hosted-tool reject paths.
Acceptance criteria:
- [ ] type: hosted, name: WebSearchTool → WebSearchTool(); FileSearchTool passes
vector_store_ids/max_num_results.
- [ ] CodeInterpreterTool params build the required nested tool_config (container spec);
missing container → clear config error (not a bare TypeError).
- [ ] Unknown hosted name → clear config error; ComputerTool → the Decision-7 error.
- [ ] On azure_openai, hosted tools load; a runtime unsupported error is propagated verbatim.
Verification: tests/unit/lib/backends/test_openai_agents_tool_adapters.py (per hosted tool).
Dependencies: A1
Files: src/holodeck/models/tool.py, schemas/agent.schema.json,
src/holodeck/lib/backends/openai_agents_tool_adapters.py
Scope: M
Task G2: Safety gate for CodeInterpreterTool (P1b)¶
Reconciled status: Pending / acceptance not established.
Description: Auto-disallow CodeInterpreterTool unless
openai.i_understand_this_is_unsafe: true; loading without the opt-in emits the canonical
migration error (FR-083). (ComputerTool is unconditionally rejected by G1, so the gate no longer
covers it.) Default guardrail gates (J1 redaction, E2 rejects) are never weakened by hosted-tool
config (FR-084 as reinterpreted — see reconciliations).
Acceptance criteria:
- [ ] Declaring CodeInterpreterTool without the opt-in fails load with the canonical error.
- [ ] With the opt-in (and a valid container tool_config), it constructs.
Verification: tests/unit/lib/backends/test_openai_agents_permissions.py.
Dependencies: G1, A1
Files: src/holodeck/lib/backends/openai_agents_tool_adapters.py,
src/holodeck/lib/backends/validators.py
Scope: S
Checkpoint G — Hosted tools (+ E2 hosted paths)¶
- 5 hosted tools declarable (nested configs built from YAML);
ComputerToolcleanly rejected; safety gate enforced; Azure load works (runtime-gated); E2'sHostedMCPToolreject + hosted load-fail paths verified.
Phase H — Tracing (US7)¶
Task H1: OTel-mirroring TracingProcessor + processor-list management¶
Reconciled status: Partial; see reconciled register for remaining acceptance.
Description: New openai_agents_tracing.py: a TracingProcessor that mirrors SDK trace events
into the OTel pipeline. Remove the MVP's set_tracing_disabled(True) call in
_build_model (Azure path) — it makes the provider return NoOpTrace/NoOpSpan and starves
every processor, including the mirror. Processor configuration happens once at backend
initialize(), before any run emits spans (the default platform exporter is lazily registered
and set_trace_processors does not flush what the replaced processor already queued):
provider: openai → agents.add_trace_processor(mirror) (default platform.openai.com exporter
retained alongside the mirror); provider: azure_openai → agents.set_trace_processors([mirror])
(replaces the default exporter — spans keep flowing, nothing uploads). Add
observability.disable_provider_tracing: bool (default False): when true, use
set_trace_processors([mirror]) regardless of provider. Sensitive-payload inclusion in uploaded
traces is governed by A3 (trace_include_sensitive_data). Verify RedactingSpanProcessor scrubs
the mirrored spans (FR-088 — expected no code).
Acceptance criteria:
- [ ] The set_tracing_disabled(True) call is gone; Azure runs deliver spans to the mirror
(regression test against the NoOp failure mode).
- [ ] provider: openai → OTel mirror receives spans AND upload retained (unless override).
- [ ] provider: azure_openai → OTel mirror receives spans; no default exporter registered.
- [ ] disable_provider_tracing: true suppresses upload for either provider — mirror still
receives spans.
- [ ] A credential-shaped tool.output.* span attribute is redacted before export.
Verification: tests/unit/lib/backends/test_openai_agents_tracing.py.
Dependencies: A1, A3
Files: src/holodeck/lib/backends/openai_agents_tracing.py,
src/holodeck/models/observability.py, openai_agents_backend.py
Scope: M
Phase I — Serve / deploy parity + P1a / P2a hardening (US2 / US6)¶
Task I1: Serve active-turn cap, 429, readiness, config echo¶
Reconciled status: Pending / acceptance not established.
Description: In serve/server.py, derive an active-turn semaphore for openai_agents from
openai.max_concurrent_sessions (explicit) or floor(memory_mib / session_memory_estimate_mib)
(default 100). Overflow → 429 + Retry-After + problem+json type:
…/session-cap-exceeded (FR-081). /ready exists but currently returns lifecycle-only HTTP 200. Require HTTP 503 until backend prerequisites and required tool initialization succeed, then HTTP 200; include shutdown and verified existing tool data (spec D10). Add
backend credential preflight to serve startup; add an "OpenAI Agents" resolved-config echo section.
Acceptance criteria:
- [ ] Concurrent in-flight turns are capped at the resolved value; overflow → 429 + Retry-After.
- [ ] Idle sessions do not consume slots (cap is on active turns, FR-014).
- [ ] Startup echoes the resolved cap + derivation; missing creds exit non-zero at startup (FR-012).
Verification: tests/unit/serve/test_session_semaphore_openai_agents.py; serve smoke.
Dependencies: A2
Files: src/holodeck/serve/server.py, src/holodeck/cli/commands/serve.py
Scope: M
Task I2: Dockerfile pure-Python branch (P2a)¶
Reconciled status: Partial; see reconciled register for remaining acceptance.
Description: deploy/dockerfile.py + the deploy CLI already gate Node.js on
agent_needs_nodejs (stdio MCP only). Confirm the openai_agents path emits a pure-Python
image by default and only installs Node when an MCP stdio server needs it (FR-011/085). Corpus
chmod a-w + /tmp / /var/holodeck/work EmptyDir already exist — verify they apply.
Acceptance criteria:
- [ ] An openai_agents agent with no Node-MCP → Dockerfile has no apt-get install nodejs.
- [ ] An agent with a Node stdio MCP → Node is installed.
- [ ] Corpus dirs read-only; scratch dirs writable.
Verification: tests/unit/deploy/test_dockerfile_generation_openai_agents.py.
Dependencies: A1
Files: src/holodeck/deploy/dockerfile.py (likely verify-only),
src/holodeck/cli/commands/deploy.py
Scope: S
Task I3: ACA default sizing 1 CPU / 2 GiB + provider-aware session-cap echo (P1a)¶
Reconciled status: Pending / acceptance not established.
Description: In deploy/deployers/azure_containerapps.py, retain supported cpu=1.0 / memory=2Gi
when model.provider ∈ {openai, azure_openai} (spec D11), and make the resolved
session-cap echo provider-aware (floor(memory_mib / 100) for openai_agents) instead of the
Claude cpu*2 formula (FR-080).
Acceptance criteria:
- [ ] An openai_agents deploy defaults to 1 CPU / 2 GiB; echo reports "20 (derived from 2048 MiB @
100 MiB/session)".
- [ ] Claude sizing is unchanged.
Verification: tests/unit/deploy/test_aca_template_openai_agents.py.
Dependencies: A1
Files: src/holodeck/deploy/deployers/azure_containerapps.py,
src/holodeck/models/deployment.py
Scope: S
Checkpoint I — Serve/deploy parity¶
-
holodeck serve+deploy buildwork on an openai_agents agent; pure-Python image; 429 cap; /health + /ready behave; sizing echo correct.
Phase J — P2b credential redaction + subprocess scrub (US6)¶
Task J1: Default credential-redaction output guardrail¶
Reconciled status: Pending / acceptance not established.
Description: In openai_agents_guardrails.py (shared with E2), attach a default-on
tool_output_guardrail to every HoloDeck-built FunctionTool (function, vectorstore,
hier-doc, skill) that scrubs the 5 credential patterns (anthropic key, AWS access key, GitHub
token, JWT, Bearer) from tool output before the model sees it: clean output → allow();
pattern found → reject_content(<redacted text>) (the SDK replaces the model-visible tool result
with the message, and the redacted value is also what lands in the span output). Reuse
redact_credentials() from claude_hooks.py. Opt-out: openai.disable_default_hooks: true with
the loud warning. Coverage note (documented + tested): hosted tools execute server-side, and
MCP-server tool outputs flow through SDK-built FunctionTools HoloDeck cannot wrap — both bypass
this redaction (K3 matrix; SC-009 scope is HoloDeck-built tools; J2's OTel-layer redaction still
covers MCP/hosted span attributes).
Acceptance criteria:
- [ ] A tool returning sk-ant-api03-… is seen by the model as [REDACTED:anthropic-key] (via
the output guardrail, not return-value mutation).
- [ ] Clean outputs pass through unmodified (allow() path).
- [ ] Opt-out disables it with a warning; OTel redaction (J2) still runs.
Verification: tests/unit/lib/backends/test_openai_agents_guardrails.py (redaction + opt-out).
Dependencies: B1, E2 (shared guardrail module)
Files: src/holodeck/lib/backends/openai_agents_guardrails.py,
src/holodeck/lib/backends/openai_agents_tool_adapters.py
Scope: M
Task J2: Subprocess env scrub + OTel-redaction verification¶
Reconciled status: Partial; see reconciled register for remaining acceptance.
Description: When the agent has MCP stdio servers or function tools that shell out, default-on a
subprocess env scrub stripping OpenAI/Azure/Anthropic credential vars from children; opt-out
openai.disable_subprocess_env_scrub: true (FR-089). Verify RedactingSpanProcessor already
covers openai_agents spans (FR-088 — expected no code beyond a test).
Acceptance criteria:
- [ ] A spawned MCP stdio child does not inherit the scrubbed credential vars.
- [ ] An openai_agents span's tool.output.* credential value is redacted (regression test).
Verification: tests/unit/lib/backends/test_openai_agents_hooks.py;
tests/unit/lib/backends/test_otel_redaction.py (add openai case).
Dependencies: C1
Files: src/holodeck/lib/backends/openai_agents_hooks.py (or a small subprocess wrapper)
Scope: S
Checkpoint J — Hardening P2b¶
- Synthetic prompt-injection test (SC-009, HoloDeck-built-tool scope) passes: model sees redacted value AND span is scrubbed AND (A3) the uploaded trace excludes sensitive payloads by default.
Phase K — Sample, integration smokes, docs¶
Task K1: sample/financial-assistant/openai¶
Reconciled status: Pending / acceptance not established.
Description: Create the sample fresh — there is no in-tree financial-assistant sample to copy
(sample/ currently holds research-agent / test-openai-sdk-agent / test-vec, and /sample is
gitignored, so nothing under it is exercised by CI). Model it on the historical
financial-assistant layout (vectorstore + function tools) with model.provider: azure_openai (or
openai) + model.name. Proves SC-001. Verification is manual and creds-gated only.
Acceptance criteria:
- [ ] holodeck test on the sample runs the full tool loop (creds-gated, manual).
Verification: Manual creds-gated run.
Dependencies: B1, C1
Files: sample/financial-assistant/openai/** (untracked)
Scope: S
Task K2: Creds-gated integration smokes¶
Reconciled status: Partial; see reconciled register for remaining acceptance.
Description: Add integration tests (skip without creds): tool-init endpoints, a hosted-tool
turn, a handoff scenario, a structured-output turn (F5), and the redaction e2e. Mirrors the spec's
test list (minus P3/sandbox/computer-use).
Acceptance criteria:
- [ ] Each skips cleanly without creds; passes with Azure creds.
Verification: make test-integration with creds; skip-clean without.
Dependencies: D1, E2, F5, G1, J1
Files: tests/integration/test_openai_agents_*.py
Scope: M
Task K3: Docs + per-backend semantics matrix¶
Reconciled status: Partial; see reconciled register for remaining acceptance.
Description: Backend docs for openai_agents (openai: block reference); hooks per-backend
semantics matrix: reject via tool input guardrails (synchronous, run continues) vs Claude's
PreToolUse deny; input-matched reject aborts the turn (tripwire) vs tool-matched (run
continues); modify inert on both; coverage table — hosted tools AND MCP-server tools bypass
hooks, guardrails, and model-visible redaction (OTel-layer redaction still applies);
effort: max → xhigh mapping; FR-084 reinterpretation; tracing behaviour incl.
disable_provider_tracing + trace_include_sensitive_data/capture_content coupling. Note in
docs/security/aca-limitations.md that P3 hardened, US8 sandbox, and computer-use are deferred
to follow-up specs; migration note that hooks/subagents for this backend live under openai:,
not claude:.
Acceptance criteria:
- [ ] Docs describe the openai: block, hosted tools, the hook/guardrail coverage table (incl.
MCP gap), tracing behaviour, and the deferred surfaces.
Verification: Docs build / link check; grep for stale openai_agents: block references.
Dependencies: all prior
Files: docs/api/backends.md, docs/guides/*, docs/security/aca-limitations.md, AGENTS.md
Scope: S
Checkpoint K — Complete¶
-
make format lint type-check securityclean;make test(parallel) green. - SC-001/003/004/006/007/009 met by tests (SC-009 scoped to HoloDeck-built tools); sample runs end-to-end (creds-gated).
- Review with user.
Dependency graph (summary)¶
A1 (openai: config) ──┬── A2 (backend consumes + side-effect-free validate) ── I1 (serve)
├── A3 (RunConfig) ── H1 (tracing; removes MVP set_tracing_disabled)
├── B1 (vectorstore/hier-doc + prompt warning) ──┐
├── C1 (MCP) ──────────────────────────────────┤
│ ├── D1 (subagents) ── D2 (SkillTool model + skill) ── D3 (AG-UI events)
├── E1 (hooks + failure wrapper) ── E2 (reject via guardrails; hosted paths ←─ G1)
│ └── J1 (redaction guardrail) ←─ needs B1
├── F1 (effort) F2 (disallowed) F3 (budget) F4 (fallback) F5 (structured/thinking ←─ F1)
├── G1 (hosted ×5) ── G2 (safety gate)
├── I2 (dockerfile) I3 (sizing)
└── C1 → J2 (env scrub)
K1/K2/K3 depend on the feature phases they exercise.
Parallelisable after A1: A3, B, C, F, G, I are largely independent. E2's hosted paths need G1 (verified at Checkpoint G); J1 needs E2 + B1. D needs B/C (tool inheritance). Land in separate PRs per phase.
Risks and mitigations¶
| Risk | Impact | Mitigation |
|---|---|---|
build_sdk_tools signature change (now returns tools + mcp_servers + handoffs) ripples into the backend |
Med | Single small refactor in A2/B1; covered by existing adapter tests before adding types |
| Hosted tools on Azure fail at runtime in confusing ways (Decision 4) | Med | Propagate the SDK error verbatim + a "this hosted tool may be unavailable on your Azure resource" hint; document in K3 |
| Hosted and MCP-server tools silently bypass hooks/guardrails/model-visible redaction | High | Load warnings on unreachable matchers (E1); reject on MCP tools fails load (E2); explicit coverage table in K3; SC-009 scoped; OTel-layer redaction (J2) still covers their spans |
max_budget_usd price table staleness |
Low | Bundled constant + unknown-model warning (no crash); documented update path |
Guardrail reject_content semantics differ subtly from Claude's PreToolUse deny; input rejects abort the turn |
Low | Document both in the per-backend matrix (K3) |
Fallback wrapper double-retries with ModelSettings.retry |
Med | F4 defines ordering (primary retries exhaust → one fallback attempt); unit-tested |
Cost/fallback wrapping Model interferes with Responses/reasoning models |
Med | Pin fallback to function/MCP; unit-test against a mocked reasoning model; caveat in docs |
Nested hosted-tool tool_config objects drift with openai-types upgrades |
Low | Factories isolate construction in one module; per-tool unit tests catch signature changes |
| Trace-processor swap timing leaks early spans to the platform exporter | Low | H1 configures processors at backend initialize() before any run emits spans |
| Serve session-cap divergence from Claude (active turns vs open sessions) confuses operators | Low | Echo the derivation at startup (I1); document the in-process rationale |
Out of scope (deferred to follow-up specs)¶
- P3 hardened profile — Envoy credential sidecar,
deployment.security_profile, egress allowlist,OPENAI_BASE_URL/AZURE_OPENAI_ENDPOINTrewriting (FR-090…093). Net-new cross-backend infra; own spec covering Claude + openai_agents. - US8 sandbox mode —
agent_mode: sandbox,SandboxAgent/Manifest, sandbox clients (FR-094…099). Deferred as a scoping decision (the surface exists in 0.17.4, includingUnixLocalSandboxClient); deserves its own spec + threat model. - Computer-use —
ComputerToolneeds a liveComputer/AsyncComputerharness; own follow-up spec (Decision 7). - Human-in-the-loop tool approval —
needs_approval+RunState.approve()/.reject()resume loop (including MCP serverrequire_approval, which resolves to the same interrupt). A distinct interactive feature, not an implementation detail ofreject(Decision 5). - Guardrail coverage for MCP-server tools — requires an SDK attachment point on its
internally built
FunctionTools (or upstream feature request); v1 documents the gap and fails load onreject-matching MCP tools. modifyhook action implementation — v1 inert + warning (Decision 6; SDK output guardrails make modify-on-output feasible later).type: promptruntime support — no backend has an adapter today; openai_agents skips with a warning (B1) to match the status quo.- Unmapped SDK surfaces, explicitly disposed (not accidental omissions):
tool_use_behavior/reset_tool_choice/ModelSettings.tool_choice(tool-loop policy), agent/runoutput_guardrailsbeyond F5,handoff_input_filter/nest_handoff_history(handoff history shaping),tool_error_formatter/tool_not_found_behavior. All stay at SDK defaults in v1; revisit on demand. - Live
provider: openaivalidation — coded to spec, validated via mocks until a key exists (carried over from the MVP). - Cross-backend hooks/subagents namespace unification — this plan keeps
openai:andclaude:separate by Decision 1.
Open questions (resolved in this plan)¶
- Namespacing → new top-level
openai:block (Decision 1). - P3 / sandbox / computer-use → deferred (Decisions 2, 3, 7).
- Azure hosted tools → allowed, runtime-gated (Decision 4).
rejectmechanism → native tool guardrails for HoloDeck-built tools;on_approval_requestfor HostedMCPTool; load-fail for MCP-server + other hosted tools; tripwire abort for input matchers (Decision 5).- Price-table source → bundled constant + unknown-model warning (spec Open Q3, v1).
modifyaction → inert + warning (spec Open Q2 + Decision 6, v1).effort: max→"xhigh"(reconciliation vs FR-030/031).