Tasks: mount tiny-harness agents onto an existing FastAPI or Starlette app
A DAG derived from the approved
design.mdandtesting-plan.md. Each task's_Test:_names a row of the plan's matrix. Single repository; the code lands on the work item's branchloop/issue-36, PR #37.
There are seventeen tasks. Every task is test-first: write the named test, watch it fail, then make it pass. Tasks 1–10 are the isolation and identity groundwork, each usable alone. Tasks 11–14 assemble Harness and its app. Tasks 15–17 add FastAPI, the five-agent acceptance and the docs.
Task list
- [x] 1.
[agent]configuration and the task queue default- Add
AgentConfig(idwith its pattern,name,description) toSettings. - Make
TemporalConfig.task_queueoptional, addeffective_task_queue, and fill it fromagent.idin aSettingsvalidator. - Remove
runtime.AGENT_ID. Every reader moves tosettings.agent.idoreffective_task_queue:workflow_config,commands.py,process.pyand the worker. - Depends on: none
- Requirements: R4.6, R5.3, R7.2
- Test: T1
tests/unit/test_config_agent.py(defaults, explicit queue kept, bad id refused); T6/T13examples/demo/config.tomlloads with id and queuetiny-harness
- Add
- [x] 2. New errors
- Add
HarnessConflictError,HarnessAlreadyStartedError,HarnessStoppedErrorandHarnessNotStartedErrorunderTinyHarnessErrorintiny_harness/errors.py. - Depends on: none
- Requirements: design § Error handling
- Test: T1
tests/unit/test_errors.py(hierarchy, messages)
- Add
- [x] 3.
Claimsregistry- Add
tiny_harness/service/claims.py. It claims the agent id, the namespace and queue, and the resolved store path, all or nothing, under a lock, and releases them. - Depends on: 2
- Requirements: R5.3, R5.8; abuse case 4
- Test: T1
tests/unit/service/test_claims.py(each clash names the holder, two spellings of one path clash, release frees, all-or-nothing)
- Add
- [x] 4.
TemporalPool- Add
tiny_harness/service/durable/pool.py: connections reference-counted per key, with the embedded key(embedded, database_path)and the remote key(address, namespace, sha256(api_key)). - Reimplement
temporal_client(settings)on it, keeping its signature. - Depends on: none
- Requirements: R1.3, R5.2, NFR startup
- Test: T1
tests/unit/service/test_pool.py(shared per key, the last release closes, no API key in the key, over a fake connector); T6 thetests/integration/embeddedsuite unchanged
- Add
- [x] 5.
SecretRegistryin every redactor- Add a process-wide registry with a version counter.
Redactor(registry=…)rebuilds its values when the version changes.- The service's redactors (runtime,
observe) usePROCESS_SECRETS. - Depends on: none
- Requirements: abuse case 6
- Test: T1
tests/unit/test_redactor_registry.py; T4tests/security/test_multi_agent_isolation.py::test_one_agents_log_masks_another_agents_key
- [x] 6. The agent on every log line and span
- Add the
current_agentcontext variable and the"agent"field inJsonFormatter. - Add
AgentContextInterceptoron the worker, and thetiny_harness.agentspan attribute in the o11y hook. - Depends on: 1
- Requirements: NFR observability
- Test: T1
tests/unit/service/test_logging_agent.py; T2Scenario: Each log line names the agent that wrote it(in task 13's suite)
- Add the
- [x] 7. Task ownership by queue
TemporalTaskStore(task_queue=…):lookupdescribes the workflow first, a foreign queue givesNone, and an LRU of 4,096 ids caches the result.ListTasksaddsTaskQueue = '<queue>', and the fallback comparesexecution.task_queue.context_idis escaped in the visibility query.running_task_workflows(client, task_queue)gets the same filter.- Depends on: 1
- Requirements: R5.4, R5.6; abuse case 2
- Test: T1
tests/unit/service/test_task_ownership.py(owned, foreign,describefailing ⇒ own store, LRU hit makes no second call, quote escaping); T4Scenario: Agent a treats every task of agent b as unknown
- [x] 8. Push ownership
PushReceiver(client, task_queue=…)describes the stand-in before the update. A foreign queue gives401, the same as a wrong token.- Depends on: 1
- Requirements: R5.4; abuse case 3
- Test: T1
tests/unit/service/test_push_receiver.py(foreign queue ⇒ 401, no update call); T4Scenario: A push for agent b's stand-in at agent a's route is refused
- [x] 9. Heartbeat per agent
schedule_id(agent_id)keepstiny-harness-heartbeatfor the default agent.ensure_heartbeat_scheduleanddelete_heartbeat_scheduletake the agent id, andtiny-harness schedules deletepasses it.- Depends on: 1, 7
- Requirements: R5.6, R7.1
- Test: T1
tests/unit/service/test_heartbeat_id.py; T6 thetests/integration/heartbeatsuite unchanged
- [x] 10. Participant resolver and strict mode
HarnessContextBuilder(resolver): with a resolver it never reads the header, setsSTRICT_STATE_KEY, and a raising resolver gives500.sender_ofrefuses a message that has no resolved participant in strict mode.- Depends on: none
- Requirements: R4.1–R4.3; abuse case 1
- Test: T1
tests/unit/service/test_context_builder.py; T4tests/security/test_participant_resolver.py(Scenario: With a resolver a spoofed participant header is ignored)
- [x] 11.
public_urland the card- Add
public_url(base_url, prefix)per the design's table, with normalization and aConfigErroron mismatch. build_agent_cardtakes the public URL and the[agent]name and description.- For the default agent at the root, the card stays byte-identical.
- Depends on: 1
- Requirements: R2.3, R2.4, R4.6, R7.3
- Test: T1
tests/unit/service/test_public_url.py; T5tests/contract/test_agent_card.pyunchanged
- Add
- [x] 12.
build_routes,DeferredRequestHandler,RouteOptions,HarnessApp- Replace
create_app. - The routes are built at definition time over a deferred handler, and the card comes through
card_modifier. RouteOptionstoggles limits, CORS, monitor, push and ui.HarnessAppadds the 503 gate, a lifespan when it is the top-level app, and the mount check on the first request.- Depends on: 10, 11
- Requirements: R2.1, R2.2, R2.5, R4.4, R4.5; abuse case 5
- Test: T1
tests/unit/service/test_route_options.py,tests/unit/service/test_harness_app.py(503 before start, mount-check refusal)
- Replace
- [x] 13.
Harnessandharnesses_lifespan- Add
tiny_harness/service/harness.py: the constructor validates without I/O.__aenter__claims, acquires the pool, builds the runtime (llmseam), publishes the commands, builds the card, ensures the heartbeat, starts the worker, and registers the secrets, all on oneAsyncExitStack. push=Falsegivespush_base_url=None.- Export it lazily from
tiny_harness.service. - Depends on: 3, 4, 5, 6, 7, 8, 9, 12
- Requirements: R1.1–R1.5, R4.5, R5.1–R5.3, R5.5, R5.7
- Test: T1
tests/unit/service/test_harness_lifecycle.py(second start, start after stop,clientbefore start, part-way failure undoes); T2tests/integration/mount/test_starlette.py(lifespan, client passed in not closed, prefix card, 503 window, wrong mount, push off ⇒ follow, log agent field, several harnesses in order); T4Scenario: A second harness on the same namespace and queue does not start,Scenario: A request before start never reaches Temporal
- Add
- [x] 14.
running_harness,serveandworkeronHarnessrunning_harnessbecomesHarnessplus uvicorn overasgi_app(), with the same signature and the same yieldedRunningHarness.tiny-harness workeruseseffective_task_queueand the per-agent schedule id.- Depends on: 13
- Requirements: R2.5, R7.1–R7.3
- Test: T6 the full existing suite, unmodified except for
AGENT_IDimports (each listed); T2Scenario: A top-level harness app starts itself from the server lifespan
- [x] 15. FastAPI extra and
harness_router- Add
[project.optional-dependencies] fastapi = ["a2a-sdk[fastapi]>=1.2.2", "fastapi>=0.143"]and add FastAPI to the dev group. - Add
tiny_harness/fastapi.py, with theImportErrornaming the extra. - The routes go through a2a-sdk's helper on a scratch app and are re-added to an
APIRouter(prefix, lifespan)withadd_api_route. - Add the
gateandlimitsdependencies and the bounded body read. - The first-request prefix check, and warnings for
cors_originsandui_dir. - Depends on: 13
- Requirements: R3.1–R3.4
- Test: T2
tests/integration/mount/test_fastapi.py(Scenario: A FastAPI app includes an agent router and lists its routes in OpenAPI,Scenario: A router included under another prefix refuses every request); T7tests/integration/test_package.py; T5tiny_harness.fastapiAPI snapshot
- Add
- [x] 16. The five-agent example and acceptance tests
- Add
examples/multi_agent/:a.toml…e.toml,app.py(Starlette withharnesses_lifespan),app_fastapi.pyandREADME.md. - Add
tests/integration/mount/test_multi_agent.py: real uvicorn on an ephemeral port, scripted models,a→bandb→ddelegation,b's push toa's prefixed route, one pool connection, and the FastAPI variant. - Depends on: 14, 15
- Requirements: R5.1–R5.7, R6.1–R6.4, NFR startup
- Test: T3
Scenario: five agents in one app delegate to each other,Scenario: A parked sub-task on agent b pushes to agent a's prefixed route,Scenario: five agents in one FastAPI app delegate to each other; T4 the cross-agent cases on the same fixture; T11 the asserted single connection
- Add
- [x] 17. Contract snapshots and documentation
- Update the snapshots on purpose:
settings.schema.json,tiny_harness.service,tiny_harness.config,tiny_harness.errors, the newtiny_harness.fastapi; the card stays unchanged. - New docs per the docs-writing skill:
docs/sdk/mount.md(quickstart first: Starlette, FastAPI, several agents) anddocs/configuration/agent.md. - Updates: the deploy guide (who authenticates in each setup; one agent id per process),
docs/configuration/server.md(prefix inbase_url), anddocs/.vitepress/config.mts. - Capability docs:
a2a-server.md,durable-execution.md,heartbeat.md,configuration.md,observability.md. - Also
docs/architecture/architecture.mdand decision-009 (ownership by queue; no shared stores). - Depends on: 16
- Requirements: R8.1–R8.4
- Test: T5
uv run pytest tests/contract; T15cd docs && bun run docs:build; T16uv run pre-commit run --all-files
- Update the snapshots on purpose:
Dependency graph (DAG)
Tasks 1, 2, 4, 5 and 10 have no dependencies and can start first.
Checkpoints
- After each task: its named tests go red → green;
uv run pytest tests/unitstays green; the task's box is ticked and the work committed. - After task 9:
uv run pytest tests/integration/heartbeat tests/integration/a2a— the isolation groundwork has not changed single-agent behaviour. - After task 14: the full T6 regression run (
uv run pytest tests/unit tests/integration tests/contract tests/security tests/ui). - After task 17:
uv run pre-commit run --all-filesand the docs build. - Then the verification node executes
testing-plan.md: it ticks each activity and records its command, outcome and committed evidence. Only after that do the review phases run: the self and critic rounds, and the security review gate (recorded inevidence/security-review.md).
Review comments
Appended by the-loop's
record-feedbackhook when a human gate approves with comments (issue-109). Append-only and attributed: an approval never silently discards a reviewer's suggestions, and the feedback travels with the document it concerns rather than living in a side-channel tracker.