diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index b499d93c..805c3e4e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -106,6 +106,8 @@ jobs:
env:
QITOS_DOCS_TOOLS: ${{ runner.temp }}/qitos-doc-tools
run: |
+ python scripts/sync_tutorial_docs.py --check
+ python scripts/sync_api_reference.py --check
python scripts/validate_docs.py
node scripts/validate_docs_mdx.mjs
- python -m pytest -q tests/test_tutorial_snippets.py tests/test_docs_golden_paths.py
+ python -m pytest -q tests/test_tutorial_snippets.py tests/test_docs_golden_paths.py tests/test_docs_page_execution.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b47dc57..2a546c8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,12 @@ How to update:
## Unreleased
+### Added
+
+- Self-contained EN/zh notes-Agent lessons with complete inline files, source-synchronized excerpts, core API signatures and page-extracted installed-wheel checks.
+- Explicit serialized handoff lesson and documented same-head concurrent callback limitation; runtime unchanged.
+
+
### Fixed
- Avoid reparsing unchanged journal bytes on each append/read while retaining full content hashing, external-write validation, crash recovery and durability checks.
diff --git a/README.md b/README.md
index 8f4df5ae..0e553f01 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Prototype methods, run benchmarks, and inspect long-horizon trajectories on one
QitOS core is the small framework. Product-grade applications and showcase agents live in `qitos-zoo`, including planned apps such as `qitos-coder` and `qitos-cyber-agent`.
-[Quickstart](https://qitor.mintlify.app/quickstart) · [Tutorial Track](https://qitor.mintlify.app/tutorials) · [Benchmarks](https://qitor.mintlify.app/benchmarks/overview) · [CLI Reference](https://qitor.mintlify.app/reference/cli) · [Changelog](CHANGELOG.md) · [Chinese README](README.zh.md)
+[Quickstart](https://qitor.mintlify.app/quickstart) · [Tutorial Track](https://qitor.mintlify.app/tutorials/index) · [Benchmarks](https://qitor.mintlify.app/benchmarks/overview) · [CLI Reference](https://qitor.mintlify.app/reference/cli) · [Changelog](CHANGELOG.md) · [Chinese README](README.zh.md)
## What you can build
@@ -25,12 +25,15 @@ read-only qita. Framework correctness does not guarantee arbitrary model task su
## What's New
+- Self-contained web tutorials: complete notes-Agent code, bilingual learning path and source-checked core API reference; tests execute the files shown on each page.
+
+
- Master fixes Python 3.10 publication, repeated journal parsing and portable historical evidence verification; these are separately tested successors to the historical G5 runtime.
- G5 framework qualification passed; S4 local integration complete. Runtime identity: `717b4cf1b23f2ed252cd03234ffd8605038d9567`.
- Bilingual docs converge on installation → project → configuration → Session → inspection → recovery/extension.
- The default development branch is `master`, with CI/docs checks on pushes and PRs. Publication remains explicit.
-- Documentation/tutorial qualification is recorded separately. Remote synchronization is verified. Docs CI passed; successor CI stabilization is tracked with exact results in the [CI plan](docs/internal/plans/master_ci_stabilization.md). No package release or docs deployment was performed.
+- Documentation/tutorial qualification is recorded separately. Remote synchronization is verified. Docs CI passed; successor CI stabilization is tracked with exact results in the [CI plan](docs/internal/plans/master_ci_stabilization.md). Package releases and documentation deployments are tracked separately from CI qualification.
## Start developing
diff --git a/README.zh.md b/README.zh.md
index d69dde06..14aa7d6a 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -25,6 +25,9 @@ store、sink 和 sandbox 可扩展;qita 只读检查 Trajectory。
## What's New
+- 网页自足教程:资料整理 Agent 的完整代码、中英文学习路径、可核对源码签名的核心 API Reference;测试直接执行网页文件。
+
+
- master 修复 Python 3.10 publication、journal 重复解析和历史证据可移植核验;这些后继修复独立验证,不改写 G5 历史资格。
- G5 框架资格通过,S4 本地集成完成,runtime 身份固定为 `717b4cf1b23f2ed252cd03234ffd8605038d9567`。
- 双语文档统一到安装 → 项目 → 配置 → Session → 检查 → 恢复/扩展。
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
index f3b0f01c..4a6ce390 100644
--- a/docs/AGENTS.md
+++ b/docs/AGENTS.md
@@ -18,3 +18,10 @@ This file never relaxes them and does not authorize plugins, deployments or mode
compatibility, hard-thread-cancellation, automatic publication or lossless-redaction claim.
- Use task-local pinned documentation dependencies. Run docs/tests/link/MDX checks
and inspect actual desktop/mobile pages before promotion. No deployment is implied.
+
+- Core tutorials must contain all runnable files inline; links are optional conveniences.
+ Synchronize named excerpts and complete-file regions with scripts/sync_tutorial_docs.py.
+ CI uses --check and executes extracted EN/zh files from an installed wheel.
+- Keep docs/api-contracts.json explicit. Synchronize signatures with
+ scripts/sync_api_reference.py; every tutorial qitos import needs a reference entry.
+ Do not infer that submodule symbols are root-package exports.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 103c525e..effd5af0 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -17,3 +17,13 @@ above historical records; preserve historical evidence and raw-log digests.
Do not publish secrets, raw provider payloads or local paths. Remote branch
sync, default-branch promotion, package release and docs deployment are separate
operations requiring the applicable authorization.
+
+## Self-contained teaching pages
+
+Edit executable sources under `examples/tutorials/notes`, then synchronize with
+`python scripts/sync_tutorial_docs.py`. Every chapter includes all dependencies
+inline; source links are optional. API imports and source signatures are declared
+in `docs/api-contracts.json`; run `python scripts/sync_api_reference.py` in an
+environment with the matching QitOS installation. CI uses both scripts with
+`--check` and executes page-extracted files through `tests/test_docs_page_execution.py`.
+Maintain EN/zh prose together; do not translate executable identifiers or defaults.
diff --git a/docs/README.md b/docs/README.md
index 628b9b97..ba11cd6e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -21,3 +21,13 @@ in the promotion ledger; change them deliberately after verification.
Inspect both languages for introduction, Quickstart, Session, multi-agent,
sandbox, qita and configuration, including a narrow mobile viewport.
This is local preview only; this task does not deploy the documentation site.
+
+## Self-contained teaching pages
+
+Edit executable sources under `examples/tutorials/notes`, then synchronize with
+`python scripts/sync_tutorial_docs.py`. Every chapter includes all dependencies
+inline; source links are optional. API imports and source signatures are declared
+in `docs/api-contracts.json`; run `python scripts/sync_api_reference.py` in an
+environment with the matching QitOS installation. CI uses both scripts with
+`--check` and executes page-extracted files through `tests/test_docs_page_execution.py`.
+Maintain EN/zh prose together; do not translate executable identifiers or defaults.
diff --git a/docs/api-contracts.json b/docs/api-contracts.json
new file mode 100644
index 00000000..bce564c2
--- /dev/null
+++ b/docs/api-contracts.json
@@ -0,0 +1,379 @@
+{
+ "baseline": "60809b3be388d22ea40ea41b4aaa1f5540c76fda",
+ "groups": [
+ {
+ "slug": "composition",
+ "tutorial": "quickstart",
+ "symbols": [
+ {
+ "module": "qitos.config",
+ "name": "AgentComposition",
+ "methods": [
+ "session",
+ "restore",
+ "fork",
+ "close"
+ ],
+ "example": "# After loading config and creating the explicit provider:\nwith build_agent_composition(config, model_override=provider) as composition:\n session = composition.session(\"Index the notes\")\n result = session.run()"
+ },
+ {
+ "module": "qitos.config",
+ "name": "build_agent_composition",
+ "methods": [],
+ "example": "composition = build_agent_composition(config, model_override=provider)\ntry:\n result = composition.session(\"Index the notes\").run()\nfinally:\n composition.close()"
+ },
+ {
+ "module": "qitos.config",
+ "name": "load_agent_config",
+ "methods": [],
+ "example": "config = load_agent_config(\"agent.yaml\")\nprint(config.digest())"
+ },
+ {
+ "module": "qitos.config",
+ "name": "AgentConfig",
+ "methods": [
+ "to_dict",
+ "digest"
+ ],
+ "example": "config = load_agent_config(\"agent.yaml\")\nassert config.runtime.session.store == \"sqlite\"\nprint(config.to_dict()[\"model\"][\"credential\"])"
+ },
+ {
+ "module": "qitos.config",
+ "name": "CredentialRef",
+ "methods": [],
+ "example": "reference = CredentialRef(\"notes-provider\")\nprint(reference)"
+ },
+ {
+ "module": "qitos.config",
+ "name": "LocalCredentialFileResolver",
+ "methods": [
+ "resolve"
+ ],
+ "example": "from pathlib import Path\nresolver = LocalCredentialFileResolver(\n Path.home() / \".config/qitos/credentials.yaml\",\n repository_root=Path.cwd(),\n)\n# Pass resolver as credential_resolver to build_agent_composition."
+ },
+ {
+ "module": "qitos.config",
+ "name": "BudgetConfig",
+ "methods": [],
+ "example": "budget = BudgetConfig(max_steps=6, max_requests=6, max_runtime_seconds=30)"
+ },
+ {
+ "module": "qitos.config",
+ "name": "EnvironmentConfig",
+ "methods": [],
+ "example": "environment = EnvironmentConfig(workspace=\"./source\", image=\"python:3.12-slim\")"
+ },
+ {
+ "module": "qitos.config",
+ "name": "ModelConfig",
+ "methods": [],
+ "example": "model = ModelConfig(provider=\"openai_compatible\", model=\"notes-fake\")"
+ },
+ {
+ "module": "qitos.config",
+ "name": "RuntimeConfig",
+ "methods": [],
+ "example": "runtime = RuntimeConfig(environment=EnvironmentConfig(workspace=\"./source\"))"
+ },
+ {
+ "module": "qitos.config",
+ "name": "TrajectoryConfig",
+ "methods": [],
+ "example": "trajectory = TrajectoryConfig(output=\"./notes-run/trajectory.journal\")"
+ }
+ ]
+ },
+ {
+ "slug": "agent-runtime",
+ "tutorial": "guides/build-your-first-agent",
+ "symbols": [
+ {
+ "module": "qitos",
+ "name": "AgentModule",
+ "methods": [
+ "init_state",
+ "decide",
+ "reduce",
+ "should_stop",
+ "run"
+ ],
+ "example": "# NotesAgent subclasses AgentModule in the complete tutorial.\nagent = NotesAgent()\nresult = Engine(agent, runtime=RuntimeComposition()).session(\"Index notes\").run()"
+ },
+ {
+ "module": "qitos",
+ "name": "StateSchema",
+ "methods": [],
+ "example": "from dataclasses import dataclass\n@dataclass\nclass MyState(StateSchema):\n completed: int = 0\nstate = MyState(task=\"Index notes\", max_steps=3)"
+ },
+ {
+ "module": "qitos",
+ "name": "Task",
+ "methods": [],
+ "example": "task = Task(objective=\"Index the notes\")\nprint(task.objective)"
+ },
+ {
+ "module": "qitos",
+ "name": "Decision",
+ "methods": [
+ "act",
+ "final"
+ ],
+ "example": "decision = Decision.act([Action(name=\"summarize_note\", args={\"index\": 0})])\nfinished = Decision.final(\"Session\")"
+ },
+ {
+ "module": "qitos",
+ "name": "Action",
+ "methods": [],
+ "example": "action = Action(name=\"summarize_note\", args={\"index\": 0})\nprint(action.name)"
+ },
+ {
+ "module": "qitos",
+ "name": "Engine",
+ "methods": [
+ "session",
+ "run",
+ "restore"
+ ],
+ "example": "engine = Engine(NotesAgent(), runtime=RuntimeComposition())\nresult = engine.session(\"Index notes\").run()"
+ },
+ {
+ "module": "qitos.engine.engine",
+ "name": "EngineResult",
+ "methods": [],
+ "example": "result = session.run()\nprint(result.state.final_result, result.state.stop_reason)\nfor record in result.records:\n for tool_result in record.action_results:\n print(tool_result.tool_name, tool_result.status)"
+ },
+ {
+ "module": "qitos.engine.states",
+ "name": "RuntimeBudget",
+ "methods": [],
+ "example": "budget = RuntimeBudget(max_steps=3)\nengine = Engine(NotesAgent(), budget=budget)"
+ },
+ {
+ "module": "qitos",
+ "name": "StopReason",
+ "methods": [],
+ "example": "print([reason.value for reason in StopReason])"
+ },
+ {
+ "module": "qitos.engine.runtime",
+ "name": "RuntimeComposition",
+ "methods": [],
+ "example": "runtime = RuntimeComposition()\nengine = Engine(NotesAgent(), runtime=runtime)\n# The default checkpoint store is process-local."
+ }
+ ]
+ },
+ {
+ "slug": "sessions",
+ "tutorial": "tutorials/checkpoint-and-fork",
+ "symbols": [
+ {
+ "module": "qitos.engine.session_runtime",
+ "name": "Session",
+ "methods": [
+ "run",
+ "inspect",
+ "pause",
+ "steer",
+ "fork",
+ "capabilities"
+ ],
+ "example": "session = composition.session(\"Index notes\")\nresult = session.run()\ninspection = session.inspect()\nprint(session.session_id.value, result.state.final_result)"
+ },
+ {
+ "module": "qitos.engine.session_runtime",
+ "name": "SessionInspection",
+ "methods": [],
+ "example": "inspection = session.inspect()\nprint(inspection.work_graph)"
+ },
+ {
+ "module": "qitos.checkpoint.store",
+ "name": "CheckpointStore",
+ "methods": [
+ "get_session_head",
+ "commit_session_snapshot"
+ ],
+ "example": "# From an open composition configured with SQLite:\nstore = composition.runtime.checkpoint_store\nhead = store.get_session_head(session.session_id.value)\nprint(head)"
+ }
+ ]
+ },
+ {
+ "slug": "tools",
+ "tutorial": "concepts/tools-and-registry",
+ "symbols": [
+ {
+ "module": "qitos",
+ "name": "ToolRegistry",
+ "methods": [
+ "register"
+ ],
+ "example": "registry = ToolRegistry()\nregistry.register(summarize_note)"
+ },
+ {
+ "module": "qitos.core.function_tool_decorator",
+ "name": "function_tool",
+ "methods": [],
+ "example": "@function_tool(read_only=True, concurrency_safe=True)\ndef title(text: str) -> str:\n return text.split(\":\", 1)[0]"
+ },
+ {
+ "module": "qitos.core.tool",
+ "name": "BaseTool",
+ "methods": [
+ "execute"
+ ],
+ "example": "# Class tools implement execute, not the compatibility run method.\nclass EchoTool(BaseTool):\n name = \"echo\"\n description = \"Return a trusted input\"\n def execute(self, args, runtime_context=None):\n return ToolResult(output=args)"
+ },
+ {
+ "module": "qitos.core.tool_result",
+ "name": "ToolResult",
+ "methods": [],
+ "example": "result = ToolResult(output={\"title\": \"Session\"})\nprint(result.status, result.output, result.outcome_unknown)"
+ },
+ {
+ "module": "qitos.core.artifact",
+ "name": "ArtifactRef",
+ "methods": [
+ "from_dict"
+ ],
+ "example": "# ref is an ArtifactRef obtained from a tool result.\nprint(ref.sha256)\nbody = composition.agent.config[\"artifact_resolver\"].resolve(ref).body"
+ },
+ {
+ "module": "qitos.engine.action_executor",
+ "name": "ActionExecutionPolicy",
+ "methods": [],
+ "example": "policy = ActionExecutionPolicy(mode=\"parallel\", max_concurrency=2)\nengine = Engine(NotesAgent(), runtime=RuntimeComposition(), action_execution_policy=policy)"
+ },
+ {
+ "module": "qitos.kit.tool.internal.publication",
+ "name": "SandboxPublicationTool",
+ "methods": [
+ "execute"
+ ],
+ "example": "# Only after sandbox execution, with explicit publication authority:\npublication = SandboxPublicationTool(\n composition.env, paths=[\"report.txt\"],\n expected_input_digest=composition.env.input_digest,\n)\ncomposition.tool_registry.register(publication)"
+ }
+ ]
+ },
+ {
+ "slug": "context",
+ "tutorial": "guides/memory-and-history",
+ "symbols": [
+ {
+ "module": "qitos.core.context",
+ "name": "StaticContextContributor",
+ "methods": [],
+ "example": "contributor = StaticContextContributor(\"notes.project\", \"project\", \"Use supplied notes only.\")"
+ },
+ {
+ "module": "qitos.core.context",
+ "name": "PriorityContextSelectionPolicy",
+ "methods": [
+ "select"
+ ],
+ "example": "class AuditedSelector(PriorityContextSelectionPolicy):\n def select(self, contributions, **options):\n contributions = tuple(contributions)\n print([item.contribution_id for item in contributions])\n return super().select(contributions, **options)"
+ },
+ {
+ "module": "qitos.core.context",
+ "name": "DeclaredContextBudgetPolicy",
+ "methods": [],
+ "example": "policy = DeclaredContextBudgetPolicy(default_max_input_units=4096)"
+ },
+ {
+ "module": "qitos.core.request_view",
+ "name": "CompactionReceipt",
+ "methods": [],
+ "example": "# receipt is returned by the compactor in the context tutorial.\nprint(receipt.declared_losses, receipt.output_digest)"
+ },
+ {
+ "module": "qitos.engine.runtime",
+ "name": "LifecyclePolicy",
+ "methods": [
+ "should_pause"
+ ],
+ "example": "class PauseFirstTool(LifecyclePolicy):\n policy_id = \"notes.pause\"\n def should_pause(self, context):\n return context.step_id == 0"
+ }
+ ]
+ },
+ {
+ "slug": "work-graph",
+ "tutorial": "guides/multi-agent-patterns",
+ "symbols": [
+ {
+ "module": "qitos.engine.session_runtime",
+ "name": "Session",
+ "methods": [
+ "delegate",
+ "spawn",
+ "fan_out",
+ "join",
+ "handoff"
+ ],
+ "example": "session = composition.session(\"Index notes\")\nresult = session.run()\ninspection = session.inspect()\nprint(session.session_id.value, result.state.final_result)"
+ },
+ {
+ "module": "qitos.core.work_graph",
+ "name": "WorkGraph",
+ "methods": [
+ "from_canonical_dict"
+ ],
+ "example": "graph = WorkGraph.from_canonical_dict(session.inspect().work_graph)\nprint(len(graph.completions), len(graph.joins))"
+ },
+ {
+ "module": "qitos.core.work_graph",
+ "name": "WorkItem",
+ "methods": [],
+ "example": "work = graph.work_items[session.work_item_id]\nprint(work.owner)"
+ },
+ {
+ "module": "qitos.core.work_graph",
+ "name": "WorkAttempt",
+ "methods": [],
+ "example": "from dataclasses import fields\nprint([field.name for field in fields(WorkAttempt)])"
+ },
+ {
+ "module": "qitos.engine.work_runtime",
+ "name": "DurableWorkRuntime",
+ "methods": [],
+ "example": "runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver(), max_workers=2))\ncomposition.runtime.work_runtime = runtime"
+ },
+ {
+ "module": "qitos.engine.work_runtime",
+ "name": "LocalWorkScheduler",
+ "methods": [],
+ "example": "scheduler = LocalWorkScheduler(Resolver(), max_workers=2)\n# Resolver.resolve(descriptor) returns a bounded callable; see the full lesson."
+ },
+ {
+ "module": "qitos.engine.work_runtime",
+ "name": "WorkRuntimeError",
+ "methods": [],
+ "example": "try:\n source.spawn(\"notes_agent\", task=\"Attempt after handoff\")\nexcept WorkRuntimeError as error:\n print(error.code)"
+ }
+ ]
+ },
+ {
+ "slug": "trajectory",
+ "tutorial": "guides/observability",
+ "symbols": [
+ {
+ "module": "qitos.qita.reader",
+ "name": "default_reader",
+ "methods": [],
+ "example": "reader = default_reader(root)\ntrajectory = reader.read_session(identity, view=PrivacyView.RAW_PRIVATE)\nprint(len(trajectory.records))"
+ },
+ {
+ "module": "qitos.tracing.exporter",
+ "name": "CanonicalTrajectoryExporter",
+ "methods": [
+ "export",
+ "reimport"
+ ],
+ "example": "exporter = CanonicalTrajectoryExporter()\nexported = exporter.export(trajectory, view=PrivacyView.REDACTED_PUBLIC)\nprint(exported.loss.is_lossless)"
+ },
+ {
+ "module": "qitos.tracing.trajectory",
+ "name": "PrivacyView",
+ "methods": [],
+ "example": "view = PrivacyView.REDACTED_PUBLIC\nprint(view.value)"
+ }
+ ]
+ }
+ ]
+}
diff --git a/docs/architecture/architecture-debt.md b/docs/architecture/architecture-debt.md
index c686fde0..b443970d 100644
--- a/docs/architecture/architecture-debt.md
+++ b/docs/architecture/architecture-debt.md
@@ -143,3 +143,13 @@ Legend: **P0** structural risk (blocks refactors / can break imports), **P1** im
- No removal of the v1 trace format before the trajectory data plane (v4/05) lands a versioned replacement.
- No repository-wide mechanical lint/type cleanup in the same PR as runtime behavior changes; Task 08 uses a ratchet.
- No generic `utils.py`/`common.py`; consolidation follows a named contract owner.
+
+## Documentation-discovered handoff scheduling boundary
+
+At runtime source 60809b3, a same-Session handoff destination that restores inside
+the source LocalWorkScheduler callback can claim the head before the source
+terminal callback persists, causing an owner CAS conflict. The teaching example
+serializes transfer admission, source cleanup and destination execution. Concurrent
+same-head dispatch requires separate runtime investigation; see
+[reproduction and scope](../internal/plans/docs_self_contained_learning.md).
+No runtime or ownership checks were weakened for the tutorial.
diff --git a/docs/concepts/tools-and-registry.mdx b/docs/concepts/tools-and-registry.mdx
index 66931762..deb05732 100644
--- a/docs/concepts/tools-and-registry.mdx
+++ b/docs/concepts/tools-and-registry.mdx
@@ -1,40 +1,325 @@
---
title: "Tools and parallel calls"
-description: "QitOS G5 · Tools and parallel calls"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Learning goal
+## Goal and prerequisites
-Register a trusted function, construct a batch and select parallel execution with max_concurrency=2. Inspect the decide and reduce methods to see where declarations and results meet.
+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.
-## Prerequisites
+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.
-Complete the [Quickstart](/quickstart) installation and lessons copy; base package, Python 3.12.7 tested, no real model requests.
+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.
-## Complete runnable example
+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.
-[`examples/tutorials/custom_agent.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/custom_agent.py)
+## Prepare the project
-The complete directory is the execution source. Sibling session_walkthrough.py is a public teaching dependency, not a repository test helper.
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
+
+### Notes and tools
+
+{/* tutorial-snippet:notes.py:fixture */}
+```python
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+```
+{/* tutorial-snippet:end */}
+
+## Run and verify
```bash
-python lessons/custom_agent.py
+python custom_agent.py
+python parallel.py
```
-## Expected output and assertions
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
+
+```text
+completion=[1, 0]; declaration=[0, 1]
+```
+
+## 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
+
+Set `max_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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
-`squares complete; completed=2; process_local=true`
-Assertions in the file verify the result; generated IDs vary. Commands exit zero; stop the board with Ctrl-C.
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
-## Support boundaries
-Declaration order and completion order are different. Canonical completion facts retain actual order; do not infer ordering from thread scheduling. Only concurrency_safe tools may overlap. Timeout does not prove hard cancellation; outcome_unknown requires reconciliation, not automatic replay.
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="custom_agent.py"
+"""A custom notes AgentModule through the public Engine/Session path."""
+from dataclasses import dataclass, field
+
+from qitos import Action, AgentModule, Decision, Engine, StateSchema, ToolRegistry
+from qitos.engine.action_executor import ActionExecutionPolicy
+from qitos.engine.runtime import RuntimeComposition
+from notes import summarize_note
+
+
+# docs:start agent
+@dataclass
+class NotesState(StateSchema):
+ titles: list[str] = field(default_factory=list)
+
+
+class NotesAgent(AgentModule):
+ def __init__(self):
+ registry = ToolRegistry()
+ registry.register(summarize_note)
+ super().__init__(tool_registry=registry)
+
+ def init_state(self, task, **kwargs):
+ return NotesState(task=task, max_steps=3)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([
+ Action(name="summarize_note", args={"index": index}) for index in (0, 1)
+ ])
+
+ def reduce(self, state, observation, decision):
+ state.titles.extend(item["output"]["title"] for item in observation.get("action_results", []))
+ return state
+# docs:end agent
+
+
+# docs:start run
+def main():
+ for mode in ("sequential", "parallel"):
+ engine = Engine(NotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode=mode, max_concurrency=2))
+ result = engine.session("Index the two notes").run()
+ assert result.state.titles == ["Session", "Artifact"]
+ assert result.state.final_result == "Session, Artifact"
+ print(f"{mode}: Session, Artifact; process_local=true")
+# docs:end run
+
+
+if __name__ == "__main__":
+ main()
+```
+
+```python title="parallel.py"
+"""Force reverse completion while retaining declaration order in reduce."""
+from threading import Event
+
+from qitos import Action, Decision, Engine, ToolRegistry
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.action_executor import ActionExecutionPolicy
+from qitos.engine.runtime import RuntimeComposition
+from custom_agent import NotesAgent
+from notes import NOTES
+
+# Explicit test instrumentation for one local invocation, not persistent state.
+second_finished = Event()
+completion_order = []
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def analyze_note(index: int) -> dict:
+ """An in-memory fixture that controls completion order without file/network I/O."""
+ if index == 0:
+ if not second_finished.wait(5):
+ raise RuntimeError("This fixture requires two parallel workers")
+ completion_order.append(index)
+ if index == 1:
+ second_finished.set()
+ return {"title": NOTES[index].split(":", 1)[0]}
+
+
+class ParallelNotesAgent(NotesAgent):
+ def __init__(self):
+ super().__init__()
+ self.tool_registry = ToolRegistry()
+ self.tool_registry.register(analyze_note)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([Action(name="analyze_note", args={"index": i}) for i in (0, 1)])
+
+
+def run():
+ second_finished.clear()
+ completion_order.clear()
+ engine = Engine(ParallelNotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode="parallel", max_concurrency=2))
+ result = engine.session("Compare completion with declaration order").run()
+ assert completion_order == [1, 0]
+ assert result.state.titles == ["Session", "Artifact"]
+ print("completion=[1, 0]; declaration=[0, 1]; titles=Session, Artifact")
+
+
+if __name__ == "__main__":
+ run()
+```
-## Common errors
+{/* tutorial-files:end */}
-Choose a new root if the directory already exists. For missing imports verify the installed source. Inspect Trajectory and the migration page for config mismatch, missing extensions or typed failures; never silently retry unknown external effects.
+## Next step and API
-## Next step
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/tutorials/checkpoint-and-fork)
-[Learning path](/tutorials/index) · [Migration and troubleshooting](/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/parallel.py) (optional; all required code is already on this page).
diff --git a/docs/docs.json b/docs/docs.json
index c35d326c..d4885899 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -10,7 +10,7 @@
"navbar": {
"primary": {
"type": "github",
- "href": "https://github.com/Qitor/qitos"
+ "href": "https://github.com/WhitzardAgent/WhitzardOS"
}
},
"navigation": {
@@ -20,7 +20,7 @@
"default": true,
"tabs": [
{
- "tab": "Getting started",
+ "tab": "Get started",
"groups": [
{
"group": "Start here",
@@ -30,9 +30,14 @@
"installation",
"quickstart"
]
- },
+ }
+ ]
+ },
+ {
+ "tab": "Learn",
+ "groups": [
{
- "group": "Core learning units",
+ "group": "Notes project",
"pages": [
"tutorials/index",
"guides/build-your-first-agent",
@@ -48,7 +53,7 @@
]
},
{
- "tab": "Advanced and reference",
+ "tab": "Task guides",
"groups": [
{
"group": "benchmarks",
@@ -114,17 +119,6 @@
"index"
]
},
- {
- "group": "reference",
- "pages": [
- "reference/api",
- "reference/cli",
- "reference/configuration",
- "reference/g5-migration",
- "reference/kit",
- "reference/model-family-matrix"
- ]
- },
{
"group": "tutorials",
"pages": [
@@ -147,18 +141,26 @@
]
},
{
- "tab": "History and compatibility",
+ "tab": "API Reference",
"groups": [
{
- "group": "History",
+ "group": "API Reference",
"pages": [
- "blog/desktop-osworld-starter",
- "blog/gold-presets-preview",
- "blog/index",
- "blog/reproducible-runs",
- "blog/single-kernel",
- "concepts/function-tool-migration",
- "guides/snowl-integration"
+ "reference/api",
+ "reference/composition",
+ "reference/agent-runtime",
+ "reference/sessions",
+ "reference/tools",
+ "reference/context",
+ "reference/work-graph",
+ "reference/trajectory",
+ "reference/extensions",
+ "reference/cli",
+ "reference/configuration",
+ "reference/model-family-matrix",
+ "reference/g5-migration",
+ "reference/kit",
+ "reference/legacy-api"
]
}
]
@@ -179,9 +181,14 @@
"zh/installation",
"zh/quickstart"
]
- },
+ }
+ ]
+ },
+ {
+ "tab": "学习教程",
+ "groups": [
{
- "group": "核心学习单元",
+ "group": "资料整理项目",
"pages": [
"zh/tutorials/index",
"zh/guides/build-your-first-agent",
@@ -197,10 +204,10 @@
]
},
{
- "tab": "高级与参考",
+ "tab": "任务指南",
"groups": [
{
- "group": "benchmarks",
+ "group": "基准测试",
"pages": [
"zh/benchmarks/cybench",
"zh/benchmarks/cybergym",
@@ -212,13 +219,13 @@
]
},
{
- "group": "community",
+ "group": "社区",
"pages": [
"zh/community"
]
},
{
- "group": "concepts",
+ "group": "概念",
"pages": [
"zh/concepts/agent-module",
"zh/concepts/domestic-model-ecosystem",
@@ -231,14 +238,14 @@
]
},
{
- "group": "contributing",
+ "group": "参与贡献",
"pages": [
"zh/contributing/development",
"zh/contributing/writing-a-method-template"
]
},
{
- "group": "guides",
+ "group": "任务指南",
"pages": [
"zh/guides/add-a-family-preset",
"zh/guides/agent-patterns",
@@ -258,24 +265,13 @@
]
},
{
- "group": "index",
+ "group": "概览",
"pages": [
"zh/index"
]
},
{
- "group": "reference",
- "pages": [
- "zh/reference/api",
- "zh/reference/cli",
- "zh/reference/configuration",
- "zh/reference/g5-migration",
- "zh/reference/kit",
- "zh/reference/model-family-matrix"
- ]
- },
- {
- "group": "tutorials",
+ "group": "高级教程",
"pages": [
"zh/tutorials/claude-code",
"zh/tutorials/code-security-audit",
@@ -296,18 +292,26 @@
]
},
{
- "tab": "历史与兼容",
+ "tab": "API Reference",
"groups": [
{
- "group": "历史",
+ "group": "API Reference",
"pages": [
- "zh/blog/desktop-osworld-starter",
- "zh/blog/gold-presets-preview",
- "zh/blog/index",
- "zh/blog/reproducible-runs",
- "zh/blog/single-kernel",
- "zh/concepts/function-tool-migration",
- "zh/guides/snowl-integration"
+ "zh/reference/api",
+ "zh/reference/composition",
+ "zh/reference/agent-runtime",
+ "zh/reference/sessions",
+ "zh/reference/tools",
+ "zh/reference/context",
+ "zh/reference/work-graph",
+ "zh/reference/trajectory",
+ "zh/reference/extensions",
+ "zh/reference/cli",
+ "zh/reference/configuration",
+ "zh/reference/model-family-matrix",
+ "zh/reference/g5-migration",
+ "zh/reference/kit",
+ "zh/reference/legacy-api"
]
}
]
diff --git a/docs/guides/build-your-first-agent.mdx b/docs/guides/build-your-first-agent.mdx
index e1ebd31d..f295b708 100644
--- a/docs/guides/build-your-first-agent.mdx
+++ b/docs/guides/build-your-first-agent.mdx
@@ -1,40 +1,298 @@
---
-title: "Custom Agent"
-description: "QitOS G5 · Custom Agent"
+title: "Write your own AgentModule"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Learning goal
+## Goal and prerequisites
-Write a StateSchema, AgentModule.decide/reduce and a final stopping decision. The example squares 3 and 4 through a two-action batch.
+A custom Agent makes the decision policy explicit. `NotesState.titles` starts empty. `decide` declares two actions, `reduce` folds their observations into state, and the next `decide` returns a final decision. This example needs no model.
-## Prerequisites
+The same notes fixture is used in the Quickstart. Here you replace the decision policy rather than the configured provider. `AgentComposition` builds its configured Agent; it does not accept an arbitrary `agent_override`. Construct the public Engine with your own AgentModule and RuntimeComposition, then create a Session.
-Complete the [Quickstart](/quickstart) installation and lessons copy; base package, Python 3.12.7 tested, no real model requests.
+Run both execution modes. They must produce the same declaration-ordered title list. In `reduce`, action observations are dictionaries; `EngineResult.records` contains typed ToolResult objects. Do not confuse those two representations.
-## Complete runnable example
+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.
-[`examples/tutorials/custom_agent.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/custom_agent.py)
+## Prepare the project
-The complete directory is the execution source. Sibling session_walkthrough.py is a public teaching dependency, not a repository test helper.
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
+
+### State and decisions
+
+{/* tutorial-snippet:custom_agent.py:agent */}
+```python
+@dataclass
+class NotesState(StateSchema):
+ titles: list[str] = field(default_factory=list)
+
+
+class NotesAgent(AgentModule):
+ def __init__(self):
+ registry = ToolRegistry()
+ registry.register(summarize_note)
+ super().__init__(tool_registry=registry)
+
+ def init_state(self, task, **kwargs):
+ return NotesState(task=task, max_steps=3)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([
+ Action(name="summarize_note", args={"index": index}) for index in (0, 1)
+ ])
+
+ def reduce(self, state, observation, decision):
+ state.titles.extend(item["output"]["title"] for item in observation.get("action_results", []))
+ return state
+```
+{/* tutorial-snippet:end */}
+
+### Run and assertions
+
+{/* tutorial-snippet:custom_agent.py:run */}
+```python
+def main():
+ for mode in ("sequential", "parallel"):
+ engine = Engine(NotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode=mode, max_concurrency=2))
+ result = engine.session("Index the two notes").run()
+ assert result.state.titles == ["Session", "Artifact"]
+ assert result.state.final_result == "Session, Artifact"
+ print(f"{mode}: Session, Artifact; process_local=true")
+```
+{/* tutorial-snippet:end */}
+
+## Run and verify
```bash
-python lessons/custom_agent.py
+python custom_agent.py
+```
+
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
+
+```text
+sequential: Session, Artifact
+parallel: Session, Artifact
+```
+
+## Behavior and support boundaries
+
+The default RuntimeComposition uses a process-local Memory checkpoint store. A Session identity alone does not make that store durable. Use the composition/SQLite lifecycle lesson for cross-process recovery.
+
+## Exercise and answer
+
+Process only the first note: change the action indices to `(0,)` and expect `Session` in both modes.
+
+## 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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
```
-## Expected output and assertions
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="custom_agent.py"
+"""A custom notes AgentModule through the public Engine/Session path."""
+from dataclasses import dataclass, field
+
+from qitos import Action, AgentModule, Decision, Engine, StateSchema, ToolRegistry
+from qitos.engine.action_executor import ActionExecutionPolicy
+from qitos.engine.runtime import RuntimeComposition
+from notes import summarize_note
+
-`squares complete; completed=2; process_local=true`
+# docs:start agent
+@dataclass
+class NotesState(StateSchema):
+ titles: list[str] = field(default_factory=list)
-Assertions in the file verify the result; generated IDs vary. Commands exit zero; stop the board with Ctrl-C.
-## Support boundaries
+class NotesAgent(AgentModule):
+ def __init__(self):
+ registry = ToolRegistry()
+ registry.register(summarize_note)
+ super().__init__(tool_registry=registry)
-This advanced Python path uses Engine with RuntimeComposition and a process-local Memory store. AgentComposition creates ConfiguredAgent; it has no arbitrary agent_override argument. Use this public Engine path for a custom AgentModule, and configure durable stores/resolvers for cross-process recovery.
+ def init_state(self, task, **kwargs):
+ return NotesState(task=task, max_steps=3)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([
+ Action(name="summarize_note", args={"index": index}) for index in (0, 1)
+ ])
+
+ def reduce(self, state, observation, decision):
+ state.titles.extend(item["output"]["title"] for item in observation.get("action_results", []))
+ return state
+# docs:end agent
+
+
+# docs:start run
+def main():
+ for mode in ("sequential", "parallel"):
+ engine = Engine(NotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode=mode, max_concurrency=2))
+ result = engine.session("Index the two notes").run()
+ assert result.state.titles == ["Session", "Artifact"]
+ assert result.state.final_result == "Session, Artifact"
+ print(f"{mode}: Session, Artifact; process_local=true")
+# docs:end run
+
+
+if __name__ == "__main__":
+ main()
+```
-## Common errors
+{/* tutorial-files:end */}
-Choose a new root if the directory already exists. For missing imports verify the installed source. Inspect Trajectory and the migration page for config mismatch, missing extensions or typed failures; never silently retry unknown external effects.
+## Next step and API
-## Next step
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/concepts/tools-and-registry)
-[Learning path](/tutorials/index) · [Migration and troubleshooting](/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/custom_agent.py) (optional; all required code is already on this page).
diff --git a/docs/guides/memory-and-history.mdx b/docs/guides/memory-and-history.mdx
index 033ff2df..b64e8ab0 100644
--- a/docs/guides/memory-and-history.mdx
+++ b/docs/guides/memory-and-history.mdx
@@ -1,40 +1,271 @@
---
title: "Context and memory"
-description: "QitOS G5 · Context and memory"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Learning goal
+## Goal and prerequisites
-Register contributor, memory source, selector, budget policy and compactor by explicit extension factories. Verify selection and a declared compaction loss in the persisted Trajectory.
+The notes Agent needs project instructions and remembered facts. Register contributor factories in `extensions`, then reference their names from `context` and `memory`. The selector records which contributions were actually considered; a dictionary entry alone is not proof the model saw it.
-## Prerequisites
+The custom compactor deliberately omits a closed exchange without a summary and returns a CompactionReceipt declaring that loss. Inspect the journal for a compaction record whose loss is not lossless. This demonstrates how to report a lossy transformation; it is not a recommended production summarization strategy.
-Complete the [Quickstart](/quickstart) installation and lessons copy; base package, Python 3.12.7 tested, no real model requests.
+Read `AuditedSelector.select`, `OmitClosedExchange.compact`, and the configuration in `run` in that order. The fixture sets protected recent exchanges to zero only to exercise its explicitly declared omission. It does not disable codec loss checks.
-## Complete runnable example
+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.
-[`examples/tutorials/context_memory.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/context_memory.py)
+## Prepare the project
-The complete directory is the execution source. Sibling session_walkthrough.py is a public teaching dependency, not a repository test helper.
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
+
+## Run and verify
```bash
-python lessons/context_memory.py --root ./context-run
+python context.py --root context-run
+```
+
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
+
+```text
+compaction loss recorded
+```
+
+## Behavior and support boundaries
+
+Memory contributors are context inputs; a checkpoint store persists execution state. They solve different problems. Public redacted export is not a raw backup, even when record counts match.
+
+## Exercise and answer
+
+Change the memory text, retain its contribution ID, and verify the selector still reports it. Inspect the resulting compaction loss instead of asserting losslessness.
+
+## 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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
```
-## Expected output and assertions
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="context.py"
+"""Explicit contributor, memory, selector and compactor factories; no network."""
+import argparse
+from dataclasses import replace
+import hashlib
+from pathlib import Path
+
+from qitos.config import build_agent_composition
+from qitos.core.context import (
+ DeclaredContextBudgetPolicy, PriorityContextSelectionPolicy, StaticContextContributor,
+)
+from qitos.core.request_view import CompactionReceipt
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+from notes import FakeProvider, summarize_note, configuration
+
+
+class AuditedSelector(PriorityContextSelectionPolicy):
+ def __init__(self):
+ self.seen = set()
-`context selected; memory selected; compaction loss recorded`
+ def select(self, contributions, **options):
+ contributions = tuple(contributions)
+ self.seen.update(item.contribution_id for item in contributions)
+ return super().select(contributions, **options)
-Assertions in the file verify the result; generated IDs vary. Commands exit zero; stop the board with Ctrl-C.
-## Support boundaries
+class OmitClosedExchange:
+ """Explicitly lossy omission for this arithmetic fixture only."""
+ policy_id = "tutorial.omit_closed"
-The example omits closed arithmetic exchanges with a declared loss; it does not disable codec loss checks. The static memory source is application-owned and process-local; Session persistence does not turn it into a durable knowledge store. Required context and incompatible continuation fail closed.
+ def __init__(self):
+ self.calls = 0
+
+ def compact(self, **values):
+ self.calls += 1
+ return CompactionReceipt(
+ receipt_id="compaction_" + values["selected_digest"][:24],
+ input_exchange_ids=tuple(values["exchange_ids"]), policy_id=self.policy_id,
+ output_digest=hashlib.sha256(b"").hexdigest(),
+ declared_losses=("closed_exchange_omitted_without_summary",),
+ )
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ config = replace(
+ configuration(root),
+ context={"contributors": ["project"], "selector": "selector", "budget_policy": "budget"},
+ memory={"sources": ["memory"]}, compaction={"provider": "compactor"})
+ selector, compactor = AuditedSelector(), OmitClosedExchange()
+ with build_agent_composition(config, model_override=FakeProvider(), extensions={
+ "project": lambda: StaticContextContributor("lesson.project", "project", "Use only the supplied notes."),
+ "memory": lambda: StaticContextContributor("lesson.memory", "memory", "Session and Artifact are the two note titles."),
+ "selector": selector, "compactor": compactor,
+ "budget": lambda: DeclaredContextBudgetPolicy(default_max_input_units=4096, protected_recent_exchanges=0),
+ }) as composition:
+ composition.tool_registry.register(summarize_note)
+ session = composition.session("Index both notes")
+ assert session.run().state.final_result == "Indexed 2 notes: Session, Artifact."
+ assert {"lesson.project", "lesson.memory"} <= selector.seen
+ assert compactor.calls > 0
+ trajectory = default_reader(root).read_session(session.session_id.value, view=PrivacyView.RAW_PRIVATE)
+ assert any(record.kind.value == "compaction" and not record.loss.is_lossless for record in trajectory.records)
+ print("context selected; memory selected; compaction loss recorded")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ run(parser.parse_args().root.resolve())
+```
-## Common errors
+{/* tutorial-files:end */}
-Choose a new root if the directory already exists. For missing imports verify the installed source. Inspect Trajectory and the migration page for config mismatch, missing extensions or typed failures; never silently retry unknown external effects.
+## Next step and API
-## Next step
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/guides/sandbox-and-artifacts)
-[Learning path](/tutorials/index) · [Migration and troubleshooting](/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/context.py) (optional; all required code is already on this page).
diff --git a/docs/guides/multi-agent-patterns.mdx b/docs/guides/multi-agent-patterns.mdx
index 2d1d6923..a74f519e 100644
--- a/docs/guides/multi-agent-patterns.mdx
+++ b/docs/guides/multi-agent-patterns.mdx
@@ -1,42 +1,352 @@
---
-title: "Multi-agent work"
-description: "QitOS G5 · Multi-agent work"
+title: "Delegate, join and handoff"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Learning goal
+## Goal and prerequisites
-Use delegate, spawn and fan-out to execute four real child Sessions in fresh processes, then join their durable results. The local resolver explicitly selects this tutorial worker.
+The parent first creates a durable paused Session. Its explicit resolver maps the known `notes_agent` descriptor to local subprocess execution. Each child restores a real Session using the installed package; there is no distributed worker service hidden behind this example.
-## Prerequisites
+`delegate` and `spawn` submit child work; `fan_out` submits a batch; `join` waits for the operation IDs under an explicit policy. Inspect four child completions and a closed join. The resolver handles join without rerunning its referenced children. Child final responses are scripted in this bounded scheduler lesson; successful orchestration is not proof of independent model reasoning.
-Complete the [Quickstart](/quickstart) installation and lessons copy; base package, Python 3.12.7 tested, no real model requests.
+Run `handoff.py` separately. The destination resumes the same work item in another process, ownership changes, and dispatch from the superseded source is rejected. In contrast, the lifecycle lesson's fork creates an independent branch and preserves the source head.
-## Complete runnable example
+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.
-[`examples/tutorials/work_graph.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/work_graph.py)
+## Prepare the project
-The complete directory is the execution source. Sibling session_walkthrough.py is a public teaching dependency, not a repository test helper.
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
+
+## Run and verify
```bash
-python lessons/work_graph.py --root ./work-run
+python multi_agent.py --root work-run
+python handoff.py --root handoff-run
+```
+
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
+
+```text
+durable children=4; join=closed
+handoff destination ran
+```
+
+Handoff is explicitly serialized here: persist transfer admission, close the source composition, then start the destination process. Admission is not destination task completion; the destination subprocess assertion proves execution. At this source version, concurrent same-Session destination restore and source terminal callbacks can conflict on owner CAS. This example does not qualify concurrent same-head handoff scheduling.
+
+## Behavior and support boundaries
+
+A timeout or outcome_unknown is terminal for the lesson wait, not a trigger to resubmit work. LocalWorkScheduler is process-local scheduling; durable receipts do not turn it into a distributed queue.
+
+## Exercise and answer
+
+Inspect the four completion records and distinguish the two individual operations from the two fan-out children. Keep join references as operation IDs, not display names.
+
+## 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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
```
-## Expected output and assertions
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="multi_agent.py"
+"""Durable spawn/delegate/fan-out/join with real child Session execution.
+
+The local scheduler resolves only this tutorial's Agent; no distributed service.
+"""
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+import time
+
+from qitos.core.work_graph import WorkGraph
+from qitos.engine.work_runtime import DurableWorkRuntime, LocalWorkScheduler
+from dataclasses import replace
+from qitos.config import build_agent_composition
+from notes import FakeProvider, PauseAfterTool, summarize_note, configuration
+
+
+def compose(root, *, pause=False, finish=False):
+ config = configuration(root)
+ config = replace(config, budgets=replace(config.budgets, max_requests=16),
+ lifecycle={"policy": "pause"})
+ result = build_agent_composition(config, model_override=FakeProvider(start=2 if finish else 0),
+ extensions={"pause": PauseAfterTool})
+ result.tool_registry.register(summarize_note)
+ return result
+
-`durable children=4; join=closed; parent retains ownership`
+def wait(session, operation):
+ deadline = time.monotonic() + 30
+ while time.monotonic() < deadline:
+ graph = WorkGraph.from_canonical_dict(session.inspect().work_graph)
+ receipt = next(item for item in graph.operation_receipts if item.operation_id == operation.operation_id)
+ if receipt.state in {"completed", "failed", "outcome_unknown"}:
+ assert receipt.state == "completed", receipt.state
+ return graph
+ time.sleep(0.02)
+ raise AssertionError("child deadline exceeded; inspect before retrying")
-Assertions in the file verify the result; generated IDs vary. Commands exit zero; stop the board with Ctrl-C.
-## Support boundaries
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
-Delegate creates awaited child work; spawn creates detached child work; fan-out declares a set; join consumes outcomes. Handoff transfers ownership of the same WorkItem and fences the old owner; it is not a child or fork. Fork creates independent lineage without advancing the source. The executable lesson covers child operations; handoff execution is a next-round E2E scenario. No distributed scheduler or external exactly-once effects are claimed.
+ class Resolver:
+ resolver_id = "tutorial.notes_agent.worker"
-## Common errors
+ def resolve(self, descriptor):
+ def execute():
+ for identity in (() if descriptor.operation == "join" else descriptor.child_session_ids):
+ subprocess.run([sys.executable, __file__, "--root", str(root), "--child", identity],
+ check=True, capture_output=True, text=True, timeout=20)
+ return {"children": list(descriptor.child_session_ids)}
+ return execute
+
+ with compose(root, pause=True) as composition:
+ composition.runtime.work_runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver(), max_workers=2))
+ parent = composition.session("Index, then ask independent note workers")
+ parent.run()
+ assert parent.lifecycle.value == "paused"
+ delegated = parent.delegate("notes_agent", task="Describe the Session note")
+ wait(parent, delegated)
+ spawned = parent.spawn("notes_agent", task="Describe the Artifact note")
+ wait(parent, spawned)
+ batch = parent.fan_out([{"agent": "notes_agent", "task": "Review Session", "budget": {"model_requests": 2}},
+ {"agent": "notes_agent", "task": "Review Artifact", "budget": {"model_requests": 2}}])
+ wait(parent, batch)
+ joined = parent.join([delegated.operation_id, spawned.operation_id, batch.operation_id], policy="all")
+ graph = wait(parent, joined)
+ assert graph.joins[-1].state == "closed"
+ assert len(graph.completions) == 4
+ print("durable children=4; join=closed; parent retains ownership")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--child")
+ args = parser.parse_args()
+ root = args.root.resolve()
+ if args.child:
+ with compose(root, finish=True, pause=True) as composition:
+ result = composition.restore(args.child).run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ else:
+ run(root)
+```
+
+```python title="handoff.py"
+"""Transfer one work item's owner; demonstrate the superseded source fence."""
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+
+from notes import compose
+from multi_agent import wait
+from qitos.engine.work_runtime import DurableWorkRuntime, LocalWorkScheduler, WorkRuntimeError
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+
+ class Resolver:
+ resolver_id = "notes.handoff.worker"
+
+ def resolve(self, descriptor):
+ def execute():
+ # Acknowledge transfer before the destination claims this same
+ # Session head. This receipt is not destination task completion.
+ return {"destination": descriptor.parent_session_id, "admitted": True}
+ return execute
+
+ with compose(root, pause=True) as composition:
+ composition.runtime.work_runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver()))
+ source = composition.session("Index notes, then transfer ownership")
+ source.run()
+ identity = source.work_item_id
+ operation = source.handoff("notes_agent", rationale="Finish with the destination worker")
+ graph = wait(source, operation)
+ transfer = graph.transfers[-1]
+ assert transfer.from_agent_id != transfer.to_agent_id
+ assert graph.work_items[identity].owner.agent_id == transfer.to_agent_id
+ try:
+ source.spawn("notes_agent", task="A stale owner must not dispatch")
+ except WorkRuntimeError as error:
+ assert error.code == "superseded_owner"
+ else:
+ raise AssertionError("Superseded source unexpectedly dispatched")
+ identity = source.session_id.value
+ # Serialized handoff: source callbacks and resource cleanup finish first.
+ result = subprocess.run([
+ sys.executable, __file__, "--root", str(root), "--destination", identity,
+ ], capture_output=True, text=True, timeout=20)
+ if result.returncode:
+ raise RuntimeError(result.stderr)
+ print("handoff destination ran; owner changed; source fenced")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--destination")
+ args = parser.parse_args()
+ if args.destination:
+ with compose(args.root.resolve(), start=1, pause=True) as composition:
+ result = composition.restore(args.destination).run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ else:
+ run(args.root.resolve())
+```
-Choose a new root if the directory already exists. For missing imports verify the installed source. Inspect Trajectory and the migration page for config mismatch, missing extensions or typed failures; never silently retry unknown external effects.
+{/* tutorial-files:end */}
-## Next step
+## Next step and API
-[Learning path](/tutorials/index) · [Migration and troubleshooting](/reference/g5-migration)
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/guides/observability)
-The parent request ceiling is 16, with two explicitly granted requests per fan-out sibling. Without sibling grants the first child can reserve the remainder.
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/handoff.py) (optional; all required code is already on this page).
diff --git a/docs/guides/observability.mdx b/docs/guides/observability.mdx
index 5528c8bd..5eeb77dd 100644
--- a/docs/guides/observability.mdx
+++ b/docs/guides/observability.mdx
@@ -1,55 +1,248 @@
---
-title: "Trajectory and qita"
-description: "QitOS G5 · Trajectory and qita"
+title: "Inspect and export with qita"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Learning goal
+## Goal and prerequisites
-Read the canonical journal through default_reader, inspect steering and fork lineage, export a redacted public projection and reimport that projection.
+Run the notes project, then inspect exactly the Session ID stored in `control.json`. `default_reader` selects the canonical journal while retaining explicit historical trace support. `read_session` reconstructs an observation view; it does not resume execution.
-## Prerequisites
+`inspect_run.py` writes a redacted public export and verifies it can be reimported with the same number of records. It also creates the run selector directory currently required by the qita HTML export CLI. This directory does not replace the authoritative journal.
-Complete the [Quickstart](/quickstart) installation and lessons copy; base package, Python 3.12.7 tested, no real model requests.
+Use the CLI commands below to inspect, replay and export. Replay displays recorded execution; it does not call the model or repeat tools. Stop the read-only board with Ctrl-C.
-## Complete runnable example
+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.
-[`examples/tutorials/session_walkthrough.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/session_walkthrough.py)
-
-The complete directory is the execution source. Sibling session_walkthrough.py is a public teaching dependency, not a repository test helper.
+## Prepare the project
```bash
-python lessons/session_walkthrough.py create --root ./observe-run
-python lessons/session_walkthrough.py restore --root ./observe-run
-qita board --logdir ./observe-run
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
```
-## Expected output and assertions
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
-`records>0; public_export_lossless=false; board opens locally`
+When continuing Quickstart with an existing notes-run, skip the first notes.py creation command and inspect that run. Replay starts a local web server; stop it with Ctrl-C before running export/board.
-Assertions in the file verify the result; generated IDs vary. Commands exit zero; stop the board with Ctrl-C.
+## Run and verify
-## HTML export and replay
+```bash
+python notes.py --root notes-run
+python inspect_run.py --root notes-run
+```
---run uses a logical run ID path under the log directory, not the journal filename. Replay starts a read-only local server; stop it with Ctrl-C.
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
+
+```text
+"records":
+```
+
+### Inspect the saved Session
+
+```bash
+session_id=$(python -c 'import json; print(json.load(open("notes-run/control.json"))["session_id"])')
+qit session inspect --config notes-run/agent.json --session-id "$session_id"
+qita inspect session "$session_id" --logdir ./notes-run
+```
```bash
-run_id=$(python -c 'import json; print(json.load(open("observe-run/control.json"))["run_id"])')
-mkdir -p "./observe-run/$run_id"
-qita export --run "./observe-run/$run_id" --html ./observe-run/replay.html
-qita replay --run "./observe-run/$run_id"
+run_id=$(python -c 'import json; print(json.load(open("notes-run/control.json"))["run_id"])')
+qita replay --run "notes-run/$run_id"
+qita export --run "notes-run/$run_id" --html notes-run/trajectory.html
+qita board --logdir ./notes-run
```
-## Support boundaries
+## Behavior and support boundaries
+
+Inspect needs neither Docker nor credentials. Journal reads are complete but currently load the full journal. Public export declares losses and cannot replace private recovery data.
+
+## Exercise and answer
+
+Compare private and public records without exposing private payloads. Equal record counts do not establish lossless payload equivalence.
+
+## 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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
-qita is read-only and owns no execution semantics. Default discovery reads journal and historical trace. Whole reads are complete but currently load all frames; query limits are not total memory bounds. Public export is not a lossless raw backup. CLI qita export produces HTML, while CanonicalTrajectoryExporter produces canonical JSON. Keep private runs out of Git.
-## Common errors
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="inspect_run.py"
+"""Read an existing notes run, verify the index, and export a public view."""
+import argparse
+import json
+from pathlib import Path
+
+from qitos.qita.reader import default_reader
+from qitos.tracing.exporter import CanonicalTrajectoryExporter
+from qitos.tracing.trajectory import PrivacyView
+
+
+def inspect(root):
+ control = json.loads((root / "control.json").read_text())
+ trajectory = default_reader(root).read_session(control["session_id"], view=PrivacyView.RAW_PRIVATE)
+ assert trajectory.records
+ exporter = CanonicalTrajectoryExporter()
+ exported = exporter.export(trajectory, view=PrivacyView.REDACTED_PUBLIC)
+ imported = exporter.reimport(exported)
+ assert len(imported.records) == len(trajectory.records)
+ (root / "public-trajectory.json").write_bytes(exported.data)
+ # qita's --run selector currently requires an existing run directory.
+ # This directory is only a selector; the journal remains authoritative.
+ (root / control["run_id"]).mkdir(exist_ok=True)
+ print(json.dumps({"records": len(trajectory.records), "lossless": exported.loss.is_lossless,
+ "run_selector": str(root / control["run_id"])}))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ inspect(parser.parse_args().root.resolve())
+```
-Choose a new root if the directory already exists. For missing imports verify the installed source. Inspect Trajectory and the migration page for config mismatch, missing extensions or typed failures; never silently retry unknown external effects.
+{/* tutorial-files:end */}
-## Next step
+## Next step and API
-[Learning path](/tutorials/index) · [Migration and troubleshooting](/reference/g5-migration)
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/guides/third-party-extensions)
-G5 CLI still requires the --run path to exist, so create the empty selector directory first. Actual data is read from the parent journal. This is a CLI compatibility limitation, not a new trace format.
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/inspect_run.py) (optional; all required code is already on this page).
diff --git a/docs/guides/sandbox-and-artifacts.mdx b/docs/guides/sandbox-and-artifacts.mdx
index 8a50dde9..930c399e 100644
--- a/docs/guides/sandbox-and-artifacts.mdx
+++ b/docs/guides/sandbox-and-artifacts.mdx
@@ -1,43 +1,314 @@
---
-title: "Sandbox and artifacts"
-description: "QitOS G5 · Sandbox and artifacts"
+title: "Sandbox, artifacts and publication"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Learning goal
+## Goal and prerequisites
-Run real Docker Env file/command tools, retain and resolve a large output by digest, then opt in to publication of answer.txt in a separate fixture.
+This chapter adds actual file and command tools. It therefore requires the Docker CLI, a running daemon, and the `python:3.12-slim` image. The fake model declares tool calls, but QitOS really executes them in Docker. The Python client runs on the host; its container has networking disabled.
-## Prerequisites
+Run once without publication. A source `report.txt` initially contains `original`; the sandbox writes `Session, Artifact`, generates 20,000 characters of output, pauses and restores. Artifact references are resolved and checked against their SHA-256 digests. The source report must remain unchanged after cleanup.
-Complete the [Quickstart](/quickstart) installation and lessons copy; base package, Python 3.12.7 tested, no real model requests.
+Run again in a different root with `--publish`. Only this invocation registers SandboxPublicationTool for the existing top-level `report.txt` and the attested input digest. Now the source report must change. The final assertion checks the container was removed in both cases.
-## Complete runnable example
+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.
-[`examples/tutorials/sandbox_artifacts.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/sandbox_artifacts.py)
+## Prepare the project
-The complete directory is the execution source. Sibling session_walkthrough.py is a public teaching dependency, not a repository test helper.
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
+
+## Run and verify
```bash
docker info
docker pull python:3.12-slim
-python lessons/sandbox_artifacts.py --root ./sandbox-retained
-python lessons/sandbox_artifacts.py --root ./sandbox-published --publish
+python sandbox.py --root sandbox-private
+python sandbox.py --root sandbox-published --publish
+```
+
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
+
+```text
+"published": false
+"published": true
```
-## Expected output and assertions
+## Behavior and support boundaries
+
+Cleanup never implies publication. Publication is limited to the supported existing top-level regular-file shape on the qualified Docker platform; it is not arbitrary directory synchronization. Missing Docker is a preflight/environment failure; never switch these file tools to unsafe host.
+
+## Exercise and answer
+
+Change the report text and its assertions together. The private run must still preserve `original`, while the explicitly published run must match the new text.
+
+## 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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
-`docker=true; artifacts>0; container_absent=true; published=false then true`
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
-Assertions in the file verify the result; generated IDs vary. Commands exit zero; stop the board with Ctrl-C.
+ def should_pause(self, context):
+ return context.step_id == 0
-## Support boundaries
-Docker CLI/daemon and python:3.12-slim are required; [docker] does not install a daemon. These commands create only new tutorial directories. Cleanup retains output and never publishes it. --publish explicitly registers authority for one top-level regular file and its expected input digest; the composition approval policy applies. Current publication requires supported POSIX descriptor operations (qualified here on macOS), rejects links/nested/special/protected paths and source conflicts. Docker is not a VM guarantee.
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="sandbox.py"
+"""Real Docker lesson: retained output and opt-in top-level file publication.
+
+Uses a fake provider but real Env tools, Session, artifact store and reader.
+Only --publish registers publication authority over report.txt in a new fixture.
+"""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+
+from qitos.config import (
+ AgentConfig, BudgetConfig, EnvironmentConfig, ModelConfig, RuntimeConfig,
+ TrajectoryConfig, build_agent_composition,
+)
+from qitos.core.artifact import ArtifactRef
+from notes import PauseAfterTool
+from qitos.kit.tool.internal.publication import SandboxPublicationTool
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+
+class FakeProvider:
+ model = "sandbox-tutorial-fake"
+ qitos_protocol = "json_decision_multi_v1"
+
+ def __init__(self, publish, stage=0):
+ self.actions = [
+ ("write_file", {"path": "report.txt", "content": "Session, Artifact\n"}),
+ ("run_command", {"command": "python3 -c 'print(\"x\" * 20000)'", "timeout": 10}),
+ ]
+ if publish:
+ self.actions.append(("publish_workspace", {}))
+ self.stage = stage
+
+ def call_raw(self, messages, **options):
+ if self.stage == len(self.actions):
+ return {"choices": [{"message": {"content": "Final Answer: sandbox lesson complete"}}]}
+ name, args = self.actions[self.stage]
+ self.stage += 1
+ return {"choices": [{"message": {"content": None, "tool_calls": [{
+ "id": f"lesson-{self.stage}", "type": "function",
+ "function": {"name": name, "arguments": json.dumps(args)},
+ }]}}]}
+
+
+def references(value):
+ if isinstance(value, dict):
+ if value.get("schema_version") == "qitos.artifact_ref/v1":
+ yield ArtifactRef.from_dict(value)
+ for item in value.values():
+ yield from references(item)
+ elif isinstance(value, (list, tuple)):
+ for item in value:
+ yield from references(item)
+
+
+def run(root: Path, image: str, publish: bool):
+ root.mkdir(parents=True, exist_ok=False)
+ source = root / "source"
+ source.mkdir()
+ (source / "report.txt").write_text("original\n", encoding="utf-8")
+ config = AgentConfig(
+ lifecycle={"policy": "pause"},
+ name="sandbox-lesson", protocol="json_decision_multi_v1", tool_preset="env_coding",
+ model=ModelConfig(provider="openai_compatible", model="sandbox-tutorial-fake"),
+ tool_options={"native_tool_calls_required": True},
+ budgets=BudgetConfig(max_steps=6, max_requests=6, max_runtime_seconds=60),
+ runtime=RuntimeConfig(
+ data_root=str(root / "data"),
+ trajectory=TrajectoryConfig(output=str(root / "trajectory.journal")),
+ environment=EnvironmentConfig(workspace=str(source), image=image,
+ cpus=0.5, memory_mb=256, pids_limit=32)),
+ )
+ with build_agent_composition(config, model_override=FakeProvider(publish),
+ extensions={"pause": PauseAfterTool}) as composition:
+ session = composition.session("Write the notes report and retain a large output")
+ session.run()
+ assert session.lifecycle.value == "paused"
+ identity = session.session_id.value
+ assert (source / "report.txt").read_text() == "original\n"
+ with build_agent_composition(config, model_override=FakeProvider(publish, stage=1),
+ extensions={"pause": PauseAfterTool}) as composition:
+ session = composition.restore(identity)
+ if publish:
+ composition.tool_registry.register(SandboxPublicationTool(
+ composition.env, paths=["report.txt"],
+ expected_input_digest=composition.env.input_digest,
+ ))
+ result = session.run()
+ assert result.state.final_result == "sandbox lesson complete", repr(result.state.final_result)
+ trajectory = default_reader(root).read_session(session.session_id.value, view=PrivacyView.RAW_PRIVATE)
+ artifacts = [ref for record in trajectory.records for ref in references(record.payload)]
+ assert artifacts
+ for ref in artifacts:
+ body = composition.agent.config["artifact_resolver"].resolve(ref).body
+ assert body is not None and hashlib.sha256(body).hexdigest() == ref.sha256
+ assert (source / "report.txt").read_text() == ("Session, Artifact\n" if publish else "original\n")
+ assert composition.env.cleanup_receipt["container_absent"] is True
+ assert (source / "report.txt").read_text() == ("Session, Artifact\n" if publish else "original\n")
+ print(json.dumps({"docker": True, "published": publish, "artifacts": len(artifacts),
+ "container_absent": True, "session_id": session.session_id.value}))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--image", default="python:3.12-slim")
+ parser.add_argument("--publish", action="store_true")
+ args = parser.parse_args()
+ run(args.root.resolve(), args.image, args.publish)
+```
-## Common errors
+{/* tutorial-files:end */}
-Choose a new root if the directory already exists. For missing imports verify the installed source. Inspect Trajectory and the migration page for config mismatch, missing extensions or typed failures; never silently retry unknown external effects.
+## Next step and API
-## Next step
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/guides/multi-agent-patterns)
-[Learning path](/tutorials/index) · [Migration and troubleshooting](/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/sandbox.py) (optional; all required code is already on this page).
diff --git a/docs/guides/third-party-extensions.mdx b/docs/guides/third-party-extensions.mdx
index ca88c020..4405e09e 100644
--- a/docs/guides/third-party-extensions.mdx
+++ b/docs/guides/third-party-extensions.mdx
@@ -1,40 +1,240 @@
---
-title: "Third-party extensions"
-description: "QitOS G5 · Third-party extensions"
+title: "Replace a provider and add context"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Learning goal
+## Goal and prerequisites
-Replace context selection and compaction through structural extension factories; inspect the fake provider and pure tool in session_walkthrough.py as minimal teaching adapters.
+A composition accepts a public model override and explicitly named extension factories. `ObservedFakeProvider` preserves the callable interface, counts actual requests and asserts the selected project context appears in each request. It then delegates to the visible deterministic provider.
-## Prerequisites
+The project contributor is a factory returning StaticContextContributor. Its registration name matches `context.contributors`. This avoids imports from arbitrary names embedded in YAML and keeps executable extension selection under application control.
-Complete the [Quickstart](/quickstart) installation and lessons copy; base package, Python 3.12.7 tested, no real model requests.
+This is a complete provider substitution and context integration example, not a new transport client. For a real provider, use the matching configuration and explicit credential resolver described in Configuration. Consult the extension index before replacing stores, sinks or sandboxes: preserving a Python method signature alone does not preserve their durability and security contracts.
-## Complete runnable example
+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.
-[`examples/tutorials/context_memory.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/context_memory.py)
+## Prepare the project
-The complete directory is the execution source. Sibling session_walkthrough.py is a public teaching dependency, not a repository test helper.
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
+
+## Run and verify
```bash
-python lessons/context_memory.py --root ./extensions-run
+python provider_extension.py --root extension-run
```
-## Expected output and assertions
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
+
+```text
+provider replaced; context observed; requests=3
+```
+
+## Behavior and support boundaries
+
+No provider is universally compatible. Codec, continuation, tool schema and loss policy must agree. Inspect typed failures; do not disable loss checks to accept an incompatible response.
+
+## Exercise and answer
+
+Rename the contributor key in both config and extensions. Leaving only one side changed must produce a configuration/extension failure.
+
+## 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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
-`context selected; memory selected; compaction loss recorded`
-Assertions in the file verify the result; generated IDs vary. Commands exit zero; stop the board with Ctrl-C.
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
-## Support boundaries
-A fake call_raw adapter proves only the compatibility teaching path. A real provider must declare target/capabilities, codec, transport, failure normalization and supported continuation/streaming. Replace CheckpointStore and EventSink through RuntimeComposition, sandbox through its conformance contract, and evaluator through its public protocol. Each replacement needs its own conformance tests; changing a class name does not establish security or durability.
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="provider_extension.py"
+"""Replace the provider and inject context, without changing the kernel."""
+import argparse
+from dataclasses import replace
+from pathlib import Path
+
+from notes import FakeProvider, configuration, summarize_note
+from qitos.config import build_agent_composition
+from qitos.core.context import StaticContextContributor
+
+
+class ObservedFakeProvider(FakeProvider):
+ """Keep the public provider call shape and validate selected context."""
+ def __init__(self):
+ super().__init__()
+ self.requests = 0
+
+ def call_raw(self, messages, **options):
+ assert "notes-project-context" in str(messages)
+ self.requests += 1
+ return super().call_raw(messages, **options)
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ config = replace(configuration(root), context={"contributors": ["project"]})
+ provider = ObservedFakeProvider()
+ with build_agent_composition(config, model_override=provider, extensions={
+ "project": lambda: StaticContextContributor("notes.project", "project", "notes-project-context"),
+ }) as composition:
+ composition.tool_registry.register(summarize_note)
+ result = composition.session("Index both notes").run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ assert provider.requests == 3
+ print("provider replaced; context observed; requests=3")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ run(parser.parse_args().root.resolve())
+```
-## Common errors
+{/* tutorial-files:end */}
-Choose a new root if the directory already exists. For missing imports verify the installed source. Inspect Trajectory and the migration page for config mismatch, missing extensions or typed failures; never silently retry unknown external effects.
+## Next step and API
-## Next step
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/quickstart)
-[Learning path](/tutorials/index) · [Migration and troubleshooting](/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/provider_extension.py) (optional; all required code is already on this page).
diff --git a/docs/installation.mdx b/docs/installation.mdx
index 0e77353f..5ce45612 100644
--- a/docs/installation.mdx
+++ b/docs/installation.mdx
@@ -8,7 +8,7 @@ description: "QitOS G5 · Installation"
| Source | Installation | Capability identity |
|---|---|---|
| Published PyPI | `python -m pip install qitos` | Published distribution; **does not identify G5** |
-| Reproducible G5 source | Exact commit command below | `717b4cf1b23f2ed252cd03234ffd8605038d9567` |
+| Documented source (G5 successor) | Exact commit command below | `60809b3be388d22ea40ea41b4aaa1f5540c76fda` |
| GitHub development branch | `python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@master"` | Moving development tree; record the resolved SHA |
| Local wheel | `python -m pip install ./qitos-0.6.0-py3-none-any.whl` | Verify the wheel digest and source receipt |
| Framework contributor | `python -m pip install -e ".[dev]"` in your clone | Editable checkout, not installed-wheel qualification |
@@ -18,14 +18,14 @@ cannot distinguish it from a published build. This task does not publish a packa
## Install the documented runtime
-Python metadata declares **>=3.10**; G5 qualification used **3.12.7**.
+Python metadata declares **>=3.10**; historical G5 qualification used **3.12.7**.
Other declared versions were not all validated in that round. Use Git and a POSIX
shell for these commands (Windows users need corresponding activation commands).
```bash
python3 -m venv .venv
source .venv/bin/activate
-python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@717b4cf1b23f2ed252cd03234ffd8605038d9567"
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
python -m pip install pytest
qit --help
qita --help
@@ -37,9 +37,9 @@ A rebuilt wheel need not have identical archive bytes; compare its source identi
## Optional dependencies
-The fake arithmetic path needs only the base package and pytest. For an
+The fake notes path needs only the base package and pytest. For an
OpenAI or OpenAI-compatible client use the same pinned install with
-`qitos[openai] @ git+https://github.com/WhitzardAgent/WhitzardOS.git@717b4cf1b23f2ed252cd03234ffd8605038d9567`.
+`qitos[openai] @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda`.
`qitos[models]` also adds LiteLLM. `qitos[docker]` and `qitos[qita]` currently add
no Python dependencies; Docker still requires a working Docker CLI and daemon.
MCP needs `[mcp]`, browser tools `[web]`, W&B `[wandb]`, MLflow `[mlflow]`.
@@ -51,4 +51,4 @@ Both help commands must exit zero. A missing G5 import usually means a different
interpreter or an older installed distribution; verify `python -m pip show qitos`
and recreate the venv from the exact source. Continue to [Quickstart](/quickstart).
-Remote CI found that Python 3.10 publication calls an unavailable hashlib.file_digest. Use Python 3.11+ for publication; the historical local G5 qualification covers 3.12.7 specifically. The G5-pinned source retains this limitation; master now replaces that call with bounded descriptor hashing, verified separately from the historical G5 result.
+These lessons use 60809b3, which includes the Python 3.10 publication fix. Historical G5 baseline remains 717b4cf1b23f2ed252cd03234ffd8605038d9567; the historical wheel digest above belongs only to that qualification. Historical test results are not attributed to this successor.
diff --git a/docs/internal/plans/docs_self_contained_learning.md b/docs/internal/plans/docs_self_contained_learning.md
new file mode 100644
index 00000000..cd7d2780
--- /dev/null
+++ b/docs/internal/plans/docs_self_contained_learning.md
@@ -0,0 +1,61 @@
+# Self-contained learning and API reference
+
+Start/runtime source: `60809b3be388d22ea40ea41b4aaa1f5540c76fda`.
+Historical G5 qualification remains bound to `717b4cf1b23f2ed252cd03234ffd8605038d9567`.
+
+## Accepted design
+
+One synthetic notes project, independently runnable chapters, bilingual prose,
+complete visible files, source-synchronized snippets, core API reference plus
+extension index. Keep Mintlify and existing URLs. No runtime or package changes.
+
+## Execution
+
+1. Create notes fixtures and the source-to-MDX contract/checker; execute extracted files.
+2. Rewrite Quickstart/custom Agent/tools/Session and core reference.
+3. Complete context, sandbox, multi-agent, qita, extensions and EN/zh navigation.
+4. Run installed-wheel tutorial gates, Docker, MDX/link checks and browser review;
+ open PR, read exact-head checks, merge only when green, verify deployed content.
+
+## Acceptance
+
+Every chapter runs from page-extracted files outside the checkout with a wheel.
+The generated project is used in Quickstart. No hidden teaching helper or source
+PYTHONPATH. Public symbols have checked imports/signatures and reference anchors.
+No real model requests. Browser rendering and deployment are separate gates.
+
+## Progress
+
+- Isolated worktree created from the accepted baseline; concurrent V5 drafts retained.
+- Implementation and local qualification complete; see docs_self_contained_qualification.md.
+- PR checks and live publication are the remaining promotion gates.
+
+## Discovered boundary: handoff callback versus destination restore
+
+At frozen source 60809b3, dispatching the destination's restore/run of the SAME
+Session inside LocalWorkScheduler's handoff callback can advance the Session
+owner before the source terminal callback persists. The source then raises
+CheckpointConflictError("This run no longer owns the session head") at
+session_runtime._commit_work_graph -> sqlite_store._validate_session_cas;
+source wait can remain dispatched until the teaching deadline.
+
+Reproduction: paused SQLite notes Session -> local scheduler resolver launches
+`handoff.py --destination ` immediately from its callable ->
+destination restores/runs -> source completion callback attempts its old-owner CAS.
+This is an ownership/interop limitation, not model intelligence or codec relaxation.
+The runnable lesson explicitly serializes transfer acknowledgement, source
+cleanup, and destination restore. It never labels admission as destination task
+completion; a separate subprocess assertion proves destination execution.
+Runtime was not changed. Concurrent same-head handoff scheduling needs a separate
+framework investigation; do not claim this lesson qualifies that behavior.
+
+## Documentation design sources
+
+- https://docs.pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html:
+ task progression, adjacent code/results and complete source.
+- https://docs.pytorch.org/docs/2.14/generated/torch.nn.Module.html:
+ symbol signatures, arguments, examples and source links.
+- https://fastapi.tiangolo.com/tutorial/first-steps/: complete named files,
+ startup commands and user-visible verification.
+- https://docs.sqlalchemy.org/en/20/tutorial/: unified learning sequence with
+ explicit relationships between high-level composition and lower-level APIs.
diff --git a/docs/internal/plans/docs_self_contained_qualification.md b/docs/internal/plans/docs_self_contained_qualification.md
new file mode 100644
index 00000000..00001f36
--- /dev/null
+++ b/docs/internal/plans/docs_self_contained_qualification.md
@@ -0,0 +1,63 @@
+# Self-contained documentation qualification
+
+## Source identity and scope
+
+Runtime/start: `60809b3be388d22ea40ea41b4aaa1f5540c76fda`.
+Historical G5: `717b4cf1b23f2ed252cd03234ffd8605038d9567`; its historical
+2663 passed / 50 skipped result is not reattributed to this task.
+`qitos/`, package metadata/dependencies, public interface budgets and quality
+allowances are unchanged. This is a documentation/example/testing change.
+
+## Delivered
+
+- Quickstart uses the generated notes project; all required files are on the page.
+- Eight core learning units plus real-provider configuration, in EN and zh.
+- Four navigation tabs, seven current core API categories, 45 unique imported
+ API symbols, checked signatures/defaults/source links, extension index.
+- Previous API text remains explicitly historical; old entry URLs remain reachable.
+- Source-to-MDX synchronization and installed-wheel page extraction are CI gates.
+
+## Local verification
+
+Python 3.12.7; wheel built from unchanged runtime, SHA256
+`2d7a46baf4d3831bb97c0657b200d9210989b00b12c1e113186a31790f850e44`.
+
+- Combined requested docs/example/workflow/architecture/public-surface suites:
+ **787 passed, 1 skipped** in 59.32 seconds. The skip is the explicit Docker opt-in.
+- Separate Docker page extraction plus original installed golden paths,
+ examples smoke and scaffold execution: **17 passed** in 52.63 seconds.
+ Both EN/zh Docker pages execute private and explicitly published variants.
+- Independent installed page suite: **20 passed, 1 skipped**; includes sequential
+ learning in one project, generated-project install/test, qita HTTP replay and export.
+- Final API/source drift check passed; changed Python static checks passed.
+- Navigation/parity/links passed; **166 MDX pages compiled, 0 failures**.
+- Real Mintlify preview: **46 changed routes at 1440px and 390px**, all HTTP 200,
+ no page-level horizontal overflow. Reviewed desktop/mobile screenshots.
+- Clipboard copied the exact 3703-character complete notes.py; API index link
+ reached the intended composition symbol anchor. Code labels render correctly.
+- `git diff --check` and frozen-runtime scope proof passed.
+
+The JUnit evidence digest is `79b2c4899b4ffafe252a4d3951ab7f61f7465029b3f3becbe64eac75ba65cfa2`.
+Evidence includes XML, command output, browser route receipts and screenshots;
+public source identities and this digest are portable independently of local paths.
+
+## Tooling and limits
+
+Preview executed the isolated `@mintlify/cli` 4.0.1476 binary from the existing
+locked documentation tool distribution (which also contains mint 4.2.873).
+MDX compiler 3.1.1 and Playwright CLI 0.1.19. No global upgrade was performed.
+Local Mintlify search requires login and was not activated; navigation, anchors,
+code copy, page rendering and responsive layout were tested without credentials.
+
+Handoff serializes transfer admission, source cleanup and destination execution.
+Concurrent same-head destination restore/source callback owner conflict remains
+explicitly documented with reproduction in the implementation plan. No runtime
+fix or relaxed check is hidden in the example. Fake providers prove mechanisms,
+not autonomous model success. Real-provider config validation sends no requests.
+
+## Promotion
+
+The implementation is submitted through a PR to master. Exact-head GitHub checks,
+merge and live-site verification must be read from the PR and deployment evidence;
+this local report does not claim those remote operations have already succeeded.
+The concurrent V5 drafts in the main worktree are outside this change.
diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx
index 38fc437e..c21f4467 100644
--- a/docs/quickstart.mdx
+++ b/docs/quickstart.mdx
@@ -1,80 +1,298 @@
---
-title: "Quickstart"
-description: "QitOS G5 · Quickstart"
+title: "Run your notes Agent"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Goal and prerequisites
+## What you will build
-Install the specified runtime → create a project → configure model references,
-tools and resources → run a Session → inspect result, artifacts and Trajectory
-→ restore or extend your Agent. Start with [Installation](/installation).
-Python 3.12.7 is the tested interpreter. No model key or Docker is needed for
-this first arithmetic lesson.
+Create a notes project, call tools to extract two titles and word counts, run it through a Session, and inspect the recorded result with qita. You need basic Python, but no model credentials or Docker; the fake provider is an explicit scripted double.
-## Create and execute a project
-
-Install **before** using `qit`:
+## Prepare the project
```bash
python3 -m venv .venv
source .venv/bin/activate
-python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@717b4cf1b23f2ed252cd03234ffd8605038d9567"
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
python -m pip install pytest
-qit --help
-qita --help
+qit new --agent-name notes_agent --output-dir . --no-input
+cd notes_agent
```
-Download the complete teaching files, generate an installable project, then run:
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
-```bash
-git clone --branch feat/campaign-absorption --single-branch https://github.com/WhitzardAgent/WhitzardOS.git qitos-lessons
-cp -R qitos-lessons/examples/tutorials ./lessons
-qit new --agent-name my_agent --output-dir . --no-input
-python -m pip install ./my_agent
-python -m pytest -q my_agent/tests
-python lessons/session_walkthrough.py create --root ./arithmetic-run
-python lessons/session_walkthrough.py restore --root ./arithmetic-run
+### Notes and tools
+
+`NOTES` is the entire input. The decorator turns a Python function into a registrable tool; it only reads memory. Its returned dictionary is the actual tool output, checked by assertions later.
+
+{/* tutorial-snippet:notes.py:fixture */}
+```python
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+```
+{/* tutorial-snippet:end */}
+
+### Explicit fake provider
+
+This provider scripts two tool calls followed by a final answer; there is no model reasoning. It lets you observe the framework’s tool round trips before switching to a real provider in Configuration.
+
+{/* tutorial-snippet:notes.py:provider */}
+```python
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+```
+{/* tutorial-snippet:end */}
+
+### Configuration and composition
+
+Save the complete files on this page in the generated `notes_agent/` root and replace `agent.yaml`. Configuration selects SQLite and the journal; the program registers only a trusted pure function. `unsafe_host` does not provide isolation: do not add file or shell tools to it.
+
+{/* tutorial-snippet:notes.py:composition */}
+```python
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+```
+{/* tutorial-snippet:end */}
+
+### Run and assertions
+
+The `with` block closes owned resources; `composition.session` creates a Session and `run` returns EngineResult. Check the actual tool outputs before checking final text. Filter by tool name because records also include environment observations.
+
+{/* tutorial-snippet:notes.py:run */}
+```python
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
```
+{/* tutorial-snippet:end */}
-The generated `my_agent/agent.yaml` is canonical configuration. Its packaged
-test substitutes a fake provider and a temporary unisolated host workspace;
-it checks composition/Session wiring and performs no model-selected shell work.
-The complete `lessons/session_walkthrough.py` registers only a trusted pure
-`add` function. It uses a clearly named fake provider, SQLite and the real
-Trajectory writer. **This is not sandbox isolation or autonomous task success.**
+## Run and verify
-The first process prints `lifecycle: paused` and `tool_output: 42`; the second
-prints `final_result: arithmetic complete`, a distinct child Session ID and a
-positive record count. Both exit 0. Assertion failure is failure, not a usable
-result. Use a new root for a second execution to avoid overwriting retained evidence.
+```bash
+python -m pip install .
+python -m pytest -q tests
+python notes.py --root notes-run
+```
-## Inspect your result
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
-The script writes `arithmetic-run/control.json`, `sessions.sqlite3`,
-`trajectory.journal`, and `public-trajectory.json`.
+```text
+Indexed 2 notes: Session, Artifact.
+```
+
+### Inspect the saved Session
```bash
-qita board --logdir ./arithmetic-run
-qit session inspect --help
-qita inspect --help
+session_id=$(python -c 'import json; print(json.load(open("notes-run/control.json"))["session_id"])')
+qit session inspect --config notes-run/agent.json --session-id "$session_id"
+qita inspect session "$session_id" --logdir ./notes-run
+```
+
+## Behavior and support boundaries
+
+State and tools are separate from provider output. `build_agent_composition` loads the canonical choices; `composition.session` creates a Session; `session.run` returns an EngineResult. The `with` block closes owned resources.
+
+## Exercise and answer
+
+Change the first title to `Recovery`. Update the expected title and scripted final response together; the word count remains computed by the tool.
+
+## 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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
```
-The board is read-only; Ctrl-C stops its local server. The
-[Session lesson](/tutorials/checkpoint-and-fork) provides CLI inspection using
-the actual Session ID and matching configuration.
-A public export is redacted and loss-declaring, not a raw backup.
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
-## A typed failure is actionable
+{/* tutorial-files:end */}
-A malformed configuration is rejected before execution. Missing Docker on the
-coding path gives `sandbox_unavailable`; start the daemon and rerun preflight.
-Do not change coding tools to host execution as a workaround. A missing
-credential or unsupported codec is not a successful model call. See
-[migration and troubleshooting](/reference/g5-migration).
+## Next step and API
-## Use a real provider or extend the Agent
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/guides/build-your-first-agent)
-Follow [Configuration](/reference/configuration) to create the complete private
-credential file and explicit resolver. That step sends requests only when you
-explicitly run the real launch. This documentation qualification runs no real
-model requests. Continue through the [eight learning units](/tutorials/index).
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/notes.py) (optional; all required code is already on this page).
diff --git a/docs/reference/agent-runtime.mdx b/docs/reference/agent-runtime.mdx
new file mode 100644
index 00000000..2dd0ac60
--- /dev/null
+++ b/docs/reference/agent-runtime.mdx
@@ -0,0 +1,565 @@
+---
+title: "Agent and execution"
+description: "QitOS public API: agent-runtime"
+---
+
+AgentModule owns strategy and state transitions; Engine drives observe, decide, act, reduce and stopping. Decision.act declares actions and Decision.final ends the strategy. EngineResult contains state and typed step records; reduce observes dictionaries. Use the custom Agent tutorial for the complete wiring. RuntimeComposition defaults to a process-local store. AgentModule.run remains a programmatic convenience, not the canonical declarative setup.
+
+[完整可运行教程 / Complete tutorial](/guides/build-your-first-agent) · [API index](/reference/api)
+
+Signatures and fields below are extracted from the pinned source. Signatures are reference material, not standalone programs. Source links bind the same runtime baseline. Any does not imply arbitrary objects are supported; use the behavioral contract above and the linked tutorial.
+
+{/* api-reference:start */}
+
+
+## AgentModule
+
+```python
+from qitos import AgentModule
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L25)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+# NotesAgent subclasses AgentModule in the complete tutorial.
+agent = NotesAgent()
+result = Engine(agent, runtime=RuntimeComposition()).session("Index notes").run()
+```
+
+```text
+Canonical policy contract for step-based agents.
+```
+
+```text
+AgentModule(tool_registry: Any=None, toolset: Any=None, llm: Any=None, model_parser: Any=None, model_protocol: Any=None, memory: Memory | None=None, history: History | None=None, mcp_servers: List[Any] | None=None, **config: Any) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `tool_registry` | `Any` | `None` |
+| `toolset` | `Any` | `None` |
+| `llm` | `Any` | `None` |
+| `model_parser` | `Any` | `None` |
+| `model_protocol` | `Any` | `None` |
+| `memory` | `Memory | None` | `None` |
+| `history` | `History | None` | `None` |
+| `mcp_servers` | `List[Any] | None` | `None` |
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `name` | `str` | `'agent'` |
+| `handoff_targets` | `List[str] \| None` | `None` |
+
+
+### AgentModule.init_state
+
+```text
+init_state(task: str, **kwargs: Any) -> StateT
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `str` | `required` |
+
+```text
+Create and return the initial typed state for a run.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L55)
+
+
+### AgentModule.decide
+
+```text
+decide(state: StateT, observation: ObservationT) -> Optional[Decision[ActionT]]
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `state` | `StateT` | `required` |
+| `observation` | `ObservationT` | `required` |
+
+```text
+Optional custom decision hook. Return None to use Engine model decision.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L101)
+
+
+### AgentModule.reduce
+
+```text
+reduce(state: StateT, observation: ObservationT, decision: Decision[ActionT]) -> StateT
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `state` | `StateT` | `required` |
+| `observation` | `ObservationT` | `required` |
+| `decision` | `Decision[ActionT]` | `required` |
+
+```text
+Reduce observation (including action/env outputs) into next state.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L120)
+
+
+### AgentModule.should_stop
+
+```text
+should_stop(state: StateT) -> bool
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `state` | `StateT` | `required` |
+
+```text
+Optional additional stop condition.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L128)
+
+
+### AgentModule.run
+
+```text
+run(task: str | Task, return_state: bool=False, hooks: List[Any] | None=None, render_hooks: List[Any] | None=None, engine_kwargs: Dict[str, Any] | None=None, workspace: str | None=None, max_steps: int | None=None, env: Any=None, parser: Any=None, protocol: Any=None, search: Any=None, critics: List[Any] | None=None, stop_criteria: List[Any] | None=None, history_policy: Any=None, context_config: Any=None, trace: Any=None, render: Any=None, trace_logdir: str='./runs', trace_prefix: str | None=None, theme: str='research', run_spec: RunSpec | Dict[str, Any] | None=None, experiment_spec: ExperimentSpec | Dict[str, Any] | None=None, **state_kwargs: Any) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `str | Task` | `required` |
+| `return_state` | `bool` | `False` |
+| `hooks` | `List[Any] | None` | `None` |
+| `render_hooks` | `List[Any] | None` | `None` |
+| `engine_kwargs` | `Dict[str, Any] | None` | `None` |
+| `workspace` | `str | None` | `None` |
+| `max_steps` | `int | None` | `None` |
+| `env` | `Any` | `None` |
+| `parser` | `Any` | `None` |
+| `protocol` | `Any` | `None` |
+| `search` | `Any` | `None` |
+| `critics` | `List[Any] | None` | `None` |
+| `stop_criteria` | `List[Any] | None` | `None` |
+| `history_policy` | `Any` | `None` |
+| `context_config` | `Any` | `None` |
+| `trace` | `Any` | `None` |
+| `render` | `Any` | `None` |
+| `trace_logdir` | `str` | `'./runs'` |
+| `trace_prefix` | `str | None` | `None` |
+| `theme` | `str` | `'research'` |
+| `run_spec` | `RunSpec | Dict[str, Any] | None` | `None` |
+| `experiment_spec` | `ExperimentSpec | Dict[str, Any] | None` | `None` |
+
+```text
+Execute task with Engine using plain text objective or structured Task.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L265)
+
+
+
+## StateSchema
+
+```python
+from qitos import StateSchema
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/state.py#L56)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+from dataclasses import dataclass
+@dataclass
+class MyState(StateSchema):
+ completed: int = 0
+state = MyState(task="Index notes", max_steps=3)
+```
+
+```text
+Canonical typed state base for AgentModule.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `schema_version` | `int` | `1` |
+| `task` | `str` | `''` |
+| `current_step` | `int` | `0` |
+| `max_steps` | `int` | `10` |
+| `final_result` | `Optional[str]` | `None` |
+| `stop_reason` | `Optional[str]` | `None` |
+| `metadata` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `metrics` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `migration_registry` | `ClassVar[StateMigrationRegistry]` | `StateMigrationRegistry()` |
+
+
+
+## Task
+
+```python
+from qitos import Task
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/task.py#L76)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+task = Task(objective="Index the notes")
+print(task.objective)
+```
+
+```text
+Task package with objective, resources, and environment requirements.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `id` | `str` | `required` |
+| `objective` | `str` | `required` |
+| `inputs` | `Dict[str, Any]` | `dc_field(default_factory=dict)` |
+| `resources` | `List[TaskResource]` | `dc_field(default_factory=list)` |
+| `env_spec` | `Optional[EnvSpec]` | `None` |
+| `constraints` | `Dict[str, Any]` | `dc_field(default_factory=dict)` |
+| `success_criteria` | `List[str]` | `dc_field(default_factory=list)` |
+| `budget` | `TaskBudget` | `dc_field(default_factory=TaskBudget)` |
+| `metadata` | `Dict[str, Any]` | `dc_field(default_factory=dict)` |
+
+
+
+## Decision
+
+```python
+from qitos import Decision
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/decision.py#L14)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+decision = Decision.act([Action(name="summarize_note", args={"index": 0})])
+finished = Decision.final("Session")
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `mode` | `DecisionMode` | `required` |
+| `actions` | `List[ActionT]` | `field(default_factory=list)` |
+| `final_answer` | `Optional[str]` | `None` |
+| `rationale` | `Optional[str]` | `None` |
+| `meta` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `candidates` | `List['Decision[ActionT]']` | `field(default_factory=list)` |
+
+
+### Decision.act
+
+```text
+act(actions: List[ActionT], rationale: Optional[str]=None, meta: Optional[Dict[str, Any]]=None) -> 'Decision[ActionT]'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `actions` | `List[ActionT]` | `required` |
+| `rationale` | `Optional[str]` | `None` |
+| `meta` | `Optional[Dict[str, Any]]` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/decision.py#L23)
+
+
+### Decision.final
+
+```text
+final(answer: str, rationale: Optional[str]=None, meta: Optional[Dict[str, Any]]=None) -> 'Decision[ActionT]'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `answer` | `str` | `required` |
+| `rationale` | `Optional[str]` | `None` |
+| `meta` | `Optional[Dict[str, Any]]` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/decision.py#L32)
+
+
+
+## Action
+
+```python
+from qitos import Action
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/action.py#L26)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+action = Action(name="summarize_note", args={"index": 0})
+print(action.name)
+```
+
+```text
+Normalized action contract emitted by policy and consumed by executor.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `name` | `str` | `required` |
+| `args` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `kind` | `ActionKind` | `ActionKind.TOOL` |
+| `action_id` | `Optional[str]` | `None` |
+| `timeout_s` | `Optional[float]` | `None` |
+| `max_retries` | `int` | `0` |
+| `idempotent` | `bool` | `True` |
+| `classification` | `str` | `'default'` |
+| `metadata` | `Dict[str, Any]` | `field(default_factory=dict)` |
+
+
+
+## Engine
+
+```python
+from qitos import Engine
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L292)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+engine = Engine(NotesAgent(), runtime=RuntimeComposition())
+result = engine.session("Index notes").run()
+```
+
+```text
+Single execution kernel for all AgentModule workflows.
+```
+
+```text
+Engine(agent: AgentModule[StateT, ObservationT, ActionT], agent_registry: Optional[Any]=None, budget: Optional[RuntimeBudget]=None, delegate_depth: int=0, shared_memory: Any=None, validation_gate: Optional[StateValidationGate]=None, recovery_handler: Optional[RecoveryHandler]=None, recovery_policy: Optional[RecoveryPolicy]=None, trace_writer: Optional[TraceWriter]=None, parser: Optional[Parser[ActionT]]=None, protocol: Any=None, stop_criteria: Optional[List[StopCriteria]]=None, branch_selector: Optional[BranchSelector[StateT, ObservationT, ActionT]]=None, search: Optional[Search[StateT, ObservationT, ActionT]]=None, critics: Optional[List[Critic]]=None, env: Optional[Env]=None, history_policy: Optional[HistoryPolicy]=None, hooks: Optional[List[EngineHook]]=None, render_hooks: Optional[List[Any]]=None, context_config: Optional[ContextConfig | Dict[str, Any]]=None, cache_backend: Optional[Any]=None, checkpoint_manager: Optional[Any]=None, checkpoint_store: Optional[CheckpointStore]=None, checkpoint_durability: DurabilityMode=DurabilityMode.SYNC, permission_pipeline: Optional[Any]=None, read_before_write_enforcer: Optional[Any]=None, permission_interaction_callback: Optional[Any]=None, loop_detector: Optional[ToolCallLoopDetector]=None, tracing_provider: Optional[Any]=None, interceptors: Optional[List[ToolInterceptor]]=None, auto_approve: bool=False, action_execution_policy: Optional[Any]=None, runtime: Optional[RuntimeComposition]=None) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `agent` | `AgentModule[StateT, ObservationT, ActionT]` | `required` |
+| `agent_registry` | `Optional[Any]` | `None` |
+| `budget` | `Optional[RuntimeBudget]` | `None` |
+| `delegate_depth` | `int` | `0` |
+| `shared_memory` | `Any` | `None` |
+| `validation_gate` | `Optional[StateValidationGate]` | `None` |
+| `recovery_handler` | `Optional[RecoveryHandler]` | `None` |
+| `recovery_policy` | `Optional[RecoveryPolicy]` | `None` |
+| `trace_writer` | `Optional[TraceWriter]` | `None` |
+| `parser` | `Optional[Parser[ActionT]]` | `None` |
+| `protocol` | `Any` | `None` |
+| `stop_criteria` | `Optional[List[StopCriteria]]` | `None` |
+| `branch_selector` | `Optional[BranchSelector[StateT, ObservationT, ActionT]]` | `None` |
+| `search` | `Optional[Search[StateT, ObservationT, ActionT]]` | `None` |
+| `critics` | `Optional[List[Critic]]` | `None` |
+| `env` | `Optional[Env]` | `None` |
+| `history_policy` | `Optional[HistoryPolicy]` | `None` |
+| `hooks` | `Optional[List[EngineHook]]` | `None` |
+| `render_hooks` | `Optional[List[Any]]` | `None` |
+| `context_config` | `Optional[ContextConfig | Dict[str, Any]]` | `None` |
+| `cache_backend` | `Optional[Any]` | `None` |
+| `checkpoint_manager` | `Optional[Any]` | `None` |
+| `checkpoint_store` | `Optional[CheckpointStore]` | `None` |
+| `checkpoint_durability` | `DurabilityMode` | `DurabilityMode.SYNC` |
+| `permission_pipeline` | `Optional[Any]` | `None` |
+| `read_before_write_enforcer` | `Optional[Any]` | `None` |
+| `permission_interaction_callback` | `Optional[Any]` | `None` |
+| `loop_detector` | `Optional[ToolCallLoopDetector]` | `None` |
+| `tracing_provider` | `Optional[Any]` | `None` |
+| `interceptors` | `Optional[List[ToolInterceptor]]` | `None` |
+| `auto_approve` | `bool` | `False` |
+| `action_execution_policy` | `Optional[Any]` | `None` |
+| `runtime` | `Optional[RuntimeComposition]` | `None` |
+
+
+### Engine.session
+
+```text
+session(task: str | Task, session_id: Any=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `str | Task` | `required` |
+| `session_id` | `Any` | `None` |
+
+```text
+Create one durable Session facade using this Engine composition.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L648)
+
+
+### Engine.run
+
+```text
+run(task: str | Task, **kwargs: Any) -> EngineResult[StateT]
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `str | Task` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L981)
+
+
+### Engine.restore
+
+```text
+restore(session_id: Any, *, resolvers: Any=None, runtime: Optional[RuntimeComposition]=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `session_id` | `Any` | `required` |
+| `resolvers` | `Any` | `None` |
+| `runtime` | `Optional[RuntimeComposition]` | `None` |
+
+```text
+Restore a Session in a fresh Engine through explicit resolvers.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L665)
+
+
+
+## EngineResult
+
+```python
+from qitos.engine.engine import EngineResult
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L174)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+result = session.run()
+print(result.state.final_result, result.state.stop_reason)
+for record in result.records:
+ for tool_result in record.action_results:
+ print(tool_result.tool_name, tool_result.status)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `state` | `StateT` | `required` |
+| `records` | `List[StepRecord]` | `required` |
+| `events` | `List[RuntimeEvent]` | `required` |
+| `step_count` | `int` | `required` |
+| `task_result` | `Optional[TaskResult]` | `None` |
+| `runtime_seconds` | `float` | `0.0` |
+| `total_tokens` | `int` | `0` |
+| `run_id` | `str` | `''` |
+| `critic_traces` | `List[CriticTrace]` | `field(default_factory=list)` |
+| `handoff_traces` | `List[HandoffTrace]` | `field(default_factory=list)` |
+| `failure` | `Optional[Dict[str, Any]]` | `None` |
+
+
+
+## RuntimeBudget
+
+```python
+from qitos.engine.states import RuntimeBudget
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/states.py#L38)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+budget = RuntimeBudget(max_steps=3)
+engine = Engine(NotesAgent(), budget=budget)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `max_steps` | `int` | `10` |
+| `max_runtime_seconds` | `Optional[float]` | `None` |
+| `max_tokens` | `Optional[int]` | `None` |
+| `max_model_requests` | `Optional[int]` | `None` |
+
+
+
+## StopReason
+
+```python
+from qitos import StopReason
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/errors.py#L20)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+print([reason.value for reason in StopReason])
+```
+
+
+
+## RuntimeComposition
+
+```python
+from qitos.engine.runtime import RuntimeComposition
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/runtime.py#L177)
+
+[Usage and executable example](/guides/build-your-first-agent)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+runtime = RuntimeComposition()
+engine = Engine(NotesAgent(), runtime=runtime)
+# The default checkpoint store is process-local.
+```
+
+```text
+Process-local Engine components plus their serializable description.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `checkpoint_store` | `Optional[CheckpointStore]` | `None` |
+| `resolvers` | `ResolverRegistry` | `field(default_factory=ResolverRegistry)` |
+| `durability_mode` | `DurabilityMode` | `DurabilityMode.SYNC` |
+| `lifecycle_policy` | `LifecyclePolicy` | `field(default_factory=LifecyclePolicy)` |
+| `snapshot_components` | `tuple[RuntimeSnapshotComponent, ...]` | `()` |
+| `event_sink` | `Any` | `None` |
+| `event_sink_failure_policy` | `Any` | `None` |
+| `event_sink_view` | `Any` | `None` |
+| `tool_execution_policy` | `Any` | `None` |
+| `launch_metadata` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `context_model_runtime` | `Optional[ContextModelRuntime]` | `None` |
+| `work_runtime` | `Any` | `None` |
+| `event_sink_reports` | `list[Any]` | `field(default_factory=list, init=False)` |
+
+{/* api-reference:end */}
diff --git a/docs/reference/api.mdx b/docs/reference/api.mdx
index b247beb6..969353fc 100644
--- a/docs/reference/api.mdx
+++ b/docs/reference/api.mdx
@@ -1,1025 +1,88 @@
---
title: "API Reference"
-description: "Complete reference for the public QitOS Python API — everything exported from the qitos package."
+description: "Core public Python API, CLI and extension contracts."
---
-Every symbol listed here is exported from `qitos` and accessible as:
-
-Signature reference (not executable):
-
-```text
-from qitos import AgentModule, Engine, Decision, ...
-```
-
----
-
-
-
-
-
-`AgentModule` is the strategy layer of QitOS. You subclass it to define your agent's state shape, system prompt, decision logic, and reduction rules. The `Engine` drives the execution loop (the kernel) and calls each hook in order.
-
-Signature reference (not executable):
-
-```text
-class AgentModule(ABC, Generic[StateT, ObservationT, ActionT])
-```
-
-**Constructor**
-
-Signature reference (not executable):
-
-```text
-def __init__(
- self,
- tool_registry: Any = None,
- llm: Any = None,
- model_parser: Any = None,
- memory: Memory | None = None,
- history: History | None = None,
- **config: Any,
-)
-```
-
-| Parameter | Type | Description |
-|-----------|------|-------------|
-| `tool_registry` | `ToolRegistry \| None` | Registry of tools the agent can call |
-| `llm` | `Any` | LLM callable used for model decisions |
-| `model_parser` | `Any` | Parser (a component that converts raw model output into a typed Decision) that converts raw model output to a `Decision` |
-| `memory` | `Memory \| None` | Optional memory adapter |
-| `history` | `History \| None` | Optional history adapter |
-| `**config` | `Any` | Extra keyword args stored as `self.config` |
-
-**Hooks**
-
-Override these methods in your subclass. Only `init_state` and `reduce` are required.
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-@abstractmethod
-def init_state(self, task: str, **kwargs: Any) -> StateT
-```
-
-Create and return the initial typed state for a run. Called once by `Engine.run()` before the step loop begins. Use `**kwargs` to accept extra parameters forwarded from `AgentModule.run()`.
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-@abstractmethod
-def reduce(
- self,
- state: StateT,
- observation: ObservationT,
- decision: Decision[ActionT],
-) -> StateT
-```
-
-Fold the current observation and decision into the next state. Called at the end of every step. Return the updated state.
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def build_system_prompt(self, state: StateT) -> str | None
-```
-
-Return a dynamic system prompt string, or `None` to use no system prompt. Called at the start of each step's decide phase. Default returns `None`.
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def prepare(self, state: StateT) -> str
-```
-
-Convert the current state into a model-ready text string (the user turn). Default returns `str(state)`.
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def decide(
- self,
- state: StateT,
- observation: ObservationT,
-) -> Decision[ActionT] | None
-```
-
-Optional custom decision hook. Return a `Decision` to bypass the Engine's model call, or `None` to let the Engine call the LLM and parse the output. Default returns `None`.
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def should_stop(self, state: StateT) -> bool
-```
-
-Optional additional stop condition checked after each step. Return `True` to terminate the run with `StopReason.AGENT_CONDITION`. Default returns `False`.
-
-
-
-
-
-**`.run()` method**
-
-Convenience method that builds an `Engine`, runs it, and returns the final result.
-
-Signature reference (not executable):
-
-```text
-def run(
- self,
- task: str | Task,
- return_state: bool = False,
- hooks: List[Any] | None = None,
- render_hooks: List[Any] | None = None,
- engine_kwargs: Dict[str, Any] | None = None,
- workspace: str | None = None,
- max_steps: int | None = None,
- env: Any = None,
- parser: Any = None,
- search: Any = None,
- critics: List[Any] | None = None,
- stop_criteria: List[Any] | None = None,
- history_policy: Any = None,
- trace: Any = None,
- render: Any = None,
- trace_logdir: str = "./runs",
- trace_prefix: str | None = None,
- theme: str = "research",
- **state_kwargs: Any,
-) -> Any
-```
-
-| Parameter | Type | Default | Description |
-|-----------|------|---------|-------------|
-| `task` | `str \| Task` | required | Task objective or structured `Task` object |
-| `return_state` | `bool` | `False` | When `True`, returns the full `EngineResult`; otherwise returns `state.final_result` |
-| `hooks` | `List[Any] \| None` | `None` | Additional `EngineHook` instances to register |
-| `render_hooks` | `List[Any] \| None` | `None` | Additional render hook instances |
-| `engine_kwargs` | `Dict[str, Any] \| None` | `None` | Extra keyword arguments forwarded to the `Engine` constructor |
-| `workspace` | `str \| None` | `None` | Path to workspace root; auto-constructs a `HostEnv` when set |
-| `max_steps` | `int \| None` | `None` | Override the maximum number of steps |
-| `env` | `Any` | `None` | Explicit `Env` instance; takes precedence over `workspace` |
-| `parser` | `Any` | `None` | Parser to pass to the `Engine` |
-| `search` | `Any` | `None` | `Search` strategy instance |
-| `critics` | `List[Any] \| None` | `None` | List of `Critic` instances |
-| `stop_criteria` | `List[Any] \| None` | `None` | Custom stop criteria list |
-| `history_policy` | `Any` | `None` | `HistoryPolicy` instance |
-| `trace` | `Any` | `None` | `True` to enable default tracing, or a `TraceWriter` instance |
-| `render` | `Any` | `None` | `True` to enable default render hook |
-| `trace_logdir` | `str` | `"./runs"` | Directory where trace files are written |
-| `trace_prefix` | `str \| None` | `None` | Prefix for the auto-generated run ID |
-| `theme` | `str` | `"research"` | Render theme name |
-| `**state_kwargs` | `Any` | | Extra kwargs forwarded to `init_state()` |
-
-**Returns** `state.final_result` by default, or an `EngineResult` when `return_state=True`.
-
-
-
-
-
-`Engine` is the execution kernel (the core AgentModule + Engine execution loop). It owns the phase loop, tool execution, recovery, tracing, and stop-criteria evaluation. You normally obtain an `Engine` through `AgentModule.build_engine()` or `AgentModule.run()`, but you can also construct one directly.
-
-Signature reference (not executable):
-
-```text
-class Engine(Generic[StateT, ObservationT, ActionT])
-```
-
-**Constructor**
-
-Signature reference (not executable):
-
-```text
-def __init__(
- self,
- agent: AgentModule[StateT, ObservationT, ActionT],
- budget: RuntimeBudget | None = None,
- validation_gate: StateValidationGate | None = None,
- recovery_handler: RecoveryHandler | None = None,
- recovery_policy: RecoveryPolicy | None = None,
- trace_writer: TraceWriter | None = None,
- parser: Parser[ActionT] | None = None,
- stop_criteria: List[StopCriteria] | None = None,
- branch_selector: BranchSelector | None = None,
- search: Search | None = None,
- critics: List[Critic] | None = None,
- env: Env | None = None,
- history_policy: HistoryPolicy | None = None,
- hooks: List[EngineHook] | None = None,
- render_hooks: List[Any] | None = None,
-)
-```
-
-| Parameter | Type | Default | Description |
-|-----------|------|---------|-------------|
-| `agent` | `AgentModule` | required | Agent whose hooks the Engine will call |
-| `budget` | `RuntimeBudget \| None` | `RuntimeBudget(max_steps=10)` | Step, time, and token budgets |
-| `validation_gate` | `StateValidationGate \| None` | default gate | Pre/post phase state validation |
-| `recovery_handler` | `RecoveryHandler \| None` | `None` | Callable invoked on recoverable errors |
-| `recovery_policy` | `RecoveryPolicy \| None` | default policy | Controls retry behaviour on failures |
-| `trace_writer` | `TraceWriter \| None` | `None` | Writes structured trace (a structured log of all run events and steps) artifacts to disk |
-| `parser` | `Parser[ActionT] \| None` | `None` | Parser for raw model output |
-| `stop_criteria` | `List[StopCriteria] \| None` | `[FinalResultCriteria()]` | Ordered list of stop criteria |
-| `branch_selector` | `BranchSelector \| None` | `FirstCandidateSelector()` | Strategy for picking among branch candidates |
-| `search` | `Search \| None` | `None` | Search strategy for `branch` decisions |
-| `critics` | `List[Critic] \| None` | `[]` | Critics (modules that evaluate each step and can trigger retries or stops) evaluated after each step |
-| `env` | `Env \| None` | `None` | Environment for observe/step lifecycle |
-| `history_policy` | `HistoryPolicy \| None` | `HistoryPolicy()` | Controls message history assembly |
-| `hooks` | `List[EngineHook] \| None` | `[]` | Engine lifecycle hooks |
-| `render_hooks` | `List[Any] \| None` | `None` | Render hooks merged into `hooks` |
-
-**Methods**
-
-Signature reference (not executable):
-
-```text
-def run(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
-```
-
-Execute the agent loop for `task`. Resets run state, initialises the env, calls `agent.init_state()`, then iterates the decide→act→reduce→check_stop cycle until a stop condition triggers. Returns an `EngineResult`.
-
----
-
-Signature reference (not executable):
-
-```text
-def register_hook(self, hook: Any) -> None
-```
-
-Append one hook instance to the active hook list.
-
----
-
-Signature reference (not executable):
-
-```text
-def unregister_hook(self, hook: Any) -> None
-```
-
-Remove one hook instance from the active hook list (identity comparison).
-
----
-
-Signature reference (not executable):
-
-```text
-def clear_hooks(self) -> None
-```
-
-Remove all registered hooks.
-
-
-
-
-
-`AsyncEngine` provides non-blocking execution for agent workflows. It wraps the same `Engine` loop but runs blocking calls in a thread pool, making it safe to use inside `asyncio` event loops.
-
-Signature reference (not executable):
-
-```text
-class AsyncEngine(Generic[StateT, ObservationT, ActionT])
-```
-
-**Constructor**
-
-Signature reference (not executable):
-
-```text
-def __init__(
- self,
- agent: AgentModule[StateT, ObservationT, ActionT],
- **engine_kwargs: Any,
-)
-```
-
-All keyword arguments are forwarded to the internal `Engine` constructor (same parameters as `Engine.__init__`).
-
-**Methods**
-
-Signature reference (not executable):
-
-```text
-async def arun(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
-```
-
-Execute the agent loop asynchronously. Returns the same `EngineResult` as `Engine.run()`.
-
----
-
-Signature reference (not executable):
-
-```text
-async def arun_stream(self, task: str | Task, **kwargs: Any) -> AsyncIterator[EngineEvent]
-```
-
-Execute the agent loop and yield `EngineEvent` objects in real time. The stream begins with `run_start` and ends with `run_end`.
-
----
-
-Signature reference (not executable):
-
-```text
-def run(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
-```
-
-Synchronous fallback — delegates to the underlying `Engine.run()`.
-
-**Properties**
-
-| Property | Type | Description |
-|----------|------|-------------|
-| `engine` | `Engine` | The underlying sync Engine instance |
-| `agent` | `AgentModule` | Same as `engine.agent` |
-| `event_stream` | `EventStream \| None` | Active event stream during `arun_stream()`, otherwise `None` |
-
-
-
-
-
-`EngineEvent` is the structured event emitted by `AsyncEngine.arun_stream()`.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-class EngineEventType(str, Enum):
- STEP_START = "step_start"
- STEP_END = "step_end"
- PHASE_START = "phase_start"
- PHASE_END = "phase_end"
- DECIDE = "decide"
- ACT = "act"
- REDUCE = "reduce"
- CRITIC = "critic"
- CHECK_STOP = "check_stop"
- HANDOFF = "handoff"
- DELEGATE = "delegate"
- FANOUT = "fanout"
- ERROR = "error"
- RUN_START = "run_start"
- RUN_END = "run_end"
-```
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class EngineEvent:
- event_type: EngineEventType
- step_id: int = 0
- agent_id: Optional[str] = None
- phase: Optional[RuntimePhase] = None
- ok: bool = True
- payload: Dict[str, Any] = field(default_factory=dict)
- error: Optional[str] = None
- ts: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
-```
-
-`EventStream` is an async-compatible event queue for consuming engine events.
-
-Signature reference (not executable):
-
-```text
-class EventStream:
- def emit(self, event: EngineEvent) -> None # thread-safe emit
- def emit_sync(self, event: EngineEvent) -> None # alias for sync callers
- def close(self) -> None # signal end of stream
- async def __aiter__(self) -> AsyncIterator[EngineEvent]
- def subscribe(self) -> asyncio.Queue # fan-out consumption
-```
-
-
-
-
-
-`EngineResult` is the dataclass returned by `Engine.run()`.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class EngineResult(Generic[StateT]):
- state: StateT
- records: List[StepRecord]
- events: List[RuntimeEvent]
- step_count: int
- task_result: Optional[TaskResult] = None
-```
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `state` | `StateT` | Final typed state after the run |
-| `records` | `List[StepRecord]` | Per-step records including decision, actions, and observations |
-| `events` | `List[RuntimeEvent]` | Ordered list of all runtime events emitted during the run |
-| `step_count` | `int` | Number of steps executed |
-| `task_result` | `TaskResult \| None` | Structured task outcome, populated when a `Task` object was passed |
-
-
-
-
-
-`Decision` is the canonical output of the decide phase. Use the factory class methods rather than constructing directly. A Decision captures what the agent wants to do next -- execute actions, produce a final answer, wait, or propose branch candidates.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class Decision(Generic[ActionT]):
- mode: DecisionMode # "act" | "final" | "wait" | "branch"
- actions: List[ActionT]
- final_answer: Optional[str]
- rationale: Optional[str]
- meta: Dict[str, Any]
- candidates: List[Decision[ActionT]]
-```
-
-**Modes**
-
-| Mode | Meaning |
-|------|---------|
-| `"act"` | Execute one or more actions |
-| `"final"` | Produce a final answer and stop |
-| `"wait"` | Skip action execution this step |
-| `"branch"` | Propose multiple candidate decisions for the branch selector |
-
-**Factory methods**
-
-Signature reference (not executable):
-
-```text
-@classmethod
-def act(
- cls,
- actions: List[ActionT],
- rationale: Optional[str] = None,
- meta: Optional[Dict[str, Any]] = None,
-) -> Decision[ActionT]
-```
-
-Signature reference (not executable):
-
-```text
-@classmethod
-def final(
- cls,
- answer: str,
- rationale: Optional[str] = None,
- meta: Optional[Dict[str, Any]] = None,
-) -> Decision[ActionT]
-```
-
-Signature reference (not executable):
-
-```text
-@classmethod
-def wait(
- cls,
- rationale: Optional[str] = None,
- meta: Optional[Dict[str, Any]] = None,
-) -> Decision[ActionT]
-```
-
-Signature reference (not executable):
-
-```text
-@classmethod
-def branch(
- cls,
- candidates: List[Decision[ActionT]],
- rationale: Optional[str] = None,
- meta: Optional[Dict[str, Any]] = None,
-) -> Decision[ActionT]
-```
-
-**`.validate()`** — Raises `ValueError` if the decision is structurally invalid (e.g. `act` with no actions).
-
-
-
-
-
-`Action` is the normalized action (a tool invocation) contract emitted by the policy and consumed by the executor.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class Action:
- name: str
- args: Dict[str, Any] = field(default_factory=dict)
- kind: ActionKind = ActionKind.TOOL
- action_id: Optional[str] = None
- timeout_s: Optional[float] = None
- max_retries: int = 0
- idempotent: bool = True
- classification: str = "default"
- metadata: Dict[str, Any] = field(default_factory=dict)
-```
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `name` | `str` | Tool name to call |
-| `args` | `Dict[str, Any]` | Keyword arguments forwarded to the tool |
-| `kind` | `ActionKind` | Currently only `ActionKind.TOOL` (`"tool"`) |
-| `action_id` | `str \| None` | Optional unique identifier for the action |
-| `timeout_s` | `float \| None` | Per-action timeout override in seconds |
-| `max_retries` | `int` | Number of retries on failure |
-| `idempotent` | `bool` | Whether the action is safe to retry |
-| `classification` | `str` | User-defined label for grouping/filtering |
-| `metadata` | `Dict[str, Any]` | Arbitrary extra metadata |
-
-**`Action.from_dict(payload)`** — Construct from a plain dict.
-
-
-
-
-
-`StateSchema` is the canonical typed state base class. Subclass it to define your agent's state fields.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class StateSchema:
- schema_version: int = 1
- task: str = ""
- current_step: int = 0
- max_steps: int = 10
- final_result: Optional[str] = None
- stop_reason: Optional[str] = None
- metadata: Dict[str, Any] = field(default_factory=dict)
- metrics: Dict[str, Any] = field(default_factory=dict)
-```
-
-| Field | Description |
-|-------|-------------|
-| `task` | The task objective string |
-| `current_step` | Step counter incremented by `advance_step()` |
-| `max_steps` | Hard cap on steps; validated on every `advance_step()` call |
-| `final_result` | The agent's final answer string |
-| `stop_reason` | A `StopReason` value string set when the run ends |
-| `metadata` | Free-form dict for agent-specific data |
-| `metrics` | Free-form dict for numeric metrics |
-
-**Key methods**
-
-Signature reference (not executable):
-
-```text
-def set_stop(self, reason: StopReason | str, final_result: Optional[str] = None) -> None
-def advance_step(self) -> None
-def validate(self) -> None
-def to_dict(self) -> Dict[str, Any]
-
-@classmethod
-def from_dict(cls, payload: Dict[str, Any], strict: bool = True) -> StateT
-
-@classmethod
-def migrate_payload(cls, payload: Dict[str, Any], target_version: int) -> Dict[str, Any]
-```
-
-
-
-
-
-Use `Task` when you need to pass structured metadata, resources, and budget constraints alongside the objective string.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class Task:
- id: str
- objective: str
- inputs: Dict[str, Any] = field(default_factory=dict)
- resources: List[TaskResource] = field(default_factory=list)
- env_spec: Optional[EnvSpec] = None
- constraints: Dict[str, Any] = field(default_factory=dict)
- success_criteria: List[str] = field(default_factory=list)
- budget: TaskBudget = field(default_factory=TaskBudget)
- metadata: Dict[str, Any] = field(default_factory=dict)
-```
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class TaskBudget:
- max_steps: Optional[int] = None
- max_runtime_seconds: Optional[float] = None
- max_tokens: Optional[int] = None
-```
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class TaskResource:
- kind: str # "file" | "dir" | "url" | "artifact"
- path: Optional[str] = None
- uri: Optional[str] = None
- mount_to: Optional[str] = None
- required: bool = True
- description: str = ""
- metadata: Dict[str, Any] = field(default_factory=dict)
-```
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class TaskResult:
- task_id: str
- success: bool
- stop_reason: Optional[str]
- final_result: Any
- criteria: List[TaskCriterionResult] = field(default_factory=list)
- artifacts: List[TaskResourceBinding] = field(default_factory=list)
- metrics: Dict[str, Any] = field(default_factory=dict)
- metadata: Dict[str, Any] = field(default_factory=dict)
-```
-
-**`Task` helper methods**
-
-Signature reference (not executable):
-
-```text
-def validate(self) -> None
-def validate_structured(self, workspace: Optional[str] = None) -> List[TaskValidationIssue]
-def resolve_resources(self, workspace: Optional[str] = None) -> List[TaskResourceBinding]
-def to_dict(self) -> Dict[str, Any]
-
-@classmethod
-def from_dict(cls, payload: Dict[str, Any]) -> Task
-```
-
-
-
-
-
-`Env` is the abstract environment interface. Implement it to provide a custom observe/step lifecycle for your agent.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-class Env(ABC):
- @abstractmethod
- def reset(self, task: Any = None) -> Any: ...
-
- @abstractmethod
- def observe(self) -> Any: ...
-
- @abstractmethod
- def step(self, action: Any) -> Any: ...
-
- @abstractmethod
- def is_terminal(self) -> bool: ...
-
- @abstractmethod
- def close(self) -> None: ...
-```
-
-`EnvSpec` is a dataclass used inside `Task` to declare the environment type and configuration:
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class EnvSpec:
- type: str # e.g. "host", "docker", "tau_bench"
- config: Dict[str, Any] = field(default_factory=dict)
- required_tools: List[str] = field(default_factory=list)
- capabilities: List[str] = field(default_factory=list)
- metadata: Dict[str, Any] = field(default_factory=dict)
-```
-
-
-
-
-
-The `tool` decorator marks a callable as a QitOS tool and attaches metadata to it without changing its call semantics.
-
-Signature reference (not executable):
-
-```text
-def tool(
- name: Optional[str] = None,
- description: Optional[str] = None,
- timeout_s: Optional[float] = None,
- max_retries: int = 0,
- permissions: Optional[ToolPermission] = None,
- required_ops: Optional[List[str]] = None,
-)
-```
-
-| Parameter | Type | Description |
-|-----------|------|-------------|
-| `name` | `str \| None` | Override the tool name (defaults to the function's `__name__`) |
-| `description` | `str \| None` | Override the tool description (defaults to the docstring) |
-| `timeout_s` | `float \| None` | Per-call timeout in seconds |
-| `max_retries` | `int` | Number of retries on failure |
-| `permissions` | `ToolPermission \| None` | Permission flags for the tool |
-| `required_ops` | `List[str] \| None` | Runtime ops required from the environment |
-
-**Example**
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-from qitos import tool
-
-@tool(name="search_web", timeout_s=30.0, permissions=ToolPermission(network=True))
-def search_web(query: str) -> str:
- """Search the web and return results."""
- ...
-```
-
-
-
-
-
-`ToolRegistry` stores tools and toolsets and is passed to `AgentModule` and `Engine` at construction time.
-
-Signature reference (not executable):
-
-```text
-class ToolRegistry
-```
-
-**Constructor** — `ToolRegistry()` (no parameters)
-
-**Methods**
-
-Signature reference (not executable):
-
-```text
-def register(
- self,
- item: Any,
- name: Optional[str] = None,
- meta: Optional[ToolMeta] = None,
-) -> ToolRegistry
-```
-Register a single callable or `BaseTool`. Returns `self` for chaining.
-
----
-
-Signature reference (not executable):
-
-```text
-def register_toolset(
- self,
- toolset: Any,
- namespace: Optional[str] = None,
-) -> ToolRegistry
-```
-Register all tools from a toolset object. Tool names are prefixed with `namespace` (defaults to `toolset.name`).
-
----
-
-Signature reference (not executable):
-
-```text
-def include(self, obj: Any) -> ToolRegistry
-```
-Scan an object for methods decorated with `@tool` and register them all.
-
----
-
-Signature reference (not executable):
-
-```text
-def get(self, name: str) -> Optional[BaseTool]
-def list_tools(self) -> List[str]
-def list_toolsets(self) -> List[str]
-def describe_tool(self, name: str) -> Dict[str, Any]
-def call(self, name: str, runtime_context: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Any
-def get_tool_descriptions(self) -> str
-def get_all_specs(self) -> List[Dict[str, Any]]
-def setup(self, context: Optional[Dict[str, Any]] = None) -> None
-def teardown(self, context: Optional[Dict[str, Any]] = None) -> None
-```
-
-**Example**
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-from qitos import ToolRegistry, tool
-
-registry = ToolRegistry()
-
-@tool(name="greet")
-def greet(name: str) -> str:
- """Say hello."""
- return f"Hello, {name}!"
-
-registry.register(greet)
-```
-
-
-
-
-
-`Memory` is the abstract interface for long-term memory adapters.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-class Memory(ABC):
- @abstractmethod
- def append(self, record: MemoryRecord) -> None: ...
-
- @abstractmethod
- def retrieve(
- self,
- query: Optional[Dict[str, Any]] = None,
- state: Any = None,
- observation: Any = None,
- ) -> Any: ...
-
- @abstractmethod
- def summarize(self, max_items: int = 5) -> str: ...
-
- @abstractmethod
- def evict(self) -> int: ...
-
- @abstractmethod
- def reset(self, run_id: Optional[str] = None) -> None: ...
-```
-
-`MemoryRecord` is the unit of storage:
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class MemoryRecord:
- role: str
- content: Any
- step_id: int
- metadata: Dict[str, Any] = field(default_factory=dict)
-```
-
-
-
-
-
-`History` is the abstract interface for model message history adapters.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-class History(ABC):
- @abstractmethod
- def append(self, message: HistoryMessage) -> None: ...
-
- @abstractmethod
- def retrieve(
- self,
- query: Optional[Dict[str, Any]] = None,
- state: Any = None,
- observation: Any = None,
- ) -> Any: ...
-
- @abstractmethod
- def summarize(self, max_items: int = 5) -> str: ...
-
- @abstractmethod
- def evict(self) -> int: ...
-
- @abstractmethod
- def reset(self, run_id: Optional[str] = None) -> None: ...
-```
-
-`HistoryPolicy` controls how the Engine assembles history for model calls:
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class HistoryPolicy:
- roles: List[str] = field(default_factory=lambda: ["user", "assistant"])
- max_messages: int = 24
- step_window: Optional[int] = None
- max_tokens: Optional[int] = None
-```
-
-| Field | Description |
-|-------|-------------|
-| `roles` | Message roles to include |
-| `max_messages` | Maximum number of messages to include |
-| `step_window` | If set, only include messages from the last N steps |
-| `max_tokens` | If set, trim messages to fit within this token budget |
-
-`HistoryMessage` is the unit of storage:
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class HistoryMessage:
- role: str
- content: str
- step_id: int
- metadata: Dict[str, Any] = field(default_factory=dict)
-```
-
-
-
-
-
-`StopReason` is a string enum. Its value is written to `state.stop_reason` when a run ends.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-class StopReason(str, Enum):
- SUCCESS = "success"
- FINAL = "final"
- MAX_STEPS = "max_steps"
- BUDGET_STEPS = "budget_steps"
- BUDGET_TIME = "budget_time"
- BUDGET_TOKENS = "budget_tokens"
- AGENT_CONDITION = "agent_condition"
- CRITIC_STOP = "critic_stop"
- STAGNATION = "stagnation"
- ENV_TERMINAL = "env_terminal"
- TASK_VALIDATION_FAILED = "task_validation_failed"
- ENV_CAPABILITY_MISMATCH = "env_capability_mismatch"
- UNRECOVERABLE_ERROR = "unrecoverable_error"
- CANCELLED_IMMEDIATE = "cancelled_immediate"
-```
-
-| Value | When set |
-|-------|----------|
-| `success` | Agent completed successfully |
-| `final` | `Decision.final()` was accepted |
-| `max_steps` | `StateSchema.max_steps` reached |
-| `budget_steps` | `RuntimeBudget.max_steps` reached |
-| `budget_time` | `RuntimeBudget.max_runtime_seconds` elapsed |
-| `budget_tokens` | `RuntimeBudget.max_tokens` consumed |
-| `agent_condition` | `AgentModule.should_stop()` returned `True` |
-| `critic_stop` | A `Critic` returned `action="stop"` |
-| `stagnation` | No state change detected for N steps |
-| `env_terminal` | `Env.is_terminal()` returned `True` |
-| `task_validation_failed` | `Task.validate_structured()` produced issues |
-| `env_capability_mismatch` | Environment missing required ops |
-| `unrecoverable_error` | Fatal error with no recovery path |
-| `cancelled_immediate` | The Engine observed an immediate cancellation request; the trace manifest uses terminal status `stopped` |
-
-
-
-
-
-`QitosRuntimeError` is the base class for all structured runtime errors in QitOS.
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-class QitosRuntimeError(Exception):
- def __init__(self, info: RuntimeErrorInfo): ...
- info: RuntimeErrorInfo
-```
-
-`RuntimeErrorInfo` carries structured context:
-
-Illustrative fragment (not a standalone program; use the complete example linked on this page).
-
-```python
-@dataclass
-class RuntimeErrorInfo:
- category: ErrorCategory # model | parse | tool | state | task | env | system
- message: str
- phase: str
- step_id: int
- recoverable: bool = False
- details: Dict[str, Any] = field(default_factory=dict)
-```
-
-Typed subclasses: `ModelExecutionError`, `ParseExecutionError`, `ToolExecutionError`, `StateExecutionError`, `SystemExecutionError`.
-
-
-
-
+Find the current public API by task. Each category provides pinned source signatures, parameter types/defaults, usage fragments, behavior boundaries and complete tutorials. Symbols are not all exported from the root `qitos` package: use the exact import shown.
+
+- [Configuration and composition](/reference/composition)
+- [Agent and execution](/reference/agent-runtime)
+- [Sessions and persistence](/reference/sessions)
+- [Tools, results and artifacts](/reference/tools)
+- [Context and memory contracts](/reference/context)
+- [Multi-agent work](/reference/work-graph)
+- [Trajectory and readers](/reference/trajectory)
+- [CLI](/reference/cli)
+- [Configuration](/reference/configuration)
+- [Extension index](/reference/extensions)
+
+## Symbol index
+
+- [`qitos.config.AgentComposition`](/reference/composition#qitos-config-agentcomposition)
+- [`qitos.config.build_agent_composition`](/reference/composition#qitos-config-build_agent_composition)
+- [`qitos.config.load_agent_config`](/reference/composition#qitos-config-load_agent_config)
+- [`qitos.config.AgentConfig`](/reference/composition#qitos-config-agentconfig)
+- [`qitos.config.CredentialRef`](/reference/composition#qitos-config-credentialref)
+- [`qitos.config.LocalCredentialFileResolver`](/reference/composition#qitos-config-localcredentialfileresolver)
+- [`qitos.config.BudgetConfig`](/reference/composition#qitos-config-budgetconfig)
+- [`qitos.config.EnvironmentConfig`](/reference/composition#qitos-config-environmentconfig)
+- [`qitos.config.ModelConfig`](/reference/composition#qitos-config-modelconfig)
+- [`qitos.config.RuntimeConfig`](/reference/composition#qitos-config-runtimeconfig)
+- [`qitos.config.TrajectoryConfig`](/reference/composition#qitos-config-trajectoryconfig)
+- [`qitos.AgentModule`](/reference/agent-runtime#qitos-agentmodule)
+- [`qitos.StateSchema`](/reference/agent-runtime#qitos-stateschema)
+- [`qitos.Task`](/reference/agent-runtime#qitos-task)
+- [`qitos.Decision`](/reference/agent-runtime#qitos-decision)
+- [`qitos.Action`](/reference/agent-runtime#qitos-action)
+- [`qitos.Engine`](/reference/agent-runtime#qitos-engine)
+- [`qitos.engine.engine.EngineResult`](/reference/agent-runtime#qitos-engine-engine-engineresult)
+- [`qitos.engine.states.RuntimeBudget`](/reference/agent-runtime#qitos-engine-states-runtimebudget)
+- [`qitos.StopReason`](/reference/agent-runtime#qitos-stopreason)
+- [`qitos.engine.runtime.RuntimeComposition`](/reference/agent-runtime#qitos-engine-runtime-runtimecomposition)
+- [`qitos.engine.session_runtime.Session`](/reference/sessions#qitos-engine-session_runtime-session)
+- [`qitos.engine.session_runtime.SessionInspection`](/reference/sessions#qitos-engine-session_runtime-sessioninspection)
+- [`qitos.checkpoint.store.CheckpointStore`](/reference/sessions#qitos-checkpoint-store-checkpointstore)
+- [`qitos.ToolRegistry`](/reference/tools#qitos-toolregistry)
+- [`qitos.core.function_tool_decorator.function_tool`](/reference/tools#qitos-core-function_tool_decorator-function_tool)
+- [`qitos.core.tool.BaseTool`](/reference/tools#qitos-core-tool-basetool)
+- [`qitos.core.tool_result.ToolResult`](/reference/tools#qitos-core-tool_result-toolresult)
+- [`qitos.core.artifact.ArtifactRef`](/reference/tools#qitos-core-artifact-artifactref)
+- [`qitos.engine.action_executor.ActionExecutionPolicy`](/reference/tools#qitos-engine-action_executor-actionexecutionpolicy)
+- [`qitos.kit.tool.internal.publication.SandboxPublicationTool`](/reference/tools#qitos-kit-tool-internal-publication-sandboxpublicationtool)
+- [`qitos.core.context.StaticContextContributor`](/reference/context#qitos-core-context-staticcontextcontributor)
+- [`qitos.core.context.PriorityContextSelectionPolicy`](/reference/context#qitos-core-context-prioritycontextselectionpolicy)
+- [`qitos.core.context.DeclaredContextBudgetPolicy`](/reference/context#qitos-core-context-declaredcontextbudgetpolicy)
+- [`qitos.core.request_view.CompactionReceipt`](/reference/context#qitos-core-request_view-compactionreceipt)
+- [`qitos.engine.runtime.LifecyclePolicy`](/reference/context#qitos-engine-runtime-lifecyclepolicy)
+- [`qitos.engine.session_runtime.Session`](/reference/work-graph#qitos-engine-session_runtime-session)
+- [`qitos.core.work_graph.WorkGraph`](/reference/work-graph#qitos-core-work_graph-workgraph)
+- [`qitos.core.work_graph.WorkItem`](/reference/work-graph#qitos-core-work_graph-workitem)
+- [`qitos.core.work_graph.WorkAttempt`](/reference/work-graph#qitos-core-work_graph-workattempt)
+- [`qitos.engine.work_runtime.DurableWorkRuntime`](/reference/work-graph#qitos-engine-work_runtime-durableworkruntime)
+- [`qitos.engine.work_runtime.LocalWorkScheduler`](/reference/work-graph#qitos-engine-work_runtime-localworkscheduler)
+- [`qitos.engine.work_runtime.WorkRuntimeError`](/reference/work-graph#qitos-engine-work_runtime-workruntimeerror)
+- [`qitos.qita.reader.default_reader`](/reference/trajectory#qitos-qita-reader-default_reader)
+- [`qitos.tracing.exporter.CanonicalTrajectoryExporter`](/reference/trajectory#qitos-tracing-exporter-canonicaltrajectoryexporter)
+- [`qitos.tracing.trajectory.PrivacyView`](/reference/trajectory#qitos-tracing-trajectory-privacyview)
+
+## Compatibility and history
+
+[Migration](/reference/g5-migration) · [Previous reference archive](/reference/legacy-api)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx
index 9c4b311b..5dfe465d 100644
--- a/docs/reference/cli.mdx
+++ b/docs/reference/cli.mdx
@@ -3,6 +3,24 @@ title: "CLI Reference"
description: "Reference for qita and qit, including the minimal coding-agent demo and the official benchmark CLI added in QitOS v0.3."
---
+The recommended path is qit new → canonical config → Python composition/Session. The CLI does not auto-import custom Python tools. Start with Quickstart, then use inspection commands below. Older demos are advanced compatibility entrypoints. [Quickstart](/quickstart)
+
+## Current Session commands
+
+```bash
+qit --help
+qita --help
+qit new --agent-name notes_agent --output-dir . --no-input
+qit session inspect --help
+qit session restore --help
+qit session fork --help
+qita inspect --help
+```
+
+Inspect needs no model or Docker. The lifecycle lesson supplies actual Session IDs, matching configuration and complete commands. Replay and board start local servers; stop with Ctrl-C before the next command in the same terminal. [Session](/tutorials/checkpoint-and-fork)
+
+
+
QitOS ships two top-level CLIs:
- `qita` for trace inspection
@@ -10,7 +28,7 @@ QitOS ships two top-level CLIs:
## qit demo
-Use `qit demo` when you want the fastest path to a real model-backed QitOS run.
+`qit demo` is an advanced compatibility path. Use the Quickstart for the canonical beginner setup.
### `qit demo minimal`
diff --git a/docs/reference/composition.mdx b/docs/reference/composition.mdx
new file mode 100644
index 00000000..73f1855d
--- /dev/null
+++ b/docs/reference/composition.mdx
@@ -0,0 +1,478 @@
+---
+title: "Configuration and composition"
+description: "QitOS public API: composition"
+---
+
+Load canonical configuration without credentials or Docker first. At composition, explicitly resolve CredentialRef or inject the teaching provider. Use the composition as a context manager: it owns environment, store and scheduler cleanup. Session methods return the public Session facade. Ephemeral configuration cannot restore or fork. Invalid config, missing extensions or a closed composition fail before useful execution.
+
+[完整可运行教程 / Complete tutorial](/quickstart) · [API index](/reference/api)
+
+Signatures and fields below are extracted from the pinned source. Signatures are reference material, not standalone programs. Source links bind the same runtime baseline. Any does not imply arbitrary objects are supported; use the behavioral contract above and the linked tutorial.
+
+{/* api-reference:start */}
+
+
+## AgentComposition
+
+```python
+from qitos.config import AgentComposition
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L134)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+# After loading config and creating the explicit provider:
+with build_agent_composition(config, model_override=provider) as composition:
+ session = composition.session("Index the notes")
+ result = session.run()
+```
+
+```text
+Resource-owning composition root for the existing Engine and Session.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `config` | `AgentConfig` | `required` |
+| `model` | `Any` | `required` |
+| `tool_registry` | `ToolRegistry` | `required` |
+| `env` | `Any` | `required` |
+| `runtime` | `RuntimeComposition` | `required` |
+| `agent` | `ConfiguredAgent` | `required` |
+| `engine` | `Engine[Any, Any, Any]` | `required` |
+| `credential_receipt` | `Dict[str, Any]` | `required` |
+| `sandbox_backend` | `Any` | `None` |
+| `sandbox_receipt` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `trajectory_path` | `Optional[Path]` | `None` |
+
+
+### AgentComposition.session
+
+```text
+session(task: Optional[str]=None, *, session_id: Any=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `Optional[str]` | `None` |
+| `session_id` | `Any` | `None` |
+
+```text
+Create the existing durable Session from this composition.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L172)
+
+
+### AgentComposition.restore
+
+```text
+restore(session_id: Any=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `session_id` | `Any` | `None` |
+
+```text
+Restore with this composition's resolver registry and canonical Engine.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L188)
+
+
+### AgentComposition.fork
+
+```text
+fork(session_id: Any, snapshot: Any=None, *, operation_id: Optional[str]=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `session_id` | `Any` | `required` |
+| `snapshot` | `Any` | `None` |
+| `operation_id` | `Optional[str]` | `None` |
+
+```text
+Fork immutable persisted state without claiming the source owner.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L211)
+
+
+### AgentComposition.close
+
+```text
+close() -> Dict[str, Any]
+```
+
+```text
+Close every framework-owned resource once and return a stable receipt.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L222)
+
+
+
+## build_agent_composition
+
+```python
+from qitos.config import build_agent_composition
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L659)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+composition = build_agent_composition(config, model_override=provider)
+try:
+ result = composition.session("Index the notes").run()
+finally:
+ composition.close()
+```
+
+```text
+Compose the existing model/tools/Env/runtime/AgentModule/Engine stack.
+```
+
+```text
+build_agent_composition(config: AgentConfig, *, credential_resolver: Optional[CredentialResolver]=None, model_override: Any=None, env_override: Any=None, extensions: Optional[Mapping[str, Any]]=None) -> AgentComposition
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `config` | `AgentConfig` | `required` |
+| `credential_resolver` | `Optional[CredentialResolver]` | `None` |
+| `model_override` | `Any` | `None` |
+| `env_override` | `Any` | `None` |
+| `extensions` | `Optional[Mapping[str, Any]]` | `None` |
+
+
+
+## load_agent_config
+
+```python
+from qitos.config import load_agent_config
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L500)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+config = load_agent_config("agent.yaml")
+print(config.digest())
+```
+
+```text
+Load one strict canonical configuration from YAML.
+```
+
+```text
+load_agent_config(path: str | Path, *, compatibility: bool=False, environment_interpolation: bool=False) -> AgentConfig
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `path` | `str | Path` | `required` |
+| `compatibility` | `bool` | `False` |
+| `environment_interpolation` | `bool` | `False` |
+
+
+
+## AgentConfig
+
+```python
+from qitos.config import AgentConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L232)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+config = load_agent_config("agent.yaml")
+assert config.runtime.session.store == "sqlite"
+print(config.to_dict()["model"]["credential"])
+```
+
+```text
+The one canonical declarative agent launch configuration.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `name` | `str` | `'agent'` |
+| `max_steps` | `int` | `10` |
+| `model` | `ModelConfig` | `field(default_factory=ModelConfig)` |
+| `dataset` | `Sequence[DatasetItem]` | `field(default_factory=tuple)` |
+| `tools` | `Sequence[str]` | `field(default_factory=tuple)` |
+| `tool_preset` | `str` | `'none'` |
+| `tool_options` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `tool_use_policy` | `str` | `'auto'` |
+| `protocol` | `str` | `'auto'` |
+| `parser` | `str` | `'auto'` |
+| `environment` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `seed` | `int` | `0` |
+| `metadata` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `context` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `memory` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `compaction` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `lifecycle` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `failure_policy` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `runtime` | `RuntimeConfig` | `field(default_factory=RuntimeConfig)` |
+| `budgets` | `Optional[BudgetConfig]` | `None` |
+| `schema` | `str` | `CANONICAL_SCHEMA` |
+| `source` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `compatibility` | `Sequence[Mapping[str, Any]]` | `field(default_factory=tuple)` |
+| `loss` | `Sequence[Mapping[str, Any]]` | `field(default_factory=tuple)` |
+
+
+### AgentConfig.to_dict
+
+```text
+to_dict() -> Dict[str, Any]
+```
+
+```text
+Return deterministic JSON/YAML-safe canonical launch data.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L294)
+
+
+### AgentConfig.digest
+
+```text
+digest() -> str
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L338)
+
+
+
+## CredentialRef
+
+```python
+from qitos.config import CredentialRef
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/credentials.py#L25)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+reference = CredentialRef("notes-provider")
+print(reference)
+```
+
+```text
+Serializable logical identity of one credential.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `ref` | `str` | `required` |
+
+
+
+## LocalCredentialFileResolver
+
+```python
+from qitos.config import LocalCredentialFileResolver
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/credentials.py#L113)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+from pathlib import Path
+resolver = LocalCredentialFileResolver(
+ Path.home() / ".config/qitos/credentials.yaml",
+ repository_root=Path.cwd(),
+)
+# Pass resolver as credential_resolver to build_agent_composition.
+```
+
+```text
+Resolve one logical credential from a hardened local YAML file.
+```
+
+```text
+LocalCredentialFileResolver(path: str | Path, *, repository_root: str | Path) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `path` | `str | Path` | `required` |
+| `repository_root` | `str | Path` | `required` |
+
+
+### LocalCredentialFileResolver.resolve
+
+```text
+resolve(ref: CredentialRef) -> CredentialResolution
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `ref` | `CredentialRef` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/credentials.py#L155)
+
+
+
+## BudgetConfig
+
+```python
+from qitos.config import BudgetConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L218)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+budget = BudgetConfig(max_steps=6, max_requests=6, max_runtime_seconds=30)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `max_steps` | `int` | `10` |
+| `max_runtime_seconds` | `float` | `600.0` |
+| `max_requests` | `int` | `12` |
+
+
+
+## EnvironmentConfig
+
+```python
+from qitos.config import EnvironmentConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L112)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+environment = EnvironmentConfig(workspace="./source", image="python:3.12-slim")
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `type` | `str` | `'docker'` |
+| `image` | `str` | `'python:3.12-slim'` |
+| `workspace` | `str` | `'.'` |
+| `container_workspace` | `str` | `'/workspace'` |
+| `network` | `str` | `'none'` |
+| `read_only_root` | `bool` | `True` |
+| `cap_drop` | `bool` | `True` |
+| `no_new_privileges` | `bool` | `True` |
+| `pids_limit` | `Optional[int]` | `256` |
+| `memory_mb` | `Optional[int]` | `2048` |
+| `cpus` | `Optional[float]` | `2.0` |
+| `cleanup_required` | `bool` | `True` |
+
+
+
+## ModelConfig
+
+```python
+from qitos.config import ModelConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L60)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+model = ModelConfig(provider="openai_compatible", model="notes-fake")
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `provider` | `str` | `'openai'` |
+| `model` | `str` | `''` |
+| `model_name` | `str` | `''` |
+| `credential` | `Optional[CredentialRef]` | `None` |
+| `base_url` | `str` | `''` |
+| `context_window` | `Optional[int]` | `None` |
+| `api_mode` | `str` | `'chat_completions'` |
+| `request` | `ModelRequestConfig` | `field(default_factory=ModelRequestConfig)` |
+| `api_key` | `str` | `field(default='', repr=False, compare=False)` |
+| `temperature` | `Optional[float]` | `None` |
+| `max_tokens` | `Optional[int]` | `None` |
+
+
+
+## RuntimeConfig
+
+```python
+from qitos.config import RuntimeConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L202)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+runtime = RuntimeConfig(environment=EnvironmentConfig(workspace="./source"))
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `environment` | `EnvironmentConfig` | `field(default_factory=EnvironmentConfig)` |
+| `session` | `SessionConfig` | `field(default_factory=SessionConfig)` |
+| `trajectory` | `TrajectoryConfig` | `field(default_factory=TrajectoryConfig)` |
+| `data_root` | `str` | `''` |
+
+
+
+## TrajectoryConfig
+
+```python
+from qitos.config import TrajectoryConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L186)
+
+[Usage and executable example](/quickstart)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+trajectory = TrajectoryConfig(output="./notes-run/trajectory.journal")
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `enabled` | `bool` | `True` |
+| `output` | `str` | `''` |
+| `privacy` | `str` | `'private'` |
+| `failure_policy` | `str` | `'required'` |
+
+{/* api-reference:end */}
diff --git a/docs/reference/configuration.mdx b/docs/reference/configuration.mdx
index 7516b5b7..2742999d 100644
--- a/docs/reference/configuration.mdx
+++ b/docs/reference/configuration.mdx
@@ -1,154 +1,274 @@
---
-title: "Configuration"
-description: "QitOS G5 · Configuration"
+title: "Configure a real model"
+description: "Use the same notes project with explicit credentials and bounded requests."
---
-## Complete configuration and dependencies
+Continue with the same notes project. By default the script validates configuration only; `--live` explicitly opts into credential resolution and model requests. Documentation qualification never executes that option. Complete [Quickstart](/quickstart) installation first; to start independently, create an empty directory and save every file on this page.
-Install first. The real client requires `[openai]`; file tools require Docker
-CLI, daemon and image. Save this complete file as `agent.yaml`, then replace
-the example model and reserved endpoint. `provider.example` is a placeholder,
-not a working service. Validate tools, reasoning, continuation and Responses
-support individually; an OpenAI-compatible label is not a compatibility guarantee.
+## Dependencies and configuration
+
+```bash
+python -m pip install "qitos[openai] @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+```
+
+Replace `example-model` and `https://provider.example/v1` in `real_agent.yaml` with your model and endpoint; the reserved domain is not a service. These trusted pure-function tools need no Docker. Before adding file/command tools, follow the sandbox lesson and configure Docker rather than extending this unsafe_host authority.
+
+Budgets are 6 model requests, at most 512 output tokens per response, 6 steps and 30 seconds runtime; provider timeout is 30 seconds with retries 0. Budgets do not guarantee task success, and timeout is not hard cancellation. Check text protocol, tool round trips, continuation and loss policy compatibility individually.
+
+## Private credential file
+
+```bash
+mkdir -p "$HOME/.config/qitos"
+chmod 700 "$HOME/.config/qitos"
+touch "$HOME/.config/qitos/credentials.yaml"
+chmod 600 "$HOME/.config/qitos/credentials.yaml"
+```
+
+Use a local editor to write the following structure in that file. It must be a regular, non-symlink file owned by the current user, with a 0700 parent directory and 0600 file mode. Keep it outside the project, Git, command history and screenshots.
```yaml
+credentials:
+ notes-provider: REPLACE_IN_PRIVATE_EDITOR
+```
+
+`model.credential.ref` matches `notes-provider`; configuration stores a CredentialRef only. LocalCredentialFileResolver explicitly resolves it at composition. Environment variables are not the default path. The generated scaffold credential example can omit the required root key; use this complete structure.
+
+## Validate first, then explicitly run
+
+```bash
+python real_notes.py
+```
+
+```text
+configuration valid; no credentials read; no model request
+```
+
+The next command sends real model requests; run it only after configuring your provider. Assertions check actual tool results, not a model claiming completion.
+
+```bash
+python real_notes.py --live --credentials "$HOME/.config/qitos/credentials.yaml"
+```
+
+This project registers `summarize_note` in Python, so launch it through this file. `qit run --config` does not automatically import application extensions; built-in-tool configurations can use the CLI documented in the CLI reference.
+
+## Errors, boundaries and exercise
+
+Preserve typed failures for unknown keys, unsafe permissions, missing references and provider/codec incompatibility. Do not put keys in agent YAML or relax loss checks. Repeated launches create new Sessions; restore requires matching configuration and resolvers. Exercise: change request.max_tokens in YAML, then load and inspect it without credentials.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```yaml title="agent.yaml"
schema: qitos.agent
agent:
- name: example-coding-agent
- protocol: auto
- parser: auto
- seed: 0
+ name: notes_agent
+ protocol: react_text_v1
model:
provider: openai_compatible
- model: example-model
- base_url: https://provider.example/v1
+ model: notes-fake
credential:
- ref: example-openai-compatible
- api_mode: chat_completions
- context_window: 32768
+ ref: notes-provider
request:
- temperature: 0.0
- max_tokens: 2048
- timeout_seconds: 180
- extra_body: {}
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
tools:
- preset: env_coding
- include: []
- options: {}
- policy: auto
+ preset: none
runtime:
environment:
- type: docker
- image: python:3.12-slim
+ type: unsafe_host
workspace: .
- container_workspace: /workspace
- network: none
- read_only_root: true
- cap_drop: true
- no_new_privileges: true
- pids_limit: 256
- memory_mb: 2048
- cpus: 2.0
- cleanup_required: true
session:
mode: durable
store: sqlite
- path: ./.qitos/example-sessions.sqlite3
+ path: ./notes-run/sessions.sqlite3
trajectory:
enabled: true
- output: ./runs
- privacy: private
- failure_policy: required
+ output: ./notes-run/trajectory.journal
budgets:
- max_steps: 10
- max_runtime_seconds: 600
- max_requests: 12
-context: {}
-memory: {}
-compaction: {}
-lifecycle:
- policy: cooperative
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
failure_policy:
tool: fail_closed
-metadata:
- purpose: provider-neutral-launch-example
-dataset:
- - task: Inspect the workspace and report one verified fact.
```
-## Private credentials
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
-Use a directory outside the repository with mode `0700`, and a regular,
-current-user-owned non-symlink file with mode `0600`. The resolver validates
-the immediate parent; keeping the private directory chain restricted is also
-recommended. Fill the value with a local editor, not in shell history or Git.
-G5's generated `credentials.example.yaml` omits the required root key;
-use this complete structure instead.
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
-```bash
-mkdir -p "$HOME/.config/qitos"
-chmod 700 "$HOME/.config/qitos"
-touch "$HOME/.config/qitos/credentials.yaml"
-chmod 600 "$HOME/.config/qitos/credentials.yaml"
-```
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
-```yaml
-credentials:
- example-openai-compatible: REPLACE_IN_PRIVATE_EDITOR
-```
-`model.credential.ref` matches the mapping key. `CredentialRef` stores only
-the logical reference; the explicit resolver resolves secrets at composition,
-not in serialized config or Session data.
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
-## Inspect first, then run
-This qualification only loads the config; it does not execute the real launch
-below. `load_agent_config` reads neither credentials nor provider/Docker state.
-The budget is 12 requests, 2048 output tokens per response, 10 steps and 600
-seconds. Timeout is not hard cancellation.
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
-Configuration loading check (no model requests):
+ def __init__(self, start=0):
+ self.stage = start
-```python
-from qitos.config import load_agent_config
-config = load_agent_config("agent.yaml")
-assert config.budgets.max_requests == 12
-print(config.digest())
-```
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
-```bash
-docker info
-docker pull python:3.12-slim
-qit run --config agent.yaml --credentials "$HOME/.config/qitos/credentials.yaml"
-```
-Save as `run_agent.py` and run `python run_agent.py` (sends real requests):
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
-Complete run_agent.py (manual execution sends real model requests; not run in this qualification):
-```python
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="real_agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: example-model
+ base_url: https://provider.example/v1
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./real-notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./real-notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="real_notes.py"
+"""Validate configuration by default; --live explicitly opts into model requests."""
+import argparse
from pathlib import Path
+
+from notes import summarize_note
from qitos.config import LocalCredentialFileResolver, build_agent_composition, load_agent_config
-config = load_agent_config("agent.yaml")
-resolver = LocalCredentialFileResolver(
- Path.home() / ".config/qitos/credentials.yaml",
- repository_root=Path.cwd(),
-)
-with build_agent_composition(config, credential_resolver=resolver) as composition:
- session = composition.session("Inspect the workspace and report one verified fact.")
- result = session.run()
- print(session.session_id.value, result.state.stop_reason, result.state.final_result)
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--live", action="store_true")
+ parser.add_argument("--credentials", type=Path, default=Path.home() / ".config/qitos/credentials.yaml")
+ args = parser.parse_args()
+ config = load_agent_config(Path(__file__).with_name("real_agent.yaml"))
+ assert config.budgets.max_requests == 6
+ if not args.live:
+ print("configuration valid; no credentials read; no model request")
+ return
+ if config.model.base_url == "https://provider.example/v1":
+ raise ValueError("Replace the reserved provider.example endpoint and example-model first")
+ resolver = LocalCredentialFileResolver(args.credentials, repository_root=Path.cwd())
+ with build_agent_composition(config, credential_resolver=resolver) as composition:
+ composition.tool_registry.register(summarize_note)
+ result = composition.session(
+ "Call summarize_note for indices 0 and 1. Report both titles and word counts."
+ ).run()
+ print(composition.config.name, result.state.stop_reason, result.state.final_result)
+ outputs = [a.output for record in result.records for a in record.action_results
+ if a.tool_name == "summarize_note" and a.status == "success"]
+ assert {item["title"] for item in outputs} == {"Session", "Artifact"}, outputs
+
+
+if __name__ == "__main__":
+ main()
```
-## Failures, compatibility and extension
+{/* tutorial-files:end */}
+
+[Composition API](/reference/composition) · [Sandbox](/guides/sandbox-and-artifacts) · [CLI](/reference/cli) · [Next: extensions](/guides/third-party-extensions)
-Unknown/duplicate keys, invalid types, YAML tags and environment interpolation
-fail closed. `failure_policy` currently accepts only `tool: continue` or
-`tool: fail_closed`. Python must register extension names explicitly through
-`extensions`; CLI does not import arbitrary config-selected code.
-`EnvironmentCredentialResolver` is an advanced deployment compatibility adapter.
-`AgentModule.run()` and direct Engine remain useful for advanced programmatic
-control; host tools do not imply isolation.
-[Next: Session](/tutorials/checkpoint-and-fork).
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/real_notes.py)
diff --git a/docs/reference/context.mdx b/docs/reference/context.mdx
new file mode 100644
index 00000000..16604ef7
--- /dev/null
+++ b/docs/reference/context.mdx
@@ -0,0 +1,187 @@
+---
+title: "Context and memory contracts"
+description: "QitOS public API: context"
+---
+
+Register contributors, selectors, budget policies and compactors with explicit extension names. Context selection does not grant tool authority. CompactionReceipt declares the input, output identity and losses; omission is not a lossless summary. Memory input is not a checkpoint store. Missing factories fail at composition. See the executable tutorial for method arguments and an observed lossy compaction.
+
+[完整可运行教程 / Complete tutorial](/guides/memory-and-history) · [API index](/reference/api)
+
+Signatures and fields below are extracted from the pinned source. Signatures are reference material, not standalone programs. Source links bind the same runtime baseline. Any does not imply arbitrary objects are supported; use the behavioral contract above and the linked tutorial.
+
+{/* api-reference:start */}
+
+
+## StaticContextContributor
+
+```python
+from qitos.core.context import StaticContextContributor
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/context.py#L300)
+
+[Usage and executable example](/guides/memory-and-history)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+contributor = StaticContextContributor("notes.project", "project", "Use supplied notes only.")
+```
+
+```text
+Small reusable contributor for project/user/session context.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `contributor_id` | `str` | `required` |
+| `source` | `str` | `required` |
+| `value` | `Any` | `field(repr=False)` |
+| `priority` | `int` | `0` |
+| `requested_placement` | `str` | `'developer'` |
+| `required` | `bool` | `False` |
+| `persistence_horizon` | `str` | `'request'` |
+| `sensitivity` | `str` | `'internal'` |
+
+
+
+## PriorityContextSelectionPolicy
+
+```python
+from qitos.core.context import PriorityContextSelectionPolicy
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/context.py#L118)
+
+[Usage and executable example](/guides/memory-and-history)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+class AuditedSelector(PriorityContextSelectionPolicy):
+ def select(self, contributions, **options):
+ contributions = tuple(contributions)
+ print([item.contribution_id for item in contributions])
+ return super().select(contributions, **options)
+```
+
+```text
+Default deterministic priority/identity selection policy.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `policy_id` | `str` | `'qitos.context.priority/v1'` |
+
+
+### PriorityContextSelectionPolicy.select
+
+```text
+select(contributions: Iterable[ContextContribution], *, budget: ContextBudget, already_used_units: int=0, counter: UnitCounter=default_unit_counter) -> ContextSelection
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `contributions` | `Iterable[ContextContribution]` | `required` |
+| `budget` | `ContextBudget` | `required` |
+| `already_used_units` | `int` | `0` |
+| `counter` | `UnitCounter` | `default_unit_counter` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/context.py#L123)
+
+
+
+## DeclaredContextBudgetPolicy
+
+```python
+from qitos.core.context import DeclaredContextBudgetPolicy
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/context.py#L187)
+
+[Usage and executable example](/guides/memory-and-history)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+policy = DeclaredContextBudgetPolicy(default_max_input_units=4096)
+```
+
+```text
+Use adapter-declared capacity; never infer it from a model name.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `default_max_input_units` | `int` | `120000` |
+| `unit` | `str` | `'characters'` |
+| `protected_recent_exchanges` | `int` | `1` |
+| `policy_id` | `str` | `'qitos.context.declared_budget/v1'` |
+
+
+
+## CompactionReceipt
+
+```python
+from qitos.core.request_view import CompactionReceipt
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/request_view.py#L525)
+
+[Usage and executable example](/guides/memory-and-history)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+# receipt is returned by the compactor in the context tutorial.
+print(receipt.declared_losses, receipt.output_digest)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `receipt_id` | `str` | `required` |
+| `input_exchange_ids` | `tuple[str, ...]` | `required` |
+| `output_digest` | `str` | `required` |
+| `policy_id` | `str` | `required` |
+| `declared_losses` | `tuple[str, ...]` | `()` |
+| `model_reference` | `Optional[str]` | `None` |
+
+
+
+## LifecyclePolicy
+
+```python
+from qitos.engine.runtime import LifecyclePolicy
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/runtime.py#L88)
+
+[Usage and executable example](/guides/memory-and-history)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+class PauseFirstTool(LifecyclePolicy):
+ policy_id = "notes.pause"
+ def should_pause(self, context):
+ return context.step_id == 0
+```
+
+```text
+Replaceable cooperative lifecycle policy; never a worker scheduler.
+```
+
+
+### LifecyclePolicy.should_pause
+
+```text
+should_pause(context: RuntimeSnapshotContext) -> bool
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `context` | `RuntimeSnapshotContext` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/runtime.py#L94)
+
+{/* api-reference:end */}
diff --git a/docs/reference/extensions.mdx b/docs/reference/extensions.mdx
new file mode 100644
index 00000000..fd435d30
--- /dev/null
+++ b/docs/reference/extensions.mdx
@@ -0,0 +1,17 @@
+---
+title: "Third-party extension index"
+description: "Replace mechanisms while preserving their public contracts."
+---
+
+These are replaceable framework mechanisms, not an exhaustive package index or a perpetual cross-version stability promise. Assemble extensions through explicit Python factories rather than arbitrary config-selected imports. The default lesson uses pure tools and a fake provider; client dependencies are listed in Installation.
+
+| Extension | Public entry | Contract to preserve |
+| --- | --- | --- |
+| Provider | `qitos.models.base.Model`, `ModelFactory`; composition `model_override` | Model request/response, codec, continuation, typed failures; provider extra required only for that client |
+| Tool | `qitos.core.tool.BaseTool.execute`, `ToolRegistry` | Schema, permission, effect declarations, ToolResult, cancellation and unknown outcome |
+| Checkpoint store | `qitos.checkpoint.store.CheckpointStore` | Session capabilities, snapshot commit CAS, lineage, fork and durability; SQLite reference implementation |
+| Event sink | `qitos.tracing.sinks.EventSink` | Privacy view, loss, required/optional failure policy, append/close contract |
+| Sandbox | `qitos.core.env.Env` | Capability preflight, bounded tools, real isolation, artifacts, cleanup; publication is separately authorized |
+| Evaluator | `qitos.evaluate.base.TrajectoryEvaluator.evaluate` | EvaluationContext to EvaluationResult, evidence, provenance and declared loss |
+
+[Complete provider/context extension](/guides/third-party-extensions) · [API index](/reference/api)
diff --git a/docs/reference/legacy-api.mdx b/docs/reference/legacy-api.mdx
new file mode 100644
index 00000000..574c7f8d
--- /dev/null
+++ b/docs/reference/legacy-api.mdx
@@ -0,0 +1,1029 @@
+---
+title: "Previous API reference archive"
+description: "Historical reference text; use the current API index."
+---
+
+This preserves the previous page as historical text. Its signatures, defaults and export claims may not match current source; it is not the current API contract. [Current API](/reference/api)
+
+
+
+Every symbol listed here is exported from `qitos` and accessible as:
+
+Signature reference (not executable):
+
+```text
+from qitos import AgentModule, Engine, Decision, ...
+```
+
+---
+
+
+
+
+
+`AgentModule` is the strategy layer of QitOS. You subclass it to define your agent's state shape, system prompt, decision logic, and reduction rules. The `Engine` drives the execution loop (the kernel) and calls each hook in order.
+
+Signature reference (not executable):
+
+```text
+class AgentModule(ABC, Generic[StateT, ObservationT, ActionT])
+```
+
+**Constructor**
+
+Signature reference (not executable):
+
+```text
+def __init__(
+ self,
+ tool_registry: Any = None,
+ llm: Any = None,
+ model_parser: Any = None,
+ memory: Memory | None = None,
+ history: History | None = None,
+ **config: Any,
+)
+```
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `tool_registry` | `ToolRegistry \| None` | Registry of tools the agent can call |
+| `llm` | `Any` | LLM callable used for model decisions |
+| `model_parser` | `Any` | Parser (a component that converts raw model output into a typed Decision) that converts raw model output to a `Decision` |
+| `memory` | `Memory \| None` | Optional memory adapter |
+| `history` | `History \| None` | Optional history adapter |
+| `**config` | `Any` | Extra keyword args stored as `self.config` |
+
+**Hooks**
+
+Override these methods in your subclass. Only `init_state` and `reduce` are required.
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+@abstractmethod
+def init_state(self, task: str, **kwargs: Any) -> StateT
+```
+
+Create and return the initial typed state for a run. Called once by `Engine.run()` before the step loop begins. Use `**kwargs` to accept extra parameters forwarded from `AgentModule.run()`.
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+@abstractmethod
+def reduce(
+ self,
+ state: StateT,
+ observation: ObservationT,
+ decision: Decision[ActionT],
+) -> StateT
+```
+
+Fold the current observation and decision into the next state. Called at the end of every step. Return the updated state.
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def build_system_prompt(self, state: StateT) -> str | None
+```
+
+Return a dynamic system prompt string, or `None` to use no system prompt. Called at the start of each step's decide phase. Default returns `None`.
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def prepare(self, state: StateT) -> str
+```
+
+Convert the current state into a model-ready text string (the user turn). Default returns `str(state)`.
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def decide(
+ self,
+ state: StateT,
+ observation: ObservationT,
+) -> Decision[ActionT] | None
+```
+
+Optional custom decision hook. Return a `Decision` to bypass the Engine's model call, or `None` to let the Engine call the LLM and parse the output. Default returns `None`.
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def should_stop(self, state: StateT) -> bool
+```
+
+Optional additional stop condition checked after each step. Return `True` to terminate the run with `StopReason.AGENT_CONDITION`. Default returns `False`.
+
+
+
+
+
+**`.run()` method**
+
+Convenience method that builds an `Engine`, runs it, and returns the final result.
+
+Signature reference (not executable):
+
+```text
+def run(
+ self,
+ task: str | Task,
+ return_state: bool = False,
+ hooks: List[Any] | None = None,
+ render_hooks: List[Any] | None = None,
+ engine_kwargs: Dict[str, Any] | None = None,
+ workspace: str | None = None,
+ max_steps: int | None = None,
+ env: Any = None,
+ parser: Any = None,
+ search: Any = None,
+ critics: List[Any] | None = None,
+ stop_criteria: List[Any] | None = None,
+ history_policy: Any = None,
+ trace: Any = None,
+ render: Any = None,
+ trace_logdir: str = "./runs",
+ trace_prefix: str | None = None,
+ theme: str = "research",
+ **state_kwargs: Any,
+) -> Any
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `task` | `str \| Task` | required | Task objective or structured `Task` object |
+| `return_state` | `bool` | `False` | When `True`, returns the full `EngineResult`; otherwise returns `state.final_result` |
+| `hooks` | `List[Any] \| None` | `None` | Additional `EngineHook` instances to register |
+| `render_hooks` | `List[Any] \| None` | `None` | Additional render hook instances |
+| `engine_kwargs` | `Dict[str, Any] \| None` | `None` | Extra keyword arguments forwarded to the `Engine` constructor |
+| `workspace` | `str \| None` | `None` | Path to workspace root; auto-constructs a `HostEnv` when set |
+| `max_steps` | `int \| None` | `None` | Override the maximum number of steps |
+| `env` | `Any` | `None` | Explicit `Env` instance; takes precedence over `workspace` |
+| `parser` | `Any` | `None` | Parser to pass to the `Engine` |
+| `search` | `Any` | `None` | `Search` strategy instance |
+| `critics` | `List[Any] \| None` | `None` | List of `Critic` instances |
+| `stop_criteria` | `List[Any] \| None` | `None` | Custom stop criteria list |
+| `history_policy` | `Any` | `None` | `HistoryPolicy` instance |
+| `trace` | `Any` | `None` | `True` to enable default tracing, or a `TraceWriter` instance |
+| `render` | `Any` | `None` | `True` to enable default render hook |
+| `trace_logdir` | `str` | `"./runs"` | Directory where trace files are written |
+| `trace_prefix` | `str \| None` | `None` | Prefix for the auto-generated run ID |
+| `theme` | `str` | `"research"` | Render theme name |
+| `**state_kwargs` | `Any` | | Extra kwargs forwarded to `init_state()` |
+
+**Returns** `state.final_result` by default, or an `EngineResult` when `return_state=True`.
+
+
+
+
+
+`Engine` is the execution kernel (the core AgentModule + Engine execution loop). It owns the phase loop, tool execution, recovery, tracing, and stop-criteria evaluation. You normally obtain an `Engine` through `AgentModule.build_engine()` or `AgentModule.run()`, but you can also construct one directly.
+
+Signature reference (not executable):
+
+```text
+class Engine(Generic[StateT, ObservationT, ActionT])
+```
+
+**Constructor**
+
+Signature reference (not executable):
+
+```text
+def __init__(
+ self,
+ agent: AgentModule[StateT, ObservationT, ActionT],
+ budget: RuntimeBudget | None = None,
+ validation_gate: StateValidationGate | None = None,
+ recovery_handler: RecoveryHandler | None = None,
+ recovery_policy: RecoveryPolicy | None = None,
+ trace_writer: TraceWriter | None = None,
+ parser: Parser[ActionT] | None = None,
+ stop_criteria: List[StopCriteria] | None = None,
+ branch_selector: BranchSelector | None = None,
+ search: Search | None = None,
+ critics: List[Critic] | None = None,
+ env: Env | None = None,
+ history_policy: HistoryPolicy | None = None,
+ hooks: List[EngineHook] | None = None,
+ render_hooks: List[Any] | None = None,
+)
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `agent` | `AgentModule` | required | Agent whose hooks the Engine will call |
+| `budget` | `RuntimeBudget \| None` | `RuntimeBudget(max_steps=10)` | Step, time, and token budgets |
+| `validation_gate` | `StateValidationGate \| None` | default gate | Pre/post phase state validation |
+| `recovery_handler` | `RecoveryHandler \| None` | `None` | Callable invoked on recoverable errors |
+| `recovery_policy` | `RecoveryPolicy \| None` | default policy | Controls retry behaviour on failures |
+| `trace_writer` | `TraceWriter \| None` | `None` | Writes structured trace (a structured log of all run events and steps) artifacts to disk |
+| `parser` | `Parser[ActionT] \| None` | `None` | Parser for raw model output |
+| `stop_criteria` | `List[StopCriteria] \| None` | `[FinalResultCriteria()]` | Ordered list of stop criteria |
+| `branch_selector` | `BranchSelector \| None` | `FirstCandidateSelector()` | Strategy for picking among branch candidates |
+| `search` | `Search \| None` | `None` | Search strategy for `branch` decisions |
+| `critics` | `List[Critic] \| None` | `[]` | Critics (modules that evaluate each step and can trigger retries or stops) evaluated after each step |
+| `env` | `Env \| None` | `None` | Environment for observe/step lifecycle |
+| `history_policy` | `HistoryPolicy \| None` | `HistoryPolicy()` | Controls message history assembly |
+| `hooks` | `List[EngineHook] \| None` | `[]` | Engine lifecycle hooks |
+| `render_hooks` | `List[Any] \| None` | `None` | Render hooks merged into `hooks` |
+
+**Methods**
+
+Signature reference (not executable):
+
+```text
+def run(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
+```
+
+Execute the agent loop for `task`. Resets run state, initialises the env, calls `agent.init_state()`, then iterates the decide→act→reduce→check_stop cycle until a stop condition triggers. Returns an `EngineResult`.
+
+---
+
+Signature reference (not executable):
+
+```text
+def register_hook(self, hook: Any) -> None
+```
+
+Append one hook instance to the active hook list.
+
+---
+
+Signature reference (not executable):
+
+```text
+def unregister_hook(self, hook: Any) -> None
+```
+
+Remove one hook instance from the active hook list (identity comparison).
+
+---
+
+Signature reference (not executable):
+
+```text
+def clear_hooks(self) -> None
+```
+
+Remove all registered hooks.
+
+
+
+
+
+`AsyncEngine` provides non-blocking execution for agent workflows. It wraps the same `Engine` loop but runs blocking calls in a thread pool, making it safe to use inside `asyncio` event loops.
+
+Signature reference (not executable):
+
+```text
+class AsyncEngine(Generic[StateT, ObservationT, ActionT])
+```
+
+**Constructor**
+
+Signature reference (not executable):
+
+```text
+def __init__(
+ self,
+ agent: AgentModule[StateT, ObservationT, ActionT],
+ **engine_kwargs: Any,
+)
+```
+
+All keyword arguments are forwarded to the internal `Engine` constructor (same parameters as `Engine.__init__`).
+
+**Methods**
+
+Signature reference (not executable):
+
+```text
+async def arun(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
+```
+
+Execute the agent loop asynchronously. Returns the same `EngineResult` as `Engine.run()`.
+
+---
+
+Signature reference (not executable):
+
+```text
+async def arun_stream(self, task: str | Task, **kwargs: Any) -> AsyncIterator[EngineEvent]
+```
+
+Execute the agent loop and yield `EngineEvent` objects in real time. The stream begins with `run_start` and ends with `run_end`.
+
+---
+
+Signature reference (not executable):
+
+```text
+def run(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
+```
+
+Synchronous fallback — delegates to the underlying `Engine.run()`.
+
+**Properties**
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `engine` | `Engine` | The underlying sync Engine instance |
+| `agent` | `AgentModule` | Same as `engine.agent` |
+| `event_stream` | `EventStream \| None` | Active event stream during `arun_stream()`, otherwise `None` |
+
+
+
+
+
+`EngineEvent` is the structured event emitted by `AsyncEngine.arun_stream()`.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+class EngineEventType(str, Enum):
+ STEP_START = "step_start"
+ STEP_END = "step_end"
+ PHASE_START = "phase_start"
+ PHASE_END = "phase_end"
+ DECIDE = "decide"
+ ACT = "act"
+ REDUCE = "reduce"
+ CRITIC = "critic"
+ CHECK_STOP = "check_stop"
+ HANDOFF = "handoff"
+ DELEGATE = "delegate"
+ FANOUT = "fanout"
+ ERROR = "error"
+ RUN_START = "run_start"
+ RUN_END = "run_end"
+```
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class EngineEvent:
+ event_type: EngineEventType
+ step_id: int = 0
+ agent_id: Optional[str] = None
+ phase: Optional[RuntimePhase] = None
+ ok: bool = True
+ payload: Dict[str, Any] = field(default_factory=dict)
+ error: Optional[str] = None
+ ts: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
+```
+
+`EventStream` is an async-compatible event queue for consuming engine events.
+
+Signature reference (not executable):
+
+```text
+class EventStream:
+ def emit(self, event: EngineEvent) -> None # thread-safe emit
+ def emit_sync(self, event: EngineEvent) -> None # alias for sync callers
+ def close(self) -> None # signal end of stream
+ async def __aiter__(self) -> AsyncIterator[EngineEvent]
+ def subscribe(self) -> asyncio.Queue # fan-out consumption
+```
+
+
+
+
+
+`EngineResult` is the dataclass returned by `Engine.run()`.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class EngineResult(Generic[StateT]):
+ state: StateT
+ records: List[StepRecord]
+ events: List[RuntimeEvent]
+ step_count: int
+ task_result: Optional[TaskResult] = None
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `state` | `StateT` | Final typed state after the run |
+| `records` | `List[StepRecord]` | Per-step records including decision, actions, and observations |
+| `events` | `List[RuntimeEvent]` | Ordered list of all runtime events emitted during the run |
+| `step_count` | `int` | Number of steps executed |
+| `task_result` | `TaskResult \| None` | Structured task outcome, populated when a `Task` object was passed |
+
+
+
+
+
+`Decision` is the canonical output of the decide phase. Use the factory class methods rather than constructing directly. A Decision captures what the agent wants to do next -- execute actions, produce a final answer, wait, or propose branch candidates.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class Decision(Generic[ActionT]):
+ mode: DecisionMode # "act" | "final" | "wait" | "branch"
+ actions: List[ActionT]
+ final_answer: Optional[str]
+ rationale: Optional[str]
+ meta: Dict[str, Any]
+ candidates: List[Decision[ActionT]]
+```
+
+**Modes**
+
+| Mode | Meaning |
+|------|---------|
+| `"act"` | Execute one or more actions |
+| `"final"` | Produce a final answer and stop |
+| `"wait"` | Skip action execution this step |
+| `"branch"` | Propose multiple candidate decisions for the branch selector |
+
+**Factory methods**
+
+Signature reference (not executable):
+
+```text
+@classmethod
+def act(
+ cls,
+ actions: List[ActionT],
+ rationale: Optional[str] = None,
+ meta: Optional[Dict[str, Any]] = None,
+) -> Decision[ActionT]
+```
+
+Signature reference (not executable):
+
+```text
+@classmethod
+def final(
+ cls,
+ answer: str,
+ rationale: Optional[str] = None,
+ meta: Optional[Dict[str, Any]] = None,
+) -> Decision[ActionT]
+```
+
+Signature reference (not executable):
+
+```text
+@classmethod
+def wait(
+ cls,
+ rationale: Optional[str] = None,
+ meta: Optional[Dict[str, Any]] = None,
+) -> Decision[ActionT]
+```
+
+Signature reference (not executable):
+
+```text
+@classmethod
+def branch(
+ cls,
+ candidates: List[Decision[ActionT]],
+ rationale: Optional[str] = None,
+ meta: Optional[Dict[str, Any]] = None,
+) -> Decision[ActionT]
+```
+
+**`.validate()`** — Raises `ValueError` if the decision is structurally invalid (e.g. `act` with no actions).
+
+
+
+
+
+`Action` is the normalized action (a tool invocation) contract emitted by the policy and consumed by the executor.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class Action:
+ name: str
+ args: Dict[str, Any] = field(default_factory=dict)
+ kind: ActionKind = ActionKind.TOOL
+ action_id: Optional[str] = None
+ timeout_s: Optional[float] = None
+ max_retries: int = 0
+ idempotent: bool = True
+ classification: str = "default"
+ metadata: Dict[str, Any] = field(default_factory=dict)
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `name` | `str` | Tool name to call |
+| `args` | `Dict[str, Any]` | Keyword arguments forwarded to the tool |
+| `kind` | `ActionKind` | Currently only `ActionKind.TOOL` (`"tool"`) |
+| `action_id` | `str \| None` | Optional unique identifier for the action |
+| `timeout_s` | `float \| None` | Per-action timeout override in seconds |
+| `max_retries` | `int` | Number of retries on failure |
+| `idempotent` | `bool` | Whether the action is safe to retry |
+| `classification` | `str` | User-defined label for grouping/filtering |
+| `metadata` | `Dict[str, Any]` | Arbitrary extra metadata |
+
+**`Action.from_dict(payload)`** — Construct from a plain dict.
+
+
+
+
+
+`StateSchema` is the canonical typed state base class. Subclass it to define your agent's state fields.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class StateSchema:
+ schema_version: int = 1
+ task: str = ""
+ current_step: int = 0
+ max_steps: int = 10
+ final_result: Optional[str] = None
+ stop_reason: Optional[str] = None
+ metadata: Dict[str, Any] = field(default_factory=dict)
+ metrics: Dict[str, Any] = field(default_factory=dict)
+```
+
+| Field | Description |
+|-------|-------------|
+| `task` | The task objective string |
+| `current_step` | Step counter incremented by `advance_step()` |
+| `max_steps` | Hard cap on steps; validated on every `advance_step()` call |
+| `final_result` | The agent's final answer string |
+| `stop_reason` | A `StopReason` value string set when the run ends |
+| `metadata` | Free-form dict for agent-specific data |
+| `metrics` | Free-form dict for numeric metrics |
+
+**Key methods**
+
+Signature reference (not executable):
+
+```text
+def set_stop(self, reason: StopReason | str, final_result: Optional[str] = None) -> None
+def advance_step(self) -> None
+def validate(self) -> None
+def to_dict(self) -> Dict[str, Any]
+
+@classmethod
+def from_dict(cls, payload: Dict[str, Any], strict: bool = True) -> StateT
+
+@classmethod
+def migrate_payload(cls, payload: Dict[str, Any], target_version: int) -> Dict[str, Any]
+```
+
+
+
+
+
+Use `Task` when you need to pass structured metadata, resources, and budget constraints alongside the objective string.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class Task:
+ id: str
+ objective: str
+ inputs: Dict[str, Any] = field(default_factory=dict)
+ resources: List[TaskResource] = field(default_factory=list)
+ env_spec: Optional[EnvSpec] = None
+ constraints: Dict[str, Any] = field(default_factory=dict)
+ success_criteria: List[str] = field(default_factory=list)
+ budget: TaskBudget = field(default_factory=TaskBudget)
+ metadata: Dict[str, Any] = field(default_factory=dict)
+```
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class TaskBudget:
+ max_steps: Optional[int] = None
+ max_runtime_seconds: Optional[float] = None
+ max_tokens: Optional[int] = None
+```
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class TaskResource:
+ kind: str # "file" | "dir" | "url" | "artifact"
+ path: Optional[str] = None
+ uri: Optional[str] = None
+ mount_to: Optional[str] = None
+ required: bool = True
+ description: str = ""
+ metadata: Dict[str, Any] = field(default_factory=dict)
+```
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class TaskResult:
+ task_id: str
+ success: bool
+ stop_reason: Optional[str]
+ final_result: Any
+ criteria: List[TaskCriterionResult] = field(default_factory=list)
+ artifacts: List[TaskResourceBinding] = field(default_factory=list)
+ metrics: Dict[str, Any] = field(default_factory=dict)
+ metadata: Dict[str, Any] = field(default_factory=dict)
+```
+
+**`Task` helper methods**
+
+Signature reference (not executable):
+
+```text
+def validate(self) -> None
+def validate_structured(self, workspace: Optional[str] = None) -> List[TaskValidationIssue]
+def resolve_resources(self, workspace: Optional[str] = None) -> List[TaskResourceBinding]
+def to_dict(self) -> Dict[str, Any]
+
+@classmethod
+def from_dict(cls, payload: Dict[str, Any]) -> Task
+```
+
+
+
+
+
+`Env` is the abstract environment interface. Implement it to provide a custom observe/step lifecycle for your agent.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+class Env(ABC):
+ @abstractmethod
+ def reset(self, task: Any = None) -> Any: ...
+
+ @abstractmethod
+ def observe(self) -> Any: ...
+
+ @abstractmethod
+ def step(self, action: Any) -> Any: ...
+
+ @abstractmethod
+ def is_terminal(self) -> bool: ...
+
+ @abstractmethod
+ def close(self) -> None: ...
+```
+
+`EnvSpec` is a dataclass used inside `Task` to declare the environment type and configuration:
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class EnvSpec:
+ type: str # e.g. "host", "docker", "tau_bench"
+ config: Dict[str, Any] = field(default_factory=dict)
+ required_tools: List[str] = field(default_factory=list)
+ capabilities: List[str] = field(default_factory=list)
+ metadata: Dict[str, Any] = field(default_factory=dict)
+```
+
+
+
+
+
+The `tool` decorator marks a callable as a QitOS tool and attaches metadata to it without changing its call semantics.
+
+Signature reference (not executable):
+
+```text
+def tool(
+ name: Optional[str] = None,
+ description: Optional[str] = None,
+ timeout_s: Optional[float] = None,
+ max_retries: int = 0,
+ permissions: Optional[ToolPermission] = None,
+ required_ops: Optional[List[str]] = None,
+)
+```
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `name` | `str \| None` | Override the tool name (defaults to the function's `__name__`) |
+| `description` | `str \| None` | Override the tool description (defaults to the docstring) |
+| `timeout_s` | `float \| None` | Per-call timeout in seconds |
+| `max_retries` | `int` | Number of retries on failure |
+| `permissions` | `ToolPermission \| None` | Permission flags for the tool |
+| `required_ops` | `List[str] \| None` | Runtime ops required from the environment |
+
+**Example**
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+from qitos import tool
+
+@tool(name="search_web", timeout_s=30.0, permissions=ToolPermission(network=True))
+def search_web(query: str) -> str:
+ """Search the web and return results."""
+ ...
+```
+
+
+
+
+
+`ToolRegistry` stores tools and toolsets and is passed to `AgentModule` and `Engine` at construction time.
+
+Signature reference (not executable):
+
+```text
+class ToolRegistry
+```
+
+**Constructor** — `ToolRegistry()` (no parameters)
+
+**Methods**
+
+Signature reference (not executable):
+
+```text
+def register(
+ self,
+ item: Any,
+ name: Optional[str] = None,
+ meta: Optional[ToolMeta] = None,
+) -> ToolRegistry
+```
+Register a single callable or `BaseTool`. Returns `self` for chaining.
+
+---
+
+Signature reference (not executable):
+
+```text
+def register_toolset(
+ self,
+ toolset: Any,
+ namespace: Optional[str] = None,
+) -> ToolRegistry
+```
+Register all tools from a toolset object. Tool names are prefixed with `namespace` (defaults to `toolset.name`).
+
+---
+
+Signature reference (not executable):
+
+```text
+def include(self, obj: Any) -> ToolRegistry
+```
+Scan an object for methods decorated with `@tool` and register them all.
+
+---
+
+Signature reference (not executable):
+
+```text
+def get(self, name: str) -> Optional[BaseTool]
+def list_tools(self) -> List[str]
+def list_toolsets(self) -> List[str]
+def describe_tool(self, name: str) -> Dict[str, Any]
+def call(self, name: str, runtime_context: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Any
+def get_tool_descriptions(self) -> str
+def get_all_specs(self) -> List[Dict[str, Any]]
+def setup(self, context: Optional[Dict[str, Any]] = None) -> None
+def teardown(self, context: Optional[Dict[str, Any]] = None) -> None
+```
+
+**Example**
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+from qitos import ToolRegistry, tool
+
+registry = ToolRegistry()
+
+@tool(name="greet")
+def greet(name: str) -> str:
+ """Say hello."""
+ return f"Hello, {name}!"
+
+registry.register(greet)
+```
+
+
+
+
+
+`Memory` is the abstract interface for long-term memory adapters.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+class Memory(ABC):
+ @abstractmethod
+ def append(self, record: MemoryRecord) -> None: ...
+
+ @abstractmethod
+ def retrieve(
+ self,
+ query: Optional[Dict[str, Any]] = None,
+ state: Any = None,
+ observation: Any = None,
+ ) -> Any: ...
+
+ @abstractmethod
+ def summarize(self, max_items: int = 5) -> str: ...
+
+ @abstractmethod
+ def evict(self) -> int: ...
+
+ @abstractmethod
+ def reset(self, run_id: Optional[str] = None) -> None: ...
+```
+
+`MemoryRecord` is the unit of storage:
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class MemoryRecord:
+ role: str
+ content: Any
+ step_id: int
+ metadata: Dict[str, Any] = field(default_factory=dict)
+```
+
+
+
+
+
+`History` is the abstract interface for model message history adapters.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+class History(ABC):
+ @abstractmethod
+ def append(self, message: HistoryMessage) -> None: ...
+
+ @abstractmethod
+ def retrieve(
+ self,
+ query: Optional[Dict[str, Any]] = None,
+ state: Any = None,
+ observation: Any = None,
+ ) -> Any: ...
+
+ @abstractmethod
+ def summarize(self, max_items: int = 5) -> str: ...
+
+ @abstractmethod
+ def evict(self) -> int: ...
+
+ @abstractmethod
+ def reset(self, run_id: Optional[str] = None) -> None: ...
+```
+
+`HistoryPolicy` controls how the Engine assembles history for model calls:
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class HistoryPolicy:
+ roles: List[str] = field(default_factory=lambda: ["user", "assistant"])
+ max_messages: int = 24
+ step_window: Optional[int] = None
+ max_tokens: Optional[int] = None
+```
+
+| Field | Description |
+|-------|-------------|
+| `roles` | Message roles to include |
+| `max_messages` | Maximum number of messages to include |
+| `step_window` | If set, only include messages from the last N steps |
+| `max_tokens` | If set, trim messages to fit within this token budget |
+
+`HistoryMessage` is the unit of storage:
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class HistoryMessage:
+ role: str
+ content: str
+ step_id: int
+ metadata: Dict[str, Any] = field(default_factory=dict)
+```
+
+
+
+
+
+`StopReason` is a string enum. Its value is written to `state.stop_reason` when a run ends.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+class StopReason(str, Enum):
+ SUCCESS = "success"
+ FINAL = "final"
+ MAX_STEPS = "max_steps"
+ BUDGET_STEPS = "budget_steps"
+ BUDGET_TIME = "budget_time"
+ BUDGET_TOKENS = "budget_tokens"
+ AGENT_CONDITION = "agent_condition"
+ CRITIC_STOP = "critic_stop"
+ STAGNATION = "stagnation"
+ ENV_TERMINAL = "env_terminal"
+ TASK_VALIDATION_FAILED = "task_validation_failed"
+ ENV_CAPABILITY_MISMATCH = "env_capability_mismatch"
+ UNRECOVERABLE_ERROR = "unrecoverable_error"
+ CANCELLED_IMMEDIATE = "cancelled_immediate"
+```
+
+| Value | When set |
+|-------|----------|
+| `success` | Agent completed successfully |
+| `final` | `Decision.final()` was accepted |
+| `max_steps` | `StateSchema.max_steps` reached |
+| `budget_steps` | `RuntimeBudget.max_steps` reached |
+| `budget_time` | `RuntimeBudget.max_runtime_seconds` elapsed |
+| `budget_tokens` | `RuntimeBudget.max_tokens` consumed |
+| `agent_condition` | `AgentModule.should_stop()` returned `True` |
+| `critic_stop` | A `Critic` returned `action="stop"` |
+| `stagnation` | No state change detected for N steps |
+| `env_terminal` | `Env.is_terminal()` returned `True` |
+| `task_validation_failed` | `Task.validate_structured()` produced issues |
+| `env_capability_mismatch` | Environment missing required ops |
+| `unrecoverable_error` | Fatal error with no recovery path |
+| `cancelled_immediate` | The Engine observed an immediate cancellation request; the trace manifest uses terminal status `stopped` |
+
+
+
+
+
+`QitosRuntimeError` is the base class for all structured runtime errors in QitOS.
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+class QitosRuntimeError(Exception):
+ def __init__(self, info: RuntimeErrorInfo): ...
+ info: RuntimeErrorInfo
+```
+
+`RuntimeErrorInfo` carries structured context:
+
+Illustrative fragment (not a standalone program; use the complete example linked on this page).
+
+```python
+@dataclass
+class RuntimeErrorInfo:
+ category: ErrorCategory # model | parse | tool | state | task | env | system
+ message: str
+ phase: str
+ step_id: int
+ recoverable: bool = False
+ details: Dict[str, Any] = field(default_factory=dict)
+```
+
+Typed subclasses: `ModelExecutionError`, `ParseExecutionError`, `ToolExecutionError`, `StateExecutionError`, `SystemExecutionError`.
+
+
+
+
diff --git a/docs/reference/sessions.mdx b/docs/reference/sessions.mdx
new file mode 100644
index 00000000..414fa2f7
--- /dev/null
+++ b/docs/reference/sessions.mdx
@@ -0,0 +1,242 @@
+---
+title: "Sessions and persistence"
+description: "QitOS public API: sessions"
+---
+
+Obtain Session through composition.session/restore/fork or Engine.session; do not call its constructor directly. run(steering=...) executes and returns EngineResult; inspect reads a SessionInspection. pause requests a cooperative boundary and returns a receipt, not proof of immediate termination. Session identities, run identities and snapshot/checkpoint identities are distinct. SQLite supports clean-process recovery; Memory is process-local. Unresolved approvals and CLI live pause/steer remain unsupported.
+
+[完整可运行教程 / Complete tutorial](/tutorials/checkpoint-and-fork) · [API index](/reference/api)
+
+Signatures and fields below are extracted from the pinned source. Signatures are reference material, not standalone programs. Source links bind the same runtime baseline. Any does not imply arbitrary objects are supported; use the behavioral contract above and the linked tutorial.
+
+{/* api-reference:start */}
+
+
+## Session
+
+```python
+from qitos.engine.session_runtime import Session
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L119)
+
+[Usage and executable example](/tutorials/checkpoint-and-fork)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+session = composition.session("Index notes")
+result = session.run()
+inspection = session.inspect()
+print(session.session_id.value, result.state.final_result)
+```
+
+```text
+Scoped client for one durable Session identity.
+
+The facade stores identifiers and cooperative control only. Agent state is
+reconstructed from the canonical snapshot and executed by ``Engine.run``.
+```
+
+```text
+Session(*, engine: 'Engine[Any, Any, Any]', session_id: SessionIdentity, run_id: RunIdentity, agent_id: AgentIdentity, references: Iterable[ResolverReference], created_at: str, state_type: type[StateSchema], work_item_id: WorkItemIdentity, attempt_id: AttemptIdentity, fork_receipt: Optional[SessionForkReceipt]=None) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `engine` | `'Engine[Any, Any, Any]'` | `required` |
+| `session_id` | `SessionIdentity` | `required` |
+| `run_id` | `RunIdentity` | `required` |
+| `agent_id` | `AgentIdentity` | `required` |
+| `references` | `Iterable[ResolverReference]` | `required` |
+| `created_at` | `str` | `required` |
+| `state_type` | `type[StateSchema]` | `required` |
+| `work_item_id` | `WorkItemIdentity` | `required` |
+| `attempt_id` | `AttemptIdentity` | `required` |
+| `fork_receipt` | `Optional[SessionForkReceipt]` | `None` |
+
+
+### Session.run
+
+```text
+run(*, steering: Optional[str]=None) -> 'EngineResult[Any]'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `steering` | `Optional[str]` | `None` |
+
+```text
+Run or resume through the one canonical Engine loop.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1501)
+
+
+### Session.inspect
+
+```text
+inspect() -> SessionInspection
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L219)
+
+
+### Session.pause
+
+```text
+pause() -> PauseReceipt
+```
+
+```text
+Request cooperative pause; durable status is returned at a boundary.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L291)
+
+
+### Session.steer
+
+```text
+steer(text: str) -> SteeringReceipt
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `text` | `str` | `required` |
+
+```text
+Durably submit one canonical steering item to this Session.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L325)
+
+
+### Session.fork
+
+```text
+fork(snapshot: SessionSnapshot | SnapshotIdentity | str | None=None, *, operation_id: Optional[str]=None) -> 'Session'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `snapshot` | `SessionSnapshot | SnapshotIdentity | str | None` | `None` |
+| `operation_id` | `Optional[str]` | `None` |
+
+```text
+Create an isolated durable child from one verified immutable snapshot.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1713)
+
+
+### Session.capabilities
+
+```text
+capabilities() -> frozenset[str]
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L189)
+
+
+
+## SessionInspection
+
+```python
+from qitos.engine.session_runtime import SessionInspection
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L105)
+
+[Usage and executable example](/tutorials/checkpoint-and-fork)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+inspection = session.inspect()
+print(inspection.work_graph)
+```
+
+```text
+Read-only inspection result backed by the current durable head.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `head` | `SessionHead` | `required` |
+| `lifecycle` | `SessionLifecycle` | `required` |
+| `capabilities` | `tuple[str, ...]` | `required` |
+| `snapshot_integrity` | `str` | `required` |
+| `budget` | `Mapping[str, Any]` | `required` |
+| `work_graph` | `Optional[Mapping[str, Any]]` | `required` |
+| `last_request_view` | `Optional[RequestView]` | `required` |
+| `task` | `str` | `required` |
+| `tool_batch` | `Optional[ToolBatchSnapshot]` | `required` |
+
+
+
+## CheckpointStore
+
+```python
+from qitos.checkpoint.store import CheckpointStore
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/checkpoint/store.py#L174)
+
+[Usage and executable example](/tutorials/checkpoint-and-fork)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+# From an open composition configured with SQLite:
+store = composition.runtime.checkpoint_store
+head = store.get_session_head(session.session_id.value)
+print(head)
+```
+
+```text
+Abstract base class for checkpoint persistence.
+
+Borrowed from LangGraph's ``BaseCheckpointSaver`` interface
+(``references/langgraph/libs/checkpoint/langgraph/checkpoint/base/__init__.py``).
+
+Every method has both sync and async variants. Subclasses should
+override the async variants; the sync ones delegate via ``asyncio.run``
+by default.
+```
+
+
+### CheckpointStore.get_session_head
+
+```text
+get_session_head(session_id: str) -> Optional[SessionHeadRecord]
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `session_id` | `str` | `required` |
+
+```text
+Read the current mutable head for one session.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/checkpoint/store.py#L241)
+
+
+### CheckpointStore.commit_session_snapshot
+
+```text
+commit_session_snapshot(request: SessionSnapshotCommit) -> SessionCommitReceipt
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `request` | `SessionSnapshotCommit` | `required` |
+
+```text
+Atomically persist an immutable snapshot and advance its head.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/checkpoint/store.py#L235)
+
+{/* api-reference:end */}
diff --git a/docs/reference/tools.mdx b/docs/reference/tools.mdx
new file mode 100644
index 00000000..2cfd9eb5
--- /dev/null
+++ b/docs/reference/tools.mdx
@@ -0,0 +1,356 @@
+---
+title: "Tools, results and artifacts"
+description: "QitOS public API: tools"
+---
+
+Register tools before running. execute(args, runtime_context) is the class-tool contract; run is compatibility only. Inspect ToolResult status, error_code, output, artifact_refs, outcome_unknown and worker_still_running. Successful text alone is insufficient. Parallel execution requires truthful concurrency declarations. Completion and declaration order differ. Publication is opt-in and platform/file-shape limited; cleanup never publishes. SandboxPublicationTool is an advanced, explicitly registered adapter despite its internal module name, not a default tool.
+
+[完整可运行教程 / Complete tutorial](/concepts/tools-and-registry) · [API index](/reference/api)
+
+Signatures and fields below are extracted from the pinned source. Signatures are reference material, not standalone programs. Source links bind the same runtime baseline. Any does not imply arbitrary objects are supported; use the behavioral contract above and the linked tutorial.
+
+{/* api-reference:start */}
+
+
+## ToolRegistry
+
+```python
+from qitos import ToolRegistry
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool_registry.py#L20)
+
+[Usage and executable example](/concepts/tools-and-registry)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+registry = ToolRegistry()
+registry.register(summarize_note)
+```
+
+```text
+Registry for function tools, bound methods, tool objects, and ToolSets.
+```
+
+```text
+ToolRegistry(*, auto_short_aliases: bool=True) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `auto_short_aliases` | `bool` | `True` |
+
+
+### ToolRegistry.register
+
+```text
+register(item: Any, name: Optional[str]=None, meta: Optional[ToolMeta]=None) -> 'ToolRegistry'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `item` | `Any` | `required` |
+| `name` | `Optional[str]` | `None` |
+| `meta` | `Optional[ToolMeta]` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool_registry.py#L32)
+
+
+
+## function_tool
+
+```python
+from qitos.core.function_tool_decorator import function_tool
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/function_tool_decorator.py#L11)
+
+[Usage and executable example](/concepts/tools-and-registry)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+@function_tool(read_only=True, concurrency_safe=True)
+def title(text: str) -> str:
+ return text.split(":", 1)[0]
+```
+
+```text
+Decorator that creates a :class:`FunctionTool` from a plain function.
+
+Can be used with or without parentheses::
+
+ @function_tool
+ def greet(name: str) -> str: ...
+
+ @function_tool(name="custom", needs_approval=True)
+ def greet(name: str) -> str: ...
+
+Returns a :class:`FunctionTool` instance.
+```
+
+```text
+function_tool(func: Optional[Callable[..., Any]]=None, *, name: Optional[str]=None, description: Optional[str]=None, timeout_s: Optional[float]=None, max_retries: int=0, retry_policy: Optional[RetryPolicy]=None, on_failure: Optional[Callable]=None, read_only: bool=False, concurrency_safe: Optional[bool]=None, needs_approval: bool=False, **extra_meta: Any) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `func` | `Optional[Callable[..., Any]]` | `None` |
+| `name` | `Optional[str]` | `None` |
+| `description` | `Optional[str]` | `None` |
+| `timeout_s` | `Optional[float]` | `None` |
+| `max_retries` | `int` | `0` |
+| `retry_policy` | `Optional[RetryPolicy]` | `None` |
+| `on_failure` | `Optional[Callable]` | `None` |
+| `read_only` | `bool` | `False` |
+| `concurrency_safe` | `Optional[bool]` | `None` |
+| `needs_approval` | `bool` | `False` |
+
+
+
+## BaseTool
+
+```python
+from qitos.core.tool import BaseTool
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool.py#L611)
+
+[Usage and executable example](/concepts/tools-and-registry)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+# Class tools implement execute, not the compatibility run method.
+class EchoTool(BaseTool):
+ name = "echo"
+ description = "Return a trusted input"
+ def execute(self, args, runtime_context=None):
+ return ToolResult(output=args)
+```
+
+```text
+Base abstraction for callable tools.
+```
+
+```text
+BaseTool(spec: ToolSpec) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `spec` | `ToolSpec` | `required` |
+
+
+### BaseTool.execute
+
+```text
+execute(args: Dict[str, Any], runtime_context: Optional[Dict[str, Any]]=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `args` | `Dict[str, Any]` | `required` |
+| `runtime_context` | `Optional[Dict[str, Any]]` | `None` |
+
+```text
+Execute tool with optional runtime context.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool.py#L706)
+
+
+
+## ToolResult
+
+```python
+from qitos.core.tool_result import ToolResult
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool_result.py#L547)
+
+[Usage and executable example](/concepts/tools-and-registry)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+result = ToolResult(output={"title": "Session"})
+print(result.status, result.output, result.outcome_unknown)
+```
+
+```text
+Lossless terminal outcome for one declared action/tool slot.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `status` | `ToolResultStatus` | `'success'` |
+| `output` | `Any` | `None` |
+| `error` | `str \| None` | `None` |
+| `metadata` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `tool_name` | `str \| None` | `None` |
+| `action_id` | `str \| None` | `None` |
+| `model_output` | `Any` | `None` |
+| `error_kind` | `ToolErrorKind \| None` | `None` |
+| `error_code` | `str \| None` | `None` |
+| `recoverable` | `bool` | `False` |
+| `recovery_hint` | `str \| None` | `None` |
+| `next_action` | `Dict[str, Any] \| None` | `None` |
+| `complete` | `bool` | `True` |
+| `truncated` | `bool` | `False` |
+| `omitted` | `Dict[str, int]` | `field(default_factory=dict)` |
+| `attempts` | `int` | `1` |
+| `latency_ms` | `float` | `0.0` |
+| `declared_effects` | `list[Dict[str, Any]]` | `field(default_factory=list)` |
+| `filesystem_changes` | `list[Dict[str, Any]]` | `field(default_factory=list)` |
+| `artifact_refs` | `tuple[ArtifactRef, ...]` | `()` |
+| `normalized_request` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `provenance` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `worker_still_running` | `bool` | `False` |
+| `attempt_id` | `AttemptIdentity \| None` | `None` |
+| `effect_ref` | `str \| None` | `None` |
+| `effect_state` | `EffectState` | `'no_effect_declared'` |
+| `idempotency_ref` | `str \| None` | `None` |
+| `retry_disposition` | `RetryDisposition` | `'not_evaluated'` |
+| `reconciliation_required` | `bool` | `False` |
+| `outcome_unknown` | `bool` | `False` |
+| `late_result` | `bool` | `False` |
+| `owner_generation` | `int \| None` | `None` |
+| `stale_owner` | `bool` | `False` |
+| `batch_closure` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `schema_version` | `str` | `TOOL_RESULT_SCHEMA_VERSION` |
+
+
+
+## ArtifactRef
+
+```python
+from qitos.core.artifact import ArtifactRef
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/artifact.py#L78)
+
+[Usage and executable example](/concepts/tools-and-registry)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+# ref is an ArtifactRef obtained from a tool result.
+print(ref.sha256)
+body = composition.agent.config["artifact_resolver"].resolve(ref).body
+```
+
+```text
+Portable content-addressed pointer; never an artifact body or host path.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `artifact_id` | `str` | `required` |
+| `resolver_key` | `str` | `required` |
+| `sha256` | `str` | `required` |
+| `media_type` | `str` | `required` |
+| `byte_length` | `int` | `required` |
+| `encoding` | `str` | `'binary'` |
+| `sensitivity` | `str` | `'internal'` |
+| `provenance_digest` | `Optional[str]` | `None` |
+| `model_summary` | `Optional[str]` | `None` |
+| `required` | `bool` | `True` |
+| `schema_version` | `str` | `ARTIFACT_REF_SCHEMA_VERSION` |
+
+
+### ArtifactRef.from_dict
+
+```text
+from_dict(value: Any) -> 'ArtifactRef'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `value` | `Any` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/artifact.py#L159)
+
+
+
+## ActionExecutionPolicy
+
+```python
+from qitos.engine.action_executor import ActionExecutionPolicy
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/action.py#L145)
+
+[Usage and executable example](/concepts/tools-and-registry)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+policy = ActionExecutionPolicy(mode="parallel", max_concurrency=2)
+engine = Engine(NotesAgent(), runtime=RuntimeComposition(), action_execution_policy=policy)
+```
+
+```text
+Executor policy for action batches.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `mode` | `str` | `'serial'` |
+| `fail_fast` | `bool` | `False` |
+| `max_concurrency` | `int` | `4` |
+| `parallel_tool_names` | `FrozenSet[str] \| None` | `None` |
+
+
+
+## SandboxPublicationTool
+
+```python
+from qitos.kit.tool.internal.publication import SandboxPublicationTool
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/kit/tool/internal/publication.py#L15)
+
+[Usage and executable example](/concepts/tools-and-registry)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+# Only after sandbox execution, with explicit publication authority:
+publication = SandboxPublicationTool(
+ composition.env, paths=["report.txt"],
+ expected_input_digest=composition.env.input_digest,
+)
+composition.tool_registry.register(publication)
+```
+
+```text
+Opt-in tool restricted to paths and input digest approved by its caller.
+```
+
+```text
+SandboxPublicationTool(env: Any, *, paths: Iterable[str], expected_input_digest: str) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `env` | `Any` | `required` |
+| `paths` | `Iterable[str]` | `required` |
+| `expected_input_digest` | `str` | `required` |
+
+
+### SandboxPublicationTool.execute
+
+```text
+execute(args: Any, runtime_context: Any=None) -> ToolResult
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `args` | `Any` | `required` |
+| `runtime_context` | `Any` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/kit/tool/internal/publication.py#L33)
+
+{/* api-reference:end */}
diff --git a/docs/reference/trajectory.mdx b/docs/reference/trajectory.mdx
new file mode 100644
index 00000000..e30a8393
--- /dev/null
+++ b/docs/reference/trajectory.mdx
@@ -0,0 +1,120 @@
+---
+title: "Trajectory and readers"
+description: "QitOS public API: trajectory"
+---
+
+default_reader selects the canonical journal and supported historical readers. Reading and replay are observation operations, not execution or recovery. Export with REDACTED_PUBLIC explicitly declares loss; reimporting the same count of records is not a raw-data equivalence proof. Reads currently materialize the full journal. The qit/qita command reference documents exact CLI usage and exit behavior.
+
+[完整可运行教程 / Complete tutorial](/guides/observability) · [API index](/reference/api)
+
+Signatures and fields below are extracted from the pinned source. Signatures are reference material, not standalone programs. Source links bind the same runtime baseline. Any does not imply arbitrary objects are supported; use the behavioral contract above and the linked tutorial.
+
+{/* api-reference:start */}
+
+
+## default_reader
+
+```python
+from qitos.qita.reader import default_reader
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/qita/reader.py#L12)
+
+[Usage and executable example](/guides/observability)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+reader = default_reader(root)
+trajectory = reader.read_session(identity, view=PrivacyView.RAW_PRIVATE)
+print(len(trajectory.records))
+```
+
+```text
+Select canonical data with bounded trace compatibility, or explicit rollback.
+```
+
+```text
+default_reader(root: str | Path, *, selector: str='trajectory') -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `root` | `str | Path` | `required` |
+| `selector` | `str` | `'trajectory'` |
+
+
+
+## CanonicalTrajectoryExporter
+
+```python
+from qitos.tracing.exporter import CanonicalTrajectoryExporter
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/tracing/exporter.py#L95)
+
+[Usage and executable example](/guides/observability)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+exporter = CanonicalTrajectoryExporter()
+exported = exporter.export(trajectory, view=PrivacyView.REDACTED_PUBLIC)
+print(exported.loss.is_lossless)
+```
+
+```text
+Canonical JSON exporter; exact for the selected projection.
+```
+
+
+### CanonicalTrajectoryExporter.export
+
+```text
+export(trajectory: Trajectory, *, view: PrivacyView=PrivacyView.REDACTED_PUBLIC) -> ExportArtifact
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `trajectory` | `Trajectory` | `required` |
+| `view` | `PrivacyView` | `PrivacyView.REDACTED_PUBLIC` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/tracing/exporter.py#L111)
+
+
+### CanonicalTrajectoryExporter.reimport
+
+```text
+reimport(artifact: ExportArtifact) -> Trajectory
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `artifact` | `ExportArtifact` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/tracing/exporter.py#L141)
+
+
+
+## PrivacyView
+
+```python
+from qitos.tracing.trajectory import PrivacyView
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/tracing/trajectory.py#L91)
+
+[Usage and executable example](/guides/observability)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+view = PrivacyView.REDACTED_PUBLIC
+print(view.value)
+```
+
+```text
+Named projections over canonical raw data.
+```
+
+{/* api-reference:end */}
diff --git a/docs/reference/work-graph.mdx b/docs/reference/work-graph.mdx
new file mode 100644
index 00000000..4fce7ef8
--- /dev/null
+++ b/docs/reference/work-graph.mdx
@@ -0,0 +1,350 @@
+---
+title: "Multi-agent work"
+description: "QitOS public API: work-graph"
+---
+
+delegate/spawn/fan_out return operation receipts; join references operation IDs. WorkGraph records work ownership and attempts; it is not a worker executor. LocalWorkScheduler resolves descriptors through application-provided callables. handoff changes the owner of the same work item and fences the old source; fork branches independently. Transfer admission is not destination task completion. The tutorial serializes same-head handoff callbacks before destination restore to avoid the documented owner-CAS conflict.
+
+[完整可运行教程 / Complete tutorial](/guides/multi-agent-patterns) · [API index](/reference/api)
+
+Signatures and fields below are extracted from the pinned source. Signatures are reference material, not standalone programs. Source links bind the same runtime baseline. Any does not imply arbitrary objects are supported; use the behavioral contract above and the linked tutorial.
+
+{/* api-reference:start */}
+
+
+## Session
+
+```python
+from qitos.engine.session_runtime import Session
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L119)
+
+[Usage and executable example](/guides/multi-agent-patterns)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+session = composition.session("Index notes")
+result = session.run()
+inspection = session.inspect()
+print(session.session_id.value, result.state.final_result)
+```
+
+```text
+Scoped client for one durable Session identity.
+
+The facade stores identifiers and cooperative control only. Agent state is
+reconstructed from the canonical snapshot and executed by ``Engine.run``.
+```
+
+```text
+Session(*, engine: 'Engine[Any, Any, Any]', session_id: SessionIdentity, run_id: RunIdentity, agent_id: AgentIdentity, references: Iterable[ResolverReference], created_at: str, state_type: type[StateSchema], work_item_id: WorkItemIdentity, attempt_id: AttemptIdentity, fork_receipt: Optional[SessionForkReceipt]=None) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `engine` | `'Engine[Any, Any, Any]'` | `required` |
+| `session_id` | `SessionIdentity` | `required` |
+| `run_id` | `RunIdentity` | `required` |
+| `agent_id` | `AgentIdentity` | `required` |
+| `references` | `Iterable[ResolverReference]` | `required` |
+| `created_at` | `str` | `required` |
+| `state_type` | `type[StateSchema]` | `required` |
+| `work_item_id` | `WorkItemIdentity` | `required` |
+| `attempt_id` | `AttemptIdentity` | `required` |
+| `fork_receipt` | `Optional[SessionForkReceipt]` | `None` |
+
+
+### Session.delegate
+
+```text
+delegate(agent: str, *, task: str, operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `agent` | `str` | `required` |
+| `task` | `str` | `required` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1122)
+
+
+### Session.spawn
+
+```text
+spawn(agent: str, *, task: str, operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `agent` | `str` | `required` |
+| `task` | `str` | `required` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1129)
+
+
+### Session.fan_out
+
+```text
+fan_out(specs: Iterable[Mapping[str, Any]], *, operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `specs` | `Iterable[Mapping[str, Any]]` | `required` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1136)
+
+
+### Session.join
+
+```text
+join(children: Iterable[str], *, policy: str='all', quorum: int | None=None, reducer_ref: str | None=None, reducer_digest: str | None=None, operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `children` | `Iterable[str]` | `required` |
+| `policy` | `str` | `'all'` |
+| `quorum` | `int | None` | `None` |
+| `reducer_ref` | `str | None` | `None` |
+| `reducer_digest` | `str | None` | `None` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1157)
+
+
+### Session.handoff
+
+```text
+handoff(agent: str, *, rationale: str='handoff', operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `agent` | `str` | `required` |
+| `rationale` | `str` | `'handoff'` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1148)
+
+
+
+## WorkGraph
+
+```python
+from qitos.core.work_graph import WorkGraph
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/work_graph.py#L754)
+
+[Usage and executable example](/guides/multi-agent-patterns)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+graph = WorkGraph.from_canonical_dict(session.inspect().work_graph)
+print(len(graph.completions), len(graph.joins))
+```
+
+```text
+Versioned ownership graph and generation-checked record builder.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `graph_id` | `str` | `required` |
+| `work_items` | `Dict[WorkItemIdentity, WorkItem]` | `field(default_factory=dict)` |
+| `attempts` | `list[WorkAttempt]` | `field(default_factory=list)` |
+| `edges` | `list[WorkEdge]` | `field(default_factory=list)` |
+| `transfers` | `list[OwnershipTransfer]` | `field(default_factory=list)` |
+| `delegations` | `list[DelegationRecord]` | `field(default_factory=list)` |
+| `spawns` | `list[SpawnRecord]` | `field(default_factory=list)` |
+| `fan_out_groups` | `list[FanOutGroup]` | `field(default_factory=list)` |
+| `joins` | `list[JoinDependency]` | `field(default_factory=list)` |
+| `cancellations` | `list[CancellationRequest]` | `field(default_factory=list)` |
+| `detachments` | `list[DetachmentRecord]` | `field(default_factory=list)` |
+| `completions` | `list[WorkCompletion]` | `field(default_factory=list)` |
+| `late_results` | `list[LateResult]` | `field(default_factory=list)` |
+| `budget_allocations` | `list[BudgetAllocation]` | `field(default_factory=list)` |
+| `capability_allocations` | `list[CapabilityAllocation]` | `field(default_factory=list)` |
+| `operation_receipts` | `list[WorkOperationReceipt]` | `field(default_factory=list)` |
+| `schema_version` | `str` | `WORK_GRAPH_SCHEMA_VERSION` |
+
+
+### WorkGraph.from_canonical_dict
+
+```text
+from_canonical_dict(payload: Mapping[str, Any]) -> 'WorkGraph'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `payload` | `Mapping[str, Any]` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/work_graph.py#L1263)
+
+
+
+## WorkItem
+
+```python
+from qitos.core.work_graph import WorkItem
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/work_graph.py#L214)
+
+[Usage and executable example](/guides/multi-agent-patterns)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+work = graph.work_items[session.work_item_id]
+print(work.owner)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `work_item_id` | `WorkItemIdentity` | `required` |
+| `session_ref` | `SessionIdentity` | `required` |
+| `task_ref` | `str` | `required` |
+| `lifecycle` | `WorkLifecycle` | `required` |
+| `owner` | `WorkOwner` | `required` |
+| `parent_work_item_id` | `WorkItemIdentity \| None` | `None` |
+| `detached` | `bool` | `False` |
+| `budget_allocation_ref` | `str \| None` | `None` |
+| `capability_allocation_ref` | `str \| None` | `None` |
+| `context_transfer_ref` | `str \| None` | `None` |
+
+
+
+## WorkAttempt
+
+```python
+from qitos.core.work_graph import WorkAttempt
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/work_graph.py#L193)
+
+[Usage and executable example](/guides/multi-agent-patterns)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+from dataclasses import fields
+print([field.name for field in fields(WorkAttempt)])
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `attempt_id` | `AttemptIdentity` | `required` |
+| `work_item_id` | `WorkItemIdentity` | `required` |
+| `owner_generation` | `int` | `required` |
+| `state` | `AttemptState` | `required` |
+| `worker_ref` | `str \| None` | `None` |
+
+
+
+## DurableWorkRuntime
+
+```python
+from qitos.engine.work_runtime import DurableWorkRuntime
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/work_runtime.py#L202)
+
+[Usage and executable example](/guides/multi-agent-patterns)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver(), max_workers=2))
+composition.runtime.work_runtime = runtime
+```
+
+```text
+Idempotent declaration/dispatch protocol over one canonical WorkGraph.
+```
+
+```text
+DurableWorkRuntime(scheduler: WorkScheduler, *, policy: WorkRuntimePolicy | None=None) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `scheduler` | `WorkScheduler` | `required` |
+| `policy` | `WorkRuntimePolicy | None` | `None` |
+
+
+
+## LocalWorkScheduler
+
+```python
+from qitos.engine.work_runtime import LocalWorkScheduler
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/work_runtime.py#L134)
+
+[Usage and executable example](/guides/multi-agent-patterns)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+scheduler = LocalWorkScheduler(Resolver(), max_workers=2)
+# Resolver.resolve(descriptor) returns a bounded callable; see the full lesson.
+```
+
+```text
+Bounded local reference scheduler; futures are never persisted.
+```
+
+```text
+LocalWorkScheduler(resolver: WorkResolver, *, max_workers: int=4, queue_capacity: int=64) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `resolver` | `WorkResolver` | `required` |
+| `max_workers` | `int` | `4` |
+| `queue_capacity` | `int` | `64` |
+
+
+
+## WorkRuntimeError
+
+```python
+from qitos.engine.work_runtime import WorkRuntimeError
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/work_runtime.py#L24)
+
+[Usage and executable example](/guides/multi-agent-patterns)
+
+Usage fragment: continues with objects from the linked complete tutorial; not a standalone program.
+
+```python
+try:
+ source.spawn("notes_agent", task="Attempt after handoff")
+except WorkRuntimeError as error:
+ print(error.code)
+```
+
+```text
+Typed scheduler/admission/idempotency failure.
+```
+
+```text
+WorkRuntimeError(code: str, message: str, *, operation_id: str='') -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `code` | `str` | `required` |
+| `message` | `str` | `required` |
+| `operation_id` | `str` | `''` |
+
+{/* api-reference:end */}
diff --git a/docs/tutorial-contracts.json b/docs/tutorial-contracts.json
index 6881745b..394d293c 100644
--- a/docs/tutorial-contracts.json
+++ b/docs/tutorial-contracts.json
@@ -1,37 +1,363 @@
{
- "runtime_baseline": "717b4cf1b23f2ed252cd03234ffd8605038d9567",
+ "runtime_baseline": "60809b3be388d22ea40ea41b4aaa1f5540c76fda",
+ "historical_g5_baseline": "717b4cf1b23f2ed252cd03234ffd8605038d9567",
"units": [
+ {
+ "page": "quickstart",
+ "example": "examples/tutorials/notes/notes.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "notes.py",
+ "--root",
+ "notes-run"
+ ]
+ ],
+ "expected": [
+ "Indexed 2 notes: Session, Artifact."
+ ],
+ "runtime": "offline",
+ "scaffold": true
+ },
{
"page": "guides/build-your-first-agent",
- "example": "examples/tutorials/custom_agent.py"
+ "example": "examples/tutorials/notes/custom_agent.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/custom_agent.py",
+ "target": "custom_agent.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "custom_agent.py"
+ ]
+ ],
+ "expected": [
+ "sequential: Session, Artifact",
+ "parallel: Session, Artifact"
+ ],
+ "runtime": "offline",
+ "scaffold": false
},
{
"page": "concepts/tools-and-registry",
- "example": "examples/tutorials/custom_agent.py"
+ "example": "examples/tutorials/notes/parallel.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/custom_agent.py",
+ "target": "custom_agent.py"
+ },
+ {
+ "source": "examples/tutorials/notes/parallel.py",
+ "target": "parallel.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "custom_agent.py"
+ ],
+ [
+ "python",
+ "parallel.py"
+ ]
+ ],
+ "expected": [
+ "completion=[1, 0]; declaration=[0, 1]"
+ ],
+ "runtime": "offline",
+ "scaffold": false
},
{
"page": "tutorials/checkpoint-and-fork",
- "example": "examples/tutorials/session_walkthrough.py"
+ "example": "examples/tutorials/notes/lifecycle.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/lifecycle.py",
+ "target": "lifecycle.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "lifecycle.py",
+ "create",
+ "--root",
+ "session-run"
+ ],
+ [
+ "python",
+ "lifecycle.py",
+ "restore",
+ "--root",
+ "session-run"
+ ]
+ ],
+ "expected": [
+ "paused after first note",
+ "fork left parent head unchanged"
+ ],
+ "runtime": "offline",
+ "scaffold": false
},
{
"page": "guides/memory-and-history",
- "example": "examples/tutorials/context_memory.py"
+ "example": "examples/tutorials/notes/context.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/context.py",
+ "target": "context.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "context.py",
+ "--root",
+ "context-run"
+ ]
+ ],
+ "expected": [
+ "compaction loss recorded"
+ ],
+ "runtime": "offline",
+ "scaffold": false
},
{
"page": "guides/sandbox-and-artifacts",
- "example": "examples/tutorials/sandbox_artifacts.py"
+ "example": "examples/tutorials/notes/sandbox.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/sandbox.py",
+ "target": "sandbox.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "sandbox.py",
+ "--root",
+ "sandbox-private"
+ ],
+ [
+ "python",
+ "sandbox.py",
+ "--root",
+ "sandbox-published",
+ "--publish"
+ ]
+ ],
+ "expected": [
+ "\"published\": false",
+ "\"published\": true"
+ ],
+ "runtime": "docker",
+ "scaffold": false
},
{
"page": "guides/multi-agent-patterns",
- "example": "examples/tutorials/work_graph.py"
+ "example": "examples/tutorials/notes/handoff.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/multi_agent.py",
+ "target": "multi_agent.py"
+ },
+ {
+ "source": "examples/tutorials/notes/handoff.py",
+ "target": "handoff.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "multi_agent.py",
+ "--root",
+ "work-run"
+ ],
+ [
+ "python",
+ "handoff.py",
+ "--root",
+ "handoff-run"
+ ]
+ ],
+ "expected": [
+ "durable children=4; join=closed",
+ "handoff destination ran"
+ ],
+ "runtime": "offline",
+ "scaffold": false
},
{
"page": "guides/observability",
- "example": "examples/tutorials/session_walkthrough.py"
+ "example": "examples/tutorials/notes/inspect_run.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/inspect_run.py",
+ "target": "inspect_run.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "notes.py",
+ "--root",
+ "notes-run"
+ ],
+ [
+ "python",
+ "inspect_run.py",
+ "--root",
+ "notes-run"
+ ]
+ ],
+ "expected": [
+ "\"records\":"
+ ],
+ "runtime": "offline",
+ "scaffold": false
},
{
"page": "guides/third-party-extensions",
- "example": "examples/tutorials/context_memory.py"
+ "example": "examples/tutorials/notes/provider_extension.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/provider_extension.py",
+ "target": "provider_extension.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "provider_extension.py",
+ "--root",
+ "extension-run"
+ ]
+ ],
+ "expected": [
+ "provider replaced; context observed; requests=3"
+ ],
+ "runtime": "offline",
+ "scaffold": false
+ },
+ {
+ "page": "reference/configuration",
+ "example": "examples/tutorials/notes/real_notes.py",
+ "source_dir": "examples/tutorials/notes",
+ "files": [
+ {
+ "source": "examples/tutorials/notes/agent.yaml",
+ "target": "agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/notes.py",
+ "target": "notes.py"
+ },
+ {
+ "source": "examples/tutorials/notes/real_agent.yaml",
+ "target": "real_agent.yaml"
+ },
+ {
+ "source": "examples/tutorials/notes/real_notes.py",
+ "target": "real_notes.py"
+ }
+ ],
+ "commands": [
+ [
+ "python",
+ "real_notes.py"
+ ]
+ ],
+ "expected": [
+ "configuration valid; no credentials read; no model request"
+ ],
+ "runtime": "offline",
+ "scaffold": false
}
]
}
diff --git a/docs/tutorials/checkpoint-and-fork.mdx b/docs/tutorials/checkpoint-and-fork.mdx
index 3920e177..ad0fafbd 100644
--- a/docs/tutorials/checkpoint-and-fork.mdx
+++ b/docs/tutorials/checkpoint-and-fork.mdx
@@ -1,51 +1,312 @@
---
-title: "Session lifecycle"
-description: "QitOS G5 · Session lifecycle"
+title: "Pause, restore and fork"
+description: "Learn QitOS with complete, executable notes-project code."
---
-## Learning goal
+## Goal and prerequisites
-Pause after a real pure-function tool, exit the process, fork the immutable paused head, restore and steer. Both parent and child execute through Session.
+The first process pauses at a lifecycle boundary after indexing the Session note, writes the Session identity and matching configuration, and exits. The second process loads SQLite, forks the paused snapshot, runs the child, then restores and runs the parent.
-## Prerequisites
+The fake provider cursor is explicitly restarted at 1 because this controlled fixture always pauses after response 0. This is not provider continuation persistence. The Session state, ownership and journal are restored through QitOS. A separate composition is used for parent execution so it cannot accidentally reuse the child's advanced fake cursor.
-Complete the [Quickstart](/quickstart) installation and lessons copy; base package, Python 3.12.7 tested, no real model requests.
+Compare the parent head before and after child execution, then inspect a steering record in the parent's Trajectory. The parent must execute the remaining Artifact tool, not merely receive a scripted final answer.
-## Complete runnable example
+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.
-[`examples/tutorials/session_walkthrough.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/session_walkthrough.py)
-
-The complete directory is the execution source. Sibling session_walkthrough.py is a public teaching dependency, not a repository test helper.
+## Prepare the project
```bash
-python lessons/session_walkthrough.py create --root ./session-run
-python lessons/session_walkthrough.py restore --root ./session-run
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
```
-## Expected output and assertions
+Save the complete files below in this directory. No repository clone, editable install, or copied tests are needed.
+
+### First process: pause
-`tool_output=42; lifecycle=paused; final_result=arithmetic complete; parent head unchanged by fork`
+{/* tutorial-snippet:lifecycle.py:create */}
+```python
+def create(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root, pause=True) as composition:
+ session = composition.session("Index the two notes")
+ result = session.run()
+ assert session.lifecycle.value == "paused"
+ assert result.records[0].action_results[0].output["title"] == "Session"
+ document = composition.config.to_dict()
+ document["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(document), encoding="utf-8")
+ (root / "control.json").write_text(json.dumps({"session_id": session.session_id.value}), encoding="utf-8")
+ print("paused after first note; exit this process before restore")
+```
+{/* tutorial-snippet:end */}
-Assertions in the file verify the result; generated IDs vary. Commands exit zero; stop the board with Ctrl-C.
+### Second process: restore
-## CLI inspection
+{/* tutorial-snippet:lifecycle.py:restore */}
+```python
+def restore(root):
+ identity = json.loads((root / "control.json").read_text())["session_id"]
+ # This fixture always pauses after its first response. Only the fake cursor
+ # is supplied here; QitOS restores the real Session state from SQLite.
+ with compose(root, start=1, pause=True) as composition:
+ before = composition.runtime.checkpoint_store.get_session_head(identity)
+ child = composition.fork(identity)
+ child.run(steering="Finish an independent index.")
+ assert child.session_id.value != identity
+ assert composition.runtime.checkpoint_store.get_session_head(identity) == before
+ with compose(root, start=1, pause=True) as composition:
+ session = composition.restore(identity)
+ result = session.run(steering="Finish the index concisely.")
+ assert any(a.tool_name == "summarize_note" and a.output["title"] == "Artifact"
+ for record in result.records for a in record.action_results)
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ trajectory = default_reader(root).read_session(identity, view=PrivacyView.RAW_PRIVATE)
+ assert any(record.kind.value == "steering" for record in trajectory.records)
+ (root / "control.json").write_text(json.dumps({"session_id": identity, "run_id": result.run_id}), encoding="utf-8")
+ print("restored; steering recorded; fork left parent head unchanged")
+```
+{/* tutorial-snippet:end */}
-The script also writes loadable agent.json. Inspect only reads SQLite; it resolves no credentials and creates no Docker resources.
+## Run and verify
+
+```bash
+python lifecycle.py create --root session-run
+python lifecycle.py restore --root session-run
+```
+
+Expected output fragments (generated IDs vary). Each run verifies tool results or persistence assertions and exits 0 on success.
+
+```text
+paused after first note
+fork left parent head unchanged
+```
+
+### Inspect the saved Session
```bash
session_id=$(python -c 'import json; print(json.load(open("session-run/control.json"))["session_id"])')
-qit session inspect --config ./session-run/agent.json --session-id "$session_id"
+qit session inspect --config session-run/agent.json --session-id "$session_id"
qita inspect session "$session_id" --logdir ./session-run
```
-## Support boundaries
+## Behavior and support boundaries
+
+Fork a paused persisted head before restore claims ownership. Ephemeral and process-local Memory stores do not promise cross-process restore. CLI live pause/steer and unresolved-approval restore are unsupported. Steering changes instructions, not permissions.
+
+## Exercise and answer
+
+Change the child steering text and verify the parent head assertion still passes; then inspect the child and parent as distinct Session identities.
+
+## 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.
+
+{/* tutorial-files:start */}
+
+## Complete files: save in the project root
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
-SQLite is durable; Memory is process-local even in Session mode. Ephemeral execution promises no restore. Fork before claiming source ownership with restore; restoring/running heads are not arbitrary fork points. CLI live pause/steer and restore with unresolved approval are currently unsupported. Steering never grants tool permission.
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="lifecycle.py"
+"""Two processes, a durable checkpoint, steering and independent fork."""
+import argparse
+import json
+from pathlib import Path
+
+from notes import compose
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+
+# docs:start create
+def create(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root, pause=True) as composition:
+ session = composition.session("Index the two notes")
+ result = session.run()
+ assert session.lifecycle.value == "paused"
+ assert result.records[0].action_results[0].output["title"] == "Session"
+ document = composition.config.to_dict()
+ document["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(document), encoding="utf-8")
+ (root / "control.json").write_text(json.dumps({"session_id": session.session_id.value}), encoding="utf-8")
+ print("paused after first note; exit this process before restore")
+# docs:end create
+
+
+# docs:start restore
+def restore(root):
+ identity = json.loads((root / "control.json").read_text())["session_id"]
+ # This fixture always pauses after its first response. Only the fake cursor
+ # is supplied here; QitOS restores the real Session state from SQLite.
+ with compose(root, start=1, pause=True) as composition:
+ before = composition.runtime.checkpoint_store.get_session_head(identity)
+ child = composition.fork(identity)
+ child.run(steering="Finish an independent index.")
+ assert child.session_id.value != identity
+ assert composition.runtime.checkpoint_store.get_session_head(identity) == before
+ with compose(root, start=1, pause=True) as composition:
+ session = composition.restore(identity)
+ result = session.run(steering="Finish the index concisely.")
+ assert any(a.tool_name == "summarize_note" and a.output["title"] == "Artifact"
+ for record in result.records for a in record.action_results)
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ trajectory = default_reader(root).read_session(identity, view=PrivacyView.RAW_PRIVATE)
+ assert any(record.kind.value == "steering" for record in trajectory.records)
+ (root / "control.json").write_text(json.dumps({"session_id": identity, "run_id": result.run_id}), encoding="utf-8")
+ print("restored; steering recorded; fork left parent head unchanged")
+# docs:end restore
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("phase", choices=("create", "restore"))
+ parser.add_argument("--root", type=Path, required=True)
+ args = parser.parse_args()
+ {"create": create, "restore": restore}[args.phase](args.root.resolve())
+```
-## Common errors
+{/* tutorial-files:end */}
-Choose a new root if the directory already exists. For missing imports verify the installed source. Inspect Trajectory and the migration page for config mismatch, missing extensions or typed failures; never silently retry unknown external effects.
+## Next step and API
-## Next step
+[API Reference](/reference/api) · [Configuration](/reference/configuration) · [Learning path](/tutorials/index) · [Next](/guides/memory-and-history)
-[Learning path](/tutorials/index) · [Migration and troubleshooting](/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/lifecycle.py) (optional; all required code is already on this page).
diff --git a/docs/tutorials/index.mdx b/docs/tutorials/index.mdx
index 8761c869..fd5aa4c8 100644
--- a/docs/tutorials/index.mdx
+++ b/docs/tutorials/index.mdx
@@ -1,15 +1,18 @@
---
-title: "Learning path"
-description: "QitOS G5 · Learning path"
+title: "Learn by building a notes Agent"
+description: "One project, independently runnable chapters."
---
-Complete these eight units in order. Each binds a complete downloadable teaching file with assertions. Run the fake path first, then configure a real model. Older strategy tutorials are advanced extensions.
+Start with two synthetic notes and learn tools, state, persistence, context, multiple Agents and report publication. Learn mechanisms with an explicit fake provider, then switch to a real model through Configuration. Every chapter includes every file and can run independently.
-1. [Custom Agent](/guides/build-your-first-agent)
-2. [Tools and parallel calls](/concepts/tools-and-registry)
-3. [Session lifecycle](/tutorials/checkpoint-and-fork)
-4. [Context and memory](/guides/memory-and-history)
-5. [Sandbox and artifacts](/guides/sandbox-and-artifacts)
-6. [Multi-agent work](/guides/multi-agent-patterns)
-7. [Trajectory and qita](/guides/observability)
-8. [Third-party extensions](/guides/third-party-extensions)
+1. [Run your notes Agent](/quickstart)
+2. [Write your own AgentModule](/guides/build-your-first-agent)
+3. [Tools and parallel calls](/concepts/tools-and-registry)
+4. [Pause, restore and fork](/tutorials/checkpoint-and-fork)
+5. [Context and memory](/guides/memory-and-history)
+6. [Sandbox, artifacts and publication](/guides/sandbox-and-artifacts)
+7. [Delegate, join and handoff](/guides/multi-agent-patterns)
+8. [Inspect and export with qita](/guides/observability)
+9. [Replace a provider and add context](/guides/third-party-extensions)
+
+[API Reference](/reference/api) · [Real provider configuration](/reference/configuration)
diff --git a/docs/zh/concepts/tools-and-registry.mdx b/docs/zh/concepts/tools-and-registry.mdx
index c5fca8f0..9ed936fa 100644
--- a/docs/zh/concepts/tools-and-registry.mdx
+++ b/docs/zh/concepts/tools-and-registry.mdx
@@ -1,40 +1,325 @@
---
title: "工具与并行调用"
-description: "QitOS G5 · 工具与并行调用"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 学习目标
+## 目标与前置条件
-注册受信任函数,构造 batch,设置 max_concurrency=2 并行执行;阅读 decide/reduce 查看声明与结果在哪里汇合。
+函数经过装饰并注册到 ToolRegistry 后才能被 Agent 调用。函数名和参数类型构成调用接口;`read_only`、`concurrency_safe` 是工具作者对行为的声明,不会自动授予权限。
-## 前置条件
+先运行顺序和并行资料示例。再运行 `parallel.py`:第一个声明的工具等待第二个工具发出的内存事件,因此完成顺序固定反转,而 reducer 收到的仍是声明顺序。事件与列表是公开可见、仅用于本次执行的教学观测变量。
-完成[Quickstart](/zh/quickstart)的安装与 lessons 复制;基础包,Python 3.12.7 实测,无真实请求。
+五秒等待是 fixture 的截止时间,不是性能基准。只给一个 worker 无法完成此例。工具错误、超时、不完整输出和未知外部副作用必须检查 ToolResult,不能当作成功文本。
-## 完整可运行示例
+需要 Python 基础;声明支持 Python ≥3.10,本轮本地验证使用 Python 3.12.7。每章可独立运行;继续已有项目时可复用环境和同名文件。以下命令为 macOS/Linux shell。
-[`examples/tutorials/custom_agent.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/custom_agent.py)
+## 准备项目
-完整目录是唯一执行来源;同目录的 session_walkthrough.py 是公开教学依赖,不依赖仓库 tests。
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
+
+### 资料与工具
+
+{/* tutorial-snippet:notes.py:fixture */}
+```python
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+```
+{/* tutorial-snippet:end */}
+
+## 运行并验证
```bash
-python lessons/custom_agent.py
+python custom_agent.py
+python parallel.py
```
-## 预期输出与断言
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
+
+```text
+completion=[1, 0]; declaration=[0, 1]
+```
+
+## 行为与支持边界
+
+timeout 不等于硬取消。遇到 `worker_still_running` 或 `outcome_unknown` 应先核查实际结果,不自动重放未知副作用。
+
+## 练习与参考答案
+
+把逆序 fixture 的 `max_concurrency` 改为 1 并解释失败;恢复为 2 后通过,不通过延长超时隐藏依赖。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
-`squares complete; completed=2; process_local=true`
-文件内的 assert 验证结果;ID 每次不同。除 board 由 Ctrl-C 关闭外,命令应退出 0。
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
-## 支持边界
-声明顺序不等于完成顺序。canonical completion 保存实际顺序,不能依赖线程调度推断;只有 concurrency_safe 工具可并行。timeout 不证明硬取消,outcome_unknown 需要核对,不能自动重放。
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="custom_agent.py"
+"""A custom notes AgentModule through the public Engine/Session path."""
+from dataclasses import dataclass, field
+
+from qitos import Action, AgentModule, Decision, Engine, StateSchema, ToolRegistry
+from qitos.engine.action_executor import ActionExecutionPolicy
+from qitos.engine.runtime import RuntimeComposition
+from notes import summarize_note
+
+
+# docs:start agent
+@dataclass
+class NotesState(StateSchema):
+ titles: list[str] = field(default_factory=list)
+
+
+class NotesAgent(AgentModule):
+ def __init__(self):
+ registry = ToolRegistry()
+ registry.register(summarize_note)
+ super().__init__(tool_registry=registry)
+
+ def init_state(self, task, **kwargs):
+ return NotesState(task=task, max_steps=3)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([
+ Action(name="summarize_note", args={"index": index}) for index in (0, 1)
+ ])
+
+ def reduce(self, state, observation, decision):
+ state.titles.extend(item["output"]["title"] for item in observation.get("action_results", []))
+ return state
+# docs:end agent
+
+
+# docs:start run
+def main():
+ for mode in ("sequential", "parallel"):
+ engine = Engine(NotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode=mode, max_concurrency=2))
+ result = engine.session("Index the two notes").run()
+ assert result.state.titles == ["Session", "Artifact"]
+ assert result.state.final_result == "Session, Artifact"
+ print(f"{mode}: Session, Artifact; process_local=true")
+# docs:end run
+
+
+if __name__ == "__main__":
+ main()
+```
+
+```python title="parallel.py"
+"""Force reverse completion while retaining declaration order in reduce."""
+from threading import Event
+
+from qitos import Action, Decision, Engine, ToolRegistry
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.action_executor import ActionExecutionPolicy
+from qitos.engine.runtime import RuntimeComposition
+from custom_agent import NotesAgent
+from notes import NOTES
+
+# Explicit test instrumentation for one local invocation, not persistent state.
+second_finished = Event()
+completion_order = []
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def analyze_note(index: int) -> dict:
+ """An in-memory fixture that controls completion order without file/network I/O."""
+ if index == 0:
+ if not second_finished.wait(5):
+ raise RuntimeError("This fixture requires two parallel workers")
+ completion_order.append(index)
+ if index == 1:
+ second_finished.set()
+ return {"title": NOTES[index].split(":", 1)[0]}
+
+
+class ParallelNotesAgent(NotesAgent):
+ def __init__(self):
+ super().__init__()
+ self.tool_registry = ToolRegistry()
+ self.tool_registry.register(analyze_note)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([Action(name="analyze_note", args={"index": i}) for i in (0, 1)])
+
+
+def run():
+ second_finished.clear()
+ completion_order.clear()
+ engine = Engine(ParallelNotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode="parallel", max_concurrency=2))
+ result = engine.session("Compare completion with declaration order").run()
+ assert completion_order == [1, 0]
+ assert result.state.titles == ["Session", "Artifact"]
+ print("completion=[1, 0]; declaration=[0, 1]; titles=Session, Artifact")
+
+
+if __name__ == "__main__":
+ run()
+```
-## 常见错误
+{/* tutorial-files:end */}
-目录已存在时选一个新 root;导入失败核对安装来源。配置不匹配、缺失扩展或 typed failure 应检查 Trajectory 和迁移页,不能静默重试 unknown 外部效果。
+## 下一步与 API
-## 下一步
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/tutorials/checkpoint-and-fork)
-[学习目录](/zh/tutorials/index) · [迁移与排障](/zh/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/parallel.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/guides/build-your-first-agent.mdx b/docs/zh/guides/build-your-first-agent.mdx
index 34c3c046..bef3a3e4 100644
--- a/docs/zh/guides/build-your-first-agent.mdx
+++ b/docs/zh/guides/build-your-first-agent.mdx
@@ -1,40 +1,298 @@
---
-title: "自定义 Agent"
-description: "QitOS G5 · 自定义 Agent"
+title: "编写自己的 AgentModule"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 学习目标
+## 目标与前置条件
-编写 StateSchema、AgentModule.decide/reduce 和停止决策。示例通过两个 action 计算 3 和 4 的平方。
+自定义 Agent 把决策策略写成可读代码:`NotesState.titles` 起初为空,`decide` 声明两个动作,`reduce` 把观察合入状态,下一次 `decide` 返回最终决定。本章不需要模型。
-## 前置条件
+仍使用 Quickstart 的资料,但这次替换决策逻辑。AgentComposition 构造的是配置式 Agent,没有任意 `agent_override` 参数。使用公共 Engine 组合自己的 AgentModule 与 RuntimeComposition,再创建 Session。
-完成[Quickstart](/zh/quickstart)的安装与 lessons 复制;基础包,Python 3.12.7 实测,无真实请求。
+顺序与并行两种模式必须得到相同的声明顺序标题列表。注意 `reduce` 收到的动作观察是字典,而 `EngineResult.records` 中是 typed ToolResult;两种表示不能混用。
-## 完整可运行示例
+需要 Python 基础;声明支持 Python ≥3.10,本轮本地验证使用 Python 3.12.7。每章可独立运行;继续已有项目时可复用环境和同名文件。以下命令为 macOS/Linux shell。
-[`examples/tutorials/custom_agent.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/custom_agent.py)
+## 准备项目
-完整目录是唯一执行来源;同目录的 session_walkthrough.py 是公开教学依赖,不依赖仓库 tests。
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
+
+### 状态与决策
+
+{/* tutorial-snippet:custom_agent.py:agent */}
+```python
+@dataclass
+class NotesState(StateSchema):
+ titles: list[str] = field(default_factory=list)
+
+
+class NotesAgent(AgentModule):
+ def __init__(self):
+ registry = ToolRegistry()
+ registry.register(summarize_note)
+ super().__init__(tool_registry=registry)
+
+ def init_state(self, task, **kwargs):
+ return NotesState(task=task, max_steps=3)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([
+ Action(name="summarize_note", args={"index": index}) for index in (0, 1)
+ ])
+
+ def reduce(self, state, observation, decision):
+ state.titles.extend(item["output"]["title"] for item in observation.get("action_results", []))
+ return state
+```
+{/* tutorial-snippet:end */}
+
+### 运行与断言
+
+{/* tutorial-snippet:custom_agent.py:run */}
+```python
+def main():
+ for mode in ("sequential", "parallel"):
+ engine = Engine(NotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode=mode, max_concurrency=2))
+ result = engine.session("Index the two notes").run()
+ assert result.state.titles == ["Session", "Artifact"]
+ assert result.state.final_result == "Session, Artifact"
+ print(f"{mode}: Session, Artifact; process_local=true")
+```
+{/* tutorial-snippet:end */}
+
+## 运行并验证
```bash
-python lessons/custom_agent.py
+python custom_agent.py
+```
+
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
+
+```text
+sequential: Session, Artifact
+parallel: Session, Artifact
+```
+
+## 行为与支持边界
+
+默认 RuntimeComposition 使用进程内 Memory checkpoint store。有 Session ID 不意味着数据已经落盘;跨进程恢复请使用生命周期章节的 SQLite 组合。
+
+## 练习与参考答案
+
+只处理第一条:把动作索引改成 `(0,)`,两种模式都应输出 `Session`。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
```
-## 预期输出与断言
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="custom_agent.py"
+"""A custom notes AgentModule through the public Engine/Session path."""
+from dataclasses import dataclass, field
+
+from qitos import Action, AgentModule, Decision, Engine, StateSchema, ToolRegistry
+from qitos.engine.action_executor import ActionExecutionPolicy
+from qitos.engine.runtime import RuntimeComposition
+from notes import summarize_note
+
-`squares complete; completed=2; process_local=true`
+# docs:start agent
+@dataclass
+class NotesState(StateSchema):
+ titles: list[str] = field(default_factory=list)
-文件内的 assert 验证结果;ID 每次不同。除 board 由 Ctrl-C 关闭外,命令应退出 0。
-## 支持边界
+class NotesAgent(AgentModule):
+ def __init__(self):
+ registry = ToolRegistry()
+ registry.register(summarize_note)
+ super().__init__(tool_registry=registry)
-这是高级 Python 路径:Engine + RuntimeComposition + process-local Memory store。AgentComposition 创建 ConfiguredAgent,不提供任意 agent_override 参数;自定义 AgentModule 使用这里的公共 Engine 路径,跨进程恢复需另配置持久 store/resolver。
+ def init_state(self, task, **kwargs):
+ return NotesState(task=task, max_steps=3)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([
+ Action(name="summarize_note", args={"index": index}) for index in (0, 1)
+ ])
+
+ def reduce(self, state, observation, decision):
+ state.titles.extend(item["output"]["title"] for item in observation.get("action_results", []))
+ return state
+# docs:end agent
+
+
+# docs:start run
+def main():
+ for mode in ("sequential", "parallel"):
+ engine = Engine(NotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode=mode, max_concurrency=2))
+ result = engine.session("Index the two notes").run()
+ assert result.state.titles == ["Session", "Artifact"]
+ assert result.state.final_result == "Session, Artifact"
+ print(f"{mode}: Session, Artifact; process_local=true")
+# docs:end run
+
+
+if __name__ == "__main__":
+ main()
+```
-## 常见错误
+{/* tutorial-files:end */}
-目录已存在时选一个新 root;导入失败核对安装来源。配置不匹配、缺失扩展或 typed failure 应检查 Trajectory 和迁移页,不能静默重试 unknown 外部效果。
+## 下一步与 API
-## 下一步
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/concepts/tools-and-registry)
-[学习目录](/zh/tutorials/index) · [迁移与排障](/zh/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/custom_agent.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/guides/memory-and-history.mdx b/docs/zh/guides/memory-and-history.mdx
index 1cd5b21e..24d168b8 100644
--- a/docs/zh/guides/memory-and-history.mdx
+++ b/docs/zh/guides/memory-and-history.mdx
@@ -1,40 +1,271 @@
---
title: "Context 与 memory"
-description: "QitOS G5 · Context 与 memory"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 学习目标
+## 目标与前置条件
-通过显式 extension factory 注册 contributor、memory source、selector、budget policy 和 compactor;验证选择结果与持久 Trajectory 的 compaction loss。
+资料 Agent 需要项目指令和记忆事实。先在 `extensions` 注册 contributor 工厂,再通过 `context`、`memory` 引用名称。selector 记录实际参与选择的贡献;仅在字典里写入内容并不证明模型看到了它。
-## 前置条件
+自定义 compactor 有意省略已结束的 exchange,不生成摘要,并通过 CompactionReceipt 声明损失。程序检查 journal 中存在非无损的 compaction 记录。本例演示如何报告有损转换,不是生产摘要策略推荐。
-完成[Quickstart](/zh/quickstart)的安装与 lessons 复制;基础包,Python 3.12.7 实测,无真实请求。
+依次阅读 `AuditedSelector.select`、`OmitClosedExchange.compact` 和 `run` 的配置。fixture 把最近 exchange 保护数设为零,仅为了触发明确声明的省略,并未关闭 codec loss 检查。
-## 完整可运行示例
+需要 Python 基础;声明支持 Python ≥3.10,本轮本地验证使用 Python 3.12.7。每章可独立运行;继续已有项目时可复用环境和同名文件。以下命令为 macOS/Linux shell。
-[`examples/tutorials/context_memory.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/context_memory.py)
+## 准备项目
-完整目录是唯一执行来源;同目录的 session_walkthrough.py 是公开教学依赖,不依赖仓库 tests。
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
+
+## 运行并验证
```bash
-python lessons/context_memory.py --root ./context-run
+python context.py --root context-run
+```
+
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
+
+```text
+compaction loss recorded
+```
+
+## 行为与支持边界
+
+memory contributor 提供上下文,checkpoint store 保存执行状态,两者不能混为一谈。即使记录数相同,public redacted export 也不是 raw 数据备份。
+
+## 练习与参考答案
+
+修改记忆文本并保持 contribution ID,确认 selector 仍观察到它;检查真实 compaction loss,不把它断言为无损。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
```
-## 预期输出与断言
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="context.py"
+"""Explicit contributor, memory, selector and compactor factories; no network."""
+import argparse
+from dataclasses import replace
+import hashlib
+from pathlib import Path
+
+from qitos.config import build_agent_composition
+from qitos.core.context import (
+ DeclaredContextBudgetPolicy, PriorityContextSelectionPolicy, StaticContextContributor,
+)
+from qitos.core.request_view import CompactionReceipt
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+from notes import FakeProvider, summarize_note, configuration
+
+
+class AuditedSelector(PriorityContextSelectionPolicy):
+ def __init__(self):
+ self.seen = set()
-`context selected; memory selected; compaction loss recorded`
+ def select(self, contributions, **options):
+ contributions = tuple(contributions)
+ self.seen.update(item.contribution_id for item in contributions)
+ return super().select(contributions, **options)
-文件内的 assert 验证结果;ID 每次不同。除 board 由 Ctrl-C 关闭外,命令应退出 0。
-## 支持边界
+class OmitClosedExchange:
+ """Explicitly lossy omission for this arithmetic fixture only."""
+ policy_id = "tutorial.omit_closed"
-示例对已关闭的算术 exchange 做有 loss 声明的省略,不关闭 codec loss 检查。静态 memory source 由应用拥有且只在进程内存在;Session 持久化不会自动将其变成持久知识库。required context 和不兼容 continuation 必须 fail closed。
+ def __init__(self):
+ self.calls = 0
+
+ def compact(self, **values):
+ self.calls += 1
+ return CompactionReceipt(
+ receipt_id="compaction_" + values["selected_digest"][:24],
+ input_exchange_ids=tuple(values["exchange_ids"]), policy_id=self.policy_id,
+ output_digest=hashlib.sha256(b"").hexdigest(),
+ declared_losses=("closed_exchange_omitted_without_summary",),
+ )
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ config = replace(
+ configuration(root),
+ context={"contributors": ["project"], "selector": "selector", "budget_policy": "budget"},
+ memory={"sources": ["memory"]}, compaction={"provider": "compactor"})
+ selector, compactor = AuditedSelector(), OmitClosedExchange()
+ with build_agent_composition(config, model_override=FakeProvider(), extensions={
+ "project": lambda: StaticContextContributor("lesson.project", "project", "Use only the supplied notes."),
+ "memory": lambda: StaticContextContributor("lesson.memory", "memory", "Session and Artifact are the two note titles."),
+ "selector": selector, "compactor": compactor,
+ "budget": lambda: DeclaredContextBudgetPolicy(default_max_input_units=4096, protected_recent_exchanges=0),
+ }) as composition:
+ composition.tool_registry.register(summarize_note)
+ session = composition.session("Index both notes")
+ assert session.run().state.final_result == "Indexed 2 notes: Session, Artifact."
+ assert {"lesson.project", "lesson.memory"} <= selector.seen
+ assert compactor.calls > 0
+ trajectory = default_reader(root).read_session(session.session_id.value, view=PrivacyView.RAW_PRIVATE)
+ assert any(record.kind.value == "compaction" and not record.loss.is_lossless for record in trajectory.records)
+ print("context selected; memory selected; compaction loss recorded")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ run(parser.parse_args().root.resolve())
+```
-## 常见错误
+{/* tutorial-files:end */}
-目录已存在时选一个新 root;导入失败核对安装来源。配置不匹配、缺失扩展或 typed failure 应检查 Trajectory 和迁移页,不能静默重试 unknown 外部效果。
+## 下一步与 API
-## 下一步
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/guides/sandbox-and-artifacts)
-[学习目录](/zh/tutorials/index) · [迁移与排障](/zh/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/context.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/guides/multi-agent-patterns.mdx b/docs/zh/guides/multi-agent-patterns.mdx
index e551f4e6..0ca9e318 100644
--- a/docs/zh/guides/multi-agent-patterns.mdx
+++ b/docs/zh/guides/multi-agent-patterns.mdx
@@ -1,42 +1,352 @@
---
-title: "多 Agent 工作"
-description: "QitOS G5 · 多 Agent 工作"
+title: "Delegate、join 与 handoff"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 学习目标
+## 目标与前置条件
-通过 delegate、spawn、fan-out 在新进程执行四个真实 child Session,再 join 持久结果;本地 resolver 明确选择本教程 worker。
+父执行先创建一个持久化的暂停 Session。显式 resolver 把已知 `notes_agent` 描述映射到本地子进程;每个 child 使用已安装包恢复真实 Session,不存在隐藏的分布式 worker 服务。
-## 前置条件
+`delegate`、`spawn` 提交子工作,`fan_out` 批量提交,`join` 对操作 ID 使用显式策略等待。程序检查四个 child completion 与 closed join。resolver 处理 join 时不重跑已引用的 child。本章 child 的最终回答是固定脚本;编排成功不证明模型独立推理成功。
-完成[Quickstart](/zh/quickstart)的安装与 lessons 复制;基础包,Python 3.12.7 实测,无真实请求。
+独立运行 `handoff.py`:目标进程继续同一个 work item,所有权改变,旧 owner 再次派发被拒绝。相比之下,生命周期章节的 fork 创建独立分支并保持 source head 不变。
-## 完整可运行示例
+需要 Python 基础;声明支持 Python ≥3.10,本轮本地验证使用 Python 3.12.7。每章可独立运行;继续已有项目时可复用环境和同名文件。以下命令为 macOS/Linux shell。
-[`examples/tutorials/work_graph.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/work_graph.py)
+## 准备项目
-完整目录是唯一执行来源;同目录的 session_walkthrough.py 是公开教学依赖,不依赖仓库 tests。
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
+
+## 运行并验证
```bash
-python lessons/work_graph.py --root ./work-run
+python multi_agent.py --root work-run
+python handoff.py --root handoff-run
+```
+
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
+
+```text
+durable children=4; join=closed
+handoff destination ran
+```
+
+本例 handoff 明确串行执行:先持久化转交接收回执、关闭源 composition,再启动目标进程。接收回执不是目标任务完成;目标进程退出后的断言才证明执行。当前同一个 Session 的目标 restore 若与源 terminal callback 并发,会发生 owner CAS 冲突;本例不宣称支持这种并发调度。
+
+## 行为与支持边界
+
+timeout 或 outcome_unknown 会使本例等待失败,不触发重新提交。LocalWorkScheduler 是进程内调度;持久化 receipt 不意味着分布式队列。
+
+## 练习与参考答案
+
+检查四条 completion,区分两个独立操作与两个 fan-out child。join 使用 operation ID,不使用展示名称。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
```
-## 预期输出与断言
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="multi_agent.py"
+"""Durable spawn/delegate/fan-out/join with real child Session execution.
+
+The local scheduler resolves only this tutorial's Agent; no distributed service.
+"""
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+import time
+
+from qitos.core.work_graph import WorkGraph
+from qitos.engine.work_runtime import DurableWorkRuntime, LocalWorkScheduler
+from dataclasses import replace
+from qitos.config import build_agent_composition
+from notes import FakeProvider, PauseAfterTool, summarize_note, configuration
+
+
+def compose(root, *, pause=False, finish=False):
+ config = configuration(root)
+ config = replace(config, budgets=replace(config.budgets, max_requests=16),
+ lifecycle={"policy": "pause"})
+ result = build_agent_composition(config, model_override=FakeProvider(start=2 if finish else 0),
+ extensions={"pause": PauseAfterTool})
+ result.tool_registry.register(summarize_note)
+ return result
+
-`durable children=4; join=closed; parent retains ownership`
+def wait(session, operation):
+ deadline = time.monotonic() + 30
+ while time.monotonic() < deadline:
+ graph = WorkGraph.from_canonical_dict(session.inspect().work_graph)
+ receipt = next(item for item in graph.operation_receipts if item.operation_id == operation.operation_id)
+ if receipt.state in {"completed", "failed", "outcome_unknown"}:
+ assert receipt.state == "completed", receipt.state
+ return graph
+ time.sleep(0.02)
+ raise AssertionError("child deadline exceeded; inspect before retrying")
-文件内的 assert 验证结果;ID 每次不同。除 board 由 Ctrl-C 关闭外,命令应退出 0。
-## 支持边界
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
-delegate 创建等待结果的 child,spawn 创建分离 child,fan-out 声明集合,join 消费结果。handoff 转移同一 WorkItem 的所有权并 fence 旧 owner,不是 child 或 fork。fork 创建独立 lineage,不推进 source。本完整示例执行 child 操作;handoff 执行属于下一轮 E2E 场景。不承诺分布式调度或外部效果 exactly-once。
+ class Resolver:
+ resolver_id = "tutorial.notes_agent.worker"
-## 常见错误
+ def resolve(self, descriptor):
+ def execute():
+ for identity in (() if descriptor.operation == "join" else descriptor.child_session_ids):
+ subprocess.run([sys.executable, __file__, "--root", str(root), "--child", identity],
+ check=True, capture_output=True, text=True, timeout=20)
+ return {"children": list(descriptor.child_session_ids)}
+ return execute
+
+ with compose(root, pause=True) as composition:
+ composition.runtime.work_runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver(), max_workers=2))
+ parent = composition.session("Index, then ask independent note workers")
+ parent.run()
+ assert parent.lifecycle.value == "paused"
+ delegated = parent.delegate("notes_agent", task="Describe the Session note")
+ wait(parent, delegated)
+ spawned = parent.spawn("notes_agent", task="Describe the Artifact note")
+ wait(parent, spawned)
+ batch = parent.fan_out([{"agent": "notes_agent", "task": "Review Session", "budget": {"model_requests": 2}},
+ {"agent": "notes_agent", "task": "Review Artifact", "budget": {"model_requests": 2}}])
+ wait(parent, batch)
+ joined = parent.join([delegated.operation_id, spawned.operation_id, batch.operation_id], policy="all")
+ graph = wait(parent, joined)
+ assert graph.joins[-1].state == "closed"
+ assert len(graph.completions) == 4
+ print("durable children=4; join=closed; parent retains ownership")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--child")
+ args = parser.parse_args()
+ root = args.root.resolve()
+ if args.child:
+ with compose(root, finish=True, pause=True) as composition:
+ result = composition.restore(args.child).run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ else:
+ run(root)
+```
+
+```python title="handoff.py"
+"""Transfer one work item's owner; demonstrate the superseded source fence."""
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+
+from notes import compose
+from multi_agent import wait
+from qitos.engine.work_runtime import DurableWorkRuntime, LocalWorkScheduler, WorkRuntimeError
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+
+ class Resolver:
+ resolver_id = "notes.handoff.worker"
+
+ def resolve(self, descriptor):
+ def execute():
+ # Acknowledge transfer before the destination claims this same
+ # Session head. This receipt is not destination task completion.
+ return {"destination": descriptor.parent_session_id, "admitted": True}
+ return execute
+
+ with compose(root, pause=True) as composition:
+ composition.runtime.work_runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver()))
+ source = composition.session("Index notes, then transfer ownership")
+ source.run()
+ identity = source.work_item_id
+ operation = source.handoff("notes_agent", rationale="Finish with the destination worker")
+ graph = wait(source, operation)
+ transfer = graph.transfers[-1]
+ assert transfer.from_agent_id != transfer.to_agent_id
+ assert graph.work_items[identity].owner.agent_id == transfer.to_agent_id
+ try:
+ source.spawn("notes_agent", task="A stale owner must not dispatch")
+ except WorkRuntimeError as error:
+ assert error.code == "superseded_owner"
+ else:
+ raise AssertionError("Superseded source unexpectedly dispatched")
+ identity = source.session_id.value
+ # Serialized handoff: source callbacks and resource cleanup finish first.
+ result = subprocess.run([
+ sys.executable, __file__, "--root", str(root), "--destination", identity,
+ ], capture_output=True, text=True, timeout=20)
+ if result.returncode:
+ raise RuntimeError(result.stderr)
+ print("handoff destination ran; owner changed; source fenced")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--destination")
+ args = parser.parse_args()
+ if args.destination:
+ with compose(args.root.resolve(), start=1, pause=True) as composition:
+ result = composition.restore(args.destination).run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ else:
+ run(args.root.resolve())
+```
-目录已存在时选一个新 root;导入失败核对安装来源。配置不匹配、缺失扩展或 typed failure 应检查 Trajectory 和迁移页,不能静默重试 unknown 外部效果。
+{/* tutorial-files:end */}
-## 下一步
+## 下一步与 API
-[学习目录](/zh/tutorials/index) · [迁移与排障](/zh/reference/g5-migration)
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/guides/observability)
-本例父级请求预算为 16,fan-out 每个 sibling 显式授予 2 次请求;不声明时首个 child 可能获得全部余量。
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/handoff.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/guides/observability.mdx b/docs/zh/guides/observability.mdx
index 2861d375..cbb6be44 100644
--- a/docs/zh/guides/observability.mdx
+++ b/docs/zh/guides/observability.mdx
@@ -1,55 +1,248 @@
---
-title: "Trajectory 与 qita"
-description: "QitOS G5 · Trajectory 与 qita"
+title: "使用 qita 检查与导出"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 学习目标
+## 目标与前置条件
-通过 default_reader 读取 canonical journal,检查 steering 与 fork lineage,导出脱敏 public projection 并重新读入。
+先运行资料项目,再用 `control.json` 中的准确 Session ID 检查结果。`default_reader` 默认读取 canonical journal,同时保留明确的历史 trace 支持。`read_session` 重建观察视图,不恢复执行。
-## 前置条件
+`inspect_run.py` 写入脱敏 public export,并检查重新导入后记录数一致。它也创建当前 qita HTML 导出 CLI 需要的 run selector 目录;这个目录不取代权威 journal。
-完成[Quickstart](/zh/quickstart)的安装与 lessons 复制;基础包,Python 3.12.7 实测,无真实请求。
+使用下方命令检查、回放和导出。replay 展示已记录执行,不调用模型、不重做工具。只读 board 用 Ctrl-C 退出。
-## 完整可运行示例
+需要 Python 基础;声明支持 Python ≥3.10,本轮本地验证使用 Python 3.12.7。每章可独立运行;继续已有项目时可复用环境和同名文件。以下命令为 macOS/Linux shell。
-[`examples/tutorials/session_walkthrough.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/session_walkthrough.py)
-
-完整目录是唯一执行来源;同目录的 session_walkthrough.py 是公开教学依赖,不依赖仓库 tests。
+## 准备项目
```bash
-python lessons/session_walkthrough.py create --root ./observe-run
-python lessons/session_walkthrough.py restore --root ./observe-run
-qita board --logdir ./observe-run
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
```
-## 预期输出与断言
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
-`records>0; public_export_lossless=false; board opens locally`
+如果继续 Quickstart 且 notes-run 已存在,跳过下面第一条 notes.py 创建命令,直接检查已有运行。replay 启动本地网页服务;先用 Ctrl-C 停止再执行 export/board。
-文件内的 assert 验证结果;ID 每次不同。除 board 由 Ctrl-C 关闭外,命令应退出 0。
+## 运行并验证
-## HTML 导出与 replay
+```bash
+python notes.py --root notes-run
+python inspect_run.py --root notes-run
+```
---run 使用日志目录下的逻辑 run ID 路径,不是 journal 文件名。replay 启动只读本地服务,用 Ctrl-C 停止。
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
+
+```text
+"records":
+```
+
+### 检查保存的 Session
+
+```bash
+session_id=$(python -c 'import json; print(json.load(open("notes-run/control.json"))["session_id"])')
+qit session inspect --config notes-run/agent.json --session-id "$session_id"
+qita inspect session "$session_id" --logdir ./notes-run
+```
```bash
-run_id=$(python -c 'import json; print(json.load(open("observe-run/control.json"))["run_id"])')
-mkdir -p "./observe-run/$run_id"
-qita export --run "./observe-run/$run_id" --html ./observe-run/replay.html
-qita replay --run "./observe-run/$run_id"
+run_id=$(python -c 'import json; print(json.load(open("notes-run/control.json"))["run_id"])')
+qita replay --run "notes-run/$run_id"
+qita export --run "notes-run/$run_id" --html notes-run/trajectory.html
+qita board --logdir ./notes-run
```
-## 支持边界
+## 行为与支持边界
+
+inspect 不需要 Docker 或 credentials。journal 读取完整,但当前会全量加载。public export 声明损失,不能替代私有恢复数据。
+
+## 练习与参考答案
+
+在不公开私有 payload 的前提下比较 private 与 public 记录;记录数相同不能证明 payload 无损。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
-qita 只读,不拥有执行语义。默认发现 journal 与 historical trace。完整读取正确,但目前全量加载;query limit 不是总内存上限。public export 不是无损 raw 备份。CLI qita export 输出 HTML,CanonicalTrajectoryExporter 输出 canonical JSON。private runs 不进入 Git。
-## 常见错误
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="inspect_run.py"
+"""Read an existing notes run, verify the index, and export a public view."""
+import argparse
+import json
+from pathlib import Path
+
+from qitos.qita.reader import default_reader
+from qitos.tracing.exporter import CanonicalTrajectoryExporter
+from qitos.tracing.trajectory import PrivacyView
+
+
+def inspect(root):
+ control = json.loads((root / "control.json").read_text())
+ trajectory = default_reader(root).read_session(control["session_id"], view=PrivacyView.RAW_PRIVATE)
+ assert trajectory.records
+ exporter = CanonicalTrajectoryExporter()
+ exported = exporter.export(trajectory, view=PrivacyView.REDACTED_PUBLIC)
+ imported = exporter.reimport(exported)
+ assert len(imported.records) == len(trajectory.records)
+ (root / "public-trajectory.json").write_bytes(exported.data)
+ # qita's --run selector currently requires an existing run directory.
+ # This directory is only a selector; the journal remains authoritative.
+ (root / control["run_id"]).mkdir(exist_ok=True)
+ print(json.dumps({"records": len(trajectory.records), "lossless": exported.loss.is_lossless,
+ "run_selector": str(root / control["run_id"])}))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ inspect(parser.parse_args().root.resolve())
+```
-目录已存在时选一个新 root;导入失败核对安装来源。配置不匹配、缺失扩展或 typed failure 应检查 Trajectory 和迁移页,不能静默重试 unknown 外部效果。
+{/* tutorial-files:end */}
-## 下一步
+## 下一步与 API
-[学习目录](/zh/tutorials/index) · [迁移与排障](/zh/reference/g5-migration)
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/guides/third-party-extensions)
-G5 CLI 仍要求 --run 路径存在,因此先创建同名空目录;真实数据仍由 parent 目录中的 journal 读取。这是 CLI 兼容限制,不是新的 trace 数据格式。
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/inspect_run.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/guides/sandbox-and-artifacts.mdx b/docs/zh/guides/sandbox-and-artifacts.mdx
index a7a37762..f17bb91f 100644
--- a/docs/zh/guides/sandbox-and-artifacts.mdx
+++ b/docs/zh/guides/sandbox-and-artifacts.mdx
@@ -1,43 +1,314 @@
---
-title: "Sandbox 与 artifact"
-description: "QitOS G5 · Sandbox 与 artifact"
+title: "Sandbox、artifact 与显式发布"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 学习目标
+## 目标与前置条件
-执行真实 Docker Env 文件/命令工具,以 digest 取回大输出,再在独立 fixture 中显式发布 answer.txt。
+本章引入实际文件和命令工具,因此需要 Docker CLI、运行中的 daemon 和 `python:3.12-slim` 镜像。fake 模型声明调用,但工具确实在 Docker 中执行。Python 客户端位于宿主机,容器网络关闭。
-## 前置条件
+先运行不发布的模式。源 `report.txt` 初始为 `original`;sandbox 写入 `Session, Artifact`,产生 20,000 字符大输出,暂停后恢复。程序解析 artifact 引用并验证 SHA-256。cleanup 后源报告必须保持原样。
-完成[Quickstart](/zh/quickstart)的安装与 lessons 复制;基础包,Python 3.12.7 实测,无真实请求。
+再换一个 root 加 `--publish`。仅此次执行为已有顶层文件 `report.txt` 和已验证的输入 digest 注册 SandboxPublicationTool。此时源报告才应改变。两种模式最终都断言容器已清除。
-## 完整可运行示例
+需要 Python 基础;声明支持 Python ≥3.10,本轮本地验证使用 Python 3.12.7。每章可独立运行;继续已有项目时可复用环境和同名文件。以下命令为 macOS/Linux shell。
-[`examples/tutorials/sandbox_artifacts.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/sandbox_artifacts.py)
+## 准备项目
-完整目录是唯一执行来源;同目录的 session_walkthrough.py 是公开教学依赖,不依赖仓库 tests。
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
+
+## 运行并验证
```bash
docker info
docker pull python:3.12-slim
-python lessons/sandbox_artifacts.py --root ./sandbox-retained
-python lessons/sandbox_artifacts.py --root ./sandbox-published --publish
+python sandbox.py --root sandbox-private
+python sandbox.py --root sandbox-published --publish
+```
+
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
+
+```text
+"published": false
+"published": true
```
-## 预期输出与断言
+## 行为与支持边界
+
+cleanup 不意味着 publication。当前 publication 限于已限定平台上已有的顶层普通文件,不是任意目录同步。缺少 Docker 属于前置环境故障,不能把这些文件工具改为 unsafe host 来绕过。
+
+## 练习与参考答案
+
+修改报告正文及对应断言。私有运行仍必须保留 `original`,显式发布运行才应得到新正文。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
-`docker=true; artifacts>0; container_absent=true; published=false then true`
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
-文件内的 assert 验证结果;ID 每次不同。除 board 由 Ctrl-C 关闭外,命令应退出 0。
+ def should_pause(self, context):
+ return context.step_id == 0
-## 支持边界
-需要 Docker CLI/daemon 与 python:3.12-slim;[docker] 不安装 daemon。命令只创建新的教程目录。cleanup 保留输出,不自动发布。--publish 显式注册一个顶层普通文件及输入 digest 的发布授权,仍经过 composition approval policy。当前 publication 需要支持的 POSIX descriptor 操作(本轮 macOS 实测),拒绝链接、嵌套/特殊/保护路径和 source 冲突。Docker 不等于 VM 保证。
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="sandbox.py"
+"""Real Docker lesson: retained output and opt-in top-level file publication.
+
+Uses a fake provider but real Env tools, Session, artifact store and reader.
+Only --publish registers publication authority over report.txt in a new fixture.
+"""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+
+from qitos.config import (
+ AgentConfig, BudgetConfig, EnvironmentConfig, ModelConfig, RuntimeConfig,
+ TrajectoryConfig, build_agent_composition,
+)
+from qitos.core.artifact import ArtifactRef
+from notes import PauseAfterTool
+from qitos.kit.tool.internal.publication import SandboxPublicationTool
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+
+class FakeProvider:
+ model = "sandbox-tutorial-fake"
+ qitos_protocol = "json_decision_multi_v1"
+
+ def __init__(self, publish, stage=0):
+ self.actions = [
+ ("write_file", {"path": "report.txt", "content": "Session, Artifact\n"}),
+ ("run_command", {"command": "python3 -c 'print(\"x\" * 20000)'", "timeout": 10}),
+ ]
+ if publish:
+ self.actions.append(("publish_workspace", {}))
+ self.stage = stage
+
+ def call_raw(self, messages, **options):
+ if self.stage == len(self.actions):
+ return {"choices": [{"message": {"content": "Final Answer: sandbox lesson complete"}}]}
+ name, args = self.actions[self.stage]
+ self.stage += 1
+ return {"choices": [{"message": {"content": None, "tool_calls": [{
+ "id": f"lesson-{self.stage}", "type": "function",
+ "function": {"name": name, "arguments": json.dumps(args)},
+ }]}}]}
+
+
+def references(value):
+ if isinstance(value, dict):
+ if value.get("schema_version") == "qitos.artifact_ref/v1":
+ yield ArtifactRef.from_dict(value)
+ for item in value.values():
+ yield from references(item)
+ elif isinstance(value, (list, tuple)):
+ for item in value:
+ yield from references(item)
+
+
+def run(root: Path, image: str, publish: bool):
+ root.mkdir(parents=True, exist_ok=False)
+ source = root / "source"
+ source.mkdir()
+ (source / "report.txt").write_text("original\n", encoding="utf-8")
+ config = AgentConfig(
+ lifecycle={"policy": "pause"},
+ name="sandbox-lesson", protocol="json_decision_multi_v1", tool_preset="env_coding",
+ model=ModelConfig(provider="openai_compatible", model="sandbox-tutorial-fake"),
+ tool_options={"native_tool_calls_required": True},
+ budgets=BudgetConfig(max_steps=6, max_requests=6, max_runtime_seconds=60),
+ runtime=RuntimeConfig(
+ data_root=str(root / "data"),
+ trajectory=TrajectoryConfig(output=str(root / "trajectory.journal")),
+ environment=EnvironmentConfig(workspace=str(source), image=image,
+ cpus=0.5, memory_mb=256, pids_limit=32)),
+ )
+ with build_agent_composition(config, model_override=FakeProvider(publish),
+ extensions={"pause": PauseAfterTool}) as composition:
+ session = composition.session("Write the notes report and retain a large output")
+ session.run()
+ assert session.lifecycle.value == "paused"
+ identity = session.session_id.value
+ assert (source / "report.txt").read_text() == "original\n"
+ with build_agent_composition(config, model_override=FakeProvider(publish, stage=1),
+ extensions={"pause": PauseAfterTool}) as composition:
+ session = composition.restore(identity)
+ if publish:
+ composition.tool_registry.register(SandboxPublicationTool(
+ composition.env, paths=["report.txt"],
+ expected_input_digest=composition.env.input_digest,
+ ))
+ result = session.run()
+ assert result.state.final_result == "sandbox lesson complete", repr(result.state.final_result)
+ trajectory = default_reader(root).read_session(session.session_id.value, view=PrivacyView.RAW_PRIVATE)
+ artifacts = [ref for record in trajectory.records for ref in references(record.payload)]
+ assert artifacts
+ for ref in artifacts:
+ body = composition.agent.config["artifact_resolver"].resolve(ref).body
+ assert body is not None and hashlib.sha256(body).hexdigest() == ref.sha256
+ assert (source / "report.txt").read_text() == ("Session, Artifact\n" if publish else "original\n")
+ assert composition.env.cleanup_receipt["container_absent"] is True
+ assert (source / "report.txt").read_text() == ("Session, Artifact\n" if publish else "original\n")
+ print(json.dumps({"docker": True, "published": publish, "artifacts": len(artifacts),
+ "container_absent": True, "session_id": session.session_id.value}))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--image", default="python:3.12-slim")
+ parser.add_argument("--publish", action="store_true")
+ args = parser.parse_args()
+ run(args.root.resolve(), args.image, args.publish)
+```
-## 常见错误
+{/* tutorial-files:end */}
-目录已存在时选一个新 root;导入失败核对安装来源。配置不匹配、缺失扩展或 typed failure 应检查 Trajectory 和迁移页,不能静默重试 unknown 外部效果。
+## 下一步与 API
-## 下一步
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/guides/multi-agent-patterns)
-[学习目录](/zh/tutorials/index) · [迁移与排障](/zh/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/sandbox.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/guides/third-party-extensions.mdx b/docs/zh/guides/third-party-extensions.mdx
index 11ae05ca..e9824cee 100644
--- a/docs/zh/guides/third-party-extensions.mdx
+++ b/docs/zh/guides/third-party-extensions.mdx
@@ -1,40 +1,240 @@
---
-title: "第三方扩展"
-description: "QitOS G5 · 第三方扩展"
+title: "替换 provider 并添加 context"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 学习目标
+## 目标与前置条件
-通过结构化 extension factory 替换 context selection 和 compaction;阅读 session_walkthrough.py 中的 fake provider 和纯函数工具作为最小教学 adapter。
+composition 接受公共 model override 和显式命名的扩展工厂。`ObservedFakeProvider` 保持调用接口,计数实际请求,并断言每个请求都包含选中的项目 context,随后委托给网页可见的确定性 provider。
-## 前置条件
+项目 contributor 工厂返回 StaticContextContributor,注册名与 `context.contributors` 一致。不从 YAML 中任意导入代码,执行哪些扩展由应用程序明确决定。
-完成[Quickstart](/zh/quickstart)的安装与 lessons 复制;基础包,Python 3.12.7 实测,无真实请求。
+这是完整的 provider 替换和 context 集成示例,不是新的传输客户端。真实 provider 使用配置章节中的配置与显式 credential resolver。替换 store、sink、sandbox 前阅读扩展索引:只有相同 Python 方法签名不能保证持久化和安全契约不变。
-## 完整可运行示例
+需要 Python 基础;声明支持 Python ≥3.10,本轮本地验证使用 Python 3.12.7。每章可独立运行;继续已有项目时可复用环境和同名文件。以下命令为 macOS/Linux shell。
-[`examples/tutorials/context_memory.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/context_memory.py)
+## 准备项目
-完整目录是唯一执行来源;同目录的 session_walkthrough.py 是公开教学依赖,不依赖仓库 tests。
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
+```
+
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
+
+## 运行并验证
```bash
-python lessons/context_memory.py --root ./extensions-run
+python provider_extension.py --root extension-run
```
-## 预期输出与断言
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
+
+```text
+provider replaced; context observed; requests=3
+```
+
+## 行为与支持边界
+
+不存在对所有 provider 的通用兼容保证。codec、continuation、tool schema 与 loss policy 必须匹配。检查 typed failure,不能关闭 loss 检查接受不兼容响应。
+
+## 练习与参考答案
+
+同时修改 config 与 extensions 中 contributor 名称;只改一边应产生配置或扩展失败。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
-`context selected; memory selected; compaction loss recorded`
-文件内的 assert 验证结果;ID 每次不同。除 board 由 Ctrl-C 关闭外,命令应退出 0。
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
-## 支持边界
-fake call_raw adapter 只证明兼容教学路径。真实 provider 必须声明 target/capabilities、codec、transport、failure normalization 以及支持的 continuation/streaming。CheckpointStore 与 EventSink 通过 RuntimeComposition 替换,sandbox 和 evaluator 遵循其公开 contract。每个替代实现都需独立 conformance tests;改类名不证明安全或持久性。
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="provider_extension.py"
+"""Replace the provider and inject context, without changing the kernel."""
+import argparse
+from dataclasses import replace
+from pathlib import Path
+
+from notes import FakeProvider, configuration, summarize_note
+from qitos.config import build_agent_composition
+from qitos.core.context import StaticContextContributor
+
+
+class ObservedFakeProvider(FakeProvider):
+ """Keep the public provider call shape and validate selected context."""
+ def __init__(self):
+ super().__init__()
+ self.requests = 0
+
+ def call_raw(self, messages, **options):
+ assert "notes-project-context" in str(messages)
+ self.requests += 1
+ return super().call_raw(messages, **options)
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ config = replace(configuration(root), context={"contributors": ["project"]})
+ provider = ObservedFakeProvider()
+ with build_agent_composition(config, model_override=provider, extensions={
+ "project": lambda: StaticContextContributor("notes.project", "project", "notes-project-context"),
+ }) as composition:
+ composition.tool_registry.register(summarize_note)
+ result = composition.session("Index both notes").run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ assert provider.requests == 3
+ print("provider replaced; context observed; requests=3")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ run(parser.parse_args().root.resolve())
+```
-## 常见错误
+{/* tutorial-files:end */}
-目录已存在时选一个新 root;导入失败核对安装来源。配置不匹配、缺失扩展或 typed failure 应检查 Trajectory 和迁移页,不能静默重试 unknown 外部效果。
+## 下一步与 API
-## 下一步
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/quickstart)
-[学习目录](/zh/tutorials/index) · [迁移与排障](/zh/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/provider_extension.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/installation.mdx b/docs/zh/installation.mdx
index 987b8d22..ddd17e23 100644
--- a/docs/zh/installation.mdx
+++ b/docs/zh/installation.mdx
@@ -8,7 +8,7 @@ description: "QitOS G5 · 安装"
| 来源 | 安装方式 | 能力身份 |
|---|---|---|
| 已发布 PyPI | `python -m pip install qitos` | 已发布包,**不代表 G5** |
-| 可重现 G5 源码 | 下方固定 commit 命令 | `717b4cf1b23f2ed252cd03234ffd8605038d9567` |
+| 可重现 G5 源码 | 下方固定 commit 命令 | `60809b3be388d22ea40ea41b4aaa1f5540c76fda` |
| GitHub 开发分支 | `python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@master"` | 会变化,需记录实际 SHA |
| 本地 wheel | `python -m pip install ./qitos-0.6.0-py3-none-any.whl` | 核对 wheel digest 和来源 |
| 框架贡献者 | clone 内 `python -m pip install -e ".[dev]"` | editable checkout,不是 wheel 安装验收 |
@@ -23,7 +23,7 @@ G5 源码仍声明版本 0.6.0;仅凭版本号不能区分已发布构建。
```bash
python3 -m venv .venv
source .venv/bin/activate
-python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@717b4cf1b23f2ed252cd03234ffd8605038d9567"
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
python -m pip install pytest
qit --help
qita --help
@@ -36,7 +36,7 @@ qita --help
## 可选依赖
fake 算术入门只需基础包和 pytest。真实 OpenAI 或兼容客户端使用相同固定来源的
-`qitos[openai] @ git+https://github.com/WhitzardAgent/WhitzardOS.git@717b4cf1b23f2ed252cd03234ffd8605038d9567`;
+`qitos[openai] @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda`;
`[models]` 还安装 LiteLLM。`[docker]` 与 `[qita]` 当前不增加 Python 依赖,
但 Docker 仍需可用的 CLI 和 daemon。MCP 使用 `[mcp]`,浏览器工具 `[web]`,
W&B `[wandb]`,MLflow `[mlflow]`。初次运行不需要全部 extras。
@@ -47,4 +47,4 @@ W&B `[wandb]`,MLflow `[mlflow]`。初次运行不需要全部 extras。
用 `python -m pip show qitos` 核对,并按准确源码重建 venv。
接着阅读[快速开始](/zh/quickstart)。
-远端 CI 发现 Python 3.10 的 publication 调用了不存在的 hashlib.file_digest。此路径请使用 Python 3.11+;历史本地 G5 资格具体覆盖 3.12.7。固定安装 G5 源码时仍有此限制;master 已将该调用修复为有界的文件描述符分块哈希,并独立验证,不冒称历史 G5 已覆盖修复。
+本教程使用已包含 Python 3.10 publication 修复的 60809b3。历史 G5 baseline 为 717b4cf1b23f2ed252cd03234ffd8605038d9567;上面的历史 wheel digest 仅属于该历史资格。本轮不会把历史测试归属到新源码。
diff --git a/docs/zh/quickstart.mdx b/docs/zh/quickstart.mdx
index bee397b2..95944b4a 100644
--- a/docs/zh/quickstart.mdx
+++ b/docs/zh/quickstart.mdx
@@ -1,73 +1,298 @@
---
-title: "快速开始"
-description: "QitOS G5 · 快速开始"
+title: "运行资料整理 Agent"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 目标与前置条件
+## 你将完成什么
-安装指定来源 → 创建项目 → 配置模型引用、工具和资源 → 运行 Session →
-检查结果、artifact 与 Trajectory → 恢复或扩展自己的 Agent。
-先读[安装](/zh/installation)。实测解释器为 Python 3.12.7;
-本节算术入门不需要真实凭据或 Docker。
+创建一个资料整理项目,调用工具提取两条合成资料的标题和词数,通过 Session 执行,再用 qita 查看记录。你只需 Python 基础,不需要模型凭据或 Docker;这里的 fake provider 是显式脚本替身。
-## 创建并执行项目
-
-先安装,再使用 `qit`:
+## 准备项目
```bash
python3 -m venv .venv
source .venv/bin/activate
-python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@717b4cf1b23f2ed252cd03234ffd8605038d9567"
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
python -m pip install pytest
-qit --help
-qita --help
+qit new --agent-name notes_agent --output-dir . --no-input
+cd notes_agent
```
-下载完整教学文件,生成可安装项目,然后执行:
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
-```bash
-git clone --branch feat/campaign-absorption --single-branch https://github.com/WhitzardAgent/WhitzardOS.git qitos-lessons
-cp -R qitos-lessons/examples/tutorials ./lessons
-qit new --agent-name my_agent --output-dir . --no-input
-python -m pip install ./my_agent
-python -m pytest -q my_agent/tests
-python lessons/session_walkthrough.py create --root ./arithmetic-run
-python lessons/session_walkthrough.py restore --root ./arithmetic-run
+### 资料与工具
+
+`NOTES` 是全部输入。装饰器把普通 Python 函数变成可注册工具;这个函数只读取内存。返回字典是真实工具结果,随后会被断言验证。
+
+{/* tutorial-snippet:notes.py:fixture */}
+```python
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+```
+{/* tutorial-snippet:end */}
+
+### 明确的 fake provider
+
+这个 provider 按固定顺序返回两次工具调用和一次结束回答,没有真实模型推理。它让你先观察框架的工具往返;更换真实模型见配置章节。
+
+{/* tutorial-snippet:notes.py:provider */}
+```python
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+```
+{/* tutorial-snippet:end */}
+
+### 配置与组合
+
+把本页完整文件保存到刚生成的 `notes_agent/` 根目录,并替换 `agent.yaml`。配置选择 SQLite 和 journal;程序只注册受信任的纯函数。`unsafe_host` 不提供隔离,不要在此配置加入文件或 shell 工具。
+
+{/* tutorial-snippet:notes.py:composition */}
+```python
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+```
+{/* tutorial-snippet:end */}
+
+### 运行与断言
+
+`with` 负责资源关闭;`composition.session` 创建 Session,`run` 返回 EngineResult。检查实际工具输出,再检查最终文字。筛选工具名是因为记录还包含环境观察。
+
+{/* tutorial-snippet:notes.py:run */}
+```python
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
```
+{/* tutorial-snippet:end */}
-生成的 `my_agent/agent.yaml` 是 canonical 配置。生成项目的测试明确替换为
-fake provider 和临时的非隔离 host workspace,只验证 composition/Session
-接线,不执行模型选择的 shell 操作。完整的 `lessons/session_walkthrough.py`
-只注册受信任纯函数 `add`,通过明确标注的 fake provider、SQLite 和真实
-Trajectory writer 运行。**这不证明沙箱隔离,也不是自主任务成功证据。**
+## 运行并验证
-第一个进程输出 `lifecycle: paused`、`tool_output: 42`;第二个输出
-`final_result: arithmetic complete`、独立 child Session ID 和正数 record count。
-两个进程均应退出 0;断言失败就是失败。重复执行请使用新 root,保留原始证据。
+```bash
+python -m pip install .
+python -m pytest -q tests
+python notes.py --root notes-run
+```
-## 检查结果
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
-脚本写入 `arithmetic-run/control.json`、`sessions.sqlite3`、
-`trajectory.journal` 和 `public-trajectory.json`。
+```text
+Indexed 2 notes: Session, Artifact.
+```
+
+### 检查保存的 Session
```bash
-qita board --logdir ./arithmetic-run
-qit session inspect --help
-qita inspect --help
+session_id=$(python -c 'import json; print(json.load(open("notes-run/control.json"))["session_id"])')
+qit session inspect --config notes-run/agent.json --session-id "$session_id"
+qita inspect session "$session_id" --logdir ./notes-run
+```
+
+## 行为与支持边界
+
+工具的实际输出与 provider 的回答分开验证。`build_agent_composition` 组合配置,`composition.session` 创建 Session,`session.run` 返回 EngineResult;`with` 退出时关闭所拥有的资源。
+
+## 练习与参考答案
+
+把第一条标题改为 `Recovery`,同时更新标题断言和 fake provider 的固定回答;词数仍由工具计算。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
```
-board 是只读入口,Ctrl-C 停止本地服务。
-[Session 教程](/zh/tutorials/checkpoint-and-fork)给出实际 ID 与匹配配置的 CLI 检查。
-public export 有脱敏和 loss 声明,不是 raw 备份。
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
-## 理解 typed failure
+{/* tutorial-files:end */}
-无效配置在执行前拒绝。coding 路径没有 Docker 时出现 `sandbox_unavailable`;
-应启动 daemon 并重新检查,不能通过改成 host 执行绕过。
-凭据缺失或 codec 不支持都不算模型成功;见[迁移与排障](/zh/reference/g5-migration)。
+## 下一步与 API
-## 真实模型与扩展
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/guides/build-your-first-agent)
-[配置](/zh/reference/configuration)提供完整私有凭据文件与显式 resolver。
-只有实际运行真实 launch 才会发请求;本轮文档验收没有真实模型请求。
-下一步按[八个学习单元](/zh/tutorials/index)继续。
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/notes.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/reference/agent-runtime.mdx b/docs/zh/reference/agent-runtime.mdx
new file mode 100644
index 00000000..36cb4168
--- /dev/null
+++ b/docs/zh/reference/agent-runtime.mdx
@@ -0,0 +1,565 @@
+---
+title: "Agent 与执行"
+description: "QitOS public API: agent-runtime"
+---
+
+AgentModule 管理策略与状态变更,Engine 驱动 observe、decide、act、reduce 与停止。Decision.act 声明动作,Decision.final 结束策略。EngineResult 保存状态及 typed 记录,reduce 收到字典观察。完整组合见自定义 Agent 教程。RuntimeComposition 默认 store 只在进程内;AgentModule.run 保留为程序化便捷路径。
+
+[完整可运行教程 / Complete tutorial](/zh/guides/build-your-first-agent) · [API index](/zh/reference/api)
+
+以下签名和字段由固定源码提取;签名是参考,不是可直接运行的程序。每个条目的源码链接绑定同一 runtime baseline。类型中的 Any 不代表任意对象均受支持,应结合上述行为契约和教程使用。
+
+{/* api-reference:start */}
+
+
+## AgentModule
+
+```python
+from qitos import AgentModule
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L25)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+# NotesAgent subclasses AgentModule in the complete tutorial.
+agent = NotesAgent()
+result = Engine(agent, runtime=RuntimeComposition()).session("Index notes").run()
+```
+
+```text
+Canonical policy contract for step-based agents.
+```
+
+```text
+AgentModule(tool_registry: Any=None, toolset: Any=None, llm: Any=None, model_parser: Any=None, model_protocol: Any=None, memory: Memory | None=None, history: History | None=None, mcp_servers: List[Any] | None=None, **config: Any) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `tool_registry` | `Any` | `None` |
+| `toolset` | `Any` | `None` |
+| `llm` | `Any` | `None` |
+| `model_parser` | `Any` | `None` |
+| `model_protocol` | `Any` | `None` |
+| `memory` | `Memory | None` | `None` |
+| `history` | `History | None` | `None` |
+| `mcp_servers` | `List[Any] | None` | `None` |
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `name` | `str` | `'agent'` |
+| `handoff_targets` | `List[str] \| None` | `None` |
+
+
+### AgentModule.init_state
+
+```text
+init_state(task: str, **kwargs: Any) -> StateT
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `str` | `required` |
+
+```text
+Create and return the initial typed state for a run.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L55)
+
+
+### AgentModule.decide
+
+```text
+decide(state: StateT, observation: ObservationT) -> Optional[Decision[ActionT]]
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `state` | `StateT` | `required` |
+| `observation` | `ObservationT` | `required` |
+
+```text
+Optional custom decision hook. Return None to use Engine model decision.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L101)
+
+
+### AgentModule.reduce
+
+```text
+reduce(state: StateT, observation: ObservationT, decision: Decision[ActionT]) -> StateT
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `state` | `StateT` | `required` |
+| `observation` | `ObservationT` | `required` |
+| `decision` | `Decision[ActionT]` | `required` |
+
+```text
+Reduce observation (including action/env outputs) into next state.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L120)
+
+
+### AgentModule.should_stop
+
+```text
+should_stop(state: StateT) -> bool
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `state` | `StateT` | `required` |
+
+```text
+Optional additional stop condition.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L128)
+
+
+### AgentModule.run
+
+```text
+run(task: str | Task, return_state: bool=False, hooks: List[Any] | None=None, render_hooks: List[Any] | None=None, engine_kwargs: Dict[str, Any] | None=None, workspace: str | None=None, max_steps: int | None=None, env: Any=None, parser: Any=None, protocol: Any=None, search: Any=None, critics: List[Any] | None=None, stop_criteria: List[Any] | None=None, history_policy: Any=None, context_config: Any=None, trace: Any=None, render: Any=None, trace_logdir: str='./runs', trace_prefix: str | None=None, theme: str='research', run_spec: RunSpec | Dict[str, Any] | None=None, experiment_spec: ExperimentSpec | Dict[str, Any] | None=None, **state_kwargs: Any) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `str | Task` | `required` |
+| `return_state` | `bool` | `False` |
+| `hooks` | `List[Any] | None` | `None` |
+| `render_hooks` | `List[Any] | None` | `None` |
+| `engine_kwargs` | `Dict[str, Any] | None` | `None` |
+| `workspace` | `str | None` | `None` |
+| `max_steps` | `int | None` | `None` |
+| `env` | `Any` | `None` |
+| `parser` | `Any` | `None` |
+| `protocol` | `Any` | `None` |
+| `search` | `Any` | `None` |
+| `critics` | `List[Any] | None` | `None` |
+| `stop_criteria` | `List[Any] | None` | `None` |
+| `history_policy` | `Any` | `None` |
+| `context_config` | `Any` | `None` |
+| `trace` | `Any` | `None` |
+| `render` | `Any` | `None` |
+| `trace_logdir` | `str` | `'./runs'` |
+| `trace_prefix` | `str | None` | `None` |
+| `theme` | `str` | `'research'` |
+| `run_spec` | `RunSpec | Dict[str, Any] | None` | `None` |
+| `experiment_spec` | `ExperimentSpec | Dict[str, Any] | None` | `None` |
+
+```text
+Execute task with Engine using plain text objective or structured Task.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/agent_module.py#L265)
+
+
+
+## StateSchema
+
+```python
+from qitos import StateSchema
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/state.py#L56)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+from dataclasses import dataclass
+@dataclass
+class MyState(StateSchema):
+ completed: int = 0
+state = MyState(task="Index notes", max_steps=3)
+```
+
+```text
+Canonical typed state base for AgentModule.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `schema_version` | `int` | `1` |
+| `task` | `str` | `''` |
+| `current_step` | `int` | `0` |
+| `max_steps` | `int` | `10` |
+| `final_result` | `Optional[str]` | `None` |
+| `stop_reason` | `Optional[str]` | `None` |
+| `metadata` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `metrics` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `migration_registry` | `ClassVar[StateMigrationRegistry]` | `StateMigrationRegistry()` |
+
+
+
+## Task
+
+```python
+from qitos import Task
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/task.py#L76)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+task = Task(objective="Index the notes")
+print(task.objective)
+```
+
+```text
+Task package with objective, resources, and environment requirements.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `id` | `str` | `required` |
+| `objective` | `str` | `required` |
+| `inputs` | `Dict[str, Any]` | `dc_field(default_factory=dict)` |
+| `resources` | `List[TaskResource]` | `dc_field(default_factory=list)` |
+| `env_spec` | `Optional[EnvSpec]` | `None` |
+| `constraints` | `Dict[str, Any]` | `dc_field(default_factory=dict)` |
+| `success_criteria` | `List[str]` | `dc_field(default_factory=list)` |
+| `budget` | `TaskBudget` | `dc_field(default_factory=TaskBudget)` |
+| `metadata` | `Dict[str, Any]` | `dc_field(default_factory=dict)` |
+
+
+
+## Decision
+
+```python
+from qitos import Decision
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/decision.py#L14)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+decision = Decision.act([Action(name="summarize_note", args={"index": 0})])
+finished = Decision.final("Session")
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `mode` | `DecisionMode` | `required` |
+| `actions` | `List[ActionT]` | `field(default_factory=list)` |
+| `final_answer` | `Optional[str]` | `None` |
+| `rationale` | `Optional[str]` | `None` |
+| `meta` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `candidates` | `List['Decision[ActionT]']` | `field(default_factory=list)` |
+
+
+### Decision.act
+
+```text
+act(actions: List[ActionT], rationale: Optional[str]=None, meta: Optional[Dict[str, Any]]=None) -> 'Decision[ActionT]'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `actions` | `List[ActionT]` | `required` |
+| `rationale` | `Optional[str]` | `None` |
+| `meta` | `Optional[Dict[str, Any]]` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/decision.py#L23)
+
+
+### Decision.final
+
+```text
+final(answer: str, rationale: Optional[str]=None, meta: Optional[Dict[str, Any]]=None) -> 'Decision[ActionT]'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `answer` | `str` | `required` |
+| `rationale` | `Optional[str]` | `None` |
+| `meta` | `Optional[Dict[str, Any]]` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/decision.py#L32)
+
+
+
+## Action
+
+```python
+from qitos import Action
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/action.py#L26)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+action = Action(name="summarize_note", args={"index": 0})
+print(action.name)
+```
+
+```text
+Normalized action contract emitted by policy and consumed by executor.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `name` | `str` | `required` |
+| `args` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `kind` | `ActionKind` | `ActionKind.TOOL` |
+| `action_id` | `Optional[str]` | `None` |
+| `timeout_s` | `Optional[float]` | `None` |
+| `max_retries` | `int` | `0` |
+| `idempotent` | `bool` | `True` |
+| `classification` | `str` | `'default'` |
+| `metadata` | `Dict[str, Any]` | `field(default_factory=dict)` |
+
+
+
+## Engine
+
+```python
+from qitos import Engine
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L292)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+engine = Engine(NotesAgent(), runtime=RuntimeComposition())
+result = engine.session("Index notes").run()
+```
+
+```text
+Single execution kernel for all AgentModule workflows.
+```
+
+```text
+Engine(agent: AgentModule[StateT, ObservationT, ActionT], agent_registry: Optional[Any]=None, budget: Optional[RuntimeBudget]=None, delegate_depth: int=0, shared_memory: Any=None, validation_gate: Optional[StateValidationGate]=None, recovery_handler: Optional[RecoveryHandler]=None, recovery_policy: Optional[RecoveryPolicy]=None, trace_writer: Optional[TraceWriter]=None, parser: Optional[Parser[ActionT]]=None, protocol: Any=None, stop_criteria: Optional[List[StopCriteria]]=None, branch_selector: Optional[BranchSelector[StateT, ObservationT, ActionT]]=None, search: Optional[Search[StateT, ObservationT, ActionT]]=None, critics: Optional[List[Critic]]=None, env: Optional[Env]=None, history_policy: Optional[HistoryPolicy]=None, hooks: Optional[List[EngineHook]]=None, render_hooks: Optional[List[Any]]=None, context_config: Optional[ContextConfig | Dict[str, Any]]=None, cache_backend: Optional[Any]=None, checkpoint_manager: Optional[Any]=None, checkpoint_store: Optional[CheckpointStore]=None, checkpoint_durability: DurabilityMode=DurabilityMode.SYNC, permission_pipeline: Optional[Any]=None, read_before_write_enforcer: Optional[Any]=None, permission_interaction_callback: Optional[Any]=None, loop_detector: Optional[ToolCallLoopDetector]=None, tracing_provider: Optional[Any]=None, interceptors: Optional[List[ToolInterceptor]]=None, auto_approve: bool=False, action_execution_policy: Optional[Any]=None, runtime: Optional[RuntimeComposition]=None) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `agent` | `AgentModule[StateT, ObservationT, ActionT]` | `required` |
+| `agent_registry` | `Optional[Any]` | `None` |
+| `budget` | `Optional[RuntimeBudget]` | `None` |
+| `delegate_depth` | `int` | `0` |
+| `shared_memory` | `Any` | `None` |
+| `validation_gate` | `Optional[StateValidationGate]` | `None` |
+| `recovery_handler` | `Optional[RecoveryHandler]` | `None` |
+| `recovery_policy` | `Optional[RecoveryPolicy]` | `None` |
+| `trace_writer` | `Optional[TraceWriter]` | `None` |
+| `parser` | `Optional[Parser[ActionT]]` | `None` |
+| `protocol` | `Any` | `None` |
+| `stop_criteria` | `Optional[List[StopCriteria]]` | `None` |
+| `branch_selector` | `Optional[BranchSelector[StateT, ObservationT, ActionT]]` | `None` |
+| `search` | `Optional[Search[StateT, ObservationT, ActionT]]` | `None` |
+| `critics` | `Optional[List[Critic]]` | `None` |
+| `env` | `Optional[Env]` | `None` |
+| `history_policy` | `Optional[HistoryPolicy]` | `None` |
+| `hooks` | `Optional[List[EngineHook]]` | `None` |
+| `render_hooks` | `Optional[List[Any]]` | `None` |
+| `context_config` | `Optional[ContextConfig | Dict[str, Any]]` | `None` |
+| `cache_backend` | `Optional[Any]` | `None` |
+| `checkpoint_manager` | `Optional[Any]` | `None` |
+| `checkpoint_store` | `Optional[CheckpointStore]` | `None` |
+| `checkpoint_durability` | `DurabilityMode` | `DurabilityMode.SYNC` |
+| `permission_pipeline` | `Optional[Any]` | `None` |
+| `read_before_write_enforcer` | `Optional[Any]` | `None` |
+| `permission_interaction_callback` | `Optional[Any]` | `None` |
+| `loop_detector` | `Optional[ToolCallLoopDetector]` | `None` |
+| `tracing_provider` | `Optional[Any]` | `None` |
+| `interceptors` | `Optional[List[ToolInterceptor]]` | `None` |
+| `auto_approve` | `bool` | `False` |
+| `action_execution_policy` | `Optional[Any]` | `None` |
+| `runtime` | `Optional[RuntimeComposition]` | `None` |
+
+
+### Engine.session
+
+```text
+session(task: str | Task, session_id: Any=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `str | Task` | `required` |
+| `session_id` | `Any` | `None` |
+
+```text
+Create one durable Session facade using this Engine composition.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L648)
+
+
+### Engine.run
+
+```text
+run(task: str | Task, **kwargs: Any) -> EngineResult[StateT]
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `str | Task` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L981)
+
+
+### Engine.restore
+
+```text
+restore(session_id: Any, *, resolvers: Any=None, runtime: Optional[RuntimeComposition]=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `session_id` | `Any` | `required` |
+| `resolvers` | `Any` | `None` |
+| `runtime` | `Optional[RuntimeComposition]` | `None` |
+
+```text
+Restore a Session in a fresh Engine through explicit resolvers.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L665)
+
+
+
+## EngineResult
+
+```python
+from qitos.engine.engine import EngineResult
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/engine.py#L174)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+result = session.run()
+print(result.state.final_result, result.state.stop_reason)
+for record in result.records:
+ for tool_result in record.action_results:
+ print(tool_result.tool_name, tool_result.status)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `state` | `StateT` | `required` |
+| `records` | `List[StepRecord]` | `required` |
+| `events` | `List[RuntimeEvent]` | `required` |
+| `step_count` | `int` | `required` |
+| `task_result` | `Optional[TaskResult]` | `None` |
+| `runtime_seconds` | `float` | `0.0` |
+| `total_tokens` | `int` | `0` |
+| `run_id` | `str` | `''` |
+| `critic_traces` | `List[CriticTrace]` | `field(default_factory=list)` |
+| `handoff_traces` | `List[HandoffTrace]` | `field(default_factory=list)` |
+| `failure` | `Optional[Dict[str, Any]]` | `None` |
+
+
+
+## RuntimeBudget
+
+```python
+from qitos.engine.states import RuntimeBudget
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/states.py#L38)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+budget = RuntimeBudget(max_steps=3)
+engine = Engine(NotesAgent(), budget=budget)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `max_steps` | `int` | `10` |
+| `max_runtime_seconds` | `Optional[float]` | `None` |
+| `max_tokens` | `Optional[int]` | `None` |
+| `max_model_requests` | `Optional[int]` | `None` |
+
+
+
+## StopReason
+
+```python
+from qitos import StopReason
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/errors.py#L20)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+print([reason.value for reason in StopReason])
+```
+
+
+
+## RuntimeComposition
+
+```python
+from qitos.engine.runtime import RuntimeComposition
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/runtime.py#L177)
+
+[用法与可执行示例](/zh/guides/build-your-first-agent)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+runtime = RuntimeComposition()
+engine = Engine(NotesAgent(), runtime=runtime)
+# The default checkpoint store is process-local.
+```
+
+```text
+Process-local Engine components plus their serializable description.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `checkpoint_store` | `Optional[CheckpointStore]` | `None` |
+| `resolvers` | `ResolverRegistry` | `field(default_factory=ResolverRegistry)` |
+| `durability_mode` | `DurabilityMode` | `DurabilityMode.SYNC` |
+| `lifecycle_policy` | `LifecyclePolicy` | `field(default_factory=LifecyclePolicy)` |
+| `snapshot_components` | `tuple[RuntimeSnapshotComponent, ...]` | `()` |
+| `event_sink` | `Any` | `None` |
+| `event_sink_failure_policy` | `Any` | `None` |
+| `event_sink_view` | `Any` | `None` |
+| `tool_execution_policy` | `Any` | `None` |
+| `launch_metadata` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `context_model_runtime` | `Optional[ContextModelRuntime]` | `None` |
+| `work_runtime` | `Any` | `None` |
+| `event_sink_reports` | `list[Any]` | `field(default_factory=list, init=False)` |
+
+{/* api-reference:end */}
diff --git a/docs/zh/reference/api.mdx b/docs/zh/reference/api.mdx
index b8867ed1..402d356c 100644
--- a/docs/zh/reference/api.mdx
+++ b/docs/zh/reference/api.mdx
@@ -1,388 +1,88 @@
---
-title: "API 参考"
-description: "QitOS 公开 Python API 的完整参考。这里列出的符号都从 `qitos` 顶层导出。"
+title: "API Reference"
+description: "Core public Python API, CLI and extension contracts."
---
-本页列出的符号都可以直接这样导入:
-
-Signature reference (not executable):
-
-```text
-from qitos import AgentModule, Engine, Decision, ...
-```
-
----
-
-
-
-
-
-`AgentModule` 是 QitOS 的策略层。你通过继承它来定义智能体的状态形状、系统提示词、决策逻辑与归约规则;真正的执行循环由 `Engine`(执行内核)驱动。
-
-Signature reference (not executable):
-
-```text
-class AgentModule(ABC, Generic[StateT, ObservationT, ActionT])
-```
-
-**构造函数**
-
-Signature reference (not executable):
-
-```text
-def __init__(
- self,
- tool_registry: Any = None,
- llm: Any = None,
- model_parser: Any = None,
- memory: Memory | None = None,
- history: History | None = None,
- **config: Any,
-)
-```
-
-| 参数 | 类型 | 说明 |
-|---|---|---|
-| `tool_registry` | `ToolRegistry \| None` | 智能体可调用的工具注册表 |
-| `llm` | `Any` | 用于默认模型决策路径的大模型可调用对象 |
-| `model_parser` | `Any` | 解析器,把输出解析成 `Decision`(智能体每步的结构化决策) |
-| `memory` | `Memory \| None` | 可选记忆适配器 |
-| `history` | `History \| None` | 可选历史适配器 |
-| `**config` | `Any` | 额外关键字参数,保存在 `self.config` |
-
-**钩子**
-
-只要求实现 `init_state` 与 `reduce`;其余钩子全部可选。
-
-
-
-
-Signature reference (not executable):
-
-```text
-def init_state(self, task: str, **kwargs: Any) -> StateT
-```
-
-创建并返回本次运行的初始状态。
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def reduce(
- self,
- state: StateT,
- observation: ObservationT,
- decision: Decision[ActionT],
-) -> StateT
-```
-
-把当前观测结果(每步后智能体接收的结构化观察结果)与决策(智能体每步的结构化决策)折叠进下一步状态。
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def build_system_prompt(self, state: StateT) -> str | None
-```
-
-返回动态系统提示词;默认返回 `None`。
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def prepare(self, state: StateT) -> str
-```
-
-把状态转成模型输入文本;默认是 `str(state)`。
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def decide(
- self,
- state: StateT,
- observation: ObservationT,
-) -> Decision[ActionT] | None
-```
-
-自定义决策钩子。返回 `Decision` 时跳过默认模型调用;返回 `None` 时继续走 Engine 的模型路径。
-
-
-
-
-
-Signature reference (not executable):
-
-```text
-def should_stop(self, state: StateT) -> bool
-```
-
-额外停止条件;默认返回 `False`。
-
-
-
-
-**`.run()` 方法**
-
-Signature reference (not executable):
-
-```text
-def run(
- self,
- task: str | Task,
- return_state: bool = False,
- hooks: List[Any] | None = None,
- render_hooks: List[Any] | None = None,
- engine_kwargs: Dict[str, Any] | None = None,
- workspace: str | None = None,
- max_steps: int | None = None,
- env: Any = None,
- parser: Any = None,
- search: Any = None,
- critics: List[Any] | None = None,
- stop_criteria: List[Any] | None = None,
- history_policy: Any = None,
- trace: Any = None,
- render: Any = None,
- trace_logdir: str = "./runs",
- trace_prefix: str | None = None,
- theme: str = "research",
- **state_kwargs: Any,
-) -> Any
-```
-
-这是最常用入口。它会构建 `Engine`、执行任务,并默认返回 `state.final_result`;当 `return_state=True` 时,返回完整 `EngineResult`。
-
-
-
-
-
-`Engine` 是执行内核,负责阶段循环、工具执行、恢复、追踪与停止条件评估。
-
-Signature reference (not executable):
-
-```text
-class Engine(Generic[StateT, ObservationT, ActionT])
-```
-
-核心构造参数包括:
-
-- `agent`
-- `budget`(预算)
-- `parser`
-- `stop_criteria`
-- `critics`(评估器)
-- `env`
-- `history_policy`
-- `trace_writer`
-- `hooks`(钩子)
-
-最常用方法:
-
-Signature reference (not executable):
-
-```text
-def run(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
-```
-
-
-
-
-
-示意片段(非独立程序;完整执行文件见本页链接)。
-
-```python
-@dataclass
-class EngineResult(Generic[StateT]):
- state: StateT
- records: List[StepRecord]
- events: List[RuntimeEvent]
- step_count: int
- task_result: Optional[TaskResult] = None
-```
-
-其中:
-
-- `state`:最终强类型状态
-- `records`:每步 `StepRecord`
-- `events`:所有运行时事件
-- `step_count`:执行步数
-- `task_result`:结构化任务结果
-
-
-
-
-
-`AsyncEngine` 提供非阻塞的智能体执行能力。它封装了同样的 `Engine` 循环,但把阻塞调用放到线程池中执行,适用于 `asyncio` 事件循环。
-
-Signature reference (not executable):
-
-```text
-class AsyncEngine(Generic[StateT, ObservationT, ActionT])
-```
-
-**构造函数**
-
-Signature reference (not executable):
-
-```text
-def __init__(
- self,
- agent: AgentModule[StateT, ObservationT, ActionT],
- **engine_kwargs: Any,
-)
-```
-
-所有关键字参数会转发给内部的 `Engine` 构造函数(参数与 `Engine.__init__` 相同)。
-
-**方法**
-
-Signature reference (not executable):
-
-```text
-async def arun(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
-```
-
-异步执行智能体循环,返回与 `Engine.run()` 相同的 `EngineResult`。
-
----
-
-Signature reference (not executable):
-
-```text
-async def arun_stream(self, task: str | Task, **kwargs: Any) -> AsyncIterator[EngineEvent]
-```
-
-异步执行智能体循环,并实时产出 `EngineEvent` 对象。流以 `run_start` 开始,以 `run_end` 结束。
-
----
-
-Signature reference (not executable):
-
-```text
-def run(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
-```
-
-同步回退——委托给底层 `Engine.run()`。
-
-**属性**
-
-| 属性 | 类型 | 说明 |
-|------|------|------|
-| `engine` | `Engine` | 底层同步 Engine 实例 |
-| `agent` | `AgentModule` | 等同于 `engine.agent` |
-| `event_stream` | `EventStream \| None` | `arun_stream()` 运行期间的活动事件流,否则为 `None` |
-
-
-
-
-
-`EngineEvent` 是 `AsyncEngine.arun_stream()` 产出的结构化事件。
-
-示意片段(非独立程序;完整执行文件见本页链接)。
-
-```python
-class EngineEventType(str, Enum):
- STEP_START = "step_start"
- STEP_END = "step_end"
- DECIDE = "decide"
- ACT = "act"
- REDUCE = "reduce"
- CRITIC = "critic"
- CHECK_STOP = "check_stop"
- HANDOFF = "handoff"
- DELEGATE = "delegate"
- FANOUT = "fanout"
- ERROR = "error"
- RUN_START = "run_start"
- RUN_END = "run_end"
- # ... 完整列表见 EngineEventType
-```
-
-示意片段(非独立程序;完整执行文件见本页链接)。
-
-```python
-@dataclass
-class EngineEvent:
- event_type: EngineEventType
- step_id: int = 0
- agent_id: Optional[str] = None
- phase: Optional[RuntimePhase] = None
- ok: bool = True
- payload: Dict[str, Any] = field(default_factory=dict)
- error: Optional[str] = None
- ts: str
-```
-
-`EventStream` 是一个异步兼容的事件队列,用于消费引擎事件:
-
-Signature reference (not executable):
-
-```text
-class EventStream:
- def emit(self, event: EngineEvent) -> None # 线程安全发送
- def emit_sync(self, event: EngineEvent) -> None # 同步调用者别名
- def close(self) -> None # 发出流结束信号
- async def __aiter__(self) -> AsyncIterator[EngineEvent]
- def subscribe(self) -> asyncio.Queue # 扇出消费
-```
-
-
-
-
-
-`Decision` 是决策阶段的规范输出(智能体每步的结构化决策)。推荐用工厂方法构造:
-
-示意片段(非独立程序;完整执行文件见本页链接)。
-
-```python
-Decision.act(...)
-Decision.final(...)
-Decision.wait(...)
-Decision.branch(...)
-```
-
-四种模式分别对应:
-
-- `"act"`:执行动作(标准化工具调用)
-- `"final"`:给出最终答案并结束
-- `"wait"`:本步不执行动作
-- `"branch"`:提出多个候选决策
-
-
-
-
-
-常用基础数据结构包括:
-
-- `StateSchema`
-- `Task`
-- `TaskBudget`
-- `TaskResource`
-- `Action`
-- `StopReason`
-
-它们共同定义了一次运行的输入、状态与输出语义。
-
-当 Engine 已识别立即取消请求时,`StopReason.CANCELLED_IMMEDIATE` 会把
-`"cancelled_immediate"` 同步写入最终 State、任务/运行结果与 END event;对应的
-trace manifest 使用终态 `status="stopped"`,而不是正常完成状态。
-
-
-
-
+按用户任务查找当前公共 API。每个分类包含固定源码签名、参数类型与默认值、用法片段、行为边界,以及对应完整教程。并非所有符号都从 `qitos` 根包导出,请使用条目显示的准确 import。
+
+- [配置与组合](/zh/reference/composition)
+- [Agent 与执行](/zh/reference/agent-runtime)
+- [Session 与持久化](/zh/reference/sessions)
+- [工具、结果与 artifact](/zh/reference/tools)
+- [Context 与 memory 契约](/zh/reference/context)
+- [多 Agent 工作](/zh/reference/work-graph)
+- [Trajectory 与 reader](/zh/reference/trajectory)
+- [CLI](/zh/reference/cli)
+- [Configuration](/zh/reference/configuration)
+- [Extension index](/zh/reference/extensions)
+
+## 符号索引
+
+- [`qitos.config.AgentComposition`](/zh/reference/composition#qitos-config-agentcomposition)
+- [`qitos.config.build_agent_composition`](/zh/reference/composition#qitos-config-build_agent_composition)
+- [`qitos.config.load_agent_config`](/zh/reference/composition#qitos-config-load_agent_config)
+- [`qitos.config.AgentConfig`](/zh/reference/composition#qitos-config-agentconfig)
+- [`qitos.config.CredentialRef`](/zh/reference/composition#qitos-config-credentialref)
+- [`qitos.config.LocalCredentialFileResolver`](/zh/reference/composition#qitos-config-localcredentialfileresolver)
+- [`qitos.config.BudgetConfig`](/zh/reference/composition#qitos-config-budgetconfig)
+- [`qitos.config.EnvironmentConfig`](/zh/reference/composition#qitos-config-environmentconfig)
+- [`qitos.config.ModelConfig`](/zh/reference/composition#qitos-config-modelconfig)
+- [`qitos.config.RuntimeConfig`](/zh/reference/composition#qitos-config-runtimeconfig)
+- [`qitos.config.TrajectoryConfig`](/zh/reference/composition#qitos-config-trajectoryconfig)
+- [`qitos.AgentModule`](/zh/reference/agent-runtime#qitos-agentmodule)
+- [`qitos.StateSchema`](/zh/reference/agent-runtime#qitos-stateschema)
+- [`qitos.Task`](/zh/reference/agent-runtime#qitos-task)
+- [`qitos.Decision`](/zh/reference/agent-runtime#qitos-decision)
+- [`qitos.Action`](/zh/reference/agent-runtime#qitos-action)
+- [`qitos.Engine`](/zh/reference/agent-runtime#qitos-engine)
+- [`qitos.engine.engine.EngineResult`](/zh/reference/agent-runtime#qitos-engine-engine-engineresult)
+- [`qitos.engine.states.RuntimeBudget`](/zh/reference/agent-runtime#qitos-engine-states-runtimebudget)
+- [`qitos.StopReason`](/zh/reference/agent-runtime#qitos-stopreason)
+- [`qitos.engine.runtime.RuntimeComposition`](/zh/reference/agent-runtime#qitos-engine-runtime-runtimecomposition)
+- [`qitos.engine.session_runtime.Session`](/zh/reference/sessions#qitos-engine-session_runtime-session)
+- [`qitos.engine.session_runtime.SessionInspection`](/zh/reference/sessions#qitos-engine-session_runtime-sessioninspection)
+- [`qitos.checkpoint.store.CheckpointStore`](/zh/reference/sessions#qitos-checkpoint-store-checkpointstore)
+- [`qitos.ToolRegistry`](/zh/reference/tools#qitos-toolregistry)
+- [`qitos.core.function_tool_decorator.function_tool`](/zh/reference/tools#qitos-core-function_tool_decorator-function_tool)
+- [`qitos.core.tool.BaseTool`](/zh/reference/tools#qitos-core-tool-basetool)
+- [`qitos.core.tool_result.ToolResult`](/zh/reference/tools#qitos-core-tool_result-toolresult)
+- [`qitos.core.artifact.ArtifactRef`](/zh/reference/tools#qitos-core-artifact-artifactref)
+- [`qitos.engine.action_executor.ActionExecutionPolicy`](/zh/reference/tools#qitos-engine-action_executor-actionexecutionpolicy)
+- [`qitos.kit.tool.internal.publication.SandboxPublicationTool`](/zh/reference/tools#qitos-kit-tool-internal-publication-sandboxpublicationtool)
+- [`qitos.core.context.StaticContextContributor`](/zh/reference/context#qitos-core-context-staticcontextcontributor)
+- [`qitos.core.context.PriorityContextSelectionPolicy`](/zh/reference/context#qitos-core-context-prioritycontextselectionpolicy)
+- [`qitos.core.context.DeclaredContextBudgetPolicy`](/zh/reference/context#qitos-core-context-declaredcontextbudgetpolicy)
+- [`qitos.core.request_view.CompactionReceipt`](/zh/reference/context#qitos-core-request_view-compactionreceipt)
+- [`qitos.engine.runtime.LifecyclePolicy`](/zh/reference/context#qitos-engine-runtime-lifecyclepolicy)
+- [`qitos.engine.session_runtime.Session`](/zh/reference/work-graph#qitos-engine-session_runtime-session)
+- [`qitos.core.work_graph.WorkGraph`](/zh/reference/work-graph#qitos-core-work_graph-workgraph)
+- [`qitos.core.work_graph.WorkItem`](/zh/reference/work-graph#qitos-core-work_graph-workitem)
+- [`qitos.core.work_graph.WorkAttempt`](/zh/reference/work-graph#qitos-core-work_graph-workattempt)
+- [`qitos.engine.work_runtime.DurableWorkRuntime`](/zh/reference/work-graph#qitos-engine-work_runtime-durableworkruntime)
+- [`qitos.engine.work_runtime.LocalWorkScheduler`](/zh/reference/work-graph#qitos-engine-work_runtime-localworkscheduler)
+- [`qitos.engine.work_runtime.WorkRuntimeError`](/zh/reference/work-graph#qitos-engine-work_runtime-workruntimeerror)
+- [`qitos.qita.reader.default_reader`](/zh/reference/trajectory#qitos-qita-reader-default_reader)
+- [`qitos.tracing.exporter.CanonicalTrajectoryExporter`](/zh/reference/trajectory#qitos-tracing-exporter-canonicaltrajectoryexporter)
+- [`qitos.tracing.trajectory.PrivacyView`](/zh/reference/trajectory#qitos-tracing-trajectory-privacyview)
+
+## 兼容与历史
+
+[Migration](/zh/reference/g5-migration) · [Previous reference archive](/zh/reference/legacy-api)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/zh/reference/cli.mdx b/docs/zh/reference/cli.mdx
index 5d3f2f5c..729b845f 100644
--- a/docs/zh/reference/cli.mdx
+++ b/docs/zh/reference/cli.mdx
@@ -3,6 +3,24 @@ title: "CLI 参考"
description: "QitOS 的 qita 与 qit 命令行参考,包括最小编码智能体演示和 v0.3 中加入的基准测试官方入口。"
---
+当前推荐 `qit new` → canonical config → Python composition/Session。自定义 Python 工具不会由 CLI 自动导入。先读 Quickstart,再使用下面的 inspect 命令。旧 demo 是高级兼容入口。 [Quickstart](/zh/quickstart)
+
+## 当前 Session 命令
+
+```bash
+qit --help
+qita --help
+qit new --agent-name notes_agent --output-dir . --no-input
+qit session inspect --help
+qit session restore --help
+qit session fork --help
+qita inspect --help
+```
+
+inspect 无需模型或 Docker。真实 Session ID、matching config 及完整运行命令见生命周期教程。replay 与 board 启动本地服务器,需 Ctrl-C 停止后才能在同一终端执行后续命令。 [Session](/zh/tutorials/checkpoint-and-fork)
+
+
+
QitOS 提供两个顶层命令行工具:
- `qita`:追踪记录检查与回放
diff --git a/docs/zh/reference/composition.mdx b/docs/zh/reference/composition.mdx
new file mode 100644
index 00000000..3a9528de
--- /dev/null
+++ b/docs/zh/reference/composition.mdx
@@ -0,0 +1,478 @@
+---
+title: "配置与组合"
+description: "QitOS public API: composition"
+---
+
+先加载 canonical config;加载不读取 credentials、不创建 Docker。组合时显式解析 CredentialRef 或注入教学 provider。用 context manager 管理环境、store 和 scheduler。Session 方法返回公共 Session facade。ephemeral 不能 restore/fork;配置无效、缺少扩展或 composition 已关闭都不能继续执行。
+
+[完整可运行教程 / Complete tutorial](/zh/quickstart) · [API index](/zh/reference/api)
+
+以下签名和字段由固定源码提取;签名是参考,不是可直接运行的程序。每个条目的源码链接绑定同一 runtime baseline。类型中的 Any 不代表任意对象均受支持,应结合上述行为契约和教程使用。
+
+{/* api-reference:start */}
+
+
+## AgentComposition
+
+```python
+from qitos.config import AgentComposition
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L134)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+# After loading config and creating the explicit provider:
+with build_agent_composition(config, model_override=provider) as composition:
+ session = composition.session("Index the notes")
+ result = session.run()
+```
+
+```text
+Resource-owning composition root for the existing Engine and Session.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `config` | `AgentConfig` | `required` |
+| `model` | `Any` | `required` |
+| `tool_registry` | `ToolRegistry` | `required` |
+| `env` | `Any` | `required` |
+| `runtime` | `RuntimeComposition` | `required` |
+| `agent` | `ConfiguredAgent` | `required` |
+| `engine` | `Engine[Any, Any, Any]` | `required` |
+| `credential_receipt` | `Dict[str, Any]` | `required` |
+| `sandbox_backend` | `Any` | `None` |
+| `sandbox_receipt` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `trajectory_path` | `Optional[Path]` | `None` |
+
+
+### AgentComposition.session
+
+```text
+session(task: Optional[str]=None, *, session_id: Any=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `task` | `Optional[str]` | `None` |
+| `session_id` | `Any` | `None` |
+
+```text
+Create the existing durable Session from this composition.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L172)
+
+
+### AgentComposition.restore
+
+```text
+restore(session_id: Any=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `session_id` | `Any` | `None` |
+
+```text
+Restore with this composition's resolver registry and canonical Engine.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L188)
+
+
+### AgentComposition.fork
+
+```text
+fork(session_id: Any, snapshot: Any=None, *, operation_id: Optional[str]=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `session_id` | `Any` | `required` |
+| `snapshot` | `Any` | `None` |
+| `operation_id` | `Optional[str]` | `None` |
+
+```text
+Fork immutable persisted state without claiming the source owner.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L211)
+
+
+### AgentComposition.close
+
+```text
+close() -> Dict[str, Any]
+```
+
+```text
+Close every framework-owned resource once and return a stable receipt.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L222)
+
+
+
+## build_agent_composition
+
+```python
+from qitos.config import build_agent_composition
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/builder.py#L659)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+composition = build_agent_composition(config, model_override=provider)
+try:
+ result = composition.session("Index the notes").run()
+finally:
+ composition.close()
+```
+
+```text
+Compose the existing model/tools/Env/runtime/AgentModule/Engine stack.
+```
+
+```text
+build_agent_composition(config: AgentConfig, *, credential_resolver: Optional[CredentialResolver]=None, model_override: Any=None, env_override: Any=None, extensions: Optional[Mapping[str, Any]]=None) -> AgentComposition
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `config` | `AgentConfig` | `required` |
+| `credential_resolver` | `Optional[CredentialResolver]` | `None` |
+| `model_override` | `Any` | `None` |
+| `env_override` | `Any` | `None` |
+| `extensions` | `Optional[Mapping[str, Any]]` | `None` |
+
+
+
+## load_agent_config
+
+```python
+from qitos.config import load_agent_config
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L500)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+config = load_agent_config("agent.yaml")
+print(config.digest())
+```
+
+```text
+Load one strict canonical configuration from YAML.
+```
+
+```text
+load_agent_config(path: str | Path, *, compatibility: bool=False, environment_interpolation: bool=False) -> AgentConfig
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `path` | `str | Path` | `required` |
+| `compatibility` | `bool` | `False` |
+| `environment_interpolation` | `bool` | `False` |
+
+
+
+## AgentConfig
+
+```python
+from qitos.config import AgentConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L232)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+config = load_agent_config("agent.yaml")
+assert config.runtime.session.store == "sqlite"
+print(config.to_dict()["model"]["credential"])
+```
+
+```text
+The one canonical declarative agent launch configuration.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `name` | `str` | `'agent'` |
+| `max_steps` | `int` | `10` |
+| `model` | `ModelConfig` | `field(default_factory=ModelConfig)` |
+| `dataset` | `Sequence[DatasetItem]` | `field(default_factory=tuple)` |
+| `tools` | `Sequence[str]` | `field(default_factory=tuple)` |
+| `tool_preset` | `str` | `'none'` |
+| `tool_options` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `tool_use_policy` | `str` | `'auto'` |
+| `protocol` | `str` | `'auto'` |
+| `parser` | `str` | `'auto'` |
+| `environment` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `seed` | `int` | `0` |
+| `metadata` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `context` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `memory` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `compaction` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `lifecycle` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `failure_policy` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `runtime` | `RuntimeConfig` | `field(default_factory=RuntimeConfig)` |
+| `budgets` | `Optional[BudgetConfig]` | `None` |
+| `schema` | `str` | `CANONICAL_SCHEMA` |
+| `source` | `Mapping[str, Any]` | `field(default_factory=dict)` |
+| `compatibility` | `Sequence[Mapping[str, Any]]` | `field(default_factory=tuple)` |
+| `loss` | `Sequence[Mapping[str, Any]]` | `field(default_factory=tuple)` |
+
+
+### AgentConfig.to_dict
+
+```text
+to_dict() -> Dict[str, Any]
+```
+
+```text
+Return deterministic JSON/YAML-safe canonical launch data.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L294)
+
+
+### AgentConfig.digest
+
+```text
+digest() -> str
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L338)
+
+
+
+## CredentialRef
+
+```python
+from qitos.config import CredentialRef
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/credentials.py#L25)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+reference = CredentialRef("notes-provider")
+print(reference)
+```
+
+```text
+Serializable logical identity of one credential.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `ref` | `str` | `required` |
+
+
+
+## LocalCredentialFileResolver
+
+```python
+from qitos.config import LocalCredentialFileResolver
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/credentials.py#L113)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+from pathlib import Path
+resolver = LocalCredentialFileResolver(
+ Path.home() / ".config/qitos/credentials.yaml",
+ repository_root=Path.cwd(),
+)
+# Pass resolver as credential_resolver to build_agent_composition.
+```
+
+```text
+Resolve one logical credential from a hardened local YAML file.
+```
+
+```text
+LocalCredentialFileResolver(path: str | Path, *, repository_root: str | Path) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `path` | `str | Path` | `required` |
+| `repository_root` | `str | Path` | `required` |
+
+
+### LocalCredentialFileResolver.resolve
+
+```text
+resolve(ref: CredentialRef) -> CredentialResolution
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `ref` | `CredentialRef` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/credentials.py#L155)
+
+
+
+## BudgetConfig
+
+```python
+from qitos.config import BudgetConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L218)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+budget = BudgetConfig(max_steps=6, max_requests=6, max_runtime_seconds=30)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `max_steps` | `int` | `10` |
+| `max_runtime_seconds` | `float` | `600.0` |
+| `max_requests` | `int` | `12` |
+
+
+
+## EnvironmentConfig
+
+```python
+from qitos.config import EnvironmentConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L112)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+environment = EnvironmentConfig(workspace="./source", image="python:3.12-slim")
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `type` | `str` | `'docker'` |
+| `image` | `str` | `'python:3.12-slim'` |
+| `workspace` | `str` | `'.'` |
+| `container_workspace` | `str` | `'/workspace'` |
+| `network` | `str` | `'none'` |
+| `read_only_root` | `bool` | `True` |
+| `cap_drop` | `bool` | `True` |
+| `no_new_privileges` | `bool` | `True` |
+| `pids_limit` | `Optional[int]` | `256` |
+| `memory_mb` | `Optional[int]` | `2048` |
+| `cpus` | `Optional[float]` | `2.0` |
+| `cleanup_required` | `bool` | `True` |
+
+
+
+## ModelConfig
+
+```python
+from qitos.config import ModelConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L60)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+model = ModelConfig(provider="openai_compatible", model="notes-fake")
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `provider` | `str` | `'openai'` |
+| `model` | `str` | `''` |
+| `model_name` | `str` | `''` |
+| `credential` | `Optional[CredentialRef]` | `None` |
+| `base_url` | `str` | `''` |
+| `context_window` | `Optional[int]` | `None` |
+| `api_mode` | `str` | `'chat_completions'` |
+| `request` | `ModelRequestConfig` | `field(default_factory=ModelRequestConfig)` |
+| `api_key` | `str` | `field(default='', repr=False, compare=False)` |
+| `temperature` | `Optional[float]` | `None` |
+| `max_tokens` | `Optional[int]` | `None` |
+
+
+
+## RuntimeConfig
+
+```python
+from qitos.config import RuntimeConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L202)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+runtime = RuntimeConfig(environment=EnvironmentConfig(workspace="./source"))
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `environment` | `EnvironmentConfig` | `field(default_factory=EnvironmentConfig)` |
+| `session` | `SessionConfig` | `field(default_factory=SessionConfig)` |
+| `trajectory` | `TrajectoryConfig` | `field(default_factory=TrajectoryConfig)` |
+| `data_root` | `str` | `''` |
+
+
+
+## TrajectoryConfig
+
+```python
+from qitos.config import TrajectoryConfig
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/config/loader.py#L186)
+
+[用法与可执行示例](/zh/quickstart)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+trajectory = TrajectoryConfig(output="./notes-run/trajectory.journal")
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `enabled` | `bool` | `True` |
+| `output` | `str` | `''` |
+| `privacy` | `str` | `'private'` |
+| `failure_policy` | `str` | `'required'` |
+
+{/* api-reference:end */}
diff --git a/docs/zh/reference/configuration.mdx b/docs/zh/reference/configuration.mdx
index d83762ee..d2e72a7a 100644
--- a/docs/zh/reference/configuration.mdx
+++ b/docs/zh/reference/configuration.mdx
@@ -1,147 +1,274 @@
---
-title: "配置"
-description: "QitOS G5 · 配置"
+title: "配置真实模型"
+description: "Use the same notes project with explicit credentials and bounded requests."
---
-## 完整配置与依赖
+本章继续同一个资料项目。默认只校验配置;必须显式传 `--live` 才读取凭据并请求模型。本轮文档验收不会执行该选项。先完成 [Quickstart](/zh/quickstart) 安装;独立开始时,创建空目录并保存本页全部文件。
-先完成安装。真实客户端需要 `[openai]`,文件工具需要 Docker CLI、daemon 和镜像。
-将下方完整文件保存为 `agent.yaml`,修改示例模型与保留域名 endpoint;
-`provider.example` 仅为占位,不能连接。供应商的 tools、reasoning、continuation
-和 Responses 支持需分别核验,不能仅按“OpenAI-compatible”判定。
+## 依赖和配置
+
+```bash
+python -m pip install "qitos[openai] @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+```
+
+将 `real_agent.yaml` 的 `example-model` 和 `https://provider.example/v1` 替换为你的模型和 endpoint;此保留域名不是服务地址。本例使用受信任纯函数,不需要 Docker。若引入文件/命令工具,必须先按 sandbox 章节配置 Docker,不能直接扩展这里的 unsafe_host 工具权限。
+
+预算为 6 次模型请求、每次最多 512 输出 token、6 steps、30 秒 runtime;provider timeout 30 秒、retries 0。预算限制不承诺任务成功,timeout 也不是硬取消。请分别确认文本协议、工具往返、continuation 与 loss policy 兼容性。
+
+## 私有 credentials 文件
+
+```bash
+mkdir -p "$HOME/.config/qitos"
+chmod 700 "$HOME/.config/qitos"
+touch "$HOME/.config/qitos/credentials.yaml"
+chmod 600 "$HOME/.config/qitos/credentials.yaml"
+```
+
+用本地编辑器在上面的文件中填写以下结构。必须是当前用户拥有的普通非符号链接文件,父目录权限 0700、文件权限 0600。不要放进项目、Git、命令历史或截图。
```yaml
+credentials:
+ notes-provider: REPLACE_IN_PRIVATE_EDITOR
+```
+
+`model.credential.ref` 对应 `notes-provider`,配置只保存 CredentialRef。LocalCredentialFileResolver 在组合边界显式解析;环境变量不是默认路径。生成模板的 credentials 示例可能缺少所需根键,请使用这里的完整结构。
+
+## 先验证,再显式运行
+
+```bash
+python real_notes.py
+```
+
+```text
+configuration valid; no credentials read; no model request
+```
+
+下面的命令会产生真实模型请求,需你自己配置后执行。断言检查实际工具结果,不以模型自称完成为依据。
+
+```bash
+python real_notes.py --live --credentials "$HOME/.config/qitos/credentials.yaml"
+```
+
+本项目动态注册 `summarize_note`,因此启动入口是该 Python 文件。`qit run --config` 不会凭空导入你的扩展;仅使用内置工具的配置可使用 CLI,详见 CLI reference。
+
+## 常见错误、限制与练习
+
+未知配置键、权限不符、缺少引用、provider/codec 不兼容应保留 typed failure。不要把 key 写进 YAML 或放宽 loss 检查。连续运行会新建 Session;恢复必须使用匹配的配置与 resolver。练习:修改 YAML 中 request 的 max_tokens,再加载并读取该值;这一步不需要凭据。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```yaml title="agent.yaml"
schema: qitos.agent
agent:
- name: example-coding-agent
- protocol: auto
- parser: auto
- seed: 0
+ name: notes_agent
+ protocol: react_text_v1
model:
provider: openai_compatible
- model: example-model
- base_url: https://provider.example/v1
+ model: notes-fake
credential:
- ref: example-openai-compatible
- api_mode: chat_completions
- context_window: 32768
+ ref: notes-provider
request:
- temperature: 0.0
- max_tokens: 2048
- timeout_seconds: 180
- extra_body: {}
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
tools:
- preset: env_coding
- include: []
- options: {}
- policy: auto
+ preset: none
runtime:
environment:
- type: docker
- image: python:3.12-slim
+ type: unsafe_host
workspace: .
- container_workspace: /workspace
- network: none
- read_only_root: true
- cap_drop: true
- no_new_privileges: true
- pids_limit: 256
- memory_mb: 2048
- cpus: 2.0
- cleanup_required: true
session:
mode: durable
store: sqlite
- path: ./.qitos/example-sessions.sqlite3
+ path: ./notes-run/sessions.sqlite3
trajectory:
enabled: true
- output: ./runs
- privacy: private
- failure_policy: required
+ output: ./notes-run/trajectory.journal
budgets:
- max_steps: 10
- max_runtime_seconds: 600
- max_requests: 12
-context: {}
-memory: {}
-compaction: {}
-lifecycle:
- policy: cooperative
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
failure_policy:
tool: fail_closed
-metadata:
- purpose: provider-neutral-launch-example
-dataset:
- - task: Inspect the workspace and report one verified fact.
```
-## 私有凭据
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
-在仓库外创建目录,权限 `0700`;文件 `0600`,当前用户拥有,禁止 symlink。
-loader 验证文件所在的直接目录;建议整条私有目录链也禁止组/其他用户访问。
-使用本地编辑器填入密钥,不把真实值放入 shell 命令或 Git。
-生成器的 `credentials.example.yaml` 在 G5 缺少根节点,请使用下方完整结构。
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
-```bash
-mkdir -p "$HOME/.config/qitos"
-chmod 700 "$HOME/.config/qitos"
-touch "$HOME/.config/qitos/credentials.yaml"
-chmod 600 "$HOME/.config/qitos/credentials.yaml"
-```
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
-```yaml
-credentials:
- example-openai-compatible: REPLACE_IN_PRIVATE_EDITOR
-```
-上方 `model.credential.ref` 对应这里的键。`CredentialRef` 仅保存逻辑引用;
-显式 resolver 在 composition 边界解析秘密,不把它存进 config 或 Session。
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
-## 先检查,再运行
-本轮只验证加载,不执行以下真实 launch。`load_agent_config` 不读取凭据,
-不创建 provider 或 Docker。请求预算 `12`,每次输出上限 `2048` tokens,
-总步数 `10`、总时间 `600` 秒;超时不代表硬取消。
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
-配置加载校验(无模型请求):
+ def __init__(self, start=0):
+ self.stage = start
-```python
-from qitos.config import load_agent_config
-config = load_agent_config("agent.yaml")
-assert config.budgets.max_requests == 12
-print(config.digest())
-```
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
-```bash
-docker info
-docker pull python:3.12-slim
-qit run --config agent.yaml --credentials "$HOME/.config/qitos/credentials.yaml"
-```
-将下面保存为 `run_agent.py`,运行 `python run_agent.py`(会发真实请求):
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
-完整 run_agent.py(手动执行会发送真实模型请求;本轮不执行):
-```python
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="real_agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: example-model
+ base_url: https://provider.example/v1
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./real-notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./real-notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="real_notes.py"
+"""Validate configuration by default; --live explicitly opts into model requests."""
+import argparse
from pathlib import Path
+
+from notes import summarize_note
from qitos.config import LocalCredentialFileResolver, build_agent_composition, load_agent_config
-config = load_agent_config("agent.yaml")
-resolver = LocalCredentialFileResolver(
- Path.home() / ".config/qitos/credentials.yaml",
- repository_root=Path.cwd(),
-)
-with build_agent_composition(config, credential_resolver=resolver) as composition:
- session = composition.session("Inspect the workspace and report one verified fact.")
- result = session.run()
- print(session.session_id.value, result.state.stop_reason, result.state.final_result)
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--live", action="store_true")
+ parser.add_argument("--credentials", type=Path, default=Path.home() / ".config/qitos/credentials.yaml")
+ args = parser.parse_args()
+ config = load_agent_config(Path(__file__).with_name("real_agent.yaml"))
+ assert config.budgets.max_requests == 6
+ if not args.live:
+ print("configuration valid; no credentials read; no model request")
+ return
+ if config.model.base_url == "https://provider.example/v1":
+ raise ValueError("Replace the reserved provider.example endpoint and example-model first")
+ resolver = LocalCredentialFileResolver(args.credentials, repository_root=Path.cwd())
+ with build_agent_composition(config, credential_resolver=resolver) as composition:
+ composition.tool_registry.register(summarize_note)
+ result = composition.session(
+ "Call summarize_note for indices 0 and 1. Report both titles and word counts."
+ ).run()
+ print(composition.config.name, result.state.stop_reason, result.state.final_result)
+ outputs = [a.output for record in result.records for a in record.action_results
+ if a.tool_name == "summarize_note" and a.status == "success"]
+ assert {item["title"] for item in outputs} == {"Session", "Artifact"}, outputs
+
+
+if __name__ == "__main__":
+ main()
```
-## 失败、兼容与扩展
+{/* tutorial-files:end */}
+
+[Composition API](/zh/reference/composition) · [Sandbox](/zh/guides/sandbox-and-artifacts) · [CLI](/zh/reference/cli) · [Next: extensions](/zh/guides/third-party-extensions)
-未知/重复键、类型不匹配、YAML tag、环境插值都 fail closed。
-`failure_policy` 当前只接受 `tool: continue` 或 `tool: fail_closed`。
-扩展名必须由 Python `extensions` 显式注册,CLI 不导入任意配置指定代码。
-`EnvironmentCredentialResolver` 是高级部署兼容适配,不是默认入门路径。
-`AgentModule.run()` 和直接 Engine 仍用于高级程序化控制;有 host 工具时不能声称隔离。
-[下一步:Session](/zh/tutorials/checkpoint-and-fork)。
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/real_notes.py)
diff --git a/docs/zh/reference/context.mdx b/docs/zh/reference/context.mdx
new file mode 100644
index 00000000..4cf2219b
--- /dev/null
+++ b/docs/zh/reference/context.mdx
@@ -0,0 +1,187 @@
+---
+title: "Context 与 memory 契约"
+description: "QitOS public API: context"
+---
+
+通过显式扩展名注册 contributor、selector、budget policy 与 compactor。context 选择不授予工具权限。CompactionReceipt 声明输入、输出身份和损失,省略不是无损摘要。memory 输入不是 checkpoint store。缺失工厂在组合时失败;可执行教程展示方法参数及实际有损 compaction。
+
+[完整可运行教程 / Complete tutorial](/zh/guides/memory-and-history) · [API index](/zh/reference/api)
+
+以下签名和字段由固定源码提取;签名是参考,不是可直接运行的程序。每个条目的源码链接绑定同一 runtime baseline。类型中的 Any 不代表任意对象均受支持,应结合上述行为契约和教程使用。
+
+{/* api-reference:start */}
+
+
+## StaticContextContributor
+
+```python
+from qitos.core.context import StaticContextContributor
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/context.py#L300)
+
+[用法与可执行示例](/zh/guides/memory-and-history)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+contributor = StaticContextContributor("notes.project", "project", "Use supplied notes only.")
+```
+
+```text
+Small reusable contributor for project/user/session context.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `contributor_id` | `str` | `required` |
+| `source` | `str` | `required` |
+| `value` | `Any` | `field(repr=False)` |
+| `priority` | `int` | `0` |
+| `requested_placement` | `str` | `'developer'` |
+| `required` | `bool` | `False` |
+| `persistence_horizon` | `str` | `'request'` |
+| `sensitivity` | `str` | `'internal'` |
+
+
+
+## PriorityContextSelectionPolicy
+
+```python
+from qitos.core.context import PriorityContextSelectionPolicy
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/context.py#L118)
+
+[用法与可执行示例](/zh/guides/memory-and-history)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+class AuditedSelector(PriorityContextSelectionPolicy):
+ def select(self, contributions, **options):
+ contributions = tuple(contributions)
+ print([item.contribution_id for item in contributions])
+ return super().select(contributions, **options)
+```
+
+```text
+Default deterministic priority/identity selection policy.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `policy_id` | `str` | `'qitos.context.priority/v1'` |
+
+
+### PriorityContextSelectionPolicy.select
+
+```text
+select(contributions: Iterable[ContextContribution], *, budget: ContextBudget, already_used_units: int=0, counter: UnitCounter=default_unit_counter) -> ContextSelection
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `contributions` | `Iterable[ContextContribution]` | `required` |
+| `budget` | `ContextBudget` | `required` |
+| `already_used_units` | `int` | `0` |
+| `counter` | `UnitCounter` | `default_unit_counter` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/context.py#L123)
+
+
+
+## DeclaredContextBudgetPolicy
+
+```python
+from qitos.core.context import DeclaredContextBudgetPolicy
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/context.py#L187)
+
+[用法与可执行示例](/zh/guides/memory-and-history)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+policy = DeclaredContextBudgetPolicy(default_max_input_units=4096)
+```
+
+```text
+Use adapter-declared capacity; never infer it from a model name.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `default_max_input_units` | `int` | `120000` |
+| `unit` | `str` | `'characters'` |
+| `protected_recent_exchanges` | `int` | `1` |
+| `policy_id` | `str` | `'qitos.context.declared_budget/v1'` |
+
+
+
+## CompactionReceipt
+
+```python
+from qitos.core.request_view import CompactionReceipt
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/request_view.py#L525)
+
+[用法与可执行示例](/zh/guides/memory-and-history)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+# receipt is returned by the compactor in the context tutorial.
+print(receipt.declared_losses, receipt.output_digest)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `receipt_id` | `str` | `required` |
+| `input_exchange_ids` | `tuple[str, ...]` | `required` |
+| `output_digest` | `str` | `required` |
+| `policy_id` | `str` | `required` |
+| `declared_losses` | `tuple[str, ...]` | `()` |
+| `model_reference` | `Optional[str]` | `None` |
+
+
+
+## LifecyclePolicy
+
+```python
+from qitos.engine.runtime import LifecyclePolicy
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/runtime.py#L88)
+
+[用法与可执行示例](/zh/guides/memory-and-history)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+class PauseFirstTool(LifecyclePolicy):
+ policy_id = "notes.pause"
+ def should_pause(self, context):
+ return context.step_id == 0
+```
+
+```text
+Replaceable cooperative lifecycle policy; never a worker scheduler.
+```
+
+
+### LifecyclePolicy.should_pause
+
+```text
+should_pause(context: RuntimeSnapshotContext) -> bool
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `context` | `RuntimeSnapshotContext` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/runtime.py#L94)
+
+{/* api-reference:end */}
diff --git a/docs/zh/reference/extensions.mdx b/docs/zh/reference/extensions.mdx
new file mode 100644
index 00000000..53406b9a
--- /dev/null
+++ b/docs/zh/reference/extensions.mdx
@@ -0,0 +1,17 @@
+---
+title: "第三方扩展索引"
+description: "Replace mechanisms while preserving their public contracts."
+---
+
+这里列出当前可替换的框架机制,不是全包符号清单或跨版本永久稳定保证。用显式 Python 工厂装配扩展;不要让配置任意导入代码。默认示例只有纯函数工具和 fake provider,其他客户端依赖请查安装页。
+
+| 扩展 | 公共入口 | 必须保留的契约 |
+| --- | --- | --- |
+| Provider | `qitos.models.base.Model`、`ModelFactory`;composition 的 `model_override` | 模型请求/响应、codec、continuation、typed failure;仅对应客户端需要其 extra |
+| Tool | `qitos.core.tool.BaseTool.execute`、`ToolRegistry` | schema、权限、副作用声明、ToolResult、取消与未知结果 |
+| Checkpoint store | `qitos.checkpoint.store.CheckpointStore` | Session capability、snapshot commit CAS、lineage、fork 与持久性;SQLite 为参考实现 |
+| Event sink | `qitos.tracing.sinks.EventSink` | privacy view、loss、required/optional 失败策略、append/close 契约 |
+| Sandbox | `qitos.core.env.Env` | capability preflight、有界工具、真实隔离、artifact、cleanup;publication 单独授权 |
+| Evaluator | `qitos.evaluate.base.TrajectoryEvaluator.evaluate` | EvaluationContext 到 EvaluationResult、证据、provenance 与 loss |
+
+[Complete provider/context extension](/zh/guides/third-party-extensions) · [API index](/zh/reference/api)
diff --git a/docs/zh/reference/legacy-api.mdx b/docs/zh/reference/legacy-api.mdx
new file mode 100644
index 00000000..3b912904
--- /dev/null
+++ b/docs/zh/reference/legacy-api.mdx
@@ -0,0 +1,392 @@
+---
+title: "旧版 API 参考归档"
+description: "Historical reference text; use the current API index."
+---
+
+以下为此前页面的历史文字,签名、默认值和导出清单可能不匹配当前源码;不可作为当前 API 契约。 [Current API](/zh/reference/api)
+
+
+
+本页列出的符号都可以直接这样导入:
+
+Signature reference (not executable):
+
+```text
+from qitos import AgentModule, Engine, Decision, ...
+```
+
+---
+
+
+
+
+
+`AgentModule` 是 QitOS 的策略层。你通过继承它来定义智能体的状态形状、系统提示词、决策逻辑与归约规则;真正的执行循环由 `Engine`(执行内核)驱动。
+
+Signature reference (not executable):
+
+```text
+class AgentModule(ABC, Generic[StateT, ObservationT, ActionT])
+```
+
+**构造函数**
+
+Signature reference (not executable):
+
+```text
+def __init__(
+ self,
+ tool_registry: Any = None,
+ llm: Any = None,
+ model_parser: Any = None,
+ memory: Memory | None = None,
+ history: History | None = None,
+ **config: Any,
+)
+```
+
+| 参数 | 类型 | 说明 |
+|---|---|---|
+| `tool_registry` | `ToolRegistry \| None` | 智能体可调用的工具注册表 |
+| `llm` | `Any` | 用于默认模型决策路径的大模型可调用对象 |
+| `model_parser` | `Any` | 解析器,把输出解析成 `Decision`(智能体每步的结构化决策) |
+| `memory` | `Memory \| None` | 可选记忆适配器 |
+| `history` | `History \| None` | 可选历史适配器 |
+| `**config` | `Any` | 额外关键字参数,保存在 `self.config` |
+
+**钩子**
+
+只要求实现 `init_state` 与 `reduce`;其余钩子全部可选。
+
+
+
+
+Signature reference (not executable):
+
+```text
+def init_state(self, task: str, **kwargs: Any) -> StateT
+```
+
+创建并返回本次运行的初始状态。
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def reduce(
+ self,
+ state: StateT,
+ observation: ObservationT,
+ decision: Decision[ActionT],
+) -> StateT
+```
+
+把当前观测结果(每步后智能体接收的结构化观察结果)与决策(智能体每步的结构化决策)折叠进下一步状态。
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def build_system_prompt(self, state: StateT) -> str | None
+```
+
+返回动态系统提示词;默认返回 `None`。
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def prepare(self, state: StateT) -> str
+```
+
+把状态转成模型输入文本;默认是 `str(state)`。
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def decide(
+ self,
+ state: StateT,
+ observation: ObservationT,
+) -> Decision[ActionT] | None
+```
+
+自定义决策钩子。返回 `Decision` 时跳过默认模型调用;返回 `None` 时继续走 Engine 的模型路径。
+
+
+
+
+
+Signature reference (not executable):
+
+```text
+def should_stop(self, state: StateT) -> bool
+```
+
+额外停止条件;默认返回 `False`。
+
+
+
+
+**`.run()` 方法**
+
+Signature reference (not executable):
+
+```text
+def run(
+ self,
+ task: str | Task,
+ return_state: bool = False,
+ hooks: List[Any] | None = None,
+ render_hooks: List[Any] | None = None,
+ engine_kwargs: Dict[str, Any] | None = None,
+ workspace: str | None = None,
+ max_steps: int | None = None,
+ env: Any = None,
+ parser: Any = None,
+ search: Any = None,
+ critics: List[Any] | None = None,
+ stop_criteria: List[Any] | None = None,
+ history_policy: Any = None,
+ trace: Any = None,
+ render: Any = None,
+ trace_logdir: str = "./runs",
+ trace_prefix: str | None = None,
+ theme: str = "research",
+ **state_kwargs: Any,
+) -> Any
+```
+
+这是最常用入口。它会构建 `Engine`、执行任务,并默认返回 `state.final_result`;当 `return_state=True` 时,返回完整 `EngineResult`。
+
+
+
+
+
+`Engine` 是执行内核,负责阶段循环、工具执行、恢复、追踪与停止条件评估。
+
+Signature reference (not executable):
+
+```text
+class Engine(Generic[StateT, ObservationT, ActionT])
+```
+
+核心构造参数包括:
+
+- `agent`
+- `budget`(预算)
+- `parser`
+- `stop_criteria`
+- `critics`(评估器)
+- `env`
+- `history_policy`
+- `trace_writer`
+- `hooks`(钩子)
+
+最常用方法:
+
+Signature reference (not executable):
+
+```text
+def run(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
+```
+
+
+
+
+
+示意片段(非独立程序;完整执行文件见本页链接)。
+
+```python
+@dataclass
+class EngineResult(Generic[StateT]):
+ state: StateT
+ records: List[StepRecord]
+ events: List[RuntimeEvent]
+ step_count: int
+ task_result: Optional[TaskResult] = None
+```
+
+其中:
+
+- `state`:最终强类型状态
+- `records`:每步 `StepRecord`
+- `events`:所有运行时事件
+- `step_count`:执行步数
+- `task_result`:结构化任务结果
+
+
+
+
+
+`AsyncEngine` 提供非阻塞的智能体执行能力。它封装了同样的 `Engine` 循环,但把阻塞调用放到线程池中执行,适用于 `asyncio` 事件循环。
+
+Signature reference (not executable):
+
+```text
+class AsyncEngine(Generic[StateT, ObservationT, ActionT])
+```
+
+**构造函数**
+
+Signature reference (not executable):
+
+```text
+def __init__(
+ self,
+ agent: AgentModule[StateT, ObservationT, ActionT],
+ **engine_kwargs: Any,
+)
+```
+
+所有关键字参数会转发给内部的 `Engine` 构造函数(参数与 `Engine.__init__` 相同)。
+
+**方法**
+
+Signature reference (not executable):
+
+```text
+async def arun(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
+```
+
+异步执行智能体循环,返回与 `Engine.run()` 相同的 `EngineResult`。
+
+---
+
+Signature reference (not executable):
+
+```text
+async def arun_stream(self, task: str | Task, **kwargs: Any) -> AsyncIterator[EngineEvent]
+```
+
+异步执行智能体循环,并实时产出 `EngineEvent` 对象。流以 `run_start` 开始,以 `run_end` 结束。
+
+---
+
+Signature reference (not executable):
+
+```text
+def run(self, task: str | Task, **kwargs: Any) -> EngineResult[StateT]
+```
+
+同步回退——委托给底层 `Engine.run()`。
+
+**属性**
+
+| 属性 | 类型 | 说明 |
+|------|------|------|
+| `engine` | `Engine` | 底层同步 Engine 实例 |
+| `agent` | `AgentModule` | 等同于 `engine.agent` |
+| `event_stream` | `EventStream \| None` | `arun_stream()` 运行期间的活动事件流,否则为 `None` |
+
+
+
+
+
+`EngineEvent` 是 `AsyncEngine.arun_stream()` 产出的结构化事件。
+
+示意片段(非独立程序;完整执行文件见本页链接)。
+
+```python
+class EngineEventType(str, Enum):
+ STEP_START = "step_start"
+ STEP_END = "step_end"
+ DECIDE = "decide"
+ ACT = "act"
+ REDUCE = "reduce"
+ CRITIC = "critic"
+ CHECK_STOP = "check_stop"
+ HANDOFF = "handoff"
+ DELEGATE = "delegate"
+ FANOUT = "fanout"
+ ERROR = "error"
+ RUN_START = "run_start"
+ RUN_END = "run_end"
+ # ... 完整列表见 EngineEventType
+```
+
+示意片段(非独立程序;完整执行文件见本页链接)。
+
+```python
+@dataclass
+class EngineEvent:
+ event_type: EngineEventType
+ step_id: int = 0
+ agent_id: Optional[str] = None
+ phase: Optional[RuntimePhase] = None
+ ok: bool = True
+ payload: Dict[str, Any] = field(default_factory=dict)
+ error: Optional[str] = None
+ ts: str
+```
+
+`EventStream` 是一个异步兼容的事件队列,用于消费引擎事件:
+
+Signature reference (not executable):
+
+```text
+class EventStream:
+ def emit(self, event: EngineEvent) -> None # 线程安全发送
+ def emit_sync(self, event: EngineEvent) -> None # 同步调用者别名
+ def close(self) -> None # 发出流结束信号
+ async def __aiter__(self) -> AsyncIterator[EngineEvent]
+ def subscribe(self) -> asyncio.Queue # 扇出消费
+```
+
+
+
+
+
+`Decision` 是决策阶段的规范输出(智能体每步的结构化决策)。推荐用工厂方法构造:
+
+示意片段(非独立程序;完整执行文件见本页链接)。
+
+```python
+Decision.act(...)
+Decision.final(...)
+Decision.wait(...)
+Decision.branch(...)
+```
+
+四种模式分别对应:
+
+- `"act"`:执行动作(标准化工具调用)
+- `"final"`:给出最终答案并结束
+- `"wait"`:本步不执行动作
+- `"branch"`:提出多个候选决策
+
+
+
+
+
+常用基础数据结构包括:
+
+- `StateSchema`
+- `Task`
+- `TaskBudget`
+- `TaskResource`
+- `Action`
+- `StopReason`
+
+它们共同定义了一次运行的输入、状态与输出语义。
+
+当 Engine 已识别立即取消请求时,`StopReason.CANCELLED_IMMEDIATE` 会把
+`"cancelled_immediate"` 同步写入最终 State、任务/运行结果与 END event;对应的
+trace manifest 使用终态 `status="stopped"`,而不是正常完成状态。
+
+
+
+
diff --git a/docs/zh/reference/sessions.mdx b/docs/zh/reference/sessions.mdx
new file mode 100644
index 00000000..5748c74c
--- /dev/null
+++ b/docs/zh/reference/sessions.mdx
@@ -0,0 +1,242 @@
+---
+title: "Session 与持久化"
+description: "QitOS public API: sessions"
+---
+
+通过 composition.session/restore/fork 或 Engine.session 获取 Session,不直接构造。run(steering=...) 执行并返回 EngineResult,inspect 返回 SessionInspection。pause 请求协作边界并返回 receipt,不保证立即终止。Session、Run、Snapshot/Checkpoint 身份不同。SQLite 支持跨进程恢复,Memory 仅进程内。未解决 approval 的恢复和 CLI live pause/steer 当前 unsupported。
+
+[完整可运行教程 / Complete tutorial](/zh/tutorials/checkpoint-and-fork) · [API index](/zh/reference/api)
+
+以下签名和字段由固定源码提取;签名是参考,不是可直接运行的程序。每个条目的源码链接绑定同一 runtime baseline。类型中的 Any 不代表任意对象均受支持,应结合上述行为契约和教程使用。
+
+{/* api-reference:start */}
+
+
+## Session
+
+```python
+from qitos.engine.session_runtime import Session
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L119)
+
+[用法与可执行示例](/zh/tutorials/checkpoint-and-fork)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+session = composition.session("Index notes")
+result = session.run()
+inspection = session.inspect()
+print(session.session_id.value, result.state.final_result)
+```
+
+```text
+Scoped client for one durable Session identity.
+
+The facade stores identifiers and cooperative control only. Agent state is
+reconstructed from the canonical snapshot and executed by ``Engine.run``.
+```
+
+```text
+Session(*, engine: 'Engine[Any, Any, Any]', session_id: SessionIdentity, run_id: RunIdentity, agent_id: AgentIdentity, references: Iterable[ResolverReference], created_at: str, state_type: type[StateSchema], work_item_id: WorkItemIdentity, attempt_id: AttemptIdentity, fork_receipt: Optional[SessionForkReceipt]=None) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `engine` | `'Engine[Any, Any, Any]'` | `required` |
+| `session_id` | `SessionIdentity` | `required` |
+| `run_id` | `RunIdentity` | `required` |
+| `agent_id` | `AgentIdentity` | `required` |
+| `references` | `Iterable[ResolverReference]` | `required` |
+| `created_at` | `str` | `required` |
+| `state_type` | `type[StateSchema]` | `required` |
+| `work_item_id` | `WorkItemIdentity` | `required` |
+| `attempt_id` | `AttemptIdentity` | `required` |
+| `fork_receipt` | `Optional[SessionForkReceipt]` | `None` |
+
+
+### Session.run
+
+```text
+run(*, steering: Optional[str]=None) -> 'EngineResult[Any]'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `steering` | `Optional[str]` | `None` |
+
+```text
+Run or resume through the one canonical Engine loop.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1501)
+
+
+### Session.inspect
+
+```text
+inspect() -> SessionInspection
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L219)
+
+
+### Session.pause
+
+```text
+pause() -> PauseReceipt
+```
+
+```text
+Request cooperative pause; durable status is returned at a boundary.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L291)
+
+
+### Session.steer
+
+```text
+steer(text: str) -> SteeringReceipt
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `text` | `str` | `required` |
+
+```text
+Durably submit one canonical steering item to this Session.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L325)
+
+
+### Session.fork
+
+```text
+fork(snapshot: SessionSnapshot | SnapshotIdentity | str | None=None, *, operation_id: Optional[str]=None) -> 'Session'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `snapshot` | `SessionSnapshot | SnapshotIdentity | str | None` | `None` |
+| `operation_id` | `Optional[str]` | `None` |
+
+```text
+Create an isolated durable child from one verified immutable snapshot.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1713)
+
+
+### Session.capabilities
+
+```text
+capabilities() -> frozenset[str]
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L189)
+
+
+
+## SessionInspection
+
+```python
+from qitos.engine.session_runtime import SessionInspection
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L105)
+
+[用法与可执行示例](/zh/tutorials/checkpoint-and-fork)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+inspection = session.inspect()
+print(inspection.work_graph)
+```
+
+```text
+Read-only inspection result backed by the current durable head.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `head` | `SessionHead` | `required` |
+| `lifecycle` | `SessionLifecycle` | `required` |
+| `capabilities` | `tuple[str, ...]` | `required` |
+| `snapshot_integrity` | `str` | `required` |
+| `budget` | `Mapping[str, Any]` | `required` |
+| `work_graph` | `Optional[Mapping[str, Any]]` | `required` |
+| `last_request_view` | `Optional[RequestView]` | `required` |
+| `task` | `str` | `required` |
+| `tool_batch` | `Optional[ToolBatchSnapshot]` | `required` |
+
+
+
+## CheckpointStore
+
+```python
+from qitos.checkpoint.store import CheckpointStore
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/checkpoint/store.py#L174)
+
+[用法与可执行示例](/zh/tutorials/checkpoint-and-fork)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+# From an open composition configured with SQLite:
+store = composition.runtime.checkpoint_store
+head = store.get_session_head(session.session_id.value)
+print(head)
+```
+
+```text
+Abstract base class for checkpoint persistence.
+
+Borrowed from LangGraph's ``BaseCheckpointSaver`` interface
+(``references/langgraph/libs/checkpoint/langgraph/checkpoint/base/__init__.py``).
+
+Every method has both sync and async variants. Subclasses should
+override the async variants; the sync ones delegate via ``asyncio.run``
+by default.
+```
+
+
+### CheckpointStore.get_session_head
+
+```text
+get_session_head(session_id: str) -> Optional[SessionHeadRecord]
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `session_id` | `str` | `required` |
+
+```text
+Read the current mutable head for one session.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/checkpoint/store.py#L241)
+
+
+### CheckpointStore.commit_session_snapshot
+
+```text
+commit_session_snapshot(request: SessionSnapshotCommit) -> SessionCommitReceipt
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `request` | `SessionSnapshotCommit` | `required` |
+
+```text
+Atomically persist an immutable snapshot and advance its head.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/checkpoint/store.py#L235)
+
+{/* api-reference:end */}
diff --git a/docs/zh/reference/tools.mdx b/docs/zh/reference/tools.mdx
new file mode 100644
index 00000000..0cf52144
--- /dev/null
+++ b/docs/zh/reference/tools.mdx
@@ -0,0 +1,356 @@
+---
+title: "工具、结果与 artifact"
+description: "QitOS public API: tools"
+---
+
+运行前注册工具。类工具契约是 execute(args, runtime_context),run 仅兼容。检查 ToolResult 的 status、error_code、output、artifact_refs、outcome_unknown 和 worker_still_running,不能只看文本。并行需要真实的并发安全声明;完成顺序不等于声明顺序。publication 需显式授权并受平台和文件形状限制,cleanup 不发布。SandboxPublicationTool 虽位于 internal 模块,但在本教程作为显式注册的高级适配器使用,不是默认工具。
+
+[完整可运行教程 / Complete tutorial](/zh/concepts/tools-and-registry) · [API index](/zh/reference/api)
+
+以下签名和字段由固定源码提取;签名是参考,不是可直接运行的程序。每个条目的源码链接绑定同一 runtime baseline。类型中的 Any 不代表任意对象均受支持,应结合上述行为契约和教程使用。
+
+{/* api-reference:start */}
+
+
+## ToolRegistry
+
+```python
+from qitos import ToolRegistry
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool_registry.py#L20)
+
+[用法与可执行示例](/zh/concepts/tools-and-registry)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+registry = ToolRegistry()
+registry.register(summarize_note)
+```
+
+```text
+Registry for function tools, bound methods, tool objects, and ToolSets.
+```
+
+```text
+ToolRegistry(*, auto_short_aliases: bool=True) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `auto_short_aliases` | `bool` | `True` |
+
+
+### ToolRegistry.register
+
+```text
+register(item: Any, name: Optional[str]=None, meta: Optional[ToolMeta]=None) -> 'ToolRegistry'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `item` | `Any` | `required` |
+| `name` | `Optional[str]` | `None` |
+| `meta` | `Optional[ToolMeta]` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool_registry.py#L32)
+
+
+
+## function_tool
+
+```python
+from qitos.core.function_tool_decorator import function_tool
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/function_tool_decorator.py#L11)
+
+[用法与可执行示例](/zh/concepts/tools-and-registry)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+@function_tool(read_only=True, concurrency_safe=True)
+def title(text: str) -> str:
+ return text.split(":", 1)[0]
+```
+
+```text
+Decorator that creates a :class:`FunctionTool` from a plain function.
+
+Can be used with or without parentheses::
+
+ @function_tool
+ def greet(name: str) -> str: ...
+
+ @function_tool(name="custom", needs_approval=True)
+ def greet(name: str) -> str: ...
+
+Returns a :class:`FunctionTool` instance.
+```
+
+```text
+function_tool(func: Optional[Callable[..., Any]]=None, *, name: Optional[str]=None, description: Optional[str]=None, timeout_s: Optional[float]=None, max_retries: int=0, retry_policy: Optional[RetryPolicy]=None, on_failure: Optional[Callable]=None, read_only: bool=False, concurrency_safe: Optional[bool]=None, needs_approval: bool=False, **extra_meta: Any) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `func` | `Optional[Callable[..., Any]]` | `None` |
+| `name` | `Optional[str]` | `None` |
+| `description` | `Optional[str]` | `None` |
+| `timeout_s` | `Optional[float]` | `None` |
+| `max_retries` | `int` | `0` |
+| `retry_policy` | `Optional[RetryPolicy]` | `None` |
+| `on_failure` | `Optional[Callable]` | `None` |
+| `read_only` | `bool` | `False` |
+| `concurrency_safe` | `Optional[bool]` | `None` |
+| `needs_approval` | `bool` | `False` |
+
+
+
+## BaseTool
+
+```python
+from qitos.core.tool import BaseTool
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool.py#L611)
+
+[用法与可执行示例](/zh/concepts/tools-and-registry)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+# Class tools implement execute, not the compatibility run method.
+class EchoTool(BaseTool):
+ name = "echo"
+ description = "Return a trusted input"
+ def execute(self, args, runtime_context=None):
+ return ToolResult(output=args)
+```
+
+```text
+Base abstraction for callable tools.
+```
+
+```text
+BaseTool(spec: ToolSpec) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `spec` | `ToolSpec` | `required` |
+
+
+### BaseTool.execute
+
+```text
+execute(args: Dict[str, Any], runtime_context: Optional[Dict[str, Any]]=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `args` | `Dict[str, Any]` | `required` |
+| `runtime_context` | `Optional[Dict[str, Any]]` | `None` |
+
+```text
+Execute tool with optional runtime context.
+```
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool.py#L706)
+
+
+
+## ToolResult
+
+```python
+from qitos.core.tool_result import ToolResult
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/tool_result.py#L547)
+
+[用法与可执行示例](/zh/concepts/tools-and-registry)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+result = ToolResult(output={"title": "Session"})
+print(result.status, result.output, result.outcome_unknown)
+```
+
+```text
+Lossless terminal outcome for one declared action/tool slot.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `status` | `ToolResultStatus` | `'success'` |
+| `output` | `Any` | `None` |
+| `error` | `str \| None` | `None` |
+| `metadata` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `tool_name` | `str \| None` | `None` |
+| `action_id` | `str \| None` | `None` |
+| `model_output` | `Any` | `None` |
+| `error_kind` | `ToolErrorKind \| None` | `None` |
+| `error_code` | `str \| None` | `None` |
+| `recoverable` | `bool` | `False` |
+| `recovery_hint` | `str \| None` | `None` |
+| `next_action` | `Dict[str, Any] \| None` | `None` |
+| `complete` | `bool` | `True` |
+| `truncated` | `bool` | `False` |
+| `omitted` | `Dict[str, int]` | `field(default_factory=dict)` |
+| `attempts` | `int` | `1` |
+| `latency_ms` | `float` | `0.0` |
+| `declared_effects` | `list[Dict[str, Any]]` | `field(default_factory=list)` |
+| `filesystem_changes` | `list[Dict[str, Any]]` | `field(default_factory=list)` |
+| `artifact_refs` | `tuple[ArtifactRef, ...]` | `()` |
+| `normalized_request` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `provenance` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `worker_still_running` | `bool` | `False` |
+| `attempt_id` | `AttemptIdentity \| None` | `None` |
+| `effect_ref` | `str \| None` | `None` |
+| `effect_state` | `EffectState` | `'no_effect_declared'` |
+| `idempotency_ref` | `str \| None` | `None` |
+| `retry_disposition` | `RetryDisposition` | `'not_evaluated'` |
+| `reconciliation_required` | `bool` | `False` |
+| `outcome_unknown` | `bool` | `False` |
+| `late_result` | `bool` | `False` |
+| `owner_generation` | `int \| None` | `None` |
+| `stale_owner` | `bool` | `False` |
+| `batch_closure` | `Dict[str, Any]` | `field(default_factory=dict)` |
+| `schema_version` | `str` | `TOOL_RESULT_SCHEMA_VERSION` |
+
+
+
+## ArtifactRef
+
+```python
+from qitos.core.artifact import ArtifactRef
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/artifact.py#L78)
+
+[用法与可执行示例](/zh/concepts/tools-and-registry)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+# ref is an ArtifactRef obtained from a tool result.
+print(ref.sha256)
+body = composition.agent.config["artifact_resolver"].resolve(ref).body
+```
+
+```text
+Portable content-addressed pointer; never an artifact body or host path.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `artifact_id` | `str` | `required` |
+| `resolver_key` | `str` | `required` |
+| `sha256` | `str` | `required` |
+| `media_type` | `str` | `required` |
+| `byte_length` | `int` | `required` |
+| `encoding` | `str` | `'binary'` |
+| `sensitivity` | `str` | `'internal'` |
+| `provenance_digest` | `Optional[str]` | `None` |
+| `model_summary` | `Optional[str]` | `None` |
+| `required` | `bool` | `True` |
+| `schema_version` | `str` | `ARTIFACT_REF_SCHEMA_VERSION` |
+
+
+### ArtifactRef.from_dict
+
+```text
+from_dict(value: Any) -> 'ArtifactRef'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `value` | `Any` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/artifact.py#L159)
+
+
+
+## ActionExecutionPolicy
+
+```python
+from qitos.engine.action_executor import ActionExecutionPolicy
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/action.py#L145)
+
+[用法与可执行示例](/zh/concepts/tools-and-registry)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+policy = ActionExecutionPolicy(mode="parallel", max_concurrency=2)
+engine = Engine(NotesAgent(), runtime=RuntimeComposition(), action_execution_policy=policy)
+```
+
+```text
+Executor policy for action batches.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `mode` | `str` | `'serial'` |
+| `fail_fast` | `bool` | `False` |
+| `max_concurrency` | `int` | `4` |
+| `parallel_tool_names` | `FrozenSet[str] \| None` | `None` |
+
+
+
+## SandboxPublicationTool
+
+```python
+from qitos.kit.tool.internal.publication import SandboxPublicationTool
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/kit/tool/internal/publication.py#L15)
+
+[用法与可执行示例](/zh/concepts/tools-and-registry)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+# Only after sandbox execution, with explicit publication authority:
+publication = SandboxPublicationTool(
+ composition.env, paths=["report.txt"],
+ expected_input_digest=composition.env.input_digest,
+)
+composition.tool_registry.register(publication)
+```
+
+```text
+Opt-in tool restricted to paths and input digest approved by its caller.
+```
+
+```text
+SandboxPublicationTool(env: Any, *, paths: Iterable[str], expected_input_digest: str) -> Any (see behavior contract)
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `env` | `Any` | `required` |
+| `paths` | `Iterable[str]` | `required` |
+| `expected_input_digest` | `str` | `required` |
+
+
+### SandboxPublicationTool.execute
+
+```text
+execute(args: Any, runtime_context: Any=None) -> ToolResult
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `args` | `Any` | `required` |
+| `runtime_context` | `Any` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/kit/tool/internal/publication.py#L33)
+
+{/* api-reference:end */}
diff --git a/docs/zh/reference/trajectory.mdx b/docs/zh/reference/trajectory.mdx
new file mode 100644
index 00000000..9bd9ba0b
--- /dev/null
+++ b/docs/zh/reference/trajectory.mdx
@@ -0,0 +1,120 @@
+---
+title: "Trajectory 与 reader"
+description: "QitOS public API: trajectory"
+---
+
+default_reader 选择 canonical journal 和支持的历史 reader。读取与回放是观察,不执行或恢复 Agent。REDACTED_PUBLIC 导出明确声明损失;重新导入后记录数相同不能证明 raw 数据等价。当前读取会全量加载 journal。qit/qita 命令参考说明 CLI 的实际用法与退出行为。
+
+[完整可运行教程 / Complete tutorial](/zh/guides/observability) · [API index](/zh/reference/api)
+
+以下签名和字段由固定源码提取;签名是参考,不是可直接运行的程序。每个条目的源码链接绑定同一 runtime baseline。类型中的 Any 不代表任意对象均受支持,应结合上述行为契约和教程使用。
+
+{/* api-reference:start */}
+
+
+## default_reader
+
+```python
+from qitos.qita.reader import default_reader
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/qita/reader.py#L12)
+
+[用法与可执行示例](/zh/guides/observability)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+reader = default_reader(root)
+trajectory = reader.read_session(identity, view=PrivacyView.RAW_PRIVATE)
+print(len(trajectory.records))
+```
+
+```text
+Select canonical data with bounded trace compatibility, or explicit rollback.
+```
+
+```text
+default_reader(root: str | Path, *, selector: str='trajectory') -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `root` | `str | Path` | `required` |
+| `selector` | `str` | `'trajectory'` |
+
+
+
+## CanonicalTrajectoryExporter
+
+```python
+from qitos.tracing.exporter import CanonicalTrajectoryExporter
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/tracing/exporter.py#L95)
+
+[用法与可执行示例](/zh/guides/observability)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+exporter = CanonicalTrajectoryExporter()
+exported = exporter.export(trajectory, view=PrivacyView.REDACTED_PUBLIC)
+print(exported.loss.is_lossless)
+```
+
+```text
+Canonical JSON exporter; exact for the selected projection.
+```
+
+
+### CanonicalTrajectoryExporter.export
+
+```text
+export(trajectory: Trajectory, *, view: PrivacyView=PrivacyView.REDACTED_PUBLIC) -> ExportArtifact
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `trajectory` | `Trajectory` | `required` |
+| `view` | `PrivacyView` | `PrivacyView.REDACTED_PUBLIC` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/tracing/exporter.py#L111)
+
+
+### CanonicalTrajectoryExporter.reimport
+
+```text
+reimport(artifact: ExportArtifact) -> Trajectory
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `artifact` | `ExportArtifact` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/tracing/exporter.py#L141)
+
+
+
+## PrivacyView
+
+```python
+from qitos.tracing.trajectory import PrivacyView
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/tracing/trajectory.py#L91)
+
+[用法与可执行示例](/zh/guides/observability)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+view = PrivacyView.REDACTED_PUBLIC
+print(view.value)
+```
+
+```text
+Named projections over canonical raw data.
+```
+
+{/* api-reference:end */}
diff --git a/docs/zh/reference/work-graph.mdx b/docs/zh/reference/work-graph.mdx
new file mode 100644
index 00000000..5f019e2a
--- /dev/null
+++ b/docs/zh/reference/work-graph.mdx
@@ -0,0 +1,350 @@
+---
+title: "多 Agent 工作"
+description: "QitOS public API: work-graph"
+---
+
+delegate/spawn/fan_out 返回 operation receipt,join 引用 operation ID。WorkGraph 记录所有权与 attempt,本身不执行 worker。LocalWorkScheduler 通过应用提供的 callable 解析 descriptor。handoff 改变同一 work item 的 owner 并阻止旧 source;fork 独立分支。转交接收不等于目标完成;教程在目标 restore 前串行完成同 head 的源回调,避开已记录的 owner CAS 冲突。
+
+[完整可运行教程 / Complete tutorial](/zh/guides/multi-agent-patterns) · [API index](/zh/reference/api)
+
+以下签名和字段由固定源码提取;签名是参考,不是可直接运行的程序。每个条目的源码链接绑定同一 runtime baseline。类型中的 Any 不代表任意对象均受支持,应结合上述行为契约和教程使用。
+
+{/* api-reference:start */}
+
+
+## Session
+
+```python
+from qitos.engine.session_runtime import Session
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L119)
+
+[用法与可执行示例](/zh/guides/multi-agent-patterns)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+session = composition.session("Index notes")
+result = session.run()
+inspection = session.inspect()
+print(session.session_id.value, result.state.final_result)
+```
+
+```text
+Scoped client for one durable Session identity.
+
+The facade stores identifiers and cooperative control only. Agent state is
+reconstructed from the canonical snapshot and executed by ``Engine.run``.
+```
+
+```text
+Session(*, engine: 'Engine[Any, Any, Any]', session_id: SessionIdentity, run_id: RunIdentity, agent_id: AgentIdentity, references: Iterable[ResolverReference], created_at: str, state_type: type[StateSchema], work_item_id: WorkItemIdentity, attempt_id: AttemptIdentity, fork_receipt: Optional[SessionForkReceipt]=None) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `engine` | `'Engine[Any, Any, Any]'` | `required` |
+| `session_id` | `SessionIdentity` | `required` |
+| `run_id` | `RunIdentity` | `required` |
+| `agent_id` | `AgentIdentity` | `required` |
+| `references` | `Iterable[ResolverReference]` | `required` |
+| `created_at` | `str` | `required` |
+| `state_type` | `type[StateSchema]` | `required` |
+| `work_item_id` | `WorkItemIdentity` | `required` |
+| `attempt_id` | `AttemptIdentity` | `required` |
+| `fork_receipt` | `Optional[SessionForkReceipt]` | `None` |
+
+
+### Session.delegate
+
+```text
+delegate(agent: str, *, task: str, operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `agent` | `str` | `required` |
+| `task` | `str` | `required` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1122)
+
+
+### Session.spawn
+
+```text
+spawn(agent: str, *, task: str, operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `agent` | `str` | `required` |
+| `task` | `str` | `required` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1129)
+
+
+### Session.fan_out
+
+```text
+fan_out(specs: Iterable[Mapping[str, Any]], *, operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `specs` | `Iterable[Mapping[str, Any]]` | `required` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1136)
+
+
+### Session.join
+
+```text
+join(children: Iterable[str], *, policy: str='all', quorum: int | None=None, reducer_ref: str | None=None, reducer_digest: str | None=None, operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `children` | `Iterable[str]` | `required` |
+| `policy` | `str` | `'all'` |
+| `quorum` | `int | None` | `None` |
+| `reducer_ref` | `str | None` | `None` |
+| `reducer_digest` | `str | None` | `None` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1157)
+
+
+### Session.handoff
+
+```text
+handoff(agent: str, *, rationale: str='handoff', operation_id: str | None=None) -> Any
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `agent` | `str` | `required` |
+| `rationale` | `str` | `'handoff'` |
+| `operation_id` | `str | None` | `None` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/session_runtime.py#L1148)
+
+
+
+## WorkGraph
+
+```python
+from qitos.core.work_graph import WorkGraph
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/work_graph.py#L754)
+
+[用法与可执行示例](/zh/guides/multi-agent-patterns)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+graph = WorkGraph.from_canonical_dict(session.inspect().work_graph)
+print(len(graph.completions), len(graph.joins))
+```
+
+```text
+Versioned ownership graph and generation-checked record builder.
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `graph_id` | `str` | `required` |
+| `work_items` | `Dict[WorkItemIdentity, WorkItem]` | `field(default_factory=dict)` |
+| `attempts` | `list[WorkAttempt]` | `field(default_factory=list)` |
+| `edges` | `list[WorkEdge]` | `field(default_factory=list)` |
+| `transfers` | `list[OwnershipTransfer]` | `field(default_factory=list)` |
+| `delegations` | `list[DelegationRecord]` | `field(default_factory=list)` |
+| `spawns` | `list[SpawnRecord]` | `field(default_factory=list)` |
+| `fan_out_groups` | `list[FanOutGroup]` | `field(default_factory=list)` |
+| `joins` | `list[JoinDependency]` | `field(default_factory=list)` |
+| `cancellations` | `list[CancellationRequest]` | `field(default_factory=list)` |
+| `detachments` | `list[DetachmentRecord]` | `field(default_factory=list)` |
+| `completions` | `list[WorkCompletion]` | `field(default_factory=list)` |
+| `late_results` | `list[LateResult]` | `field(default_factory=list)` |
+| `budget_allocations` | `list[BudgetAllocation]` | `field(default_factory=list)` |
+| `capability_allocations` | `list[CapabilityAllocation]` | `field(default_factory=list)` |
+| `operation_receipts` | `list[WorkOperationReceipt]` | `field(default_factory=list)` |
+| `schema_version` | `str` | `WORK_GRAPH_SCHEMA_VERSION` |
+
+
+### WorkGraph.from_canonical_dict
+
+```text
+from_canonical_dict(payload: Mapping[str, Any]) -> 'WorkGraph'
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `payload` | `Mapping[str, Any]` | `required` |
+
+[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/work_graph.py#L1263)
+
+
+
+## WorkItem
+
+```python
+from qitos.core.work_graph import WorkItem
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/work_graph.py#L214)
+
+[用法与可执行示例](/zh/guides/multi-agent-patterns)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+work = graph.work_items[session.work_item_id]
+print(work.owner)
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `work_item_id` | `WorkItemIdentity` | `required` |
+| `session_ref` | `SessionIdentity` | `required` |
+| `task_ref` | `str` | `required` |
+| `lifecycle` | `WorkLifecycle` | `required` |
+| `owner` | `WorkOwner` | `required` |
+| `parent_work_item_id` | `WorkItemIdentity \| None` | `None` |
+| `detached` | `bool` | `False` |
+| `budget_allocation_ref` | `str \| None` | `None` |
+| `capability_allocation_ref` | `str \| None` | `None` |
+| `context_transfer_ref` | `str \| None` | `None` |
+
+
+
+## WorkAttempt
+
+```python
+from qitos.core.work_graph import WorkAttempt
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/core/work_graph.py#L193)
+
+[用法与可执行示例](/zh/guides/multi-agent-patterns)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+from dataclasses import fields
+print([field.name for field in fields(WorkAttempt)])
+```
+
+| Field | Type | Default |
+| --- | --- | --- |
+| `attempt_id` | `AttemptIdentity` | `required` |
+| `work_item_id` | `WorkItemIdentity` | `required` |
+| `owner_generation` | `int` | `required` |
+| `state` | `AttemptState` | `required` |
+| `worker_ref` | `str \| None` | `None` |
+
+
+
+## DurableWorkRuntime
+
+```python
+from qitos.engine.work_runtime import DurableWorkRuntime
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/work_runtime.py#L202)
+
+[用法与可执行示例](/zh/guides/multi-agent-patterns)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver(), max_workers=2))
+composition.runtime.work_runtime = runtime
+```
+
+```text
+Idempotent declaration/dispatch protocol over one canonical WorkGraph.
+```
+
+```text
+DurableWorkRuntime(scheduler: WorkScheduler, *, policy: WorkRuntimePolicy | None=None) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `scheduler` | `WorkScheduler` | `required` |
+| `policy` | `WorkRuntimePolicy | None` | `None` |
+
+
+
+## LocalWorkScheduler
+
+```python
+from qitos.engine.work_runtime import LocalWorkScheduler
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/work_runtime.py#L134)
+
+[用法与可执行示例](/zh/guides/multi-agent-patterns)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+scheduler = LocalWorkScheduler(Resolver(), max_workers=2)
+# Resolver.resolve(descriptor) returns a bounded callable; see the full lesson.
+```
+
+```text
+Bounded local reference scheduler; futures are never persisted.
+```
+
+```text
+LocalWorkScheduler(resolver: WorkResolver, *, max_workers: int=4, queue_capacity: int=64) -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `resolver` | `WorkResolver` | `required` |
+| `max_workers` | `int` | `4` |
+| `queue_capacity` | `int` | `64` |
+
+
+
+## WorkRuntimeError
+
+```python
+from qitos.engine.work_runtime import WorkRuntimeError
+```
+
+[Source @ 60809b3](https://github.com/WhitzardAgent/WhitzardOS/blob/60809b3be388d22ea40ea41b4aaa1f5540c76fda/qitos/engine/work_runtime.py#L24)
+
+[用法与可执行示例](/zh/guides/multi-agent-patterns)
+
+用法片段:接续上方完整教程中的对象,不是独立程序。
+
+```python
+try:
+ source.spawn("notes_agent", task="Attempt after handoff")
+except WorkRuntimeError as error:
+ print(error.code)
+```
+
+```text
+Typed scheduler/admission/idempotency failure.
+```
+
+```text
+WorkRuntimeError(code: str, message: str, *, operation_id: str='') -> None
+```
+
+| Parameter | Type | Default |
+| --- | --- | --- |
+| `code` | `str` | `required` |
+| `message` | `str` | `required` |
+| `operation_id` | `str` | `''` |
+
+{/* api-reference:end */}
diff --git a/docs/zh/tutorials/checkpoint-and-fork.mdx b/docs/zh/tutorials/checkpoint-and-fork.mdx
index e1135c6a..00909771 100644
--- a/docs/zh/tutorials/checkpoint-and-fork.mdx
+++ b/docs/zh/tutorials/checkpoint-and-fork.mdx
@@ -1,51 +1,312 @@
---
-title: "Session 生命周期"
-description: "QitOS G5 · Session 生命周期"
+title: "暂停、恢复与 fork"
+description: "从网页完整代码学习 QitOS 资料整理项目。"
---
-## 学习目标
+## 目标与前置条件
-在真实纯函数工具完成后暂停并退出进程;从不可变 paused head fork,然后 restore 和 steer。parent 与 child 都通过 Session 执行。
+第一个进程在处理 Session 资料后,于生命周期边界暂停,保存 Session ID 与匹配配置,然后退出。第二个进程读取 SQLite,对暂停快照执行 fork,运行子 Session,再恢复并运行父 Session。
-## 前置条件
+fake provider 的游标显式从 1 开始,因为该固定 fixture 总是在第 0 个响应后暂停。这不是 provider continuation 的持久化。Session 状态、所有权和 journal 由 QitOS 恢复。父执行使用独立 composition,避免复用子执行已经前进的 fake 游标。
-完成[Quickstart](/zh/quickstart)的安装与 lessons 复制;基础包,Python 3.12.7 实测,无真实请求。
+比较子执行前后的 parent head,再检查父 Trajectory 中的 steering 记录。父 Session 必须实际执行剩余的 Artifact 工具,不能仅收到一句固定结论。
-## 完整可运行示例
+需要 Python 基础;声明支持 Python ≥3.10,本轮本地验证使用 Python 3.12.7。每章可独立运行;继续已有项目时可复用环境和同名文件。以下命令为 macOS/Linux shell。
-[`examples/tutorials/session_walkthrough.py`](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/session_walkthrough.py)
-
-完整目录是唯一执行来源;同目录的 session_walkthrough.py 是公开教学依赖,不依赖仓库 tests。
+## 准备项目
```bash
-python lessons/session_walkthrough.py create --root ./session-run
-python lessons/session_walkthrough.py restore --root ./session-run
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install "qitos @ git+https://github.com/WhitzardAgent/WhitzardOS.git@60809b3be388d22ea40ea41b4aaa1f5540c76fda"
+mkdir notes_lesson
+cd notes_lesson
```
-## 预期输出与断言
+将下方完整文件保存到当前目录。无需 clone 仓库、安装 editable 包或复制 tests。
+
+### 第一个进程:暂停
-`tool_output=42; lifecycle=paused; final_result=arithmetic complete; parent head unchanged by fork`
+{/* tutorial-snippet:lifecycle.py:create */}
+```python
+def create(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root, pause=True) as composition:
+ session = composition.session("Index the two notes")
+ result = session.run()
+ assert session.lifecycle.value == "paused"
+ assert result.records[0].action_results[0].output["title"] == "Session"
+ document = composition.config.to_dict()
+ document["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(document), encoding="utf-8")
+ (root / "control.json").write_text(json.dumps({"session_id": session.session_id.value}), encoding="utf-8")
+ print("paused after first note; exit this process before restore")
+```
+{/* tutorial-snippet:end */}
-文件内的 assert 验证结果;ID 每次不同。除 board 由 Ctrl-C 关闭外,命令应退出 0。
+### 第二个进程:恢复
-## CLI 检查
+{/* tutorial-snippet:lifecycle.py:restore */}
+```python
+def restore(root):
+ identity = json.loads((root / "control.json").read_text())["session_id"]
+ # This fixture always pauses after its first response. Only the fake cursor
+ # is supplied here; QitOS restores the real Session state from SQLite.
+ with compose(root, start=1, pause=True) as composition:
+ before = composition.runtime.checkpoint_store.get_session_head(identity)
+ child = composition.fork(identity)
+ child.run(steering="Finish an independent index.")
+ assert child.session_id.value != identity
+ assert composition.runtime.checkpoint_store.get_session_head(identity) == before
+ with compose(root, start=1, pause=True) as composition:
+ session = composition.restore(identity)
+ result = session.run(steering="Finish the index concisely.")
+ assert any(a.tool_name == "summarize_note" and a.output["title"] == "Artifact"
+ for record in result.records for a in record.action_results)
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ trajectory = default_reader(root).read_session(identity, view=PrivacyView.RAW_PRIVATE)
+ assert any(record.kind.value == "steering" for record in trajectory.records)
+ (root / "control.json").write_text(json.dumps({"session_id": identity, "run_id": result.run_id}), encoding="utf-8")
+ print("restored; steering recorded; fork left parent head unchanged")
+```
+{/* tutorial-snippet:end */}
-脚本还写入可加载的 agent.json。inspect 只读 SQLite,不解析凭据或创建 Docker。
+## 运行并验证
+
+```bash
+python lifecycle.py create --root session-run
+python lifecycle.py restore --root session-run
+```
+
+预期出现以下输出片段(随机 ID 不固定)。每次运行都检查工具结果或持久化断言,成功退出码为 0。
+
+```text
+paused after first note
+fork left parent head unchanged
+```
+
+### 检查保存的 Session
```bash
session_id=$(python -c 'import json; print(json.load(open("session-run/control.json"))["session_id"])')
-qit session inspect --config ./session-run/agent.json --session-id "$session_id"
+qit session inspect --config session-run/agent.json --session-id "$session_id"
qita inspect session "$session_id" --logdir ./session-run
```
-## 支持边界
+## 行为与支持边界
+
+应在 restore 取得所有权前 fork 已持久化的暂停 head。ephemeral 与进程内 Memory 不承诺跨进程恢复。CLI live pause/steer、未解决 approval 的 restore 当前 unsupported。Steering 改变指令,不授予权限。
+
+## 练习与参考答案
+
+修改子 Session 的 steering 文本,确认 parent head 断言仍通过;分别检查父子不同的 Session ID。
+
+## 常见错误与清理
+
+`ModuleNotFoundError`:确认已激活安装指定 wheel/源码版本的环境,并保存本页所有文件。root 已存在:换一个新 `--root`,不要覆盖需要保留的证据。断言失败:检查第一个失败的工具或 typed error;不要只依赖最终文字。退出所有进程、停止 board 后,可自行删除本章新建且不再需要的运行目录;保留要调试的 SQLite、journal 和报告。
+
+{/* tutorial-files:start */}
+
+## 完整文件:复制到项目根目录
+
+```python title="notes.py"
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
-SQLite 是持久存储;Memory 即使在 Session 模式也只在当前进程有效。ephemeral 不承诺恢复。先 fork,再用 restore 获取 source 所有权;不能任意 fork restoring/running head。CLI live pause/steer、未解决 approval 的 restore 当前 unsupported。steering 不授予工具权限。
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
+```
+
+```yaml title="agent.yaml"
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
+```
+
+```python title="lifecycle.py"
+"""Two processes, a durable checkpoint, steering and independent fork."""
+import argparse
+import json
+from pathlib import Path
+
+from notes import compose
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+
+# docs:start create
+def create(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root, pause=True) as composition:
+ session = composition.session("Index the two notes")
+ result = session.run()
+ assert session.lifecycle.value == "paused"
+ assert result.records[0].action_results[0].output["title"] == "Session"
+ document = composition.config.to_dict()
+ document["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(document), encoding="utf-8")
+ (root / "control.json").write_text(json.dumps({"session_id": session.session_id.value}), encoding="utf-8")
+ print("paused after first note; exit this process before restore")
+# docs:end create
+
+
+# docs:start restore
+def restore(root):
+ identity = json.loads((root / "control.json").read_text())["session_id"]
+ # This fixture always pauses after its first response. Only the fake cursor
+ # is supplied here; QitOS restores the real Session state from SQLite.
+ with compose(root, start=1, pause=True) as composition:
+ before = composition.runtime.checkpoint_store.get_session_head(identity)
+ child = composition.fork(identity)
+ child.run(steering="Finish an independent index.")
+ assert child.session_id.value != identity
+ assert composition.runtime.checkpoint_store.get_session_head(identity) == before
+ with compose(root, start=1, pause=True) as composition:
+ session = composition.restore(identity)
+ result = session.run(steering="Finish the index concisely.")
+ assert any(a.tool_name == "summarize_note" and a.output["title"] == "Artifact"
+ for record in result.records for a in record.action_results)
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ trajectory = default_reader(root).read_session(identity, view=PrivacyView.RAW_PRIVATE)
+ assert any(record.kind.value == "steering" for record in trajectory.records)
+ (root / "control.json").write_text(json.dumps({"session_id": identity, "run_id": result.run_id}), encoding="utf-8")
+ print("restored; steering recorded; fork left parent head unchanged")
+# docs:end restore
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("phase", choices=("create", "restore"))
+ parser.add_argument("--root", type=Path, required=True)
+ args = parser.parse_args()
+ {"create": create, "restore": restore}[args.phase](args.root.resolve())
+```
-## 常见错误
+{/* tutorial-files:end */}
-目录已存在时选一个新 root;导入失败核对安装来源。配置不匹配、缺失扩展或 typed failure 应检查 Trajectory 和迁移页,不能静默重试 unknown 外部效果。
+## 下一步与 API
-## 下一步
+[API Reference](/zh/reference/api) · [Configuration](/zh/reference/configuration) · [Learning path](/zh/tutorials/index) · [Next](/zh/guides/memory-and-history)
-[学习目录](/zh/tutorials/index) · [迁移与排障](/zh/reference/g5-migration)
+[Source file](https://github.com/WhitzardAgent/WhitzardOS/blob/master/examples/tutorials/notes/lifecycle.py) (可选;全部所需代码已在本页)
diff --git a/docs/zh/tutorials/index.mdx b/docs/zh/tutorials/index.mdx
index 06c02577..2dea62ce 100644
--- a/docs/zh/tutorials/index.mdx
+++ b/docs/zh/tutorials/index.mdx
@@ -1,15 +1,18 @@
---
-title: "学习路径"
-description: "QitOS G5 · 学习路径"
+title: "学习资料整理 Agent"
+description: "One project, independently runnable chapters."
---
-按顺序完成八个单元。每个单元绑定完整、可下载、带断言的教学文件。先运行 fake 路径,再配置真实模型;旧策略教程属于高级扩展。
+从两条合成资料开始,逐步学会工具、状态、持久化、上下文、多 Agent 和报告发布。先用明确的 fake provider 学习机制,再用配置章节切换真实模型。每章网页包含全部文件,可独立复制运行。
-1. [自定义 Agent](/zh/guides/build-your-first-agent)
-2. [工具与并行调用](/zh/concepts/tools-and-registry)
-3. [Session 生命周期](/zh/tutorials/checkpoint-and-fork)
-4. [Context 与 memory](/zh/guides/memory-and-history)
-5. [Sandbox 与 artifact](/zh/guides/sandbox-and-artifacts)
-6. [多 Agent 工作](/zh/guides/multi-agent-patterns)
-7. [Trajectory 与 qita](/zh/guides/observability)
-8. [第三方扩展](/zh/guides/third-party-extensions)
+1. [运行资料整理 Agent](/zh/quickstart)
+2. [编写自己的 AgentModule](/zh/guides/build-your-first-agent)
+3. [工具与并行调用](/zh/concepts/tools-and-registry)
+4. [暂停、恢复与 fork](/zh/tutorials/checkpoint-and-fork)
+5. [Context 与 memory](/zh/guides/memory-and-history)
+6. [Sandbox、artifact 与显式发布](/zh/guides/sandbox-and-artifacts)
+7. [Delegate、join 与 handoff](/zh/guides/multi-agent-patterns)
+8. [使用 qita 检查与导出](/zh/guides/observability)
+9. [替换 provider 并添加 context](/zh/guides/third-party-extensions)
+
+[API Reference](/zh/reference/api) · [Real provider configuration](/zh/reference/configuration)
diff --git a/examples/README.md b/examples/README.md
index 7011c042..759a6b34 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -48,3 +48,12 @@ Full applications live in `qitos-zoo`, including:
- `qitos-cyber-agent`: a PentAGI-inspired cybersecurity agent built with QitOS.
Some product-like files remain temporarily in `examples/real/` with migration banners while the zoo repository is seeded from `plans/qitos_zoo_migration/`.
+
+## Web-first notes project
+
+`examples/tutorials/notes/` is the source for the complete files displayed on the
+Quickstart and core learning pages. Every page includes its dependencies; users
+can copy from the webpage without cloning this directory. Start with notes.py,
+then custom_agent.py, parallel.py, lifecycle.py, context.py, sandbox.py,
+multi_agent.py/handoff.py, inspect_run.py and provider_extension.py. real_notes.py
+validates without credentials by default; --live is a deliberate model request.
diff --git a/examples/tutorials/notes/agent.yaml b/examples/tutorials/notes/agent.yaml
new file mode 100644
index 00000000..07357ebe
--- /dev/null
+++ b/examples/tutorials/notes/agent.yaml
@@ -0,0 +1,32 @@
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: notes-fake
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
diff --git a/examples/tutorials/notes/context.py b/examples/tutorials/notes/context.py
new file mode 100644
index 00000000..70884c78
--- /dev/null
+++ b/examples/tutorials/notes/context.py
@@ -0,0 +1,71 @@
+"""Explicit contributor, memory, selector and compactor factories; no network."""
+import argparse
+from dataclasses import replace
+import hashlib
+from pathlib import Path
+
+from qitos.config import build_agent_composition
+from qitos.core.context import (
+ DeclaredContextBudgetPolicy, PriorityContextSelectionPolicy, StaticContextContributor,
+)
+from qitos.core.request_view import CompactionReceipt
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+from notes import FakeProvider, summarize_note, configuration
+
+
+class AuditedSelector(PriorityContextSelectionPolicy):
+ def __init__(self):
+ self.seen = set()
+
+ def select(self, contributions, **options):
+ contributions = tuple(contributions)
+ self.seen.update(item.contribution_id for item in contributions)
+ return super().select(contributions, **options)
+
+
+class OmitClosedExchange:
+ """Explicitly lossy omission for this arithmetic fixture only."""
+ policy_id = "tutorial.omit_closed"
+
+ def __init__(self):
+ self.calls = 0
+
+ def compact(self, **values):
+ self.calls += 1
+ return CompactionReceipt(
+ receipt_id="compaction_" + values["selected_digest"][:24],
+ input_exchange_ids=tuple(values["exchange_ids"]), policy_id=self.policy_id,
+ output_digest=hashlib.sha256(b"").hexdigest(),
+ declared_losses=("closed_exchange_omitted_without_summary",),
+ )
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ config = replace(
+ configuration(root),
+ context={"contributors": ["project"], "selector": "selector", "budget_policy": "budget"},
+ memory={"sources": ["memory"]}, compaction={"provider": "compactor"})
+ selector, compactor = AuditedSelector(), OmitClosedExchange()
+ with build_agent_composition(config, model_override=FakeProvider(), extensions={
+ "project": lambda: StaticContextContributor("lesson.project", "project", "Use only the supplied notes."),
+ "memory": lambda: StaticContextContributor("lesson.memory", "memory", "Session and Artifact are the two note titles."),
+ "selector": selector, "compactor": compactor,
+ "budget": lambda: DeclaredContextBudgetPolicy(default_max_input_units=4096, protected_recent_exchanges=0),
+ }) as composition:
+ composition.tool_registry.register(summarize_note)
+ session = composition.session("Index both notes")
+ assert session.run().state.final_result == "Indexed 2 notes: Session, Artifact."
+ assert {"lesson.project", "lesson.memory"} <= selector.seen
+ assert compactor.calls > 0
+ trajectory = default_reader(root).read_session(session.session_id.value, view=PrivacyView.RAW_PRIVATE)
+ assert any(record.kind.value == "compaction" and not record.loss.is_lossless for record in trajectory.records)
+ print("context selected; memory selected; compaction loss recorded")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ run(parser.parse_args().root.resolve())
diff --git a/examples/tutorials/notes/custom_agent.py b/examples/tutorials/notes/custom_agent.py
new file mode 100644
index 00000000..e0274d76
--- /dev/null
+++ b/examples/tutorials/notes/custom_agent.py
@@ -0,0 +1,51 @@
+"""A custom notes AgentModule through the public Engine/Session path."""
+from dataclasses import dataclass, field
+
+from qitos import Action, AgentModule, Decision, Engine, StateSchema, ToolRegistry
+from qitos.engine.action_executor import ActionExecutionPolicy
+from qitos.engine.runtime import RuntimeComposition
+from notes import summarize_note
+
+
+# docs:start agent
+@dataclass
+class NotesState(StateSchema):
+ titles: list[str] = field(default_factory=list)
+
+
+class NotesAgent(AgentModule):
+ def __init__(self):
+ registry = ToolRegistry()
+ registry.register(summarize_note)
+ super().__init__(tool_registry=registry)
+
+ def init_state(self, task, **kwargs):
+ return NotesState(task=task, max_steps=3)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([
+ Action(name="summarize_note", args={"index": index}) for index in (0, 1)
+ ])
+
+ def reduce(self, state, observation, decision):
+ state.titles.extend(item["output"]["title"] for item in observation.get("action_results", []))
+ return state
+# docs:end agent
+
+
+# docs:start run
+def main():
+ for mode in ("sequential", "parallel"):
+ engine = Engine(NotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode=mode, max_concurrency=2))
+ result = engine.session("Index the two notes").run()
+ assert result.state.titles == ["Session", "Artifact"]
+ assert result.state.final_result == "Session, Artifact"
+ print(f"{mode}: Session, Artifact; process_local=true")
+# docs:end run
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/tutorials/notes/handoff.py b/examples/tutorials/notes/handoff.py
new file mode 100644
index 00000000..630bade5
--- /dev/null
+++ b/examples/tutorials/notes/handoff.py
@@ -0,0 +1,61 @@
+"""Transfer one work item's owner; demonstrate the superseded source fence."""
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+
+from notes import compose
+from multi_agent import wait
+from qitos.engine.work_runtime import DurableWorkRuntime, LocalWorkScheduler, WorkRuntimeError
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+
+ class Resolver:
+ resolver_id = "notes.handoff.worker"
+
+ def resolve(self, descriptor):
+ def execute():
+ # Acknowledge transfer before the destination claims this same
+ # Session head. This receipt is not destination task completion.
+ return {"destination": descriptor.parent_session_id, "admitted": True}
+ return execute
+
+ with compose(root, pause=True) as composition:
+ composition.runtime.work_runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver()))
+ source = composition.session("Index notes, then transfer ownership")
+ source.run()
+ identity = source.work_item_id
+ operation = source.handoff("notes_agent", rationale="Finish with the destination worker")
+ graph = wait(source, operation)
+ transfer = graph.transfers[-1]
+ assert transfer.from_agent_id != transfer.to_agent_id
+ assert graph.work_items[identity].owner.agent_id == transfer.to_agent_id
+ try:
+ source.spawn("notes_agent", task="A stale owner must not dispatch")
+ except WorkRuntimeError as error:
+ assert error.code == "superseded_owner"
+ else:
+ raise AssertionError("Superseded source unexpectedly dispatched")
+ identity = source.session_id.value
+ # Serialized handoff: source callbacks and resource cleanup finish first.
+ result = subprocess.run([
+ sys.executable, __file__, "--root", str(root), "--destination", identity,
+ ], capture_output=True, text=True, timeout=20)
+ if result.returncode:
+ raise RuntimeError(result.stderr)
+ print("handoff destination ran; owner changed; source fenced")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--destination")
+ args = parser.parse_args()
+ if args.destination:
+ with compose(args.root.resolve(), start=1, pause=True) as composition:
+ result = composition.restore(args.destination).run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ else:
+ run(args.root.resolve())
diff --git a/examples/tutorials/notes/inspect_run.py b/examples/tutorials/notes/inspect_run.py
new file mode 100644
index 00000000..d3a00837
--- /dev/null
+++ b/examples/tutorials/notes/inspect_run.py
@@ -0,0 +1,30 @@
+"""Read an existing notes run, verify the index, and export a public view."""
+import argparse
+import json
+from pathlib import Path
+
+from qitos.qita.reader import default_reader
+from qitos.tracing.exporter import CanonicalTrajectoryExporter
+from qitos.tracing.trajectory import PrivacyView
+
+
+def inspect(root):
+ control = json.loads((root / "control.json").read_text())
+ trajectory = default_reader(root).read_session(control["session_id"], view=PrivacyView.RAW_PRIVATE)
+ assert trajectory.records
+ exporter = CanonicalTrajectoryExporter()
+ exported = exporter.export(trajectory, view=PrivacyView.REDACTED_PUBLIC)
+ imported = exporter.reimport(exported)
+ assert len(imported.records) == len(trajectory.records)
+ (root / "public-trajectory.json").write_bytes(exported.data)
+ # qita's --run selector currently requires an existing run directory.
+ # This directory is only a selector; the journal remains authoritative.
+ (root / control["run_id"]).mkdir(exist_ok=True)
+ print(json.dumps({"records": len(trajectory.records), "lossless": exported.loss.is_lossless,
+ "run_selector": str(root / control["run_id"])}))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ inspect(parser.parse_args().root.resolve())
diff --git a/examples/tutorials/notes/lifecycle.py b/examples/tutorials/notes/lifecycle.py
new file mode 100644
index 00000000..d74dfc94
--- /dev/null
+++ b/examples/tutorials/notes/lifecycle.py
@@ -0,0 +1,56 @@
+"""Two processes, a durable checkpoint, steering and independent fork."""
+import argparse
+import json
+from pathlib import Path
+
+from notes import compose
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+
+# docs:start create
+def create(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root, pause=True) as composition:
+ session = composition.session("Index the two notes")
+ result = session.run()
+ assert session.lifecycle.value == "paused"
+ assert result.records[0].action_results[0].output["title"] == "Session"
+ document = composition.config.to_dict()
+ document["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(document), encoding="utf-8")
+ (root / "control.json").write_text(json.dumps({"session_id": session.session_id.value}), encoding="utf-8")
+ print("paused after first note; exit this process before restore")
+# docs:end create
+
+
+# docs:start restore
+def restore(root):
+ identity = json.loads((root / "control.json").read_text())["session_id"]
+ # This fixture always pauses after its first response. Only the fake cursor
+ # is supplied here; QitOS restores the real Session state from SQLite.
+ with compose(root, start=1, pause=True) as composition:
+ before = composition.runtime.checkpoint_store.get_session_head(identity)
+ child = composition.fork(identity)
+ child.run(steering="Finish an independent index.")
+ assert child.session_id.value != identity
+ assert composition.runtime.checkpoint_store.get_session_head(identity) == before
+ with compose(root, start=1, pause=True) as composition:
+ session = composition.restore(identity)
+ result = session.run(steering="Finish the index concisely.")
+ assert any(a.tool_name == "summarize_note" and a.output["title"] == "Artifact"
+ for record in result.records for a in record.action_results)
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ trajectory = default_reader(root).read_session(identity, view=PrivacyView.RAW_PRIVATE)
+ assert any(record.kind.value == "steering" for record in trajectory.records)
+ (root / "control.json").write_text(json.dumps({"session_id": identity, "run_id": result.run_id}), encoding="utf-8")
+ print("restored; steering recorded; fork left parent head unchanged")
+# docs:end restore
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("phase", choices=("create", "restore"))
+ parser.add_argument("--root", type=Path, required=True)
+ args = parser.parse_args()
+ {"create": create, "restore": restore}[args.phase](args.root.resolve())
diff --git a/examples/tutorials/notes/multi_agent.py b/examples/tutorials/notes/multi_agent.py
new file mode 100644
index 00000000..09d22da7
--- /dev/null
+++ b/examples/tutorials/notes/multi_agent.py
@@ -0,0 +1,84 @@
+"""Durable spawn/delegate/fan-out/join with real child Session execution.
+
+The local scheduler resolves only this tutorial's Agent; no distributed service.
+"""
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+import time
+
+from qitos.core.work_graph import WorkGraph
+from qitos.engine.work_runtime import DurableWorkRuntime, LocalWorkScheduler
+from dataclasses import replace
+from qitos.config import build_agent_composition
+from notes import FakeProvider, PauseAfterTool, summarize_note, configuration
+
+
+def compose(root, *, pause=False, finish=False):
+ config = configuration(root)
+ config = replace(config, budgets=replace(config.budgets, max_requests=16),
+ lifecycle={"policy": "pause"})
+ result = build_agent_composition(config, model_override=FakeProvider(start=2 if finish else 0),
+ extensions={"pause": PauseAfterTool})
+ result.tool_registry.register(summarize_note)
+ return result
+
+
+def wait(session, operation):
+ deadline = time.monotonic() + 30
+ while time.monotonic() < deadline:
+ graph = WorkGraph.from_canonical_dict(session.inspect().work_graph)
+ receipt = next(item for item in graph.operation_receipts if item.operation_id == operation.operation_id)
+ if receipt.state in {"completed", "failed", "outcome_unknown"}:
+ assert receipt.state == "completed", receipt.state
+ return graph
+ time.sleep(0.02)
+ raise AssertionError("child deadline exceeded; inspect before retrying")
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+
+ class Resolver:
+ resolver_id = "tutorial.notes_agent.worker"
+
+ def resolve(self, descriptor):
+ def execute():
+ for identity in (() if descriptor.operation == "join" else descriptor.child_session_ids):
+ subprocess.run([sys.executable, __file__, "--root", str(root), "--child", identity],
+ check=True, capture_output=True, text=True, timeout=20)
+ return {"children": list(descriptor.child_session_ids)}
+ return execute
+
+ with compose(root, pause=True) as composition:
+ composition.runtime.work_runtime = DurableWorkRuntime(LocalWorkScheduler(Resolver(), max_workers=2))
+ parent = composition.session("Index, then ask independent note workers")
+ parent.run()
+ assert parent.lifecycle.value == "paused"
+ delegated = parent.delegate("notes_agent", task="Describe the Session note")
+ wait(parent, delegated)
+ spawned = parent.spawn("notes_agent", task="Describe the Artifact note")
+ wait(parent, spawned)
+ batch = parent.fan_out([{"agent": "notes_agent", "task": "Review Session", "budget": {"model_requests": 2}},
+ {"agent": "notes_agent", "task": "Review Artifact", "budget": {"model_requests": 2}}])
+ wait(parent, batch)
+ joined = parent.join([delegated.operation_id, spawned.operation_id, batch.operation_id], policy="all")
+ graph = wait(parent, joined)
+ assert graph.joins[-1].state == "closed"
+ assert len(graph.completions) == 4
+ print("durable children=4; join=closed; parent retains ownership")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--child")
+ args = parser.parse_args()
+ root = args.root.resolve()
+ if args.child:
+ with compose(root, finish=True, pause=True) as composition:
+ result = composition.restore(args.child).run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ else:
+ run(root)
diff --git a/examples/tutorials/notes/notes.py b/examples/tutorials/notes/notes.py
new file mode 100644
index 00000000..4cfaa0b2
--- /dev/null
+++ b/examples/tutorials/notes/notes.py
@@ -0,0 +1,97 @@
+"""Synthetic notes; fake model, real tools, composition, Session and journal."""
+import argparse
+from dataclasses import replace
+import json
+from pathlib import Path
+
+from qitos.config import build_agent_composition, load_agent_config
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.runtime import LifecyclePolicy
+
+# docs:start fixture
+NOTES = (
+ "Session: A durable session can resume after a process exits.",
+ "Artifact: Large tool outputs can be retained outside model context.",
+)
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def summarize_note(index: int) -> dict:
+ """Extract a title and word count from a synthetic in-memory note."""
+ text = NOTES[index]
+ return {"title": text.split(":", 1)[0], "words": len(text.split())}
+# docs:end fixture
+
+
+# docs:start provider
+class FakeProvider:
+ """Scripted responses; this does not summarize or reason like a real model."""
+ model = "notes-fake"
+ qitos_protocol = "react_text_v1"
+
+ def __init__(self, start=0):
+ self.stage = start
+
+ def call_raw(self, messages, **options):
+ if self.stage < len(NOTES):
+ content = f"Thought: inspect a note\nAction: summarize_note(index={self.stage})"
+ else:
+ content = "Final Answer: Indexed 2 notes: Session, Artifact."
+ self.stage += 1
+ return {"choices": [{"message": {"content": content}}]}
+# docs:end provider
+
+
+class PauseAfterTool(LifecyclePolicy):
+ policy_id = "notes.pause_after_tool"
+
+ def should_pause(self, context):
+ return context.step_id == 0
+
+
+# docs:start composition
+def configuration(root):
+ config = load_agent_config(Path(__file__).with_name("agent.yaml"))
+ return replace(config, runtime=replace(
+ config.runtime, data_root=str(root / "data"),
+ environment=replace(config.runtime.environment, workspace=str(root)),
+ session=replace(config.runtime.session, path=str(root / "sessions.sqlite3")),
+ trajectory=replace(config.runtime.trajectory, output=str(root / "trajectory.journal")),
+ ))
+
+
+def compose(root, *, start=0, pause=False):
+ config = configuration(root)
+ if pause:
+ config = replace(config, lifecycle={"policy": "pause"})
+ composition = build_agent_composition(
+ config, model_override=FakeProvider(start), extensions={"pause": PauseAfterTool},
+ )
+ composition.tool_registry.register(summarize_note)
+ return composition
+# docs:end composition
+
+
+# docs:start run
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ with compose(root) as composition:
+ session = composition.session("Index both synthetic notes")
+ result = session.run()
+ outputs = [action.output for record in result.records for action in record.action_results
+ if action.tool_name == "summarize_note"]
+ assert [output["title"] for output in outputs] == ["Session", "Artifact"]
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ config = composition.config.to_dict()
+ config["runtime"]["environment"] = {"type": "unsafe_host", "workspace": str(root)}
+ (root / "agent.json").write_text(json.dumps(config), encoding="utf-8")
+ control = {"session_id": session.session_id.value, "run_id": result.run_id}
+ (root / "control.json").write_text(json.dumps(control), encoding="utf-8")
+ print(json.dumps({**control, "result": result.state.final_result, "outputs": outputs}))
+# docs:end run
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, default=Path("notes-run"))
+ run(parser.parse_args().root.resolve())
diff --git a/examples/tutorials/notes/parallel.py b/examples/tutorials/notes/parallel.py
new file mode 100644
index 00000000..52faa0b6
--- /dev/null
+++ b/examples/tutorials/notes/parallel.py
@@ -0,0 +1,52 @@
+"""Force reverse completion while retaining declaration order in reduce."""
+from threading import Event
+
+from qitos import Action, Decision, Engine, ToolRegistry
+from qitos.core.function_tool_decorator import function_tool
+from qitos.engine.action_executor import ActionExecutionPolicy
+from qitos.engine.runtime import RuntimeComposition
+from custom_agent import NotesAgent
+from notes import NOTES
+
+# Explicit test instrumentation for one local invocation, not persistent state.
+second_finished = Event()
+completion_order = []
+
+
+@function_tool(read_only=True, concurrency_safe=True)
+def analyze_note(index: int) -> dict:
+ """An in-memory fixture that controls completion order without file/network I/O."""
+ if index == 0:
+ if not second_finished.wait(5):
+ raise RuntimeError("This fixture requires two parallel workers")
+ completion_order.append(index)
+ if index == 1:
+ second_finished.set()
+ return {"title": NOTES[index].split(":", 1)[0]}
+
+
+class ParallelNotesAgent(NotesAgent):
+ def __init__(self):
+ super().__init__()
+ self.tool_registry = ToolRegistry()
+ self.tool_registry.register(analyze_note)
+
+ def decide(self, state, observation):
+ if state.titles:
+ return Decision.final(", ".join(state.titles))
+ return Decision.act([Action(name="analyze_note", args={"index": i}) for i in (0, 1)])
+
+
+def run():
+ second_finished.clear()
+ completion_order.clear()
+ engine = Engine(ParallelNotesAgent(), runtime=RuntimeComposition(),
+ action_execution_policy=ActionExecutionPolicy(mode="parallel", max_concurrency=2))
+ result = engine.session("Compare completion with declaration order").run()
+ assert completion_order == [1, 0]
+ assert result.state.titles == ["Session", "Artifact"]
+ print("completion=[1, 0]; declaration=[0, 1]; titles=Session, Artifact")
+
+
+if __name__ == "__main__":
+ run()
diff --git a/examples/tutorials/notes/provider_extension.py b/examples/tutorials/notes/provider_extension.py
new file mode 100644
index 00000000..5359a87c
--- /dev/null
+++ b/examples/tutorials/notes/provider_extension.py
@@ -0,0 +1,40 @@
+"""Replace the provider and inject context, without changing the kernel."""
+import argparse
+from dataclasses import replace
+from pathlib import Path
+
+from notes import FakeProvider, configuration, summarize_note
+from qitos.config import build_agent_composition
+from qitos.core.context import StaticContextContributor
+
+
+class ObservedFakeProvider(FakeProvider):
+ """Keep the public provider call shape and validate selected context."""
+ def __init__(self):
+ super().__init__()
+ self.requests = 0
+
+ def call_raw(self, messages, **options):
+ assert "notes-project-context" in str(messages)
+ self.requests += 1
+ return super().call_raw(messages, **options)
+
+
+def run(root):
+ root.mkdir(parents=True, exist_ok=False)
+ config = replace(configuration(root), context={"contributors": ["project"]})
+ provider = ObservedFakeProvider()
+ with build_agent_composition(config, model_override=provider, extensions={
+ "project": lambda: StaticContextContributor("notes.project", "project", "notes-project-context"),
+ }) as composition:
+ composition.tool_registry.register(summarize_note)
+ result = composition.session("Index both notes").run()
+ assert result.state.final_result == "Indexed 2 notes: Session, Artifact."
+ assert provider.requests == 3
+ print("provider replaced; context observed; requests=3")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ run(parser.parse_args().root.resolve())
diff --git a/examples/tutorials/notes/real_agent.yaml b/examples/tutorials/notes/real_agent.yaml
new file mode 100644
index 00000000..a06ce479
--- /dev/null
+++ b/examples/tutorials/notes/real_agent.yaml
@@ -0,0 +1,33 @@
+schema: qitos.agent
+agent:
+ name: notes_agent
+ protocol: react_text_v1
+model:
+ provider: openai_compatible
+ model: example-model
+ base_url: https://provider.example/v1
+ credential:
+ ref: notes-provider
+ request:
+ max_tokens: 512
+ timeout_seconds: 30
+ retries: 0
+tools:
+ preset: none
+runtime:
+ environment:
+ type: unsafe_host
+ workspace: .
+ session:
+ mode: durable
+ store: sqlite
+ path: ./real-notes-run/sessions.sqlite3
+ trajectory:
+ enabled: true
+ output: ./real-notes-run/trajectory.journal
+budgets:
+ max_steps: 6
+ max_requests: 6
+ max_runtime_seconds: 30
+failure_policy:
+ tool: fail_closed
diff --git a/examples/tutorials/notes/real_notes.py b/examples/tutorials/notes/real_notes.py
new file mode 100644
index 00000000..b6a729af
--- /dev/null
+++ b/examples/tutorials/notes/real_notes.py
@@ -0,0 +1,34 @@
+"""Validate configuration by default; --live explicitly opts into model requests."""
+import argparse
+from pathlib import Path
+
+from notes import summarize_note
+from qitos.config import LocalCredentialFileResolver, build_agent_composition, load_agent_config
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--live", action="store_true")
+ parser.add_argument("--credentials", type=Path, default=Path.home() / ".config/qitos/credentials.yaml")
+ args = parser.parse_args()
+ config = load_agent_config(Path(__file__).with_name("real_agent.yaml"))
+ assert config.budgets.max_requests == 6
+ if not args.live:
+ print("configuration valid; no credentials read; no model request")
+ return
+ if config.model.base_url == "https://provider.example/v1":
+ raise ValueError("Replace the reserved provider.example endpoint and example-model first")
+ resolver = LocalCredentialFileResolver(args.credentials, repository_root=Path.cwd())
+ with build_agent_composition(config, credential_resolver=resolver) as composition:
+ composition.tool_registry.register(summarize_note)
+ result = composition.session(
+ "Call summarize_note for indices 0 and 1. Report both titles and word counts."
+ ).run()
+ print(composition.config.name, result.state.stop_reason, result.state.final_result)
+ outputs = [a.output for record in result.records for a in record.action_results
+ if a.tool_name == "summarize_note" and a.status == "success"]
+ assert {item["title"] for item in outputs} == {"Session", "Artifact"}, outputs
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/tutorials/notes/sandbox.py b/examples/tutorials/notes/sandbox.py
new file mode 100644
index 00000000..648c7ebb
--- /dev/null
+++ b/examples/tutorials/notes/sandbox.py
@@ -0,0 +1,110 @@
+"""Real Docker lesson: retained output and opt-in top-level file publication.
+
+Uses a fake provider but real Env tools, Session, artifact store and reader.
+Only --publish registers publication authority over report.txt in a new fixture.
+"""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+
+from qitos.config import (
+ AgentConfig, BudgetConfig, EnvironmentConfig, ModelConfig, RuntimeConfig,
+ TrajectoryConfig, build_agent_composition,
+)
+from qitos.core.artifact import ArtifactRef
+from notes import PauseAfterTool
+from qitos.kit.tool.internal.publication import SandboxPublicationTool
+from qitos.qita.reader import default_reader
+from qitos.tracing.trajectory import PrivacyView
+
+
+class FakeProvider:
+ model = "sandbox-tutorial-fake"
+ qitos_protocol = "json_decision_multi_v1"
+
+ def __init__(self, publish, stage=0):
+ self.actions = [
+ ("write_file", {"path": "report.txt", "content": "Session, Artifact\n"}),
+ ("run_command", {"command": "python3 -c 'print(\"x\" * 20000)'", "timeout": 10}),
+ ]
+ if publish:
+ self.actions.append(("publish_workspace", {}))
+ self.stage = stage
+
+ def call_raw(self, messages, **options):
+ if self.stage == len(self.actions):
+ return {"choices": [{"message": {"content": "Final Answer: sandbox lesson complete"}}]}
+ name, args = self.actions[self.stage]
+ self.stage += 1
+ return {"choices": [{"message": {"content": None, "tool_calls": [{
+ "id": f"lesson-{self.stage}", "type": "function",
+ "function": {"name": name, "arguments": json.dumps(args)},
+ }]}}]}
+
+
+def references(value):
+ if isinstance(value, dict):
+ if value.get("schema_version") == "qitos.artifact_ref/v1":
+ yield ArtifactRef.from_dict(value)
+ for item in value.values():
+ yield from references(item)
+ elif isinstance(value, (list, tuple)):
+ for item in value:
+ yield from references(item)
+
+
+def run(root: Path, image: str, publish: bool):
+ root.mkdir(parents=True, exist_ok=False)
+ source = root / "source"
+ source.mkdir()
+ (source / "report.txt").write_text("original\n", encoding="utf-8")
+ config = AgentConfig(
+ lifecycle={"policy": "pause"},
+ name="sandbox-lesson", protocol="json_decision_multi_v1", tool_preset="env_coding",
+ model=ModelConfig(provider="openai_compatible", model="sandbox-tutorial-fake"),
+ tool_options={"native_tool_calls_required": True},
+ budgets=BudgetConfig(max_steps=6, max_requests=6, max_runtime_seconds=60),
+ runtime=RuntimeConfig(
+ data_root=str(root / "data"),
+ trajectory=TrajectoryConfig(output=str(root / "trajectory.journal")),
+ environment=EnvironmentConfig(workspace=str(source), image=image,
+ cpus=0.5, memory_mb=256, pids_limit=32)),
+ )
+ with build_agent_composition(config, model_override=FakeProvider(publish),
+ extensions={"pause": PauseAfterTool}) as composition:
+ session = composition.session("Write the notes report and retain a large output")
+ session.run()
+ assert session.lifecycle.value == "paused"
+ identity = session.session_id.value
+ assert (source / "report.txt").read_text() == "original\n"
+ with build_agent_composition(config, model_override=FakeProvider(publish, stage=1),
+ extensions={"pause": PauseAfterTool}) as composition:
+ session = composition.restore(identity)
+ if publish:
+ composition.tool_registry.register(SandboxPublicationTool(
+ composition.env, paths=["report.txt"],
+ expected_input_digest=composition.env.input_digest,
+ ))
+ result = session.run()
+ assert result.state.final_result == "sandbox lesson complete", repr(result.state.final_result)
+ trajectory = default_reader(root).read_session(session.session_id.value, view=PrivacyView.RAW_PRIVATE)
+ artifacts = [ref for record in trajectory.records for ref in references(record.payload)]
+ assert artifacts
+ for ref in artifacts:
+ body = composition.agent.config["artifact_resolver"].resolve(ref).body
+ assert body is not None and hashlib.sha256(body).hexdigest() == ref.sha256
+ assert (source / "report.txt").read_text() == ("Session, Artifact\n" if publish else "original\n")
+ assert composition.env.cleanup_receipt["container_absent"] is True
+ assert (source / "report.txt").read_text() == ("Session, Artifact\n" if publish else "original\n")
+ print(json.dumps({"docker": True, "published": publish, "artifacts": len(artifacts),
+ "container_absent": True, "session_id": session.session_id.value}))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--image", default="python:3.12-slim")
+ parser.add_argument("--publish", action="store_true")
+ args = parser.parse_args()
+ run(args.root.resolve(), args.image, args.publish)
diff --git a/scripts/sync_api_reference.py b/scripts/sync_api_reference.py
new file mode 100644
index 00000000..3ca476d8
--- /dev/null
+++ b/scripts/sync_api_reference.py
@@ -0,0 +1,121 @@
+"""Generate static reference MDX from explicit supported imports and source AST."""
+import argparse
+import ast
+from copy import deepcopy
+import importlib
+import inspect
+import json
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+START, END = "{/* api-reference:start */}", "{/* api-reference:end */}"
+
+
+def anchor(module, name):
+ return (module + "." + name).replace(".", "-").lower()
+
+
+def definition(obj):
+ """Use AST so defaults/signatures are stable across supported Python versions."""
+ obj = inspect.unwrap(obj)
+ filename = Path(inspect.getsourcefile(obj))
+ parts = filename.parts
+ index = len(parts) - 1 - list(reversed(parts)).index("qitos")
+ relative = Path(*parts[index:])
+ source = ROOT / relative
+ tree = ast.parse(source.read_text())
+ _, line = inspect.getsourcelines(obj)
+ nodes = [node for node in ast.walk(tree) if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))
+ and node.name == obj.__name__]
+ node = min(nodes, key=lambda n: abs(n.lineno - line))
+ return node, relative
+
+
+def signature(node):
+ args = deepcopy(node.args)
+ if args.args and args.args[0].arg in ("self", "cls"):
+ args.args.pop(0)
+ result = ast.unparse(node.returns) if node.returns else "Any (see behavior contract)"
+ return f"{node.name}({ast.unparse(args)}) -> {result}"
+
+
+def parameter_table(node):
+ positional = [*node.args.posonlyargs, *node.args.args]
+ defaults = [None] * (len(positional) - len(node.args.defaults)) + list(node.args.defaults)
+ parameters = list(zip(positional, defaults)) + list(zip(node.args.kwonlyargs, node.args.kw_defaults))
+ rows = []
+ for argument, default in parameters:
+ if argument.arg in ("self", "cls"):
+ continue
+ annotation = ast.unparse(argument.annotation) if argument.annotation else "not annotated"
+ value = ast.unparse(default) if default is not None else "required"
+ rows.append(f"| `{argument.arg}` | `{annotation.replace('|', '|')}` | `{value.replace('|', '|')}` |")
+ if not rows:
+ return []
+ return ["| Parameter | Type | Default |", "| --- | --- | --- |", *rows, ""]
+
+
+def symbol_text(item, baseline, chinese, tutorial):
+ module, name = item["module"], item["name"]
+ obj = getattr(importlib.import_module(module), name)
+ node, relative = definition(obj)
+ title = anchor(module, name)
+ text = [f'', f"## {name}", "", f"```python\nfrom {module} import {name}\n```", "",
+ f"[Source @ {baseline[:7]}](https://github.com/WhitzardAgent/WhitzardOS/blob/{baseline}/{relative.as_posix()}#L{node.lineno})", "",
+ f"[{'用法与可执行示例' if chinese else 'Usage and executable example'}]({'/zh/' if chinese else '/'}{tutorial})", ""]
+ if item.get("example"):
+ text += [("用法片段:接续上方完整教程中的对象,不是独立程序。" if chinese else
+ "Usage fragment: continues with objects from the linked complete tutorial; not a standalone program."),
+ "", "```python", item["example"], "```", ""]
+ doc = ast.get_docstring(node)
+ if doc:
+ text += ["```text", doc, "```", ""]
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ text += ["```text", signature(node), "```", "", *parameter_table(node)]
+ else:
+ constructor = next((n for n in node.body if isinstance(n, ast.FunctionDef) and n.name == "__init__"), None)
+ if constructor:
+ text += ["```text", signature(constructor).replace("__init__(", name + "("), "```", "", *parameter_table(constructor)]
+ fields = [n for n in node.body if isinstance(n, ast.AnnAssign) and isinstance(n.target, ast.Name) and not n.target.id.startswith("_")]
+ if fields:
+ text += ["| Field | Type | Default |", "| --- | --- | --- |"]
+ for field in fields:
+ annotation = ast.unparse(field.annotation).replace("|", "\\|")
+ value = ast.unparse(field.value).replace("|", "\\|") if field.value else "required"
+ text += [f"| `{field.target.id}` | `{annotation}` | `{value}` |"]
+ text += [""]
+ for method in item["methods"]:
+ member = getattr(obj, method)
+ method_node, method_file = definition(member)
+ text += [f'', f"### {name}.{method}", "", "```text", signature(method_node), "```", "", *parameter_table(method_node)]
+ doc = ast.get_docstring(method_node)
+ if doc:
+ text += ["```text", doc, "```", ""]
+ text += [f"[Source](https://github.com/WhitzardAgent/WhitzardOS/blob/{baseline}/{method_file.as_posix()}#L{method_node.lineno})", ""]
+ return "\n".join(text)
+
+
+def synchronize(check=False):
+ manifest = json.loads((ROOT / "docs/api-contracts.json").read_text())
+ errors = []
+ for group in manifest["groups"]:
+ for prefix in ("", "zh/"):
+ page = ROOT / "docs" / f"{prefix}reference/{group['slug']}.mdx"
+ text = page.read_text()
+ body = "\n\n".join(symbol_text(item, manifest["baseline"], bool(prefix), group["tutorial"]) for item in group["symbols"])
+ expected = text.split(START)[0] + START + "\n\n" + body + "\n" + END + text.split(END)[1]
+ if text != expected:
+ if check:
+ errors.append(f"{page.relative_to(ROOT)}: API signature/field/source drift")
+ else:
+ page.write_text(expected)
+ return errors
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true")
+ args = parser.parse_args()
+ failures = synchronize(args.check)
+ print("\n".join(failures) if failures else "Public API imports, signatures and source links synchronized.")
+ raise SystemExit(bool(failures))
diff --git a/scripts/sync_tutorial_docs.py b/scripts/sync_tutorial_docs.py
new file mode 100644
index 00000000..ad8896ea
--- /dev/null
+++ b/scripts/sync_tutorial_docs.py
@@ -0,0 +1,77 @@
+"""Materialize reviewed source files into MDX; --check never writes files."""
+import argparse
+import json
+from pathlib import Path
+import re
+
+ROOT = Path(__file__).resolve().parents[1]
+START = "{/* tutorial-files:start */}"
+END = "{/* tutorial-files:end */}"
+FILE_BLOCK = re.compile(r'```(?:python|yaml|json) title="([^"]+)"\n(.*?)\n```', re.S)
+
+
+def contracts():
+ return json.loads((ROOT / "docs/tutorial-contracts.json").read_text())
+
+
+def complete_files(page):
+ """Only the explicit complete-file region is executable, never prose shell."""
+ content = page.read_text(encoding="utf-8")
+ if content.count(START) != 1 or content.count(END) != 1:
+ raise ValueError(f"{page}: expected one complete-file region")
+ region = content.split(START)[1].split(END)[0]
+ files = {}
+ for name, code in FILE_BLOCK.findall(region):
+ path = Path(name)
+ if path.is_absolute() or ".." in path.parts or name in files:
+ raise ValueError(f"{page}: unsafe or duplicate filename {name}")
+ files[name] = code + "\n"
+ return files
+
+
+def render_files(unit, chinese):
+ heading = "完整文件:复制到项目根目录" if chinese else "Complete files: save in the project root"
+ blocks = [START, "", f"## {heading}", ""]
+ for item in unit["files"]:
+ source = ROOT / item["source"]
+ language = {".py": "python", ".yaml": "yaml", ".json": "json"}[source.suffix]
+ blocks.extend([f'```{language} title="{item["target"]}"', source.read_text().rstrip(), "```", ""])
+ blocks.append(END)
+ return "\n".join(blocks)
+
+
+def synchronize(check=False):
+ errors = []
+ for unit in contracts()["units"]:
+ for prefix in ("", "zh/"):
+ page = ROOT / "docs" / f"{prefix}{unit['page']}.mdx"
+ text = page.read_text()
+ if text.count(START) != 1 or text.count(END) != 1:
+ errors.append(f"{page}: missing complete-file markers")
+ continue
+ expected = text.split(START)[0] + render_files(unit, bool(prefix)) + text.split(END)[1]
+ # Named excerpts in the prose are checked against the same source.
+ pattern = r'\{/\* tutorial-snippet:([^:]+):([^ ]+) \*/\}.*?\{/\* tutorial-snippet:end \*/\}'
+
+ def excerpt(match):
+ filename, name = match.group(1, 2)
+ source = ROOT / unit["source_dir"] / filename
+ body = source.read_text().split(f"# docs:start {name}\n")[1].split(f"# docs:end {name}")[0].rstrip()
+ return (f'{{/* tutorial-snippet:{filename}:{name} */}}\n```python\n{body}\n```\n'
+ '{/* tutorial-snippet:end */}')
+ expected = re.sub(pattern, excerpt, expected, flags=re.S)
+ if text != expected:
+ if check:
+ errors.append(f"{page.relative_to(ROOT)}: source/code drift; run scripts/sync_tutorial_docs.py")
+ else:
+ page.write_text(expected, encoding="utf-8")
+ return errors
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true")
+ args = parser.parse_args()
+ failures = synchronize(args.check)
+ print("\n".join(failures) if failures else "Tutorial complete files and excerpts synchronized.")
+ raise SystemExit(bool(failures))
diff --git a/tests/test_docs_page_execution.py b/tests/test_docs_page_execution.py
new file mode 100644
index 00000000..9a6d0fa3
--- /dev/null
+++ b/tests/test_docs_page_execution.py
@@ -0,0 +1,147 @@
+"""Execute complete files copied from the public page, never hidden helpers."""
+import ast
+import importlib.util
+import json
+import os
+from pathlib import Path
+import subprocess
+import socket
+import time
+from urllib.request import urlopen
+from urllib.error import URLError
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def load_script(name):
+ spec = importlib.util.spec_from_file_location(name, ROOT / f"scripts/{name}.py")
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+SYNC = load_script("sync_tutorial_docs")
+API = load_script("sync_api_reference")
+CONTRACT = SYNC.contracts()
+CASES = [(prefix, unit) for unit in CONTRACT["units"] for prefix in ("", "zh/")
+ if unit["runtime"] == "offline"]
+
+
+def test_tutorial_source_and_api_contracts():
+ assert SYNC.synchronize(check=True) == []
+ assert API.synchronize(check=True) == []
+ symbols = {(s["module"], s["name"]) for group in json.loads((ROOT / "docs/api-contracts.json").read_text())["groups"]
+ for s in group["symbols"]}
+ for unit in CONTRACT["units"]:
+ en = SYNC.complete_files(ROOT / "docs" / f"{unit['page']}.mdx")
+ zh = SYNC.complete_files(ROOT / "docs/zh" / f"{unit['page']}.mdx")
+ assert en == zh, unit["page"]
+ assert set(en) == {item["target"] for item in unit["files"]}
+ for name, code in en.items():
+ if name.endswith(".py"):
+ tree = ast.parse(code, filename=f"{unit['page']}:{name}")
+ assert not any(isinstance(n, ast.Constant) and n.value is Ellipsis for n in ast.walk(tree))
+ for node in ast.walk(tree):
+ if isinstance(node, ast.ImportFrom) and node.module and node.module.startswith("qitos"):
+ for alias in node.names:
+ assert (node.module, alias.name) in symbols, (unit["page"], node.module, alias.name)
+
+
+# Reuse the existing wheel fixture; executing this file with the golden suite
+# shares the same installation rather than introducing an editable shortcut.
+from test_docs_golden_paths import installed as _installed, command # noqa: E402
+
+installed = _installed
+
+
+def materialize(page, directory):
+ for name, code in SYNC.complete_files(page).items():
+ target = directory / name
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_text(code, encoding="utf-8")
+
+
+def execute_unit(unit, directory, python):
+ output = []
+ for args in unit["commands"]:
+ executable = python if args[0] == "python" else python.parent / args[0]
+ output.append(command([executable, *args[1:]], directory, timeout=90))
+ text = "\n".join(output)
+ for expected in unit["expected"]:
+ assert expected in text, f"{unit['page']}: missing {expected!r}: {text}"
+ return text
+
+
+@pytest.mark.parametrize("prefix,unit", CASES, ids=[p + u["page"] for p, u in CASES])
+def test_page_files_execute_from_installed_wheel(installed, tmp_path, prefix, unit):
+ _, python, _ = installed
+ directory = tmp_path
+ if unit["scaffold"]:
+ command([python.parent / "qit", "new", "--agent-name", "notes_agent", "--output-dir", tmp_path, "--no-input"], tmp_path)
+ directory = tmp_path / "notes_agent"
+ materialize(ROOT / "docs" / f"{prefix}{unit['page']}.mdx", directory)
+ if unit["scaffold"]:
+ command([python, "-m", "pip", "install", directory], directory)
+ command([python, "-m", "pytest", "-q", "tests"], directory)
+ try:
+ execute_unit(unit, directory, python)
+ except AssertionError as error:
+ pytest.fail(f"page={prefix}{unit['page']}; complete-files execution: {error}")
+ if unit["page"] == "guides/observability":
+ control = json.loads((directory / "notes-run/control.json").read_text())
+ run = directory / "notes-run" / control["run_id"]
+ command([python.parent / "qit", "session", "inspect", "--config", "notes-run/agent.json",
+ "--session-id", control["session_id"]], directory)
+ command([python.parent / "qita", "inspect", "session", control["session_id"], "--logdir", "notes-run"], directory)
+ with socket.socket() as listener:
+ listener.bind(("127.0.0.1", 0))
+ port = listener.getsockname()[1]
+ with (directory / "replay.log").open("w") as log:
+ process = subprocess.Popen([str(python.parent / "qita"), "replay", "--run", str(run),
+ "--port", str(port)], cwd=directory, stdout=log, stderr=log)
+ try:
+ deadline = time.monotonic() + 20
+ while True:
+ assert process.poll() is None, "replay server exited before HTTP verification"
+ try:
+ with urlopen(f"http://127.0.0.1:{port}", timeout=1) as response:
+ html = response.read().decode()
+ assert response.status == 200 and "qita" in html.lower()
+ break
+ except URLError:
+ if time.monotonic() >= deadline:
+ raise AssertionError("replay HTTP endpoint did not become ready")
+ time.sleep(0.1)
+ finally:
+ process.terminate()
+ process.wait(timeout=10)
+ command([python.parent / "qita", "export", "--run", run, "--html", "trajectory.html"], directory)
+ assert (directory / "trajectory.html").stat().st_size > 100
+
+
+def test_chapters_run_in_learning_order(installed, tmp_path):
+ _, python, _ = installed
+ directory = tmp_path / "notes_project"
+ directory.mkdir()
+ for unit in CONTRACT["units"]:
+ if unit["runtime"] != "offline":
+ continue
+ materialize(ROOT / "docs" / f"{unit['page']}.mdx", directory)
+ if unit["page"] == "guides/observability":
+ unit = {**unit, "commands": unit["commands"][1:]}
+ execute_unit(unit, directory, python)
+
+
+@pytest.mark.skipif(os.environ.get("QITOS_DOCS_DOCKER") != "1", reason="explicit Docker qualification; not ordinary docs CI")
+def test_page_docker_publication(installed, tmp_path):
+ _, python, _ = installed
+ unit = next(u for u in CONTRACT["units"] if u["runtime"] == "docker")
+ for prefix in ("", "zh/"):
+ directory = tmp_path / ("zh" if prefix else "en")
+ directory.mkdir()
+ materialize(ROOT / "docs" / f"{prefix}{unit['page']}.mdx", directory)
+ execute_unit(unit, directory, python)
+ assert (directory / "sandbox-private/source/report.txt").read_text() == "original\n"
+ assert (directory / "sandbox-published/source/report.txt").read_text() == "Session, Artifact\n"