chore(docs): Add Documentation for Scopes

This commit is contained in:
Vineeth Voruganti 2026-08-26 16:58:59 -04:00
parent 168185ae2b
commit 5cf635f2d6
8 changed files with 437 additions and 10 deletions

View File

@ -10,6 +10,10 @@
{
"source": "/v3/guides/integrations/claudecode",
"destination": "/v3/guides/integrations/claude-code"
},
{
"source": "/v3/documentation/features/advanced/representation-scopes",
"destination": "/v3/documentation/features/advanced/directional-representations"
}
],
"colors": {
@ -62,7 +66,8 @@
"v3/documentation/features/advanced/reasoning-configuration",
"v3/documentation/features/advanced/summarizer",
"v3/documentation/features/advanced/peer-card",
"v3/documentation/features/advanced/representation-scopes",
"v3/documentation/features/advanced/directional-representations",
"v3/documentation/features/advanced/scopes",
"v3/documentation/features/advanced/dreaming",
"v3/documentation/features/advanced/queue-status",
"v3/documentation/features/advanced/webhooks",

View File

@ -1,6 +1,6 @@
---
title: 'Representation Scopes'
description: 'Advanced configuration and querying for representations'
title: 'Directional Representations'
description: 'How peers build and query representations of other peers'
icon: 'circle'
---

View File

@ -12,7 +12,8 @@ Advanced features give you fine-grained control over Honcho's behavior and imple
- [Configuration](/v3/documentation/features/advanced/reasoning-configuration) - Configure reasoning models and behavior
- [Summarizer](/v3/documentation/features/advanced/summarizer) - Automatic session summarization
- [Peer Card](/v3/documentation/features/advanced/peer-card) - Quick-reference profile of stable biographical facts about a peer
- [Representation Scopes](/v3/documentation/features/advanced/representation-scopes) - Directional representations for multi-peer scenarios
- [Directional Representations](/v3/documentation/features/advanced/directional-representations) - How peers build separate representations of each other
- [Scopes](/v3/documentation/features/advanced/scopes) - Named sets of sessions that act as visibility boundaries for recall
- [Dreaming](/v3/documentation/features/advanced/dreaming) - Autonomous memory consolidation and self-improvement
- [Queue Status](/v3/documentation/features/advanced/queue-status) - Monitor background processing and reasoning tasks

View File

@ -79,7 +79,7 @@ console.log(card);
## Directional Peer Cards
Peer cards follow the same observer-observed model as [representations](/v3/documentation/features/advanced/representation-scopes). When `observe_others` is enabled, a peer can have a **different** card for each peer it observes.
Peer cards follow the same observer-observed model as [representations](/v3/documentation/features/advanced/directional-representations). When `observe_others` is enabled, a peer can have a **different** card for each peer it observes.
For example, if Alice and Bob are in a session together and Alice has `observe_others: true`, Alice will build her own peer card for Bob--separate from Honcho's peer card for Bob. You can read and write these directional cards using the `target` parameter.

View File

@ -157,7 +157,7 @@ You may therefore disable observation of a peer by setting the `observe_me` flag
If the peer has a session-level configuration, it will override this configuration. If the flag is not set, or is set to `true`, the peer will be observed.
<Info>
For session-level observation controls and local representations (where peers build separate models of each other), see [Representation Scopes](/v3/documentation/features/advanced/representation-scopes).
For session-level observation controls and local representations (where peers build separate models of each other), see [Directional Representations](/v3/documentation/features/advanced/directional-representations).
</Info>
<CodeGroup>

View File

@ -0,0 +1,397 @@
---
title: 'Scopes'
description: 'Named sets of sessions that act as visibility boundaries for recall'
icon: 'shield-halved'
---
A **scope** is a named set of sessions that acts as a visibility boundary. Recall
performed through a scope sees only what happened in that scope's sessions,
while the peer keeps its single unified representation of everything it has ever
participated in.
Use scopes when one peer's history spans contexts that must not leak into each
other — a therapy app where the clinical sessions must not inform the billing
assistant, a support product where a reseller's agent may only answer from its
own tickets, a multi-tenant deployment where one human works across tenants.
## Projection, Not Partition
A scope does not split a peer into pieces. The peer's representation stays
whole; a scope is a **projection** of it — a view built only from the evidence
in that scope's member sessions.
```mermaid
graph TB
P[Peer: user-123<br/>one unified representation]
P --> S1[session: therapy-1]
P --> S2[session: therapy-2]
P --> S3[session: billing-1]
P --> S4[session: onboarding-1]
SC1[scope: therapy] -.->|projects| S1
SC1 -.->|projects| S2
SC2[scope: billing] -.->|projects| S3
style P fill:#B6DBFF,stroke:#333,color:#000
style S1 fill:#B6DBFF,stroke:#333,color:#000
style S2 fill:#B6DBFF,stroke:#333,color:#000
style S3 fill:#B6DBFF,stroke:#333,color:#000
style S4 fill:#B6DBFF,stroke:#333,color:#000
style SC1 fill:#FFE0B2,stroke:#333,color:#000
style SC2 fill:#FFE0B2,stroke:#333,color:#000
```
Three consequences worth internalizing:
- **Sessions can belong to more than one scope.** Membership is many-to-many.
- **Sessions can belong to no scope.** `onboarding-1` above is reachable
by an unscoped request and by nothing else.
- **An unscoped request still sees everything.** A scope constrains the requests
that name it; it does not hide the sessions from requests that don't.
<Warning>
Scopes are a recall boundary, not an authorization boundary. Anyone holding a
workspace key can read any session with or without a scope. Enforcing that a
given caller *must* go through a given scope is a separate feature (scope-bound
API keys) and is not shipped.
</Warning>
## The Two Arms
There are two ways to confine recall, and they behave differently. Picking the
wrong one is the most common mistake with this feature.
| | `scope="therapy"` (named scope) | `sessions=[...]` / `scope=["a","b"]` (allowlist) |
|---|---|---|
| **Mechanism** | Reads the scope's own representation of the peer | Restricts the peer's own representation to a set of sessions |
| **Conclusions** | All levels — `explicit`, plus `deductive` / `inductive` reasoned **within** the scope | `explicit` only |
| **Reasoning chains** | Available | Unavailable |
| **Setup required** | Yes — create the scope, add sessions, wait for backfill | None — pass session IDs ad hoc |
| **Accepts** | One scope name, or a list of up to 100 | Up to 1,000 session IDs |
### Named scope: depth
Passing a **single** scope name swaps the observer. Recall runs against the
scope's own view of the target peer, which the deriver and dreamer have been
building from the scope's member sessions all along. That view contains
higher-order inferences — but only ones reasoned from evidence inside the scope.
```python
answer = user.chat("What is stressing them out?", scope="therapy")
```
This is the arm you want for a durable, meaningful boundary.
### Allowlist: breadth
Passing a **list** of scopes, or a bare list of session IDs, keeps the peer as
the observer and restricts recall to the union of those sessions. Because a
dream-derived conclusion is synthesized across sessions, it cannot be attributed
to any one of them — so this arm recalls `explicit` conclusions only, and answers
from directly-stated facts rather than inference.
```python
answer = user.chat("What did they say about billing?", sessions=[s1, s2])
answer = user.chat("What did they say?", scope=["therapy", "intake"])
```
Reach for this when the set of sessions is decided per-request, or when you want
a quick boundary without provisioning a scope. See
[Scoping Recall to Sessions](/v3/documentation/features/advanced/using-filters#scoping-recall-to-sessions)
for the full allowlist rules.
<Info>
A list of scopes is the allowlist arm, not "several named scopes at once". It
gives you the union of their *sessions*, at explicit-only depth — it does not
give you the union of their reasoned views. If you need depth, query one scope.
</Info>
## Creating a Scope and Managing Membership
<CodeGroup>
```python Python
from honcho import Honcho
honcho = Honcho(workspace_id="my-app")
# Get or create — idempotent
therapy = honcho.scope("therapy")
# Add existing sessions (max 100 per call)
therapy.add_sessions(["therapy-session-1", "therapy-session-2"])
# Or attach at session creation — the scope is created if it doesn't exist
session = honcho.session("therapy-session-3", scopes=["therapy"])
# Inspect
for s in therapy.sessions():
print(s.id)
therapy.remove_session("therapy-session-1")
for scope in honcho.scopes():
print(scope.id, scope.metadata)
```
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
const honcho = new Honcho({ workspaceId: "my-app" });
// Get or create — idempotent
const therapy = await honcho.scope("therapy");
// Add existing sessions (max 100 per call)
await therapy.addSessions(["therapy-session-1", "therapy-session-2"]);
// Or attach at session creation — the scope is created if it doesn't exist
const session = await honcho.session("therapy-session-3", {
scopes: ["therapy"],
});
// Inspect
for await (const s of await therapy.sessions()) {
console.log(s.id);
}
await therapy.removeSession("therapy-session-1");
for await (const scope of await honcho.scopes()) {
console.log(scope.id, scope.metadata);
}
```
```bash REST
# Get or create (201 created / 200 existing)
curl -X POST "$HONCHO_URL/v3/workspaces/my-app/scopes" \
-H "Authorization: Bearer $HONCHO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"id": "therapy"}'
# Add sessions
curl -X POST "$HONCHO_URL/v3/workspaces/my-app/scopes/therapy/sessions" \
-H "Authorization: Bearer $HONCHO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"session_ids": ["therapy-session-1", "therapy-session-2"]}'
# List membership
curl -X POST "$HONCHO_URL/v3/workspaces/my-app/scopes/therapy/sessions/list" \
-H "Authorization: Bearer $HONCHO_API_KEY"
# Remove one session
curl -X DELETE "$HONCHO_URL/v3/workspaces/my-app/scopes/therapy/sessions/therapy-session-1" \
-H "Authorization: Bearer $HONCHO_API_KEY"
```
</CodeGroup>
Scope IDs are unprefixed, must match `^[a-zA-Z0-9_-]+$`, and are at most 506
characters.
<Note>
Every scopes route — and every read that passes `scope` — requires a
**workspace-level or admin key**. A scope's membership can exceed any single
peer's own session membership, so peer- and session-scoped keys are rejected
with `401`.
</Note>
## Membership Changes Copy, They Don't Re-Derive
A session added to a scope while empty needs nothing special: messages sent
after the change flow into the scope through the normal deriver fan-out.
```mermaid
graph LR
M[new message<br/>in therapy-1] --> D[Deriver]
D -->|fan-out| PR[peer's own<br/>representation]
D -->|fan-out| SR[scope: therapy<br/>representation]
style M fill:#B6DBFF,stroke:#333,color:#000
style D fill:#FFE0B2,stroke:#333,color:#000
style PR fill:#B6DBFF,stroke:#333,color:#000
style SR fill:#B6DBFF,stroke:#333,color:#000
```
A session that **already has messages** is handled retroactively by a background
job rather than by re-running the LLM over its history: adding it enqueues a
`scope_backfill` that copies the session's existing `explicit` conclusions into
the scope's collections, and removing it enqueues a `scope_removal` that
soft-deletes those copies, follows dependent derived conclusions out
(fail-closed), and rebuilds the scope's peer card from whatever evidence remains.
Copying rather than re-deriving is why membership changes are cheap and
deterministic — and why they are also **asynchronous**.
Poll `status()` to tell "the scope hasn't caught up yet" apart from "the scope
has caught up and there is genuinely nothing to recall":
<CodeGroup>
```python Python
therapy.add_sessions(["old-session-with-history"])
status = therapy.status()
# {"old-session-with-history": {"state": "pending", "updated_at": "..."}}
# → later: {"state": "completed", "docs_copied": 42, "updated_at": "..."}
```
```typescript TypeScript
await therapy.addSessions(["old-session-with-history"]);
const status = await therapy.status();
// { "old-session-with-history": { state: "pending", updatedAt: "..." } }
```
```bash REST
curl "$HONCHO_URL/v3/workspaces/my-app/scopes/therapy/status" \
-H "Authorization: Bearer $HONCHO_API_KEY"
```
</CodeGroup>
`state` is `pending`, `completed`, or `failed`; `docs_copied` appears once a
backfill completes. Only sessions that have had a backfill enqueued appear, so an
empty result means none have — not that the scope is empty.
## Reading Through a Scope
```mermaid
graph LR
Q["chat(..., scope='therapy')"] --> R{scope<br/>resolution}
R -->|single scope| O[observer swaps<br/>to the scope]
R -->|list of scopes| A[union of member sessions<br/>as an allowlist]
O --> D1[all conclusion levels<br/>+ reasoning chains]
A --> D2[explicit conclusions only]
style Q fill:#B6DBFF,stroke:#333,color:#000
style R fill:#FFE0B2,stroke:#333,color:#000
style O fill:#B6DBFF,stroke:#333,color:#000
style A fill:#B6DBFF,stroke:#333,color:#000
style D1 fill:#B6DBFF,stroke:#333,color:#000
style D2 fill:#B6DBFF,stroke:#333,color:#000
```
`scope` is accepted on four surfaces:
| Surface | Accepts | Notes |
|---------|---------|-------|
| [`peer.chat()`](/v3/documentation/features/chat) | one scope or a list | Confines both conclusion recall and the messages the agent reads |
| `peer.representation()` | one scope or a list | Confines conclusion recall |
| [`session.context()`](/v3/documentation/features/get-context) | one scope only | Perspective source for `peer_target`'s representation and card. Requires `peer_target`; mutually exclusive with `peer_perspective` |
| `honcho.search()` | one scope only | Restricts message search to the scope's member sessions |
<CodeGroup>
```python Python
# Chat — answered only from the therapy sessions
answer = user.chat("What is stressing them out?", scope="therapy")
# Representation
rep = user.representation(scope="therapy")
# Session context, using the scope as the perspective source
ctx = session.context(peer_target="user-123", scope="therapy")
# Message search, restricted to the scope's sessions
messages = honcho.search("insomnia", scope="therapy")
```
```typescript TypeScript
// Chat — answered only from the therapy sessions
const answer = await user.chat("What is stressing them out?", {
scope: "therapy",
});
// Representation
const rep = await user.representation({ scope: "therapy" });
// Session context, using the scope as the perspective source
const ctx = await session.context({
peerTarget: "user-123",
scope: "therapy",
});
// Message search, restricted to the scope's sessions
const messages = await honcho.search("insomnia", { scope: "therapy" });
```
```bash REST
curl -X POST "$HONCHO_URL/v3/workspaces/my-app/peers/user-123/chat" \
-H "Authorization: Bearer $HONCHO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "What is stressing them out?", "scope": "therapy"}'
```
</CodeGroup>
### Rules
Like the session allowlist, `scope` **fails closed** — a contradiction is
rejected rather than silently widened, and an empty boundary recalls nothing
rather than everything.
| Rule | Behavior |
|------|----------|
| `scope` with `filters` | `422` |
| `scope` with `session` / `session_id` | `422` |
| `scope` with `sessions` | `422` |
| `scope` with `peer_perspective` (context only) | `422` |
| `scope` without `peer_target` (context only) | `422` |
| `scope` with a `session_id` filter (search only) | `422` |
| A scope with no member sessions | Recalls nothing |
| An empty list (`scope=[]`) | Rejected — never treated as "no boundary" |
| More than 100 scopes in one `scope` option | `422` |
| A resolved session union over 1,000 sessions | `422` |
| A peer- or session-scoped key | `401` |
## Provenance, Not Topic
A scope is defined by **where a fact was said**, not what it is about.
If a user mentions a therapy detail in a billing session, that conclusion is
formed from the billing session and lands in the `billing` scope. Querying
`scope="therapy"` will not find it, and querying `scope="billing"` will.
<Warning>
Scopes give you provenance-based privacy, not topic-based privacy. If you need
"no clinical content in the billing assistant's answers" regardless of where it
was said, that is content classification and has to be enforced above Honcho —
by controlling what reaches which session in the first place, or by filtering
the answer.
</Warning>
Design accordingly: keep the session boundary aligned with the confidentiality
boundary you actually care about, since that session boundary is the one scopes
can enforce.
## Underneath the Facade
The `/scopes` routes are a facade. A scope named `therapy` is a peer named
`scope.therapy` that observes its member sessions (`observe_others=true`) and
never speaks (`observe_me=false`), marked with `{"kind": "scope"}` in
server-controlled internal metadata. That is why the single-scope arm has depth:
the scope occupies an observer position, so the deriver and dreamer build a real
representation there, exactly as they do for a peer observing another peer — see
[Directional Representations](/v3/documentation/features/advanced/directional-representations)
for that mechanism.
You never need this detail to use scopes, but it explains the guardrails:
- **The `scope.` prefix is reserved.** Creating a peer, or adding a peer to a
session, with a `scope.`-prefixed name is rejected — the prefix contains `.`,
which is outside the peer-name charset, so no validated API call can occupy the
namespace.
- **Scope peers are hidden from peer listings by default.** Pass
`kind="scope"` to list only scopes, or `kind="all"` to see both.
- **A scope can't be observed.** No representation is formed *of* a scope, so a
scope is rejected in any `target` / observed position, including as a dream
target.
- **Membership is managed only through the scopes surface.** The session
add-peers, set-peers, and remove-peers routes reject scope peers and point you
at `/scopes/{scope_id}/sessions` or the `scopes` field on session create.
## Limits
| Limit | Value |
|-------|-------|
| Scope ID length | 506 characters |
| Scope ID charset | `^[a-zA-Z0-9_-]+$` |
| Sessions per membership call | 100 |
| Scopes in one `scope` read option | 100 |
| Scopes on session create | 100 |
| Sessions in a resolved allowlist | 1,000 |

View File

@ -875,10 +875,34 @@ curl -X POST "$HONCHO_URL/v3/workspaces/my-app/peers/user-123/representation" \
```
</CodeGroup>
Both SDKs expose this as a `sessions` option, which goes on the wire as the
`filters` body above:
<CodeGroup>
```python Python
answer = user.chat("What did the user ask about billing?",
sessions=["support-chat-1", "support-chat-2"])
rep = user.representation(sessions=["support-chat-1", "support-chat-2"])
```
```typescript TypeScript
const answer = await user.chat("What did the user ask about billing?", {
sessions: ["support-chat-1", "support-chat-2"],
});
const rep = await user.representation({
sessions: ["support-chat-1", "support-chat-2"],
});
```
</CodeGroup>
<Note>
The session allowlist is REST-only today. The SDKs cover the single-session case
with `session`, but do not yet expose the allowlist — call the endpoint directly
when you need a set of sessions.
If the same set of sessions is a boundary you reuse, name it: a
[scope](/v3/documentation/features/advanced/scopes) is a persistent version of
this allowlist, and querying a single scope recalls at full depth rather than
`explicit`-only. `sessions` is the right tool when the set is decided
per-request.
</Note>
### Rules

View File

@ -130,7 +130,7 @@ The plugin registers the following Honcho tools for Paperclip agents:
Review how workspaces, peers, and sessions fit together.
</Card>
<Card title="Representation Scopes" icon="messages" href="../../documentation/features/advanced/representation-scopes">
<Card title="Directional Representations" icon="messages" href="../../documentation/features/advanced/directional-representations">
Review how `observe_me` and `observe_others` change what peers can model.
</Card>
</CardGroup>