Skip to content
Merged
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
43 changes: 42 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0] - 2026-10-15

### Added
- Capabilities. `chat.start(..., thread_capabilities=AgentCapabilities(...))` overrides some
of the agent's capabilities for the thread, frozen for its lifetime (not accepted on
`chat.send`). `message_scope=MessageScope(...)` on `start` and `send` turns a
capability on or off, or narrows its allowlist, for one message (sent as `message.scope`).
Every capability is an explicit object with a required activation:
`WebCapability`, `IndexedDocumentsCapability`, `McpCapability`, `SkillsCapability`,
`ImageGenerationCapability`, `VideoGenerationCapability`, `MusicGenerationCapability`
(`"always"`, `"on_request"` or `"never"`; skills take `"always"` or `"never"`). Message
scopes are `WebScope`, `IndexedDocumentsScope`, `McpScope`, `ImageGenerationScope`,
`VideoGenerationScope`, `MusicGenerationScope`, each with a required `enabled`. There are no
shortcuts such as `web=True` or `web="on_request"`. Filters (`source_ids`, `document_ids`,
`connections`, `include`) default to the new `ALL` (`"*"`, no restriction), which can also be
passed explicitly; `None`, `[]` and `"*"` inside a list raise `InvalidParams`, and so does a
filter combined with `"never"` or `enabled=False`.
- `MemoryCapability(activation="always", namespace=...)` / `MemoryCapability(activation="never")`
in `AgentCapabilities(memory=...)`: sets the thread's memory bag (1 to 128 characters), or
runs it without memory. Without a `memory` entry, the thread uses the agent's namespace, or
no memory if the agent has none.
- New exports: `ALL`, `AgentCapabilities`, `MessageScope`, the `*Capability` and `*Scope`
classes above, and `MemoryCapability`.
- `examples/11_capabilities.py`.

### Removed
- BREAKING: `source_ids`, `document_ids` and `disabled_tools` on `chat.start` / `chat.send`,
plus `DisablableTool`, `HumanMessage.source_ids/document_ids/disabled_tools`. Migrate to
`message_scope`:
`disabled_tools=["web"]` -> `MessageScope(web=WebScope(enabled=False))`;
`"company_documents"` -> `indexed_documents=IndexedDocumentsScope(enabled=False)`;
`"mcp:*"` -> `mcp=McpScope(enabled=False)`;
`"mcp:notion"` -> `mcp=McpScope(enabled=True, connections=[<connections to keep>])`;
`source_ids=[101]` -> `indexed_documents=IndexedDocumentsScope(enabled=True, source_ids=[101])`;
`document_ids=[...]` -> `IndexedDocumentsScope(enabled=True, document_ids=[...])`.
- BREAKING: `memory_namespace` on `chat.start`, and `StartChatOptions.memory_namespace`. The
memory bag is now a capability, like the others. Migrate:
`memory_namespace="support-bot"` ->
`thread_capabilities=AgentCapabilities(memory=MemoryCapability(activation="always", namespace="support-bot"))`.

## [0.5.0] - 2026-10-07

### Added
Expand Down Expand Up @@ -151,7 +191,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Initial async client `AsyncCominty` covering threads, chat, messages
(send/stream/export/cancel), files (upload/download), usage, and API tokens.

