diff --git a/docs/docs.json b/docs/docs.json index b9d0e498..6ddbe0cb 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -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", diff --git a/docs/v3/documentation/features/advanced/representation-scopes.mdx b/docs/v3/documentation/features/advanced/directional-representations.mdx similarity index 99% rename from docs/v3/documentation/features/advanced/representation-scopes.mdx rename to docs/v3/documentation/features/advanced/directional-representations.mdx index 30e2ae32..12e9329b 100644 --- a/docs/v3/documentation/features/advanced/representation-scopes.mdx +++ b/docs/v3/documentation/features/advanced/directional-representations.mdx @@ -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' --- diff --git a/docs/v3/documentation/features/advanced/overview.mdx b/docs/v3/documentation/features/advanced/overview.mdx index 09f92fea..d0d7cc2c 100644 --- a/docs/v3/documentation/features/advanced/overview.mdx +++ b/docs/v3/documentation/features/advanced/overview.mdx @@ -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 diff --git a/docs/v3/documentation/features/advanced/peer-card.mdx b/docs/v3/documentation/features/advanced/peer-card.mdx index fbc08772..5db6a1db 100644 --- a/docs/v3/documentation/features/advanced/peer-card.mdx +++ b/docs/v3/documentation/features/advanced/peer-card.mdx @@ -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. diff --git a/docs/v3/documentation/features/advanced/reasoning-configuration.mdx b/docs/v3/documentation/features/advanced/reasoning-configuration.mdx index 8642254a..492dd4b9 100644 --- a/docs/v3/documentation/features/advanced/reasoning-configuration.mdx +++ b/docs/v3/documentation/features/advanced/reasoning-configuration.mdx @@ -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. -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). diff --git a/docs/v3/documentation/features/advanced/scopes.mdx b/docs/v3/documentation/features/advanced/scopes.mdx new file mode 100644 index 00000000..5d538d7b --- /dev/null +++ b/docs/v3/documentation/features/advanced/scopes.mdx @@ -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
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. + + +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. + + +## 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. + + +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. + + +## Creating a Scope and Managing Membership + + +```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" +``` + + +Scope IDs are unprefixed, must match `^[a-zA-Z0-9_-]+$`, and are at most 506 +characters. + + +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`. + + +## 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
in therapy-1] --> D[Deriver] + D -->|fan-out| PR[peer's own
representation] + D -->|fan-out| SR[scope: therapy
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": + + +```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" +``` + + +`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
resolution} + R -->|single scope| O[observer swaps
to the scope] + R -->|list of scopes| A[union of member sessions
as an allowlist] + O --> D1[all conclusion levels
+ 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 | + + +```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"}' +``` + + +### 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. + + +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. + + +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 | diff --git a/docs/v3/documentation/features/advanced/using-filters.mdx b/docs/v3/documentation/features/advanced/using-filters.mdx index 4a1eefec..3af0fdbe 100644 --- a/docs/v3/documentation/features/advanced/using-filters.mdx +++ b/docs/v3/documentation/features/advanced/using-filters.mdx @@ -875,10 +875,34 @@ curl -X POST "$HONCHO_URL/v3/workspaces/my-app/peers/user-123/representation" \ ```
+Both SDKs expose this as a `sessions` option, which goes on the wire as the +`filters` body above: + + +```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"], +}); +``` + + -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. ### Rules diff --git a/docs/v3/guides/integrations/paperclip.mdx b/docs/v3/guides/integrations/paperclip.mdx index 3b2aa156..d5c216a4 100644 --- a/docs/v3/guides/integrations/paperclip.mdx +++ b/docs/v3/guides/integrations/paperclip.mdx @@ -130,7 +130,7 @@ The plugin registers the following Honcho tools for Paperclip agents: Review how workspaces, peers, and sessions fit together. - + Review how `observe_me` and `observe_others` change what peers can model.