Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ Workflow: `.github/workflows/sync-agent-sdk-openapi.yml`
- Use Mintlify components (`<Note>`, `<Warning>`, `<Tabs>`, etc.) where appropriate.
- When linking internally, prefer **absolute** doc paths (e.g. `/overview/quickstart`).


## AI-only invariants in SDK architecture docs

- AI-only invariants live in sidecar files alongside the architecture pages:
- `sdk/arch/<page>.ai-invariants.md`
- These files are excluded from the human docs and injected into `llms-full.txt`
by `scripts/generate-llms-files.py`.

## LLM API Key Options

The SDK documentation maintains three ways for users to obtain LLM access:
Expand Down
416 changes: 408 additions & 8 deletions llms-full.txt

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion scripts/generate-llms-files.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
BASE_URL = "https://docs.openhands.dev"

EXCLUDED_DIRS = {".git", ".github", ".agents", "tests", "openapi", "logo"}
AI_INVARIANTS_SUFFIX = ".ai-invariants.md"


@dataclass(frozen=True)
Expand All @@ -60,6 +61,7 @@ class DocPage:
title: str
description: str | None
body: str
ai_invariants: str | None


_FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\n", re.DOTALL)
Expand Down Expand Up @@ -99,6 +101,15 @@ def parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
return fm, body


def load_ai_invariants(rel_path: Path) -> str | None:
sidecar_path = rel_path.with_suffix(AI_INVARIANTS_SUFFIX)
full_path = ROOT / sidecar_path
if not full_path.exists():
return None
content = full_path.read_text(encoding="utf-8").strip()
return content or None


def rel_to_route(rel_path: Path) -> str:
p = rel_path.as_posix()
if p.endswith(".mdx"):
Expand Down Expand Up @@ -132,6 +143,7 @@ def iter_doc_pages() -> list[DocPage]:

raw = mdx_path.read_text(encoding="utf-8")
fm, body = parse_frontmatter(raw)
ai_invariants = load_ai_invariants(rel_path)

title = fm.get("title")
if not title:
Expand All @@ -147,6 +159,7 @@ def iter_doc_pages() -> list[DocPage]:
title=title,
description=description,
body=body.strip(),
ai_invariants=ai_invariants,
)
)

Expand Down Expand Up @@ -228,7 +241,7 @@ def build_llms_txt(pages: list[DocPage]) -> str:
"",
"> LLM-friendly index of OpenHands documentation (V1). Legacy V0 docs pages are intentionally excluded.",
"",
"The sections below intentionally separate OpenHands product documentation (Web App Server / Cloud / CLI)",
"The sections below intentionally separate OpenHands applications documentation (Web App Server / Cloud / CLI)",
"from the OpenHands Software Agent SDK.",
"",
]
Expand Down Expand Up @@ -285,6 +298,10 @@ def build_llms_full_txt(pages: list[DocPage]) -> str:
lines.append(page.body)
lines.append("")

if page.ai_invariants:
lines.append(page.ai_invariants)
lines.append("")

return "\n".join(lines).rstrip() + "\n"


Expand Down
73 changes: 73 additions & 0 deletions sdk/arch/agent.ai-invariants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## Invariants (Normative)

### AgentBase: Configuration is Stateless and Immutable

Natural language invariant:

- An `AgentBase` instance is a **pure configuration object**. It may cache materialized `ToolDefinition` instances internally, but it must remain valid to re-create those tools from its declarative spec.

OCL-like:

- `context AgentBase inv Frozen: self.model_config.frozen = true`


### Initialization: System Prompt Precedes Any User Message

`Agent.init_state(state, on_event=...)` is responsible for creating the initial system prompt event.

Natural language invariant:

- A `ConversationState` must not contain a user `MessageEvent` before it contains a `SystemPromptEvent`.

OCL-like (conceptual):

- `context ConversationState inv SystemBeforeUser: self.events->select(e|e.oclIsKindOf(SystemPromptEvent))->size() >= 1 implies self.events->forAll(e| e.oclIsKindOf(MessageEvent) and e.source='user' implies e.index > systemPromptIndex )`


