Observation retains its dict compatibility but stores each field once.
obs.task = value and obs["task"] = value update the same authority; mapping
step and step_id name the step_id attribute. Contradictory aliases, invalid
field types and deletion of schema fields reject atomically. update and |=
validate the whole mutation first; extension keys support pop, del, setdefault
and popitem. clear is rejected because it would remove required identity.
State/metadata nested edits are shared between attribute and mapping access.
Constructor inputs are copied; to_dict and to_legacy_dict are independent
snapshots. Attribute action results are canonical ToolResult objects and
to_dict serializes their canonical shape. Mapping action results remain a
fresh legacy projection for existing reducers; assign obs["action_results"]
to validate and replace them. Unbound dict mutators are outside this contract.
Legacy host Read uses a zero-based offset and a positive line limit: offset 2,
limit 2 selects only lines 3/4. Empty files and EOF return empty output; negative
or non-integer windows fail. Selection metadata reports truncation and more lines.
Edit requires a unique match unless replace_all=True; no match, ambiguous match,
permission denial and explicit expected_sha256 conflicts leave the file unchanged.
These two aliases now return canonical ToolResult (read numbered text in .output,
status/error in their named fields), rather than successful strings containing errors.
Direct callers expecting strings should use .output after checking .status.
The existing validation/permission pipeline remains authoritative. Host compatibility
remains host execution; Env read_file/edit_file keep their Env-only operations and
one-based line_offset interface. No fallback to host or new isolation guarantee is implied.
Goal and prerequisites
Register a decorated function with ToolRegistry before an Agent can call it. The name and typed parameters form the callable interface;read_only and concurrency_safe describe this tool’s behavior. These are commitments by the tool author, not an automatic permission grant.
First run the sequential and parallel notes example. Then run parallel.py: the first declared tool waits for an explicit in-memory event from the second. Two workers therefore finish in reverse order, while the reducer still receives results in declaration order. The event and list are visible, process-local teaching instrumentation.
The five-second wait is a fixture deadline, not a performance test. A single worker cannot satisfy it. Tool errors, timeout, partial outputs and unknown external effects must be inspected through ToolResult rather than interpreted as successful text.
Basic Python is required. The package declares Python ≥3.10; local qualification uses Python 3.12.7. Each chapter runs independently; reuse the environment and matching files when continuing your project. Commands below use a macOS/Linux shell.
Prepare the project
Notes and tools
Run and verify
Behavior and support boundaries
Timeout is not hard cancellation.worker_still_running and outcome_unknown require reconciliation; do not automatically replay an operation with unknown effects.
Exercise and answer
Setmax_concurrency=1 in the reverse-order fixture and explain the failure. Restore 2 to pass; do not increase the timeout to hide the dependency.
Common errors and cleanup
ModuleNotFoundError: activate the environment with the specified installation and save every file on this page. Existing run root: choose a new --root instead of overwriting evidence. Assertion failure: inspect the first failed tool or typed error, not only the final text. After all processes and the board stop, remove only this lesson’s newly created run directories if no longer needed; retain SQLite, journals and reports you want to debug.
Complete files: save in the project root
notes.py
agent.yaml
custom_agent.py
parallel.py