[Unreleased]: https://github.com/cominty/python-sdk/compare/v0.5.0...HEAD
[Unreleased]: https://github.com/cominty/python-sdk/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/cominty/python-sdk/compare/v0.5.0...v1.0.0
[0.5.0]: https://github.com/cominty/python-sdk/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/cominty/python-sdk/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/cominty/python-sdk/compare/v0.2.0...v0.3.0
Expand Down
84 changes: 78 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,81 @@ A few other things worth knowing:
- `content` may be an empty string; there's no minimum length.
- `memory.delete()` is not idempotent: deleting an already-deleted path
raises `NotFoundError`, not a repeated success.
- `chat.start(..., memory_namespace=...)` attaches a thread to a bag; the
bag is frozen once the thread starts (see [Message parameters](#message-parameters)).
- A thread is attached to a bag with `MemoryCapability` in `chat.start(thread_capabilities=...)`;
the bag is frozen once the thread starts (see [Capabilities](#capabilities)).

### Capabilities

A capability is something the agent may use: `web`, `indexed_documents`, `mcp`, `skills`,
`image_generation`, `video_generation`, `music_generation`, plus `memory`. Each one is an
explicit object, and the keyword name tells you the scope:

```python
from cominty_sdk import (
ALL,
AgentCapabilities,
ImageGenerationCapability,
ImageGenerationScope,
IndexedDocumentsCapability,
IndexedDocumentsScope,
McpCapability,
McpScope,
MemoryCapability,
MessageScope,
SkillsCapability,
WebCapability,
WebScope,
)

run = await client.chat.start(
agent_id=AGENT_ID,
message="Summarize every board deck",
thread_capabilities=AgentCapabilities( # frozen for the thread, start only
web=WebCapability(activation="always"),
# document_ids omitted: defaults to ALL
indexed_documents=IndexedDocumentsCapability(activation="always", source_ids=[101]),
# explicit ALL, same as omitting connections
mcp=McpCapability(activation="on_request", connections=ALL),
skills=SkillsCapability(activation="always"),
image_generation=ImageGenerationCapability(activation="on_request"),
memory=MemoryCapability(activation="always", namespace="support-bot"),
),
message_scope=MessageScope( # this message only
image_generation=ImageGenerationScope(enabled=True)
),
)

await client.chat.send(
run.thread.id,
agent_id=AGENT_ID,
message="Only deck 2024",
message_scope=MessageScope(
indexed_documents=IndexedDocumentsScope(enabled=True, source_ids=[101]),
mcp=McpScope(enabled=True, connections=["linear"]),
web=WebScope(enabled=False),
),
)
```

- There are no shortcuts: no `web=True`, no `web="on_request"`. Thread capabilities are
`<Name>Capability(activation=...)` objects, message scopes are `<Name>Scope(enabled=...)`
objects, and the activation is always required.
- Activation is `"always"` (on by default), `"on_request"` (off until a message enables it) or
`"never"` (not installed). `SkillsCapability` and `MemoryCapability` have no `"on_request"`.
A capability you do not pass keeps the agent's value.
- Filters (`source_ids`, `document_ids`, `connections`, `include`) default to `ALL` (the string
`"*"`, no restriction), so passing `ALL` only makes the intent explicit. A capability you send
replaces the stored one, so an omitted filter resets to `ALL`. Otherwise pass a non-empty
list: `None`, `[]` and a `"*"` inside a list raise `InvalidParams`.
- `"never"` and `enabled=False` send nothing else: passing a filter with them raises
`InvalidParams`.
- `MemoryCapability(activation="always", namespace=...)` attaches the thread to a memory bag,
`MemoryCapability(activation="never")` runs it without memory. A message has no memory scope.
Without a `memory` entry, the thread uses the agent's namespace, or no memory if the agent
has none. The bag is frozen for the thread: a follow-up cannot change it.
- `MessageScope` has no `skills` or `memory`.
- A message can only narrow what the thread allows. The server answers 422 (`APIError`)
when it widens an allowlist or targets a capability the thread does not have.

## Examples

Expand All @@ -275,6 +348,7 @@ Runnable scripts for each scenario live in [`examples/`](examples/):
| [`07_custom_agent.py`](examples/07_custom_agent.py) | Call a custom managed agent (your own model + instructions) |
| [`08_mcp_linear.py`](examples/08_mcp_linear.py) | Custom agent pulls live context from an MCP server (Linear) |
| [`10_max_steps.py`](examples/10_max_steps.py) | Cap tool rounds per message, then continue past the cap |
| [`11_capabilities.py`](examples/11_capabilities.py) | Thread and message capabilities |

They render colored, aligned output with [`rich`](https://github.com/Textualize/rich),
which ships in the dev extras:
Expand All @@ -295,10 +369,8 @@ Both `chat.start` and `chat.send` accept:
| `message` | `str` | **Required.** The user's message (max 30,000 chars). |
| `name` | `str` | `start` only: names the new thread. |
| `file_ids` | `list[str]` | Attach previously-uploaded files (max 5). |
| `source_ids` | `list[int]` | Restrict retrieval to specific knowledge sources. |
| `document_ids` | `list[str]` | Restrict retrieval to specific documents. |
| `disabled_tools` | `list[str]` | Turn tools off: `"web"`, `"company_documents"`, `"mcp:<server>"`, or `"mcp:*"` for all MCP. |
| `memory_namespace` | `str` | `start` only: the memory bag for this thread (max 128 chars), frozen for its lifetime. A follow-up cannot change it. |
| `thread_capabilities` | `AgentCapabilities` | `start` only: capability override, frozen for the thread. Includes the memory bag (`MemoryCapability`). |
| `message_scope` | `MessageScope` | Turn capabilities on/off or narrow them for this message (sent as `message.scope`). |
| `max_steps` | `int` | Cap on tool rounds for **this message** (integer `>= 1`). Per message, not per thread. Unset (or `SERVER_DEFAULT`) means the server default, 60 today. |

Invalid values raise `InvalidParams` **before** any request is sent.
Expand Down
8 changes: 7 additions & 1 deletion examples/07_custom_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import _pretty as pretty
from _shared import CUSTOM_AGENT_ID, make_client

from cominty_sdk import IndexedDocumentsScope, McpScope, MessageScope, WebScope

# Deliberately dense and technical: the agent's job is to make this legible to
# a non-technical executive, in French.
TECHNICAL_INPUT = """\
Expand Down Expand Up @@ -50,7 +52,11 @@ async def main() -> None:
message=TECHNICAL_INPUT,
# Pure transformation: no tools needed, so turn them off for speed
# and determinism.
disabled_tools=["web", "company_documents", "mcp:*"],
message_scope=MessageScope(
web=WebScope(enabled=False),
indexed_documents=IndexedDocumentsScope(enabled=False),
mcp=McpScope(enabled=False),
),
)

async for event in run:
Expand Down
14 changes: 9 additions & 5 deletions examples/08_mcp_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
- The Linear MCP server is connected to your org / available to the agent.
- The custom agent has tools (MCP) enabled.

Tool control is by *exclusion*: tools are on by default and you disable what you
don't want. Here we disable web search and company documents so the agent draws
context only from MCP (Linear). Watch the stream for the Linear tool calls.
Capability control is per message: capabilities are on by default and you turn off
what you don't want with ``MessageScope``. Here we turn off web search and
indexed documents so the agent draws context only from MCP (Linear). Watch the
stream for the Linear tool calls.
"""

from __future__ import annotations
Expand All @@ -24,7 +25,7 @@
import _pretty as pretty
from _shared import CUSTOM_AGENT_ID, make_client

from cominty_sdk import events
from cominty_sdk import IndexedDocumentsScope, MessageScope, WebScope, events

REQUEST = (
"Using Linear, list the tasks assigned to Hiroshi in the current sprint. "
Expand All @@ -48,7 +49,10 @@ async def main() -> None:
agent_id=CUSTOM_AGENT_ID,
message=REQUEST,
# Keep MCP (Linear) ON; cut the rest so context comes only from Linear.
disabled_tools=["web", "company_documents"],
message_scope=MessageScope(
web=WebScope(enabled=False),
indexed_documents=IndexedDocumentsScope(enabled=False),
),
)

# The stream surfaces the Linear MCP calls as ToolCall events.
Expand Down
66 changes: 66 additions & 0 deletions examples/11_capabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Control what the agent can use.

python examples/11_capabilities.py

Two scopes, one rule: the keyword name tells you the scope.

- ``thread_capabilities``: set when the thread starts, frozen for its lifetime.
- ``message_scope``: turn a capability on/off or narrow it for one message.

Every capability is an explicit object (``WebCapability``, ``McpCapability``, ...)
and its activation is always spelled out. Filters (``source_ids``, ``connections``,
...) default to ``ALL``, so passing ``ALL`` only makes the intent explicit.
"""

from __future__ import annotations

import asyncio

import _pretty as pretty
from _shared import AGENT_ID, make_client

from cominty_sdk import (
ALL,
AgentCapabilities,
ImageGenerationCapability,
IndexedDocumentsCapability,
IndexedDocumentsScope,
McpCapability,
MemoryCapability,
MessageScope,
WebCapability,
WebScope,
)


async def main() -> None:
async with make_client() as client:
run = await client.chat.start(
agent_id=AGENT_ID,
message="What is the VAT rate for food in France?",
thread_capabilities=AgentCapabilities(
web=WebCapability(activation="always"),
# source_ids and document_ids omitted: both default to ALL.
indexed_documents=IndexedDocumentsCapability(activation="on_request"),
# Explicit ALL, same as leaving connections out.
mcp=McpCapability(activation="on_request", connections=ALL),
image_generation=ImageGenerationCapability(activation="never"),
memory=MemoryCapability(activation="always", namespace="vat-demo"),
),
)
pretty.answer(await run.text())

follow_up = await client.chat.send(
run.thread.id,
agent_id=AGENT_ID,
message="Now answer from our documents only.",
message_scope=MessageScope(
web=WebScope(enabled=False),
indexed_documents=IndexedDocumentsScope(enabled=True),
),
)
pretty.answer(await follow_up.text())


if __name__ == "__main__":
asyncio.run(main())
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ python examples/01_stream_events.py
| [`08_mcp_linear.py`](08_mcp_linear.py) | Custom agent pulls live context from the Linear MCP server |
| [`09_memory.py`](09_memory.py) | Create, list, read, update, and delete a memory file |
| [`10_max_steps.py`](10_max_steps.py) | Cap tool rounds per message with `max_steps`, then continue past the cap |
| [`11_capabilities.py`](11_capabilities.py) | Thread and message capabilities |

> Shared client setup lives in [`_shared.py`](_shared.py).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cominty-sdk"
version = "0.5.0"
version = "1.0.0"
description = "Official async Python client for the Cominty managed agent chat API"
readme = "README.md"
requires-python = ">=3.9"
Expand Down
38 changes: 36 additions & 2 deletions src/cominty_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,30 @@
ServerError,
StreamInterrupted,
)
from .models.capabilities import (
ALL,
AgentCapabilities,
ImageGenerationCapability,
ImageGenerationScope,
IndexedDocumentsCapability,
IndexedDocumentsScope,
McpCapability,
McpScope,
MemoryCapability,
MessageScope,
MusicGenerationCapability,
MusicGenerationScope,
SkillsCapability,
VideoGenerationCapability,
VideoGenerationScope,
WebCapability,
WebScope,
)
from .models.chat import (
SERVER_DEFAULT,
Agent,
ContentOrigin,
ConversationFile,
DisablableTool,
HumanMessage,
MaxSteps,
Message,
Expand Down Expand Up @@ -73,10 +91,26 @@
"InvalidParam",
"InvalidParams",
# models
"ALL",
"AgentCapabilities",
"ImageGenerationCapability",
"ImageGenerationScope",
"IndexedDocumentsCapability",
"IndexedDocumentsScope",
"McpCapability",
"McpScope",
"MemoryCapability",
"MessageScope",
"MusicGenerationCapability",
"MusicGenerationScope",
"SkillsCapability",
"VideoGenerationCapability",
"VideoGenerationScope",
"WebCapability",
"WebScope",
"Agent",
"ContentOrigin",
"ConversationFile",
"DisablableTool",
"HumanMessage",
"MaxSteps",
"Message",
Expand Down
2 changes: 1 addition & 1 deletion src/cominty_sdk/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class InvalidParam(TypedDict):
"""One offending parameter in an :class:`InvalidParams` error."""

param: str
"""The public argument that failed, e.g. ``"disabled_tools[0]"``."""
"""The public argument that failed, e.g. ``"file_ids[0]"``."""
message: str
"""Why it failed (the underlying validation message)."""
input: Any
Expand Down
Loading
Loading