Metadata-Version: 2.4
Name: zeno-tools-things
Version: 1.1.0
Summary: Zeno Things tools: list/add/complete @tool wrappers backed by things-sdk.
Project-URL: Homepage, https://github.com/nkootstra/zeno
Project-URL: Repository, https://github.com/nkootstra/zeno
Project-URL: Issues, https://github.com/nkootstra/zeno/issues
Project-URL: Changelog, https://github.com/nkootstra/zeno/blob/main/CHANGELOG.md
Author: Niels Kootstra
License-Expression: MIT
License-File: LICENSE
Keywords: agent,ai,tasks,things,todo,tools,zeno
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Scheduling
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: things-sdk<1,>=0.3
Requires-Dist: zeno-core
Description-Content-Type: text/markdown

# zeno-tools-things

Things 3 / Things Cloud `@tool` wrappers for the Zeno AI assistant
framework, backed by [`things-sdk`](https://pypi.org/project/things-sdk/).

Three agent-callable tools:

- `things_list(when="today"|"inbox"|"anytime"|"someday")` — read tasks
- `things_add(title, notes=None, when="inbox", deadline=None)` — create a task
- `things_complete(uuid)` — mark a task complete

A `ThingsAccount` owns one local SQLite mirror plus one Things Cloud HTTP
client, with a TTL guard so a single conversational turn doesn't double-pull
from the cloud.

## Install

```bash
uv add 'zeno-framework[things]'
```

Set credentials in the environment:

```bash
export THINGS_EMAIL="you@example.com"
export THINGS_PASSWORD="..."
```

## Wiring

```python
import asyncio
import contextlib
from pathlib import Path

from zeno.agent import Agent
from zeno.app import ZenoApp
from zeno.channels.cli import CliChannel
from zeno.memory import ThreeLayer
from zeno.memory.sqlite.conversation_store import SqliteConversationStore
from zeno.memory.sqlite.session_store import SqliteSessionStore
from zeno.memory.sqlite.user_memory_store import SqliteUserMemoryStore
from zeno.providers.claude_sdk import ClaudeSDKProvider
from zeno.tools_things import (
    configure_things,
    things_add,
    things_complete,
    things_list,
)


async def amain() -> None:
    data = Path.home() / ".zeno"
    data.mkdir(parents=True, exist_ok=True)

    # Reads THINGS_EMAIL / THINGS_PASSWORD from the environment.
    account = configure_things(
        db_path=str(data / "things.db"),
        sync_ttl_seconds=60,
    )

    agent = Agent(
        name="root",
        instructions="You manage the user's Things tasks.",
        tools=[things_list, things_add, things_complete],
    )

    app = ZenoApp(
        agent=agent,
        memory=ThreeLayer(
            session=SqliteSessionStore(data / "sessions.db"),
            user_memory=SqliteUserMemoryStore(data / "user_memory.db"),
            knowledge=...,  # plug in zeno-chroma or zeno-qdrant
            conversation=SqliteConversationStore(data / "conversations.db"),
        ),
        channels=[CliChannel()],
        provider=ClaudeSDKProvider(),
    )

    try:
        await app.run()
    finally:
        with contextlib.suppress(Exception):
            await account.stop()


asyncio.run(amain())
```

`configure_things(...)` installs a process-wide handle. Apps that prefer
per-`Ctx` wiring can set `ctx.state["things"] = ThingsAccount(...)`
directly — the tools resolve `ctx.state` first and fall back to the global.

## Grouping the tools as a `SubAgent`

If your root agent has many responsibilities, isolate Things behind a
specialist:

```python
from zeno.agent import Agent, SubAgent

things_agent = SubAgent(
    name="things",
    instructions=(
        "You manage the user's Things tasks. "
        "Use `things_list` to read, `things_add` to create, "
        "and `things_complete` to mark a task done."
    ),
    tools=[things_list, things_add, things_complete],
)

root = Agent(
    name="root",
    instructions="Delegate todo-related questions to the `things` sub-agent.",
    sub_agents=[things_agent],
)
```

Sub-agent dispatch works on `ClaudeSDKProvider` (Claude Agent SDK's `Task`
tool). `OpenAIProvider` does not support sub-agents — `ZenoApp.start()`
raises `ProviderCapabilityError` if you mix them.

## Sync model

- **Reads**: `things_list` calls `pull_sync` only if the last successful
  pull was outside the configured TTL (default 60 s). Back-to-back calls
  within the window reuse the cached SQLite state.
- **Writes**: `things_add` and `things_complete` write locally then call
  `push_sync` immediately so changes show up on every Things device right
  away.

## License

MIT — see [LICENSE](https://github.com/nkootstra/zeno/blob/main/LICENSE).

Part of the [Zeno framework](https://github.com/nkootstra/zeno).
