diff --git a/docs/features-showcase.md b/docs/features-showcase.md
deleted file mode 100644
index bb988b06..00000000
--- a/docs/features-showcase.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Feature Showcase
-
-
-
- 📈 24/7 Real-Time Market Analysis |
- 🚀 Full-Stack Software Engineer |
- 📅 Smart Daily Routine Manager |
- 📚 Personal Knowledge Assistant |
-
-
- 
|
- 
|
- 
|
- 
|
-
-
- | Discovery • Insights • Trends |
- Develop • Deploy • Scale |
- Schedule • Automate • Organize |
- Learn • Memory • Reasoning |
-
-
diff --git a/docs/memory.md b/docs/memory.md
index 2fde1c68..763e0643 100644
--- a/docs/memory.md
+++ b/docs/memory.md
@@ -1,7 +1,5 @@
# Memory in nanobot
-> **Note:** This design is currently an experiment in the latest source code version and is planned to officially ship in `v0.1.5`.
-
nanobot's memory is built on a simple belief: memory should feel alive, but it should not feel chaotic.
Good memory is not a pile of notes. It is a quiet system of attention. It notices what is worth keeping, lets go of what no longer needs the spotlight, and turns lived experience into something calm, durable, and useful.
diff --git a/docs/python-sdk.md b/docs/python-sdk.md
index d035238d..5ee66a34 100644
--- a/docs/python-sdk.md
+++ b/docs/python-sdk.md
@@ -1,31 +1,219 @@
# Python SDK
-Use nanobot as a library — no CLI, no gateway, just Python:
+Use nanobot as a library — no CLI, no gateway, just Python.
+
+## Quick Start
+
+```python
+import asyncio
+
+from nanobot import Nanobot
+
+
+async def main() -> None:
+ bot = Nanobot.from_config()
+ result = await bot.run("What time is it in Tokyo?")
+ print(result.content)
+
+
+asyncio.run(main())
+```
+
+`Nanobot.from_config()` reuses your normal `~/.nanobot/config.json`, so the SDK follows the same provider, model, tools, and workspace defaults as the CLI unless you override them.
+
+## Common Patterns
+
+### Use a specific config or workspace
```python
from nanobot import Nanobot
-bot = Nanobot.from_config()
-result = await bot.run("Summarize the README")
-print(result.content)
+bot = Nanobot.from_config(
+ config_path="~/.nanobot/config.json",
+ workspace="/my/project",
+)
```
-Each call carries a `session_key` for conversation isolation — different keys get independent history:
+### Isolate conversations with `session_key`
+
+Different session keys keep independent conversation history:
```python
await bot.run("hi", session_key="user-alice")
await bot.run("hi", session_key="task-42")
```
-Add lifecycle hooks to observe or customize the agent:
+### Attach hooks for observability
+
+Hooks let you inspect tool calls, streaming, and iteration state without modifying nanobot internals:
```python
from nanobot.agent import AgentHook, AgentHookContext
+
class AuditHook(AgentHook):
- async def before_execute_tools(self, ctx: AgentHookContext) -> None:
- for tc in ctx.tool_calls:
+ async def before_execute_tools(self, context: AgentHookContext) -> None:
+ for tc in context.tool_calls:
print(f"[tool] {tc.name}")
-result = await bot.run("Hello", hooks=[AuditHook()])
+
+result = await bot.run("Review this change", hooks=[AuditHook()])
+```
+
+## API Reference
+
+### `Nanobot.from_config(config_path=None, *, workspace=None)`
+
+Create a `Nanobot` instance from a config file.
+
+| Param | Type | Default | Description |
+|-------|------|---------|-------------|
+| `config_path` | `str \| Path \| None` | `None` | Path to `config.json`. Defaults to `~/.nanobot/config.json`. |
+| `workspace` | `str \| Path \| None` | `None` | Override the workspace directory from config. |
+
+Raises `FileNotFoundError` if an explicit config path does not exist.
+
+### `await bot.run(message, *, session_key="sdk:default", hooks=None)`
+
+Run the agent once and return a `RunResult`.
+
+| Param | Type | Default | Description |
+|-------|------|---------|-------------|
+| `message` | `str` | *(required)* | The user message to process. |
+| `session_key` | `str` | `"sdk:default"` | Session identifier for conversation isolation. Different keys get independent history. |
+| `hooks` | `list[AgentHook] \| None` | `None` | Lifecycle hooks for this run only. |
+
+### `RunResult`
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `content` | `str` | The agent's final text response. |
+| `tools_used` | `list[str]` | Reserved for richer SDK introspection; may be empty in current versions. |
+| `messages` | `list[dict]` | Reserved for richer SDK introspection; may be empty in current versions. |
+
+## Hooks
+
+Hooks let you observe or customize the agent loop. Subclass `AgentHook` and override the methods you need.
+
+### Hook lifecycle
+
+| Method | When |
+|--------|------|
+| `wants_streaming()` | Return `True` if you want token-by-token `on_stream()` callbacks |
+| `before_iteration(context)` | Before each LLM call |
+| `on_stream(context, delta)` | On each streamed token when streaming is enabled |
+| `on_stream_end(context, *, resuming)` | When streaming finishes |
+| `before_execute_tools(context)` | Before tool execution |
+| `after_iteration(context)` | After each iteration |
+| `finalize_content(context, content)` | Transform final output text |
+
+Useful fields on `AgentHookContext` include:
+
+- `iteration`
+- `messages`
+- `response`
+- `usage`
+- `tool_calls`
+- `tool_results`
+- `tool_events`
+- `final_content`
+- `stop_reason`
+- `error`
+
+### Example: audit tool calls
+
+```python
+from nanobot.agent import AgentHook, AgentHookContext
+
+
+class AuditHook(AgentHook):
+ def __init__(self) -> None:
+ super().__init__()
+ self.calls: list[str] = []
+
+ async def before_execute_tools(self, context: AgentHookContext) -> None:
+ for tc in context.tool_calls:
+ self.calls.append(tc.name)
+ print(f"[audit] {tc.name}({tc.arguments})")
+```
+
+```python
+hook = AuditHook()
+result = await bot.run("List files in /tmp", hooks=[hook])
+print(result.content)
+print(f"Tools observed: {hook.calls}")
+```
+
+### Example: receive streaming tokens
+
+```python
+from nanobot.agent import AgentHook, AgentHookContext
+
+
+class StreamingHook(AgentHook):
+ def wants_streaming(self) -> bool:
+ return True
+
+ async def on_stream(self, context: AgentHookContext, delta: str) -> None:
+ print(delta, end="", flush=True)
+
+ async def on_stream_end(self, context: AgentHookContext, *, resuming: bool) -> None:
+ print()
+```
+
+### Compose multiple hooks
+
+Pass multiple hooks when you want to combine behaviors:
+
+```python
+result = await bot.run("hi", hooks=[AuditHook(), MetricsHook()])
+```
+
+Async hook methods are fan-out with error isolation. `finalize_content` is a pipeline: each hook receives the previous hook's output.
+
+### Example: post-process final content
+
+```python
+from nanobot.agent import AgentHook
+
+
+class Censor(AgentHook):
+ def finalize_content(self, context, content):
+ return content.replace("secret", "***") if content else content
+```
+
+## Full Example
+
+```python
+import asyncio
+import time
+
+from nanobot import Nanobot
+from nanobot.agent import AgentHook, AgentHookContext
+
+
+class TimingHook(AgentHook):
+ def __init__(self) -> None:
+ super().__init__()
+ self._started_at = 0.0
+
+ async def before_iteration(self, context: AgentHookContext) -> None:
+ self._started_at = time.perf_counter()
+
+ async def after_iteration(self, context: AgentHookContext) -> None:
+ elapsed_ms = (time.perf_counter() - self._started_at) * 1000
+ print(f"[timing] iteration {context.iteration} took {elapsed_ms:.1f}ms")
+
+
+async def main() -> None:
+ bot = Nanobot.from_config(workspace="/my/project")
+ result = await bot.run(
+ "Explain the main function",
+ session_key="sdk:demo",
+ hooks=[TimingHook()],
+ )
+ print(result.content)
+
+
+asyncio.run(main())
```