aiml
SearchMy learning
Menu

Lesson 3 of 9Module 1

Delegate independent tasks with explicit worker state

Fan out two documentation requests into separate bounded runtimes. Gather results by task ID and account for dispatch, merge and incomplete-worker behavior.

Prerequisites

Run a search-and-read decision loop; Python functions and context managers. The shared runtime already supplies tool contracts and iteration limits.

By the end, you can

  • Choose independent work that can execute without sharing observations.
  • Keep worker identity, local state and budgets explicit.
  • Distinguish concurrent execution, completion and correct outcomes.
Runtime
Python 3.12+
Dependencies
Standard library only

Lab code, tests and fixtures (ZIP, 32 KB)Quiz check your understanding

Progress is saved in this browser.

Dispatch independent documentation questions to separate worker runtimes. Keep observations local to each worker, collect results by task identity and refuse a complete merge when any required worker stops early.

Problem

When you need this. Independent workers need separate state and an explicit rule for combining their results.

A release checklist needs two independent answers: the staging rollback command and the staging token-rotation command. Each answer can be found in its own documentation page. Passing the first worker’s entire conversation to the second adds state without helping either lookup.

You will dispatch these requests to separate worker runtimes, collect their results by task ID and refuse a complete merge when a worker stops early. The code uses a real Python thread pool. The worker policies remain scripted, so this experiment measures local control work rather than model speed or answer quality.

Concepts

Delegation assigns a specific task and output contract to another runtime. Fanout starts several such tasks; fan-in collects their results. Independence requires more than separate prompts: one worker must not accidentally inherit another worker’s document observations.

The worker task ID identifies which result satisfies which requirement. A missing worker result cannot be repaired by concatenating the outputs that did arrive. This lab studies ephemeral worker state; persistent memory lifecycle belongs to the Context and Memory course.

Architecture

Task listunique IDsDispatchat most twoRollback workerfresh observationsand source mapToken workerfresh observationsand source mapAll workersfinal?Aggregateby task IDIncompletekeep worker recordsyesnoEach worker reuses the bounded loop. Completion still needs a separate task-outcome check.Task list with unique IDsDispatch at most two workersRollbackworkerown traceand sourcesTokenworkerown traceand sourcesAll workersfinal?Aggregate by task IDthen check task outcomesIncompletekeep every worker recordyesnoFinal is a stop state, not correctness.
The workers execute independent requests with separate local state. The coordinator limits fan-out and refuses a complete aggregate when any worker stops early.

The coordinator validates unique tasks and a fanout limit, then starts the worker runtimes in a Python thread pool. Each worker searches and reads with independent observations. The coordinator joins by task ID and returns a complete checklist only when every required task has a valid final result.

Working code

Decompose the task at a real independence boundary

The rollback worker does not need the token worker’s answer, and the token worker does not need the rollback worker’s observations. Each job has its own question, documentation version and staging-only environment. This makes them suitable for independent execution.

delegation-jobs.jsonSource · jsonDownload
[
  {
    "id": "rollback-direct",
    "family": "rollback",
    "question": "rollback staging",
    "version": "v2"
  },
  {
    "id": "tokens-direct",
    "family": "tokens",
    "question": "tokens rotate staging",
    "version": "v2"
  }
]

The coordinator is deliberately fixed: it accepts one or two jobs and checks that their IDs are unique. It does not ask a model how many workers to create or recursively delegate work. That keeps both the supported fan-out and the result mapping visible in code.

A dependent task would require another design. If a worker must read the first worker’s selected release before searching, schedule that handoff explicitly. Starting both immediately would create a race over missing information rather than useful concurrency.

Start each worker with fresh runtime state

Download the complete lab and enter agent-engineering/:

Compare sequential and delegated executionTerminal · bash
python3.12 -m venv .venv
source .venv/bin/activate
python delegation.py
python -m unittest -v test_delegation.py

worker() calls the bounded runtime once per task. Each call constructs its own document-tool instance, observation list, trace and seen source map. The workers share read-only fixture files; they do not share conversation state.

This separation affects correctness. A rollback worker cannot satisfy its citation check using a page read only by the token worker. The source must have been read in that worker’s own run. If both workers pointed to one mutable seen dictionary, a citation could appear supported by a source the answering worker never received.

delegation.pySource · pythonDownload
"""Account for independent worker contexts and fixed delegation overhead."""
from concurrent.futures import ThreadPoolExecutor
from core import Task, load, run, scripted


def worker(task, *, turns=4):
    return run(task, scripted, max_turns=turns)


def gather(jobs):
    ids = [task.id for task in jobs]
    if len(ids) != len(set(ids)):
        raise ValueError('worker task IDs must be unique')
    if not jobs or len(jobs) > 2:
        raise ValueError('this coordinator accepts one or two workers')
    with ThreadPoolExecutor(max_workers=2) as pool:
        results = list(pool.map(worker, jobs))
    if any(r['stop'] != 'final' for r in results):
        return {'stop': 'incomplete', 'workers': dict(zip(ids, results))}
    return {'stop': 'final', 'workers': dict(zip(ids, results)),
            'tool_calls': sum(r['tool_calls'] for r in results),
            'policy_turns': sum(r['turns'] for r in results),
            'dispatches': len(jobs), 'merge_checks': 1}


