IPCClient

The high-level facade for talking to a running jaato daemon from Python. IPCClient.session(...) opens a connection and a session, yielding a Session handle whose ask / complete / stream methods own the send-and-wait recipe — so the common path can never hang or reproduce the low-level event plumbing. This is the recommended Python entry point.

from jaato_sdk import IPCClient, Session, ask, AgentError, PermissionUnhandled
Which client?
This page documents the Python IPCClient (from jaato_sdk import IPCClient), which talks to a jaato daemon over IPC. Don't confuse it with two same-role siblings:
  • The in-process JaatoClient (from jaato import JaatoClient) — runs the agent runtime inside your own process, no daemon.
  • The TypeScript SDK's JaatoClient (import { JaatoClient } from "@jaato/sdk") — the same name, but a different package over a WebSocket transport.
For automatic reconnection on top of this facade, see IPCRecoveryClient, which exposes the same session(...) entry point.
Quick example
import asyncio
from jaato_sdk import IPCClient, ask

async def main():
    # Open a session, ask, tear down on exit.
    async with IPCClient.session(
        profile={"model": "gpt-4o", "provider": "openai", "plugins": []},
    ) as s:
        print(await s.ask("Who are you? One sentence."))

    # ...or the one-shot module helper, for a throwaway call.
    print(await ask(
        "Who are you? One sentence.",
        profile={"model": "gpt-4o", "provider": "openai", "plugins": []},
    ))

asyncio.run(main())

IPCClient.session()

session classmethod

classmethod
IPCClient.session(**kwargs) -> async context manager[Session]

Returns an async context manager that yields a Session. On __aenter__ it connects to (or auto-starts) the daemon and creates the session — failing loud rather than yielding a dead session. On __aexit__ it disconnects. Use it with async with.

profile, agent, agent_params, and cascade_driver_id are forwarded to create_session unchanged, so both the declarative (named profile) and programmatic (inline-dict spec) styles work. The remaining kwargs are facade and connection knobs with sensible defaults.

  • profile str | dict optional
    Either a profile name (str) referencing a file under .jaato/profiles/, or an inline spec dict with keys such as model, provider, plugins, system_instructions. An inline spec requires a plugins key (use [] for a tool-less session). The two forms are mutually exclusive.
  • agent str optional
    Agent name. The agent's rendered markdown becomes the session's system instructions. Composes freely with profile.
  • agent_params dict optional
    Parameter values for the agent's {{param}} placeholders. Only used when agent is specified.
  • cascade_driver_id str optional
    Cascade-sharing tenant ID. Sessions sharing the same opaque ID reuse a warm pool slot across cascade stages. None (default) = standalone session.
  • client_tools list optional
    Host/client tool specs to register after connect but before create_session — so the runner-tier model sees them. Lets host-tool clients use the facade instead of the low-level connect/register/create dance.
  • on_permission callable optional
    Callback invoked for each gated-tool permission request, receiving the permission event and returning the response (e.g. "y" / "n"); may be a coroutine. When unset, a gated tool is auto-denied and the in-flight turn raises PermissionUnhandled.
  • on_status_change callable optional
    Callback invoked when the connection status changes (e.g. RECONNECTINGCONNECTED), wired before connect. Most relevant with the auto-reconnect client (IPCRecoveryClient); see Connection Recovery.
  • socket_path str optional
    Path to the daemon's Unix domain socket (or Windows named pipe). Defaults to the standard socket path.
  • env_file str optional
    Path to the .env file passed to an auto-started daemon. Default: ".env".
  • workspace_path str | Path optional
    Workspace path so the daemon resolves project-level .jaato/ assets (profiles, personas, completion schemas, reactors). Pass this when using a named profile or agent.
  • auto_start bool optional
    Whether to auto-start the daemon if it is not already running. Default: True.
  • client_type ClientType optional
    Identifies the kind of client for server-side presentation and lifecycle filters. Defaults to ClientType.API for headless orchestrators.
  • connect_timeout float optional
    Timeout in seconds for the connect step on __aenter__.
