Implementation Plan: HoloDeck Agents on Temporal (spec 040)¶
Overview¶
Build the four deliverables of docs/product-specs/040-holodeck-temporal/spec.md: an
activity library that turns an EdgeNode into a Temporal activity (D1), gate
validation inside the activity (D2), deterministic helpers that pass Temporal's
workflow sandbox (D3), and a holodeck worker CLI that hosts agents from a
worker.yaml (D4). The 036 primitives (edge.py, table_eval.py, feel.py,
models/workflow.py, models/decision_table.py) are reused, never rewritten.
There is deliberately no no-code way to author a workflow. worker.yaml
registers agents as named activities; the workflow that calls them is always
user-authored Temporal Python.
Architecture Decisions (grilling session, 2026-08-29)¶
All decisions were put to the user and confirmed. SDK facts were verified
against a live install of temporalio 1.32.0 (closures as activities,
pydantic_data_converter, SandboxedWorkflowRunner.prepare_workflow,
ApplicationError mapping, activities-only workers, TracingInterceptor
span nesting).
| # | Decision |
|---|---|
| 1 | The activity factory takes an EdgeNode (id + edge.agent + gate.schema). Gate is mandatory by construction; resolve_agent_path + load_gate_schema are the only path/schema seams. |
| 2 | No ungated agent activity exists in v1. An escape hatch is an ask-first decision. |
| 3 | Activity input is a typed Pydantic model: AgentActivityInput {message: str, context: dict \| None}. |
| 4 | Activity return is an envelope: AgentActivityResult {output: <gate-validated dict>, token_usage, num_turns, agent_id}. output is the canonical value (FR-008); raw model text never crosses. |
| 5 | Stateless: one activity call = one invoke_once. No AgentSession continuity in v1. |
| 6 | Retryability is decided activity-side with typed errors. Gate failure → retryable GateValidationError; authoring faults (bad agent.yaml, unloadable gate, path escape) → ApplicationError(non_retryable=True); backend transport / model is_error → retryable. SC-003's model-vs-authoring split, translated to Temporal. |
| 7 | Decision tables load at workflow-module import time (policy-as-code, versioned with workflow code). Not loaded in activities, not passed through history. |
| 8 | D4 config is worker.yaml (Pydantic model) + env-var overrides + --config/--task-queue flags. |
| 9 | Edge-node entries are declared inline in worker.yaml under nodes:; paths resolve relative to the worker.yaml directory through resolve_agent_path. nodes: is registration only — zero control flow. |
| 10 | (amended 2026-08-29 after Codex review of PR #366) Timeout/retry options are caller-side in Temporal: they ride the workflow's execute_activity command, and the caller must set start_to_close_timeout or schedule_to_close_timeout — no server default exists. ActivityParameters is therefore a workflow-side, sandbox-safe helper (ships with the D3 surface) that expands into execute_activity kwargs; worker.yaml carries no execution timeouts (registration only). heartbeat_timeout is unsupported in v1 — the activity does not heartbeat, and exposing the knob without heartbeats invites concurrent duplicate LLM calls. |
| 11 | Activity name = edge-node id (replay-load-bearing; rename-safe against file moves). |
| 12 | temporalio ships only in the holodeck[temporal] extra. Import guard gives a clear install message. |
| 13 | Exact pin temporalio==1.32.0 — the sandbox-safety unit test leans on APIs marked "not yet stable". Same policy precedent as bkflow-feel. Loosen when the sandbox surface stabilizes. |
| 14 | Developer API is both layers: factory functions are the testable core; HoloDeckPlugin (a SimplePlugin that sets the pydantic data converter and registers activities) is sugar over them. |
| 15 | temporalio.contrib.pydantic.pydantic_data_converter is required and set by the plugin; payload models stay Pydantic. |
| 16 | Sandbox-safety testing is two layers: SandboxedWorkflowRunner.prepare_workflow as a fast serverless unit test, plus Worker-init validation in the integration suite as the public-API backstop. |
| 17 | OTel: the activity emits the existing GenAI spans from the agent's observability config; holodeck worker also enables TracingInterceptor so GenAI spans nest under Temporal's activity spans. The experimental OpenTelemetryPlugin/ReplaySafe providers are not adopted. |
| 18 | Sample is the hardship-determination story (evidence-extractor agent → DMN table → letter-writer agent), committed under tests/integration/temporal/fixtures/ with a thin runnable copy in git-ignored sample/. |
Granularity note: temporalio.contrib.openai_agents puts the agent loop in
workflow code and makes each model call an activity. Spec 040 does the
opposite — one activity per whole agent run, gate at the activity boundary.
What transfers from that precedent: the plugin pattern, instance-method
activities carrying worker-side state (credentials never enter payloads or
history), and the timeout/retry parameters object.
Task List¶
Phase 1: Foundation (D1 core)¶
- T1:
holodeck[temporal]extra, exact pin, package skeleton - T2: Payload and parameter models
- T3: Activity factory
- T4: Error taxonomy and retry classification
- T15: Live phase-1 e2e (real workflow, live Claude)
Checkpoint 1: Foundation¶
- Factory output is a valid Temporal activity definition (mocked backend)
-
make test-unit,make lint,make type-check,make securityclean
Phase 2: Deterministic helpers + plugin (D3, D1 sugar)¶
- T5: D3 helper surface + sandbox-safety unit test
- T6:
HoloDeckPlugin
Checkpoint 2: Helpers¶
- D3 modules pass
prepare_workflowsandbox validation - Plugin registers activities and sets the pydantic data converter
Phase 3: Worker CLI (D4)¶
- T7:
WorkerConfigmodel and loader - T16: live
holodeck workerintegration test (real dev server, no mocks) - T8:
holodeck workercommand
Checkpoint 3: Worker¶
-
holodeck worker --helpworks without temporalio installed (guarded import) - Worker starts (mocked client) from a fixture worker.yaml
Phase 4: Integration, sample, acceptance criteria¶
- T9: Hardship fixtures
- T10: Integration tests AC-1/AC-2/AC-3
- T11: Integration tests AC-4/AC-5 + sandbox Worker-init backstop
- T12: OTel test AC-6
- T13: Live smoke test,
sample/copy, index row - T14: moved to spec 041 (2026-08-30)
- T17: docsite update — temporal guide + worker.yaml reference (
docs/)
Checkpoint: Complete¶
- AC-1 through AC-6 demonstrated by tests
-
make ciclean; spec status updated indocs/product-specs/inventory.md— merged to main 2026-08-30 (stack #369)
Tasks¶
Task 1: holodeck[temporal] extra, exact pin, package skeleton¶
Description: Add the optional dependency and the package that guards it.
pyproject.toml gains extra temporal = ["temporalio==1.32.0"] with a comment
naming the sandbox-test dependency as the reason for the exact pin.
src/holodeck/temporal/__init__.py raises a clear ConfigError ("install
holodeck[temporal]") on import when temporalio is missing.
Acceptance criteria:
- [x] uv sync --extra temporal installs temporalio==1.32.0 on Python 3.10
- [x] Importing holodeck.temporal without the extra raises the guarded error, not ModuleNotFoundError
- [x] Core install (uv sync) does not pull temporalio
Verification:
- [x] pytest tests/unit/temporal/test_import_guard.py -n auto
- [x] make lint && make type-check && make security
Dependencies: None
Files likely touched: pyproject.toml, uv.lock, src/holodeck/temporal/__init__.py, tests/unit/temporal/test_import_guard.py
Estimated scope: S
Task 2: Payload and parameter models¶
Description: Pydantic models in src/holodeck/temporal/models.py:
AgentActivityInput (message, optional context dict), AgentActivityResult
(output, token_usage, num_turns, agent_id — all JSON-serializable),
ActivityParameters (start_to_close / schedule_to_close / schedule_to_start
timeouts, retry policy fields; no heartbeat — decision 10). Payload models
round-trip through pydantic_data_converter. ActivityParameters is a
workflow-side helper: to_activity_kwargs() expands it into
execute_activity keyword arguments, and validation requires at least one of
start_to_close/schedule_to_close (Temporal has no server default). The
module must stay sandbox-safe (it ships to workflow code with the D3 surface).
Acceptance criteria:
- [x] Payload models serialize/deserialize through temporalio.contrib.pydantic converter
- [x] AgentActivityResult.output holds a plain dict (the gate-validated object), never model text
- [x] ActivityParameters.to_activity_kwargs() yields valid RetryPolicy/timedelta kwargs; a parameters object with neither closing timeout is refused at validation
- [x] (follow-up, 2026-08-30) AgentActivityResult.output_as(Model) validates the output dict into a caller-supplied Pydantic model (workflow-side sugar; wire stays a dict)
Verification:
- [x] pytest tests/unit/temporal/test_models.py -n auto
- [ ] Quality gates as in T1
Dependencies: T1
Files likely touched: src/holodeck/temporal/models.py, tests/unit/temporal/test_models.py
Estimated scope: S
Task 3: Activity factory¶
Description: src/holodeck/temporal/activity.py:
agent_activity(node: EdgeNode, base_dir: Path) -> Callable. (No timeout/retry
kwargs — those are caller-side, decision 10.)
Resolves the agent through edge.resolve_agent_path, loads the gate through
load_gate_schema, invokes through BackendSelector (invoke_once,
stateless), validates structured_output against the gate, returns the
envelope. Activity name = node.id via activity.defn(name=...). Backend
and gate are bound at factory time (worker-side state; nothing secret in
payloads). Async activity, async backend call.
Acceptance criteria:
- [ ] Factory output passes Temporal activity-definition introspection with name = node id
- [ ] Gate-validated dict lands in AgentActivityResult.output; raw response text is absent from the envelope
- [ ] Agent path escaping the base dir refuses via resolve_agent_path (existing ConfigError)
Verification:
- [ ] pytest tests/unit/temporal/test_activity_factory.py -n auto (mocked backend)
- [ ] Quality gates
Dependencies: T2
Files likely touched: src/holodeck/temporal/activity.py, tests/unit/temporal/test_activity_factory.py
Estimated scope: M
Task 4: Error taxonomy and retry classification¶
Description: Map failures to Temporal semantics inside the activity.
Gate validation failure raises GateValidationError (plain exception →
retryable ApplicationError typed by class name). Authoring faults
(ConfigError, unloadable gate schema, missing agent.yaml) re-raise as
ApplicationError(non_retryable=True). Backend transport errors and
ExecutionResult.is_error stay retryable. Document the class-name contract
(RetryPolicy(non_retryable_error_types=[...]) matches by string).
Acceptance criteria:
- [ ] Gate failure → retryable error carrying the validation detail
- [ ] Authoring faults → non_retryable=True
- [ ] Model-fault vs authoring-fault channels never mix (SC-003)
Verification:
- [ ] pytest tests/unit/temporal/test_retry_classification.py -n auto
- [ ] Quality gates
Dependencies: T3
Files likely touched: src/holodeck/temporal/activity.py (or errors.py), tests/unit/temporal/test_retry_classification.py
Estimated scope: S
Task 15: Live phase-1 end-to-end (real workflow, live Claude)¶
Description: (added 2026-08-30, user-requested Phase 1 addition) One live
integration test proving the whole phase-1 stack with zero mocks: a
user-authored Temporal workflow (EvidenceWorkflow) schedules the T3
activity by name with ActivityParameters, on a WorkflowEnvironment.start_local
dev server with the pydantic data converter, against a real ClaudeBackend
call (oauth). Asserts the workflow receives exactly the gate-validated dict.
Skip conventions match the other live suites: requires
CLAUDE_CODE_OAUTH_TOKEN in tests/integration/.env and
SKIP_LLM_INTEGRATION_TESTS=false in the shell env.
Acceptance criteria:
- [x] Workflow completes against live Claude; output dict passes the gate schema exactly
- [x] Skips cleanly without credentials or with SKIP_LLM_INTEGRATION_TESTS=true
Verification:
- [x] SKIP_LLM_INTEGRATION_TESTS=false pytest tests/integration/temporal/ -m slow
Dependencies: T3, T4
Files likely touched: tests/integration/temporal/test_live_agent_workflow.py
Estimated scope: S
Task 5: D3 helper surface + sandbox-safety unit test¶
Description: src/holodeck/temporal/deterministic.py re-exports the
workflow-safe surface: evaluate (table_eval), a standalone
check_gate(obj, schema) function, ActivityParameters (the workflow-side
scheduling helper from T2), and load_decision_table documented as
import-time-only. Unit test builds a tiny workflow that imports the D3
helpers (not passed through) and calls
SandboxedWorkflowRunner().prepare_workflow(...); a restricted call must
fail the same harness (positive control). Extends
tests/unit/workflow/test_import_purity.py coverage to the new module.
Acceptance criteria:
- [ ] D3 modules (table_eval, feel, decision_table, gate-check half of edge) pass sandbox validation
- [ ] Harness proves it can fail (workflow with import-time nondeterminism is rejected)
- [ ] holodeck.temporal.deterministic imports without the backend stack
Verification:
- [ ] pytest tests/unit/temporal/test_sandbox_safety.py tests/unit/workflow/test_import_purity.py -n auto
- [ ] Quality gates
Dependencies: T1 (independent of T3)
Files likely touched: src/holodeck/temporal/deterministic.py, tests/unit/temporal/test_sandbox_safety.py, tests/unit/workflow/test_import_purity.py
Estimated scope: M
Task 6: HoloDeckPlugin¶
Description: src/holodeck/temporal/plugin.py: a SimplePlugin that sets
pydantic_data_converter and registers the activities built from a list of
EdgeNodes (calls the T3 factory; no parallel implementation). Client
plugins auto-propagate to workers.
Acceptance criteria: - [ ] Plugin sets the data converter and registers one activity per node - [ ] Plugin and manual factory wiring produce identical activity definitions
Verification:
- [ ] pytest tests/unit/temporal/test_plugin.py -n auto
- [ ] Quality gates
Dependencies: T3
Files likely touched: src/holodeck/temporal/plugin.py, tests/unit/temporal/test_plugin.py
Estimated scope: S
Task 7: WorkerConfig model and loader¶
Description: src/holodeck/temporal/worker_config.py: Pydantic model for
worker.yaml — temporal: (address, namespace, task_queue, TLS) and
nodes: (inline EdgeNode entries). No execution timeouts or retry policy in
this file — those are caller-side (decision 10); worker.yaml is registration
only. Env-var overrides
(TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, …) follow the existing
ConfigLoader precedence. Node paths resolve relative to the worker.yaml
directory through resolve_agent_path.
Acceptance criteria:
- [x] Valid worker.yaml parses; unknown keys refuse (extra="forbid")
- [x] Env vars override file values
- [x] A node whose agent path escapes the config directory is refused at load
Verification:
- [x] pytest tests/unit/temporal/test_worker_config.py -n auto
- [x] Quality gates
Dependencies: T2
Files likely touched: src/holodeck/temporal/worker_config.py, tests/unit/temporal/test_worker_config.py
Estimated scope: M
Task 8: holodeck worker command¶
Description: src/holodeck/cli/commands/worker.py: Click command with
--config (default worker.yaml) and --task-queue override. Loads
WorkerConfig, builds activities via the factory, connects with
pydantic_data_converter + TracingInterceptor, starts an activities-only
Worker, handles graceful shutdown (graceful_shutdown_timeout, SIGINT).
Import of temporalio stays inside the command so holodeck --help works
without the extra.
Acceptance criteria:
- [x] holodeck worker --help works with and without the extra installed
- [x] Startup registers one activity per configured node (mocked client test)
- [x] Missing extra yields the T1 guard message, not a traceback
Verification:
- [x] pytest tests/unit/temporal/test_worker_command.py -n auto
- [x] Quality gates
Dependencies: T6, T7
Files likely touched: src/holodeck/cli/commands/worker.py, src/holodeck/cli/main.py (register verb), tests/unit/temporal/test_worker_command.py
Estimated scope: M
Task 16: Live holodeck worker integration test¶
Description: tests/integration/temporal/test_live_worker_command.py:
end-to-end test of the T8 CLI with no mocks. Uses
WorkflowEnvironment.start_local — the real Temporal dev server binary
(auto-downloaded, sqlite-backed), chosen over a docker compose stack because
it gives identical server fidelity with zero docker dependency and works in
CI unchanged. The test:
- starts the dev server and reads its
host:port, - writes a fixture
worker.yaml(tmp_path) whosetemporal.addresspoints at it, with one evidence node (reuse T15's AGENT_YAML/GATE_SCHEMA), - spawns
holodeck worker --config worker.yamlas a real subprocess (same interpreter/venv,CLAUDECODEunset), - waits for the worker to poll (readiness by executing the workflow with a generous start-to-close, or by polling task-queue pollers via the client),
- runs the sample workflow through a client + local
Workerhosting only the workflow (activities come from the CLI subprocess), asserts the gated output dict, - sends SIGINT to the subprocess and asserts exit code 0 within the graceful-shutdown window.
Live-test conventions of T15: tests/integration/.env, skip unless
SKIP_LLM_INTEGRATION_TESTS=false and CLAUDE_CODE_OAUTH_TOKEN set,
@pytest.mark.integration @pytest.mark.slow.
Acceptance criteria: - [x] Worker subprocess registers activities from worker.yaml and serves a real workflow execution (LLM call included, no mocks anywhere) - [x] Gated output crosses the wire typed (pydantic converter) and matches the gate schema exactly - [x] SIGINT produces graceful shutdown: exit code 0, no orphan processes
Verification:
- [x] SKIP_LLM_INTEGRATION_TESTS=false pytest tests/integration/temporal/test_live_worker_command.py -v
- [x] Skip path: collected-but-skipped without env override
- [x] Quality gates
Dependencies: T7, T8
Files likely touched: tests/integration/temporal/test_live_worker_command.py
Estimated scope: M
Task 17: Docsite update (holodeck-docs, docs/)¶
Description: Documentation for the Temporal integration on the docsite
(https://docs.useholodeck.ai/). A guide under docs/guides/ covering both
personas:
- Python developer — activity factory (
agent_activity), typed payloads (AgentActivityInput/AgentActivityResult,output_as()), the D3 workflow-safe surface (check_gate,evaluate,ActivityParameters), sandbox rules learned in this spec: workflows live in their own module, decision tables load at workflow-module import time (decision 7),imports_passed_through()for the D3 imports, error taxonomy (model faults retryable, authoring faultsnon_retryable=True), and the workflow-owned business-rule feedback-retry pattern (bounded loop,contextre-invoke, Context (JSON) rendering), and the human-in-the-loop approval pattern —@workflow.signal+workflow.wait_conditionwith a timer race for SLA escalation (nothing shipped in code; Temporal natives suffice, decided 2026-08-30, recorded in docs/product-specs/041-temporal-file-inputs/spec.md §4). - worker.yaml host —
worker.yamlreference (temporal block, nodes,TEMPORAL_*env overrides fail-closed, path confinement),holodeck workerCLI (activities-only, graceful shutdown,--task-queue), install viaholodeck-ai[temporal].
Wire the page into docs nav/index as the docsite convention requires.
Acceptance criteria: - [x] Guide covers both personas with runnable snippets consistent with the shipped API - [x] worker.yaml reference documents every field and the env-override semantics - [x] Docs build/nav intact (docsite conventions followed)
Verification:
- [x] Snippets cross-checked against src/holodeck/temporal/ signatures
- [x] Quality gates (docs-affecting checks)
Dependencies: T8, T16 (API surface final for phases 1–3); revisit after T9–T12 land
Files likely touched: docs/guides/…, docs/index.md (nav)
Estimated scope: M
Task 9: Hardship fixtures¶
Description: Committed fixtures under
tests/integration/temporal/fixtures/hardship/: two agent.yaml files
(evidence extractor, letter writer) with response_format, two gate schemas,
one .dmn.yaml decision table (reusing the 036 test-suite table so AC-3 can
compare Verdicts), a worker.yaml, and the sample workflow
(workflow.py: extract → table verdict → letter). The workflow schedules
each activity with ActivityParameters.to_activity_kwargs() — the sample is
the proof that timeout/retry configuration is caller-side and functional
(feeds AC-2).
Acceptance criteria:
- [x] Fixture agents load through the existing Agent model
- [x] Table is the same one the 036 test suite evaluates (AC-3 comparability)
- [x] Workflow module passes the T5 sandbox harness
Verification:
- [x] pytest tests/unit/temporal/test_fixtures_load.py -n auto
- [x] Quality gates
Dependencies: T5 (sandbox harness), T7 (worker.yaml shape)
Files likely touched: tests/integration/temporal/fixtures/hardship/*
Estimated scope: S
Task 10: Integration tests AC-1/AC-2/AC-3¶
Description: Mocked backend (deterministic canned structured_output),
against WorkflowEnvironment.start_local (amended 2026-08-30: no
temporal server start-dev / CLI-absent skip — start_local downloads its own
dev server). Runs unconditionally in CI; the live-LLM counterpart is T13's
smoke. AC-1: workflow receives a gate-validated envelope. AC-2: backend
rigged to emit a gate-failing object first, passing object second — activity
fails once, retry succeeds, history shows both attempts. AC-3: table helper
in workflow code returns the same Verdict as the 036 suite for the same
inputs.
Acceptance criteria: - [x] AC-1, AC-2, AC-3 each demonstrated by a named test - [x] History assertion: no unvalidated payload in any completed activity result
Verification:
- [x] pytest tests/integration/temporal/ -n auto -m integration
- [x] Quality gates
Dependencies: T4, T9
Files likely touched: tests/integration/temporal/test_activity_acceptance.py, tests/integration/temporal/conftest.py
Estimated scope: M
Task 11: Integration tests AC-4/AC-5 + sandbox backstop¶
Description: AC-4: spawn holodeck worker as a subprocess against
start-dev with the fixture worker.yaml; run the sample workflow end to end.
AC-5: replay the completed workflow history with the Temporal SDK replayer;
assert zero backend invocations (mock call counter). Sandbox backstop:
constructing the real Worker with the sample workflow validates it through
public API.
Acceptance criteria:
- [x] AC-4 and AC-5 each demonstrated by a named test
- [x] Replay executes table_eval deterministically without any activity re-execution
Verification:
- [x] pytest tests/integration/temporal/ -n auto -m integration
- [x] Quality gates
Dependencies: T8, T10
Files likely touched: tests/integration/temporal/test_worker_e2e.py, tests/integration/temporal/test_replay.py
Estimated scope: M
Task 12: OTel test AC-6¶
Description: With an in-memory span exporter: run the same agent once
through the holodeck test execution path and once through the activity;
assert the GenAI span set (names + GenAI semconv attributes) matches. With
TracingInterceptor active, assert GenAI spans are children of the Temporal
activity span.
Acceptance criteria:
- [x] Span parity between holodeck test and activity execution (AC-6)
- [x] GenAI spans nest under the interceptor's activity span
Verification:
- [x] pytest tests/integration/temporal/test_otel.py -n auto -m integration
- [x] Quality gates
Dependencies: T10
Files likely touched: tests/integration/temporal/test_otel.py
Estimated scope: M
Task 13: Live smoke, sample/ copy, index row¶
Description: One @pytest.mark.slow test running a real Claude call
through the sample workflow (manual execution). Thin runnable demo under
sample/temporal-hardship/ (git-ignored). Update docs/product-specs/inventory.md row 040
and the spec status line. Docsite content moved to T17.
Acceptance criteria:
- [x] Smoke test passes manually with credentials
- [x] docs/product-specs/inventory.md row reflects final task count and status
Verification:
- [x] pytest -m slow tests/integration/temporal/ -n auto (manual)
- [x] make ci
Dependencies: T11, T12
Files likely touched: tests/integration/temporal/test_smoke_live.py, docs/…, docs/product-specs/inventory.md, docs/product-specs/040-holodeck-temporal/spec.md
Estimated scope: S
Task 14: Gate-schema codegen (holodeck generate models)¶
Moved to spec 041 (2026-08-30): codegen pairs with the file-input
work and 040 is otherwise complete. The task text below is retained for
history; implementation tracking lives in docs/product-specs/041-temporal-file-inputs/.
Description: (added 2026-08-30, user-requested scope addition) CLI verb
that generates typed Pydantic models from gate JSON Schemas, so workflow code
gets static typing over AgentActivityResult.output instead of a bare dict.
Reads edge-node declarations (a worker.yaml nodes: list or explicit
agent.yaml paths), resolves each gate schema through the existing
load_gate_schema seam, and emits a generated module (e.g. models_gen.py)
via datamodel-code-generator. Pairs with AgentActivityResult.output_as()
(added in T2 follow-up): result.output_as(EvidenceOutput). Generated code
must be pure Pydantic — sandbox-safe, importable from workflow code. The wire
envelope is untouched: output stays a plain dict in history (FR-008);
codegen is developer-ergonomics only.
Acceptance criteria:
- [ ] holodeck generate models --config worker.yaml emits a module with one model per edge node gate schema, deterministic output (stable ordering, no timestamps)
- [ ] Generated module imports cleanly inside the workflow sandbox (reuse the T5 harness)
- [ ] result.output_as(GeneratedModel) round-trips the hardship fixtures
- [ ] Staleness detectable: regenerating over an unchanged schema is a no-op diff (CI-checkable)
Verification:
- [ ] pytest tests/unit/temporal/test_codegen.py -n auto
- [ ] Quality gates as in T1
Dependencies: T5 (sandbox harness), T7 (WorkerConfig/worker.yaml loader), T9 (fixtures)
Files likely touched: src/holodeck/cli/commands/generate.py, src/holodeck/temporal/codegen.py, tests/unit/temporal/test_codegen.py, pyproject.toml (dev/extra dep datamodel-code-generator)
Estimated scope: M
Risks and Mitigations¶
| Risk | Impact | Mitigation |
|---|---|---|
Sandbox APIs (prepare_workflow, workflow_sandbox) marked "not yet stable" |
Med | Exact pin temporalio==1.32.0; Worker-init public-API backstop (T11); keep the unit harness tolerant of minor drift |
temporal CLI binary unavailable in CI |
Med | Integration tests skip cleanly when absent; add an install step to CI in T10; unit suite carries the contract regardless |
| Claude backend subprocess (Node.js) inside activity workers | Med | Reuse existing validators.py preflight at worker startup; fail fast with the existing error, not mid-activity |
| Envelope bloats event history | Low | Envelope carries only the validated dict + small usage ints; no raw text, no message history |
| Import-time table load conflicts with sandbox re-imports | Low | T5 harness proves the pattern before T9 depends on it; fallback documented pattern is passthrough of the table module |
pydantic_data_converter interaction with sandbox |
Low | README-recommended pydantic passthrough applied in worker/test setup |
Open Questions¶
None. The spec §12 open questions were resolved: extra name = holodeck[temporal] (D12), worker config = worker.yaml + env + flags (D8–D10), and the contrib.openai_agents seam study concluded (granularity note above).
T3 open questions, resolved 2026-08-30 with the user:
- Backend mode in the activity:
mode="test"stands. Withpermission_mode: manualthat maps to the SDK's strictdefault— no silent permission escalation in a durable, retried, headless context. A dedicated"worker"mode would re-open the legacy test-mode escalation footgun the backend already removed; revisit only if the T13 live smoke shows real friction. Agents needing tools declare them explicitly in agent.yaml. AgentActivityInput.contextrendering: resolved 2026-08-30 with the user — the JSON block stands. Dict rendered as sorted-keys JSON appended under aContext (JSON):header. Deterministic, structured, keys carry semantics, and prose steering fits inside values (e.g.{"instruction": ...}). Alternatives (prompt templates, conversation-turn injection, message-only) rejected as scope creep or loss of structure. This is the feedback channel for workflow-owned validation loops: re-invoke the activity withcontext={"business_rules_validation_outcome": ...}.