def main():
    jobs = [Task(**job) for job in load('delegation-jobs.json')]
    # A sequential controller performs the same two sub-tasks, with the same limits.
    sequential = [worker(job) for job in jobs]
    delegated = gather(jobs)
    print('Same 2 tasks, same read-only tools, fresh state for each task.')
    print('sequential:', sum(r['turns'] for r in sequential), 'policy turns;',
          sum(r['tool_calls'] for r in sequential), 'tool calls')
    print('delegated:', delegated['policy_turns'], 'policy turns;',
          delegated['tool_calls'], 'tool calls;', delegated['dispatches'], 'dispatches; 1 merge check')
    for id, result in delegated['workers'].items():
        print(id + ':', result['citation'])
    print('Scripted policies; no model latency, price or quality advantage was measured.')


if __name__ == '__main__':
    main()

The executor’s map() schedules the worker calls concurrently and yields results in input order. dict(zip(ids, results)) then preserves the identity of each result. A fast task finishing first does not become the answer to whichever job happens to be listed first by completion time.

test_workers_have_independent_document_observations inspects the actual read observations in both worker traces. Different final citation strings alone would be a weaker check: a worker could print a desired ID while reading another source.

Count the work that delegation adds

Recorded local worker comparisonRecorded output · textDownload
Same 2 tasks, same read-only tools, fresh state for each task.
sequential: 6 policy turns; 4 tool calls
delegated: 6 policy turns; 4 tool calls; 2 dispatches; 1 merge check
rollback-direct: rollback-v2
tokens-direct: tokens-v2
Scripted policies; no model latency, price or quality advantage was measured.

Both variants execute the same tasks with the same per-worker limits. Both therefore use the same policy turns and tool calls on these fixtures. Delegation adds dispatches and a coordinator merge check; it does not reduce the underlying lookup work.

Those counts are useful even without a timing claim. With a remote policy, independent workers may overlap waits, but they still consume their own requests and context. A concurrency benefit would need a latency experiment with the same workload, provider limits and outcome checks. This small, fast local demonstration does not provide that experiment.

A total request budget also needs an owner. Per-worker limits bound one invocation; the coordinator must account for their sum before increasing the worker count. This lab caps fan-out before starting the pool, rather than creating an unbounded set of workers and hoping each remains small.

Block the merge when a worker has not finished

gather() checks every worker’s stop state before returning a final aggregate. If any worker returns turn_limit, tool_limit or another non-final state, the coordinator returns incomplete and retains the individual worker records. It does not silently omit the missing answer.

test_unfinished_worker_blocks_the_merge supplies an unfinished worker result and verifies this branch. test_duplicate_worker_ids_are_rejected prevents one result from overwriting another under a duplicate dictionary key. test_fanout_has_an_explicit_limit verifies that an extra job is rejected before execution.

A final aggregate means all workers reached their final state. It does not prove that their commands are correct. The task expectations still need the independent command-and-citation checker introduced in the first lesson. A final answer containing the wrong command should fail that checker even when every worker completes normally.

The executor has no timeout here. Runtime iteration limits bound the scripted policy’s decisions, but a blocking worker would hold up the gather. Exceptions from a worker also propagate when its result is retrieved. A service built around remote tools would need deadlines, cancellation and an explicit partial-result contract; this program does not claim those behaviors.

Decide whether another worker is justified

Use delegation when the task has independent work, the result identity is explicit and the combined budget is acceptable. Keep shared state limited to the information each worker actually needs. Adding workers to a sequential dependency chain cannot remove that dependency.

Design exercise: change the checklist so the token task must inspect a release selected by the rollback task. Draw the required artifact handoff and describe why the current fan-out is no longer valid. Then specify how the coordinator should report one successful task and one timed-out task without presenting the checklist as complete.

The optimization module will keep this control structure fixed while examining the task traces and retrieval policy. That separates improvements to source selection from changes to worker orchestration.

Failure cases

A worker that stops early blocks the complete merge. Duplicate IDs refuse before dispatch because they would make result attribution ambiguous. The state-isolation test checks that one worker’s successful read does not become a source observed by another worker.

Check the input and failure boundaries
Trigger Required behavior
duplicate worker ids are rejected Refuse duplicate task identities before starting workers.
unfinished worker blocks the merge Withhold a complete merged result if a worker did not finish.
workers have independent document observations Give each worker its own document observations.
fanout has an explicit limit Refuse a fanout beyond the configured worker limit.

Deployment

Define the worker input, output and failure contract before adding a distributed queue. Keep task identity and observations with each worker result, and choose how partial completion is presented to callers. The current thread-pool demo does not provide cross-process retries or durable workflow state.

Scaling

More workers can increase pressure on shared dependencies and may not reduce elapsed time when those dependencies serialize work. Bound fanout and measure completed-task latency and failure handling under the intended workload. The scripted workers do not provide a model-speed comparison.

Key takeaways

  • Fan-in needs identities and completion rules, not only multiple output strings.
  • Independent tasks need independent observations.
  • A partial result must not be presented as a complete checklist.
Both delegated workers stop with final, so the coordinator produces an aggregate. What does that establish about their answers?

Choose one answer, then check your reasoning.

Read the explanation

The merge checks worker completion state. A worker’s final stop reason does not prove its command and citation satisfy the task.

Best practices

  • Validate task IDs before launching work.
  • Bound fanout at the coordinator.
  • Keep partial completion explicit when a required worker fails.

References

References checked October 24, 2026. The actual counts above come from the two bundled scripted tasks; no provider latency comparison was run.

Written by Arjun Prabhulal.

Module 1

Agent design