### Tool Materialization: Names Resolve to Registered ToolDefinitions

An `Agent` is configured with a list of tool *specs* (`openhands.sdk.tool.spec.Tool`) that reference registered `ToolDefinition` factories.

Natural language invariant:

- `resolve_tool(Tool(name=X))` must succeed (tool name present in registry) for all tools the agent intends to use.
- Tool factories must return a **sequence** of `ToolDefinition` instances; tool sets (e.g., browser tool sets) are represented as multi-element sequences.

### Multi-Tool Calls: Shared Thought Only on First ActionEvent

When an LLM returns parallel tool calls, the SDK represents this as multiple `ActionEvent`s that share the same `llm_response_id`.

Natural language invariant:

- For a batch of `ActionEvent`s with the same `llm_response_id`, only the first action carries `thought` / `reasoning_content` / `thinking_blocks`; subsequent actions must have empty `thought`.

OCL-like (as modeled in `event.base._combine_action_events`):

- `context ActionEvent inv BatchedThoughtOnlyFirst: (self.llm_response_id = other.llm_response_id and self <> first) implies self.thought->isEmpty()`


### Confirmation Mode: Requires Both Analyzer and Policy

`conversation.is_confirmation_mode_active` is true iff:

- A `SecurityAnalyzer` is configured, and
- The confirmation policy is not `NeverConfirm`.

OCL-like (conceptual):

- `context BaseConversation inv ConfirmationModeIff: self.is_confirmation_mode_active = (self.state.security_analyzer <> null and not self.state.confirmation_policy.oclIsKindOf(NeverConfirm))`


**Execution Modes:**

| Mode | Behavior | Use Case |
|------|----------|----------|
| **Direct** | Execute immediately | Development, trusted environments |
| **Confirmation** | Store as pending, wait for user approval | High-risk actions, production |

**Security Integration:**

Before execution, the security analyzer evaluates each action:
- **Low Risk:** Execute immediately
- **Medium Risk:** Log warning, execute with monitoring
- **High Risk:** Block execution, request user confirmation
15 changes: 8 additions & 7 deletions sdk/arch/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
**Key Characteristics:**
- **Stateless:** Agent holds no mutable state between steps
- **Event-Driven:** Reads from event history, writes new events
- **Interruptible:** Each step is atomic and can be paused/resumed

Check warning on line 150 in sdk/arch/agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/agent.mdx#L150

Did you really mean 'Interruptible'?

## Agent Context

Expand Down Expand Up @@ -199,26 +199,26 @@
flowchart TB
LLM["LLM generates tool_call"]
Convert["Convert to ActionEvent"]

Decision{"Confirmation<br>mode?"}
Defer["Store as pending"]

Execute["Execute tool"]
Success{"Success?"}

Obs["ObservationEvent<br><i>with result</i>"]
Error["ObservationEvent<br><i>with error</i>"]

LLM --> Convert
Convert --> Decision

Decision -->|Yes| Defer
Decision -->|No| Execute

Execute --> Success
Success -->|Yes| Obs
Success -->|No| Error

style Convert fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
style Execute fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
style Decision fill:#fff4df,stroke:#b7791f,stroke-width:2px
Expand All @@ -238,6 +238,7 @@
- **Medium Risk:** Log warning, execute with monitoring
- **High Risk:** Block execution, request user confirmation


## Component Relationships

### How Agent Interacts
Expand Down
6 changes: 5 additions & 1 deletion sdk/arch/condenser.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ title: Condenser
description: High-level architecture of the conversation history compression system
---