Inline spec needs plugins
An inline profile dict requires a plugins key (even []); omitting it is rejected with InvalidSessionSpec.
Returns
async context manager — yields a Session.
Raises
ConnectionError if the daemon can't be reached or auto-started.
RuntimeError if session creation fails (check provider auth / the daemon log).
Inline spec (programmatic)
from jaato_sdk import IPCClient

# An inline spec REQUIRES a `plugins` key.
# Credentials resolve from a `pass:` knob, never a raw key.
async with IPCClient.session(profile={
    "model": "gpt-4o",
    "provider": "openai",
    "plugins": [],
    "plugin_configs": {
        "openai": {"api_key": "pass://jaato/openai/api-key"},
    },
}) as s:
    print(await s.ask("Hello!"))
Named profile (declarative)
from pathlib import Path
from jaato_sdk import IPCClient

# A named profile lives under .jaato/profiles/ — pass
# workspace_path so the daemon resolves it.
async with IPCClient.session(
    profile="person-extractor",
    workspace_path=Path.cwd(),
) as s:
    print(await s.ask("Alice is 30."))
Reuse one connection across turns
async with IPCClient.session(
    profile={"model": "gpt-4o", "provider": "openai", "plugins": []},
) as s:
    # The session is conversational memory — both turns
    # share one daemon connection and history.
    print(await s.ask("My name is Sam."))
    print(await s.ask("What is my name?"))

Session

A high-level handle over an open session. Construct it via IPCClient.session, not directly. Each method owns the send-and-wait recipe (first-of {TURN_COMPLETED, SESSION_TERMINATED}), so a turn can never hang in user code.

ask

async ask(
  prompt: str,
  *,
  sources: Collection[str] | None = ("model",),
  parallel_tools: Optional[bool] = None,
  attachments: Optional[list] = None
) -> str

Send prompt, wait for the turn to finish (first-of {TURN_COMPLETED, SESSION_TERMINATED}), and return the collected text. sources filters AGENT_OUTPUT chunks by their .source — the default ("model",) keeps the model's clean answer; None collects everything (tool output included).

  • sources Collection[str] | None optional
    Which output sources to keep. Default ("model",); None = all.
  • parallel_tools bool optional
    Per-turn override of parallel tool execution. None (default) keeps the configured behavior.
  • attachments list optional
    File attachments to include with the message.
Raises
AgentError on an error terminal.
PermissionUnhandled if a gated tool went unanswered.

complete

async complete(
  prompt: str,
  *,
  parallel_tools: Optional[bool] = None,
  attachments: Optional[list] = None
) -> Optional[dict]

For completion-gated profiles: send prompt and return the typed AGENT_COMPLETED.payload (server-validated against the profile's completion_payload_schema). Returns None if the profile declared no completion schema or the model didn't complete.

Raises
AgentError on an error terminal.

stream

stream(
  prompt: str,
  *,
  sources: Collection[str] | None = ("model",),
  parallel_tools: Optional[bool] = None,
  attachments: Optional[list] = None
) -> AsyncIterator[str]

The async-iterator counterpart to ask — yields each text chunk the moment it streams in, then stops at the terminal. Use it with async for chunk in s.stream(...). Raises AgentError / PermissionUnhandled after the stream drains.

client property

The underlying low-level client (IPCClient / IPCRecoveryClient). The facade is purely additive — drop to the full event API (subscribe, events, respond_to_permission, attach_session, …) on the same connection while still using ask / complete / stream for the common turns. Listeners you add via s.client persist across turns; the facade methods only clean up their own subscriptions.

ask — collect the answer
async with IPCClient.session(
    profile={"model": "gpt-4o", "provider": "openai", "plugins": []},
) as s:
    answer = await s.ask("Who are you? One sentence.")
    print(answer)
stream — print chunks live
async with IPCClient.session(
    profile={"model": "gpt-4o", "provider": "openai", "plugins": []},
) as s:
    async for chunk in s.stream("Tell me a short story."):
        print(chunk, end="", flush=True)
    print()
complete — typed completion gate
# The "person-extractor" profile declares a
# completion_payload_schema; the daemon validates the
# payload server-side and complete() returns the dict.
async with IPCClient.session(
    profile="person-extractor",
    workspace_path=Path.cwd(),
) as s:
    person = await s.complete("Alice is 30.")   # dict | None
    print(person["name"], person["age"])
.client — drop to the event API
from jaato_sdk.events import EventType

async with IPCClient.session(profile={
    "model": "gpt-4o", "provider": "openai", "plugins": [],
}) as s:
    # Low-level observer on the SAME connection...
    s.client.subscribe(EventType.TOOL_CALL_END, observer)
    # ...while still using the facade for the turn.
    print(await s.ask("List files in the workspace."))

Host (client) tools

Pass client_tools to register tools whose handler runs in your process: the daemon calls back into the client whenever the model invokes one. Each spec is a plain dict with name, description, parameters (a JSON Schema), and handler.

Host tools must be registered before the session is created so the runner-tier model sees them — the facade does this for you when you pass client_tools= to IPCClient.session(...).

client_tools — a tool handled in your process
from jaato_sdk import IPCClient

async with IPCClient.session(
    profile={"model": "gpt-4o", "provider": "openai", "plugins": []},
    client_tools=[{
        "name": "get_weather",
        "description": "Return the weather for a city.",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
        # handler runs in YOUR process
        "handler": lambda args: {"weather": f"{args['city']}: sunny, 24C"},
    }],
) as s:
    print(await s.ask("Weather in Paris?"))

Typed completion gate

A profile that declares a completion_payload_schema forces the model to call signal_completion with a matching payload. await s.complete(prompt) returns the server-validated dict — or None if the profile declared no schema or the model didn't complete.

Because the daemon validates against the schema, the typed result is a contract: a non-None return has already passed server-side validation.

complete — server-validated payload
from pathlib import Path
from jaato_sdk import IPCClient

# The "person-extractor" profile declares a
# completion_payload_schema; the model must call
# signal_completion with a matching payload.
async with IPCClient.session(
    profile="person-extractor",
    workspace_path=Path.cwd(),
) as s:
    person = await s.complete("Alice is 30.")   # dict | None
    print(person["name"], person["age"])

Module-level ask()

async ask(
  prompt: str,
  *,
  sources: Collection[str] | None = ("model",),
  **session_kwargs
) -> str

One-shot: open a session, ask, return the answer, tear down. Sugar over IPCClient.session — it accepts the same kwargs (profile, agent, connection knobs, on_permission). One daemon connect per call, so for repeated calls use the IPCClient.session context manager instead.

Returns
str — the collected answer text.
Raises
AgentError / PermissionUnhandled, same as Session.ask.
One-shot helper
from jaato_sdk import ask

# A throwaway call — connect, ask, tear down.
answer = await ask(
    "Who are you? One sentence.",
    profile={"model": "gpt-4o", "provider": "openai", "plugins": []},
)
print(answer)

Errors

AgentError

class AgentError(Exception)

Raised when a turn ends in error (an error terminal). Carries the daemon's diagnostics so callers can branch without parsing strings.

  • error_type (str | None) — the daemon's error class.
  • error_summary (str | None) — a human-readable summary.

PermissionUnhandled

class PermissionUnhandled(Exception)

Raised when a gated tool requested permission but no on_permission callback was supplied to IPCClient.session. The facade auto-denies (to unstick the daemon) and raises this rather than hang or silently degrade. Pass on_permission= to session(...), or drop to the low-level subscribe(EventType.PERMISSION_REQUESTED) API via s.client.

  • tool_name (str) — the gated tool that went unanswered.
Handling errors
from jaato_sdk import (
    IPCClient,
    AgentError,
    PermissionUnhandled,
)

async with IPCClient.session(profile={
    "model": "gpt-4o", "provider": "openai", "plugins": ["cli"],
}) as s:
    try:
        print(await s.ask("List files in the workspace."))
    except PermissionUnhandled as e:
        # A gated tool was auto-denied — supply on_permission=
        # to session(...) to approve it.
        print(f"tool {e.tool_name} needed approval")
    except AgentError as e:
        print(f"{e.error_type}: {e.error_summary}")
Approving gated tools
# on_permission receives the request and returns the
# response ("y"/"n"); it may be a coroutine.
async with IPCClient.session(
    profile={"model": "gpt-4o", "provider": "openai", "plugins": ["cli"]},
    on_permission=lambda ev: "y",
) as s:
    print(await s.ask("List files in the workspace."))