The **Condenser** system manages conversation history compression to keep agent context within LLM token limits. It reduces long event histories into condensed summaries while preserving critical information for reasoning. For more details, read the [blog here](https://openhands.dev/blog/openhands-context-condensensation-for-more-efficient-ai-agents).
The **Condenser** system manages conversation history compression to keep agent context within LLM token limits. It reduces long event histories into condensed summaries while preserving critical information for reasoning.

For how condensation is represented in the event system (`Condensation`, `CondensationRequest`, and how they transform the LLM view), see **[Events Architecture](/sdk/arch/events)**.

For more details, read the [blog here](https://openhands.dev/blog/openhands-context-condensensation-for-more-efficient-ai-agents).

**Source:** [`openhands-sdk/openhands/sdk/context/condenser/`](https://github.com/OpenHands/software-agent-sdk/tree/main/openhands-sdk/openhands/sdk/context/condenser)

Expand Down
40 changes: 40 additions & 0 deletions sdk/arch/conversation.ai-invariants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Invariants (Normative)

### Conversation Factory: Workspace Chooses Implementation

Natural language invariant:

- `Conversation(...)` is a factory that returns `LocalConversation` unless the provided `workspace` is a `RemoteWorkspace`.
- When `workspace` is remote, `persistence_dir` must be unset (`None`).

OCL-like (conceptual):

- `context Conversation::__new__ pre RemoteNoPersistence: workspace.oclIsKindOf(RemoteWorkspace) implies persistence_dir = null`


### ConversationState: Validated Snapshot + Event Log

Natural language invariants:

- `ConversationState` is the **only** component intended to hold mutable execution status (`IDLE`, `RUNNING`, `WAITING_FOR_CONFIRMATION`, etc.).
- `ConversationState` owns persistence (`FileStore`) and the event store; all other components treat persistence as an implementation detail.

### Confirmation Mode Predicate

The SDK exposes a single predicate for confirmation mode:

- Confirmation mode is active iff `state.security_analyzer != None` **and** the confirmation policy is not `NeverConfirm`.

### ask_agent() Must Be Stateless

Natural language invariant (from the public contract):

- `BaseConversation.ask_agent(question)` **must not** append events, mutate execution status, or persist anything. It is safe to call concurrently with `run()`.

### Secrets Persistence Requires a Cipher

Natural language invariant:

- If `ConversationState` is persisted without a cipher, secret values are redacted and **cannot be recovered on restore**.

(Implication: use `Cipher` when persistence is enabled and you expect to resume with secrets intact.)
1 change: 1 addition & 0 deletions sdk/arch/conversation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
| **[`Conversation`](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/conversation.py)** | Unified entrypoint | Returns correct implementation based on workspace type |
| **[`LocalConversation`](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py)** | Local execution | Runs agent directly in process |
| **[`RemoteConversation`](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py)** | Remote execution | Delegates to agent-server via HTTP/WebSocket |
| **[`ConversationState`](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/state.py)** | State container | Pydantic model with validation and serialization |

Check warning on line 67 in sdk/arch/conversation.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/conversation.mdx#L67

Did you really mean 'Pydantic'?
| **[`EventLog`](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/event_store.py)** | Event storage | Immutable append-only store with efficient queries |

## Factory Pattern
Expand Down Expand Up @@ -178,12 +178,12 @@

## Auxiliary Services

The conversation system provides pluggable services that operate independently on the event stream:

Check warning on line 181 in sdk/arch/conversation.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/conversation.mdx#L181

Did you really mean 'pluggable'?

| Service | Purpose | Architecture Pattern |
|---------|---------|---------------------|
| **[Event Log](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/event_store.py)** | Append-only immutable storage | Event sourcing with indexing |
| **[Persistence](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/state.py)** | Auto-save & resume | Debounced writes, incremental events |

Check warning on line 186 in sdk/arch/conversation.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/conversation.mdx#L186

Did you really mean 'Debounced'?
| **[Stuck Detection](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/stuck_detector.py)** | Loop prevention | Sliding window pattern matching |
| **[Visualization](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/visualizer/)** | Execution diagrams | Event stream → visual representation |
| **[Secret Registry](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/secret_registry.py)** | Secure value storage | Memory-only with masked logging |
Expand All @@ -193,6 +193,7 @@
- Easy to add new services without changing core orchestration
- Event stream acts as the integration point


## Component Relationships

### How Conversation Interacts
Expand Down
71 changes: 71 additions & 0 deletions sdk/arch/design.ai-invariants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
## Design Invariants (Normative)

This page describes the **architectural invariants** the SDK relies on. These are treated as *contracts* between components.

Where appropriate, we express invariants in a lightweight OCL-like notation:

- `context X inv Name: <predicate>`
- `pre:` / `post:` for pre/post-conditions

If an invariant cannot be expressed precisely in OCL without significant auxiliary modeling, we state it in precise natural language.

### Single Source of Truth for Runtime State

The SDK is designed so that **all runtime state that affects agent execution is representable as an event log plus a small, validated state snapshot**.

- **Configuration objects are immutable** (Pydantic `frozen=True` where applicable).
- **The only intentionally mutable entity is `ConversationState`**, which owns the event log, execution status, secrets registry, and persistence handles.

OCL-like:

- `context AgentBase inv StatelessConfiguration: self.model_config.frozen = true`
- `context Event inv Immutable: self.model_config.frozen = true`


Natural language invariant:

- `ConversationState` is the single coordination point for execution. Other objects may maintain private runtime caches, but **must not** be required to restore or replay a conversation.

### Workspace Boundary is the I/O Boundary

All side effects against the environment (filesystem, processes, git operations) must occur **through a Workspace** (local or remote), which becomes the **I/O boundary**.

- Tools may execute in different runtimes (local process vs inside agent-server), but *conceptually* they always operate against a workspace rooted at `workspace.working_dir`.

OCL-like:

- `context BaseWorkspace inv WorkingDirIsString: self.working_dir.oclIsTypeOf(String)`


### Event Log is the Execution Trace

The event stream is the single authoritative trace of what the agent *saw* and *did*.

Natural language invariant:

- Any agent decision that should be reproducible on replay must be representable as an `LLMConvertibleEvent` (for LLM context) plus associated non-LLM events (e.g., state updates, errors).

### Tool Calls are Explicit, Typed, and Linkable

The SDK assumes an explicit `Action -> Observation` pairing.

OCL-like (conceptual):

- `context ActionEvent inv HasToolCallId: self.tool_call_id <> null`
- `context ObservationEvent inv RefersToAction: self.action_id <> null`


Natural language invariant:

- Observations must be attributable to a specific action/tool call so that conversations can be audited, visualized, and resumed.

### Remote vs Local is an Execution Detail

The SDK makes *deployment mode* (local vs remote) a **runtime selection behind a common interface**, not two separate programming models.

- `Conversation(...)` returns either `LocalConversation` or `RemoteConversation` based on the provided workspace.
- User-facing code typically should not need to change when switching workspaces; you mostly swap configuration.

<Note>
This does **not** mean every optional method behaves identically across workspace types (e.g., `pause()` / `resume()` may be a no-op locally and meaningful remotely). The core conversation API (`send_message`, `run`, events) stays consistent.
</Note>
2 changes: 2 additions & 0 deletions sdk/arch/design.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@

The **OpenHands Software Agent SDK** is part of the [OpenHands V1](https://openhands.dev/blog/the-path-to-openhands-v1) effort — a complete architectural rework based on lessons from **OpenHands V0**, one of the most widely adopted open-source coding agents.

[Over the last eighteen months](https://openhands.dev/blog/one-year-of-openhands-a-journey-of-open-source-ai-development), OpenHands V0 evolved from a scrappy prototype into a widely used open-source coding agent. The project grew to tens of thousands of GitHub stars, hundreds of contributors, and multiple production deployments. That growth exposed architectural tensions — tight coupling between research and production, mandatory sandboxing, mutable state, and configuration sprawl — which informed the design principles of agent-sdk in V1.

Check warning on line 9 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L9

Did you really mean 'sandboxing'?

## Optional Isolation over Mandatory Sandboxing

Check warning on line 11 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L11

Did you really mean 'Sandboxing'?

<Info>
**V0 Challenge:**
Every tool call in V0 executed in a sandboxed Docker container by default. While this guaranteed reproducibility and security, it also created friction — the agent and sandbox ran as separate processes, states diverged easily, and multi-tenant workloads could crash each other.

Check warning on line 15 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L15

Did you really mean 'sandboxed'?
Moreover, with the rise of the Model Context Protocol (MCP), which assumes local execution and direct access to user environments, V0's rigid isolation model became incompatible.
</Info>

**V1 Principle:**
**Sandboxing should be opt-in, not universal.**

Check warning on line 20 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L20

Did you really mean 'Sandboxing'?
V1 unifies agent and tool execution within a single process by default, aligning with MCP's local-execution model.

Check warning on line 21 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L21

Did you really mean 'MCP's'?
When isolation is needed, the same stack can be transparently containerized, maintaining flexibility without complexity.

## Stateless by Default, One Source of Truth for State
Expand All @@ -30,7 +30,7 @@

**V1 Principle:**
**Keep everything stateless, with exactly one mutable state.**
All components (agents, tools, LLMs, and configurations) are immutable Pydantic models validated at construction.

Check warning on line 33 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L33

Did you really mean 'LLMs'?

Check warning on line 33 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L33

Did you really mean 'Pydantic'?
The only mutable entity is the [conversation state](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/event/conversation_state.py), a single source of truth that enables deterministic replay and robust persistence across sessions or distributed systems.

## Clear Boundaries between Agent and Applications
Expand All @@ -47,7 +47,7 @@
Applications communicate with the agent via APIs rather than embedding it directly, ensuring research and production can evolve independently.


## Composable Components for Extensibility

Check warning on line 50 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L50

Did you really mean 'Composable'?

<Info>
**V0 Challenge:**
Expand All @@ -55,6 +55,8 @@
</Info>

**V1 Principle:**
**Everything should be composable and safe to extend.**

Check warning on line 58 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L58

Did you really mean 'composable'?
Agents are defined as graphs of interchangeable components—tools, prompts, LLMs, and contexts—each described declaratively with strong typing.

Check warning on line 59 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L59

Did you really mean 'LLMs'?

Check warning on line 59 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L59

Did you really mean 'declaratively'?
Developers can reconfigure capabilities (e.g., swap toolsets, override prompts, add delegation logic) without modifying core code, preserving stability while fostering rapid innovation.

Check warning on line 60 in sdk/arch/design.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

sdk/arch/design.mdx#L60

Did you really mean 'toolsets'?

---
50 changes: 50 additions & 0 deletions sdk/arch/events.ai-invariants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Invariants (Normative)

### Event Immutability

All events inherit from `Event` / `LLMConvertibleEvent` with Pydantic config `frozen=True` and `extra="forbid"`.

Natural language invariant:

- Once appended to the event log, an event must be treated as immutable. Mutations are represented as *new events*, not edits.

OCL-like:

- `context Event inv Frozen: self.model_config.frozen = true`


### LLM-Convertible Stream Can Be Reconstructed Deterministically

Natural language invariant:

- `LLMConvertibleEvent.events_to_messages(events)` must produce the exact LLM message stream used for decision making, including batching of parallel tool calls.

### Parallel Tool Calls are Batched by llm_response_id

When multiple `ActionEvent`s share the same `llm_response_id`, they represent a single assistant turn with multiple tool calls.

Natural language invariant:

- In a batch, only the first `ActionEvent` may contain `thought`/reasoning; subsequent actions must have empty `thought`. This is asserted when combining events.

### Condensation is a Pure View Transformation

`Condensation.apply(events)` removes forgotten events and optionally inserts a synthetic `CondensationSummaryEvent` at `summary_offset`.

Natural language invariants:

- Condensation never mutates existing events; it returns a new list.
- `forgotten_event_ids` must refer to events that exist in the input list (otherwise the operation is a no-op for those IDs).
- If `summary` is present, `summary_offset` must also be present to insert the summary into the view; otherwise the summary is metadata only.

OCL-like (conceptual):

- `context Condensation inv SummaryOffsetPair: (self.summary <> null) implies (self.summary_offset <> null) or true -- insertion requires both; metadata-only summary allowed`


For the condenser algorithms, thresholds, and configuration, see **[Condenser Architecture](/sdk/arch/condenser)**.

**Source Types:**
- **user**: Event originated from user input
- **agent**: Event generated by agent logic
- **environment**: Event from system/framework/tools
Loading
Loading