Skip to content

Chat API

GeneralManager chat exposes selected managers to a tool-capable LLM through HTTP, server-sent events (SSE), or WebSocket. Enable it through GENERAL_MANAGER["CHAT"]; GeneralManager installs the routes during Django application startup.

See Add LLM chat to a GeneralManager project for a complete setup and How LLM chat works for the runtime model.

Settings

The minimal configuration is:

GENERAL_MANAGER = {
    "CHAT": {
        "enabled": True,
        "provider": "general_manager.chat.providers.OllamaProvider",
        "provider_config": {"model": "qwen3.5:9b"},
    }
}

Top-level chat settings

Setting Default Behavior
enabled False Register chat HTTP, SSE, and eligible WebSocket routes at startup.
url "/chat/" Base path used for every chat transport. Leading and trailing slashes are normalized for Django routing.
provider "general_manager.chat.providers.OllamaProvider" Dotted provider class path. The class is constructed without arguments for each HTTP/SSE request or WebSocket connection.
provider_config {} Mapping read by the selected provider. Supported keys depend on the adapter.
provider_profiles {} Optional named provider-profile mappings used by planned roles. Each profile supplies provider, provider_config, and trust_group.
planned {} Optional planned read-orchestration settings. enabled defaults to False; see Planned read orchestration.
permission None Callable or dotted path receiving (user, scope). Returning False denies the request or socket.
allowed_origins None Explicit WebSocket origin list. When empty, Channels' allowed-host origin validator is used.
allowed_mutations [] Exact generated GraphQL mutation names the mutate tool may execute.
confirm_mutations [] Allowed mutation names that require client confirmation. Every name must also be in allowed_mutations.
confirm_timeout_seconds 30 Lifetime of a pending mutation confirmation.
max_results 200 Maximum query page size accepted by the chat query tool.
query_timeout_seconds None Optional database query timeout in seconds. It is converted to milliseconds for supported database execution.
max_retries_per_message 3 Maximum non-mutation tool-loop retries in one user turn.
max_mutations_per_message 8 Maximum mutation tool executions in one user turn, including a mutation resumed after confirmation. Set 0 to disallow writes for a turn.
max_total_rounds_per_message None Optional positive cap for legacy provider rounds in a turn, including summaries and recovery. When omitted, the cap is max_retries_per_message + max_mutations_per_message + 2. Planned reads retain their separate orchestration budgets.
tool_strategy "discovery" discovery exposes the stable discovery tool set; direct adds one query tool per exposed manager.
recover_missing_tool_calls False Add bounded recovery prompts when a model answers without required tools or returns no answer after tools.
system_prompt "" Project-specific instructions appended to the built-in system prompt.
max_recent_messages 20 Recent persisted messages retained verbatim when conversation context is built.
summarize_after 10 Message count after which older history may be summarized by the provider.
ttl_hours 24 Retention threshold used by python manage.py chat_cleanup. Cleanup is not scheduled automatically.

Rate limits

The rate_limit mapping is merged with these defaults:

{
    "requests": 60,
    "window_seconds": 3600,
    "tokens": None,
    "input_tokens": None,
    "output_tokens": None,
}

Positive integer limits are enforced through the Django cache. The scope is the authenticated user ID, then the anonymous session key, then the client IP. None, zero, and negative values do not create a budget for that counter. Provider usage events supply token counts.

Once a token budget is exhausted, GeneralManager records the completed provider usage and rejects the next tool, confirmation write, summary, or provider round. A rate_limited event includes retry_after_seconds.

Audit settings

The audit mapping is merged with:

{
    "enabled": False,
    "level": "off",
    "logger": None,
    "max_result_size": 4096,
    "redact_fields": ["password", "secret", "token", "key", "credential"],
}

For WebSocket chat, logger is a callable or dotted callable path receiving one sanitized event mapping. level="messages" emits user and assistant messages; level="all" also emits tool activity. max_result_size truncates serialized tool results. Redaction recursively replaces values whose key contains a configured term. All transports separately expose Django signals for chat activity and errors.

Provider adapters

Provider credentials can be passed in provider_config. When an SDK supports its own environment variables, omitting api_key delegates credential lookup to that SDK. Built-in providers retain zero-argument construction for legacy chat. They also accept an explicit mapping through from_config(config) and expose the copied, read-only mapping through provider_config; planned profile construction uses these two hooks without changing legacy settings.

An explicitly named provider_profiles entry is always constructed with its own provider_config, including {}. It never inherits credentials, model, or endpoint values from the legacy provider_config. Only an omitted provider_profiles mapping creates the implicit legacy profile.

Provider history keeps tool call IDs, names, arguments, and results as structured data. The built-in adapters translate that history to each SDK's native tool-call and tool-result format; custom providers receive it through the Message tool fields.

Two timeout keys apply to every adapter at the GeneralManager provider loop:

  • timeout_seconds defaults to 60 and limits the wait for the first provider event. Ollama, OpenAI, and Anthropic also pass it to their SDK client.
  • stream_timeout_seconds defaults to 30 and limits the wait between later streamed events.
Provider path Extra Provider configuration
general_manager.chat.providers.OllamaProvider chat-ollama model (default gemma4:e4b), base_url (default http://127.0.0.1:11434), timeout_seconds (default 60)
general_manager.chat.providers.OpenAIProvider chat-openai model (default gpt-4.1-mini), api_key, base_url, timeout_seconds (default 60)
general_manager.chat.providers.AnthropicProvider chat-anthropic model (default claude-3-5-haiku-latest), api_key, max_tokens (default 1024), timeout_seconds (default 60)
general_manager.chat.providers.GeminiProvider chat-google model (default gemini-2.5-flash), api_key, timeout_seconds (first event), stream_timeout_seconds (between events)

GoogleProvider is an alias of GeminiProvider. The OpenAI provider accepts a base_url for OpenAI-compatible services.

The configured model must implement structured tool or function calling.

Routes and request contracts

With the default url="/chat/", startup registers three Django routes and one WebSocket route. HTTP views are POST-only and CSRF-protected.

Non-streaming HTTP

POST /chat/
Content-Type: application/json
X-CSRFToken: <token>

{"text": "Which projects use aluminum parts?"}

Successful and tool-level error responses use HTTP 200 with ordered events:

{
  "events": [
    {"type": "text_chunk", "content": "Mercury uses ..."},
    {"type": "done", "usage": {"input_tokens": 100, "output_tokens": 20}}
  ],
  "answer": "Mercury uses ..."
}

answer concatenates all text_chunk.content values. Permission denial uses HTTP 403. This endpoint cannot pause and resume a mutation listed in confirm_mutations; it emits confirmation_required_transport instead.

SSE

POST /chat/stream/
Content-Type: application/json
Accept: text/event-stream
X-CSRFToken: <token>

{"text": "Which projects use aluminum parts?"}

Each event is encoded as data: <JSON>\n\n and the response content type is text/event-stream. Because the endpoint is POST-based, use streaming fetch or an SSE client that supports POST rather than the browser's GET-only EventSource constructor.

The server creates an anonymous session before SSE headers are sent, so the response includes a usable Django session cookie even before its first event. Chat stores a deterministic hash only when a signed-cookie value exceeds the conversation key column limit; the cookie itself is unchanged.

Resolve a pending SSE confirmation through:

POST /chat/confirm/
Content-Type: application/json
X-CSRFToken: <token>

{"confirmation_id": "call-1", "confirmed": true}

The confirmation endpoint returns the same {events, answer} envelope as the non-streaming endpoint and resumes the provider after the tool result.

WebSocket

Connect to /chat/ using ws or wss. The application is wrapped in Channels' AuthMiddlewareStack, so Django session authentication is available as scope["user"].

Send a user message:

{"type": "message", "text": "List all materials"}

Resolve a mutation confirmation:

{
  "type": "confirm",
  "confirmation_id": "call-1",
  "confirmed": true
}

Only one turn and one pending confirmation may be active on a socket. Unknown event types produce bad_event; an overlapping message produces turn_in_progress or confirmation_pending.

Persistent context

Long legacy HTTP, SSE, and WebSocket turns share one bounded-context path. It retains recent messages and summarizes the older prefix using the selected provider's whole-request timeout. A summary records the exact last message it covers; a missing or outdated marker regenerates the summary rather than reusing unknown coverage. Tool windows retain each selected assistant call group and only that group's linked results. Historical rows without a stored call identity are labeled assistant context, never sent as native tool-result messages.

Server event contract

Events arrive in order. Clients should ignore unknown fields so compatible metadata can be added later.

tool_call

The provider requested a server-side tool:

{"type": "tool_call", "id": "call-1", "name": "query", "args": {}}

tool_result

The server validated and executed a tool:

{
  "type": "tool_result",
  "id": "call-1",
  "name": "query",
  "result": {"data": [], "total_count": 0, "has_more": false}
}

text_chunk

One assistant text fragment:

{"type": "text_chunk", "content": "No matching records were found."}

done

The turn is complete:

{
  "type": "done",
  "usage": {"input_tokens": 100, "output_tokens": 20}
}

Usage values depend on the provider SDK and may be zero when the provider does not report them.

confirm_mutation

An allow-listed mutation is waiting for explicit approval:

{
  "type": "confirm_mutation",
  "id": "call-1",
  "mutation": "createPart",
  "input": {"name": "Bolt"}
}

error

A public, non-sensitive failure:

{
  "type": "error",
  "message": "Chat rate limit exceeded. Try again later.",
  "code": "rate_limited",
  "retry_after_seconds": 3600
}

The optional fields depend on code. Unexpected internal exceptions are reported as the generic chat_error event and are emitted through the chat error signal for server-side observability.

Code Transport Meaning and client action
bad_message HTTP, SSE, WebSocket text is absent, blank, or not a string. Correct the payload before retrying.
bad_event Confirmation HTTP, WebSocket The event shape, confirmation payload, or confirmation ID is invalid. Do not retry unchanged.
confirmation_pending WebSocket A new message arrived before the current mutation confirmation was resolved. Resolve or reject it first.
confirmation_unavailable WebSocket Durable confirmation state was already claimed, resolved, or expired. Refresh the conversation state.
turn_in_progress WebSocket Another turn is still streaming on this socket. Wait for its terminal event.
rate_limited HTTP, SSE, WebSocket The actor exceeded a configured budget. Retry after retry_after_seconds.
tool_retry_limit HTTP, SSE, WebSocket The model exceeded max_retries_per_message; the turn is terminal.
mutation_limit HTTP, SSE, WebSocket The turn exhausted max_mutations_per_message; no further mutation is executed.
turn_limit HTTP, SSE, WebSocket The turn exhausted max_total_rounds_per_message; no further provider round is started.
mutation_batch_unsupported HTTP, SSE, WebSocket A completion requested mutate with another tool call. Request one mutation in a completion by itself.
confirmation_required_transport HTTP A confirmed mutation needs SSE or WebSocket. Retry the workflow on a confirmation-capable transport.
chat_error HTTP, SSE, WebSocket An unexpected server or provider failure. Show the generic message and correlate server-side logs or signals.

Permission denial returns HTTP 403 for HTTP/SSE and closes a WebSocket with code 4403. A WebSocket startup failure closes with 1011. CSRF rejection and non-POST HTTP methods use Django's standard HTTP 403 and 405 responses before a chat event is produced.

Chat tools

The discovery strategy exposes:

Tool Important inputs Result
search_managers query Matching exposed manager summaries
get_manager_schema manager Fields, filters, descriptions, and relations
find_path from_manager, to_manager Exposed relation path or no path
query manager, filters, fields, limit, offset Bounded GraphQL data page
mutate mutation, input Mutation result, denial, or confirmation requirement

Only managers with chat_exposed = True are accepted. Query fields, nested selections, filters, limits, and offsets are validated against the indexed schema before execution. Mutations require an authenticated user and an exact name in allowed_mutations.

Read-only tool calls may be batched in one provider completion. A completion that includes mutate and any other tool call is rejected before any tool runs; request each mutation by itself so the client-confirmation protocol can retain one pending write safely.

Persistence and cleanup

Chat persistence is installed with the GeneralManager Django migrations:

python manage.py migrate

ChatConversation belongs to an authenticated user or anonymous Django session. ChatMessage records ordered conversation and tool items. ChatPendingConfirmation stores confirmation state, including expiry and resolution metadata, until cleanup removes the record. A successful HTTP or WebSocket confirmation atomically claims the pending record scoped to the current authenticated actor or anonymous session before the server executes the mutation. This prevents durable approval replay across processes when persistence is enabled. When persistence is unavailable, a WebSocket pending confirmation remains owned by that socket session and can be consumed once; it does not provide cross-process replay protection.

Prune stale conversations and resolved or expired confirmations using:

python manage.py chat_cleanup

The command reads ttl_hours. Schedule it with the deployment's normal task runner; GeneralManager does not schedule it automatically.

Signals

general_manager.chat.signals exposes four Django signals for application observability:

Signal Emitted for
chat_message_received An accepted user message with user and conversation context
chat_tool_called A completed server-side tool call with arguments and result
chat_mutation_executed Mutation tool outcomes, including immediate execution and confirmation resolution (execution, rejection, or timeout); inspect result.status
chat_error A transport or provider failure with server-side context

Receivers should avoid raising exceptions; GeneralManager dispatches these signals with Django's robust signal delivery.

general_manager.chat.signals.chat_message_received module-attribute

chat_message_received = Signal()

general_manager.chat.signals.chat_tool_called module-attribute

chat_tool_called = Signal()

general_manager.chat.signals.chat_mutation_executed module-attribute

chat_mutation_executed = Signal()

general_manager.chat.signals.chat_error module-attribute

chat_error = Signal()

Provider protocol

A custom provider class is instantiated without arguments and must implement the asynchronous provider protocol. It receives provider-neutral messages and tool definitions and yields text, tool-call, and terminal events:

from collections.abc import AsyncIterator

from general_manager.chat.providers.base import (
    ChatEvent,
    DoneEvent,
    Message,
    TokenUsage,
    ToolDefinition,
)


class MyProvider:
    async def complete(
        self,
        messages: list[Message],
        tools: list[ToolDefinition],
    ) -> AsyncIterator[ChatEvent]:
        # Adapt the provider SDK's streaming response here.
        yield DoneEvent(usage=TokenUsage())

Yield TextChunkEvent for assistant text and ToolCallEvent with a stable ID, tool name, and decoded argument mapping for tool requests. Finish every normal completion with DoneEvent. A provider may define check_configuration() for startup validation and required_extra for an installation hint.

general_manager.chat.providers.base.BaseLLMProvider

Bases: Protocol

Minimal streaming protocol implemented by chat LLM adapters.

provider_config property

provider_config

Return the read-only configuration used by this provider instance.

from_config classmethod

from_config(config)

Construct a provider using an instance-scoped configuration.

complete

complete(messages, tools)

Stream text, tool calls, and completion metadata for one turn.

general_manager.chat.providers.base.Message dataclass

Provider-neutral chat history, including structured tool exchanges.

general_manager.chat.providers.base.ToolDefinition dataclass

Provider-agnostic tool schema exposed to an LLM provider.

general_manager.chat.providers.base.TextChunkEvent dataclass

Streaming assistant text emitted by a provider.

general_manager.chat.providers.base.ToolCallEvent dataclass

Provider request to execute one configured chat tool.

general_manager.chat.providers.base.DoneEvent dataclass

Terminal provider event carrying optional usage metadata.

general_manager.chat.providers.openai.OpenAIProvider

Bases: BaseLLMProvider

Streaming provider backed by the OpenAI Python SDK.

provider_config property

provider_config

Return the read-only configuration for this provider instance.

__init__

__init__(config=None)

Create a provider with explicit config or legacy chat settings.

from_config classmethod

from_config(config)

Create a provider with a copied profile configuration.

check_configuration classmethod

check_configuration(config=None)

Validate that the OpenAI SDK is available before use.

complete async

complete(messages, tools)

Stream OpenAI text, tool calls, and usage events for one chat turn.

general_manager.chat.providers.anthropic.AnthropicProvider

Bases: BaseLLMProvider

Streaming provider backed by the Anthropic Python SDK.

provider_config property

provider_config

Return the read-only configuration for this provider instance.

__init__

__init__(config=None)

Create a provider with explicit config or legacy chat settings.

from_config classmethod

from_config(config)

Create a provider with a copied profile configuration.

check_configuration classmethod

check_configuration(config=None)

Validate that the Anthropic SDK is available before use.

complete async

complete(messages, tools)

Stream Anthropic text, tool calls, and usage events for one chat turn.

general_manager.chat.providers.google.GeminiProvider

Bases: BaseLLMProvider

Streaming provider backed by the Google GenAI Python SDK.

provider_config property

provider_config

Return the read-only configuration for this provider instance.

__init__

__init__(config=None)

Create a provider with explicit config or legacy chat settings.

from_config classmethod

from_config(config)

Create a provider with a copied profile configuration.

check_configuration classmethod

check_configuration(config=None)

Validate that the Google GenAI SDK is available before use.

complete async

complete(messages, tools)

Stream Gemini text, tool calls, and usage events for one chat turn.

general_manager.chat.providers.google.GoogleProvider module-attribute

GoogleProvider = GeminiProvider

general_manager.chat.providers.ollama.OllamaProvider

Bases: BaseLLMProvider

Streaming provider backed by the official Ollama Python client.

provider_config property

provider_config

Return the read-only configuration for this provider instance.

__init__

__init__(config=None)

Create a provider with explicit config or legacy chat settings.

from_config classmethod

from_config(config)

Create a provider with a copied profile configuration.

check_configuration classmethod

check_configuration(config=None)

Validate that the Ollama SDK and base URL are usable.

complete async

complete(messages, tools)

Stream Ollama text, tool calls, and usage events for one chat turn.

general_manager.chat.providers.openai.OpenAIDependencyImportError

Bases: ImportError

Raised when the optional OpenAI dependency is unavailable.

general_manager.chat.providers.anthropic.AnthropicDependencyImportError

Bases: ImportError

Raised when the optional Anthropic dependency is unavailable.

general_manager.chat.providers.google.GoogleDependencyImportError

Bases: ImportError

Raised when the optional Google GenAI dependency is unavailable.

general_manager.chat.providers.ollama.OllamaDependencyImportError

Bases: ImportError

Raised when the optional Ollama dependency is unavailable.

general_manager.chat.providers.ollama.OllamaBaseUrlError

Bases: ValueError

Raised when the configured Ollama base URL is unsupported or malformed.

Settings, errors, and audit helpers

general_manager.chat.settings.ChatConfigurationError

Bases: ValueError

Raised when chat settings are invalid.

invalid_settings_mapping classmethod

invalid_settings_mapping()

Build the error for a non-mapping chat settings value.

invalid_permission classmethod

invalid_permission()

Build the error for an invalid chat permission setting.

missing_graphql_schema classmethod

missing_graphql_schema()

Build the error for chat startup without a GraphQL schema.

unknown_allowed_mutations classmethod

unknown_allowed_mutations(names)

Build the error for configured mutations not present in GraphQL.

invalid_confirm_mutations classmethod

invalid_confirm_mutations(names)

Build the error for confirmed mutations not allowed for chat.

invalid_planned_settings classmethod

invalid_planned_settings(detail)

Build an error for invalid planned-chat configuration.

invalid_turn_limit classmethod

invalid_turn_limit(name, expected)

Build the error for an invalid bounded-turn setting.

general_manager.chat.settings.get_chat_settings

get_chat_settings()

Return chat settings merged with defaults.

general_manager.chat.settings.validate_chat_settings

validate_chat_settings()

Validate chat settings and return the normalized configuration.

general_manager.chat.errors.PublicChatError dataclass

Sanitized chat error safe to return to clients.

as_event

as_event()

Render the error as the public chat event payload.

general_manager.chat.errors.public_chat_error

public_chat_error(_exc)

Map an internal exception to a generic public chat error.

general_manager.chat.errors.planned_public_error

planned_public_error(reason)

Map a stable planned terminal reason without exposing internal details.

planned_public_error(reason) accepts only the stable planned reasons listed below. It returns a PublicChatError; invalid or unknown reasons use the generic chat_error code and message. public_chat_error(exc) preserves an exception's valid public_reason, maps a plain TimeoutError to deadline_exceeded, and maps other unexpected exceptions to chat_error.

Code Public message
invalid_plan I could not prepare a safe plan for that request.
manager_unresolved I could not resolve the required application data.
dependency_blocked A required part of the request could not be completed.
budget_exhausted The request reached its execution limit.
deadline_exceeded The request reached its time limit.
provider_failed The provider could not complete the request.
synthesis_failed I could not produce a grounded answer from the available data.

general_manager.chat.audit.planned_audit_lineage_id

planned_audit_lineage_id(value)

Return a deterministic opaque audit identifier for planner-controlled IDs.

general_manager.chat.audit.emit_planned_audit_event

emit_planned_audit_event(event_type, payload, *, sink=None)

Emit only category-approved planned diagnostics through the generic sink.

emit_planned_audit_event(event_type, payload, *, sink=None) accepts only the allowlisted planned event categories. It hashes planner-controlled identifiers and canonical call identities, validates category-specific fields, and drops raw plans, manager names, profiles, trust groups, credentials, results, and exceptions before forwarding to the generic audit sink.

Django system checks

When chat is enabled, python manage.py check can report:

ID Meaning
general_manager.chat.E001 The generated GraphQL schema is not initialized.
general_manager.chat.E002 Chat settings, permissions, or mutation allow-lists are invalid.
general_manager.chat.E003 The selected provider's optional dependency is missing.
general_manager.chat.E004 The provider import failed for another reason.

Installed evaluation CLI

python -m general_manager.chat.evals [OPTIONS]

The module command configures Django, optionally registers a built-in schema fixture, loads packaged YAML datasets, constructs one or more providers, runs the selected cases synchronously, prints a report, and returns no Python value. python -m general_manager.chat.evals --help can run without Django settings; every evaluation run requires either --settings MODULE or a nonempty DJANGO_SETTINGS_MODULE.

Option Value and behavior
--settings Import path for the Django settings module. Overrides an existing DJANGO_SETTINGS_MODULE for this process.
--provider Provider class import path. When omitted, GeneralManager imports the provider configured in GENERAL_MANAGER["CHAT"].
--model Model name merged into the selected provider configuration before provider construction.
--dataset One legacy-compatible packaged dataset name. When omitted, the runner selects all legacy-compatible datasets; planned_orchestration is excluded because it requires deterministic role-pinned providers.
--fixture toy or large; registers the matching built-in eval schema before the run.
--tier Integer tier filter.
--tag Required tag filter; repeat the option to pass multiple tags.
--compare Comma-separated provider class import paths. Runs each provider and prints a comparison report instead of the single-provider report.
--verbose, -v Includes detailed failure information in a single-provider report.
--trace-jsonl File path that receives per-case JSONL traces.

The packaged dataset names are basic_queries, demo_readiness, edge_cases, follow_ups, large_schema, multi_hop, and planned_orchestration. The installed CLI's legacy suite runs the first six; selecting planned_orchestration explicitly is rejected. That packaged dataset is instead exercised by the deterministic planned tests described in the task guide.

Eval exit status

  • Status 0: argument help was displayed, or every selected result passed.
  • Status 1: at least one selected result failed.
  • Status 2: argument parsing failed, including a run without Django settings.
  • Other nonzero termination: Django setup, provider import or construction, fixture registration, dataset loading, trace writing, or evaluation raised an exception.

--settings takes precedence over DJANGO_SETTINGS_MODULE; --provider takes precedence over the configured provider; and --compare takes precedence over single-provider selection.

Application automation should prefer the module CLI rather than import eval runner internals. See the task guide and command cookbook.

Planned read orchestration

Planned orchestration is opt-in. Legacy provider and provider_config keep their existing behavior; when planned.enabled is false, GeneralManager selects the legacy loop before it creates a planner. A planned read that the planner classifies as a mutation is sent through that unchanged legacy loop, so planned executors never receive the mutate tool.

GENERAL_MANAGER = {
    "CHAT": {
        "provider": "myproject.providers.LegacyProvider",
        "provider_config": {},
        "provider_profiles": {
            "fast_local": {
                "provider": "myproject.providers.LocalProvider",
                "provider_config": {"model": "small"},
                "trust_group": "local",
            },
            "strong_local": {
                "provider": "myproject.providers.StrongProvider",
                "provider_config": {"model": "large"},
                "trust_group": "local",
            },
        },
        "planned": {
            "enabled": True,
            "catalog": "myproject.chat.get_manager_catalog",
            "roles": {
                "planner": "strong_local",
                "simple_executor": "fast_local",
                "complex_executor": "strong_local",
                "synthesizer": "strong_local",
                "fallback_executor": "strong_local",
            },
            "max_concurrent_tasks": 3,
            "evidence_timeout_seconds": 90,
            "synthesis_timeout_seconds": 30,
        },
    },
}

The required role names are planner, simple_executor, complex_executor, synthesizer, and fallback_executor. If provider_profiles is omitted, planned mode creates the implicit default profile from the legacy provider and configuration, assigns every role to it, and uses trust group default. Every profile used by a normal turn must share one trust_group; client HTTP, SSE, and WebSocket payloads cannot choose a profile or trust group. planned.catalog may be a mapping, callable, or dotted callable path. A catalog entry has the exact chat-exposed manager name as its key and domain, aliases, use_when, and distinguish_from fields:

{
    "PartManager": {
        "domain": "manufacturing",
        "aliases": ["part", "component", "item"],
        "use_when": "The question concerns designed or purchased components.",
        "distinguish_from": ["MaterialManager"],
    },
}

Catalog metadata only ranks candidates; it never changes schema visibility, permissions, field access, or query authorization.

Planned mode keeps the normal transport vocabulary. Actual tool events add task_id; final synthesis produces text_chunk; exactly one done reports complete or partial coverage; and an error is terminal only when no grounded answer is available. Stable planned error codes are invalid_plan, manager_unresolved, dependency_blocked, budget_exhausted, deadline_exceeded, provider_failed, and synthesis_failed. Their messages are stable and do not include profiles, trust groups, catalog data, plans, or exceptions. Other exceptions remain the generic chat_error mapping.

Planned audit events use the existing audit setting and are allowlisted before the generic audit sink. They can record deterministic opaque hashes of plan/task lineage, role (never profile), trust-group validation outcome, match-source categories, a SHA-256 canonical call hash, duplicate/progress state, round budgets, latency, reported token usage/cost, evidence-kind counts, coverage, and terminal reason. Raw tool results, manager names, plans, credentials, and exceptions are excluded; the existing configured field redaction and result-size limits still apply to the generic audit layer.

Planned settings and catalog

The settings helpers normalize the nested planned mapping into immutable profile and role data. get_planned_chat_settings() returns disabled settings with the legacy provider as an implicit default profile when planned mode is off. When enabled, all five required roles must resolve to configured profiles, all mapped profiles must share one trust_group, and each configured provider must support the explicit configuration construction used by build_profile_provider().

general_manager.chat.planned.config.REQUIRED_ROLES module-attribute

REQUIRED_ROLES = (
    "planner",
    "simple_executor",
    "complex_executor",
    "synthesizer",
    "fallback_executor",
)

general_manager.chat.planned.config.ProviderProfile dataclass

One configured provider available to planned chat.

general_manager.chat.planned.config.PlannedChatSettings dataclass

Immutable normalized settings used by planned chat.

general_manager.chat.planned.config.get_planned_chat_settings

get_planned_chat_settings()

Return immutable planned-chat settings normalized from chat settings.

general_manager.chat.planned.config.profile_for_role

profile_for_role(settings, role)

Return the configured provider profile assigned to a planned-chat role.

general_manager.chat.planned.config.build_profile_provider

build_profile_provider(profile)

Build a provider from a profile without changing legacy settings.

general_manager.chat.planned.config.validate_profile_provider

validate_profile_provider(profile)

Build a profile provider and run its supported configuration check.

load_manager_catalog(source, schema_index) accepts None, a mapping, a callable, or a dotted callable path. It returns an immutable catalog whose entries contain domain, normalized aliases, use_when, and distinguish_from; a catalog entry for a manager absent from schema_index raises ChatConfigurationError. Catalog metadata ranks candidates but does not change schema exposure or authorization.

general_manager.chat.planned.catalog.ManagerCatalogEntry dataclass

Normalized metadata used to rank one chat-exposed manager.

general_manager.chat.planned.catalog.ManagerCatalog dataclass

Immutable validated catalog and its canonical content fingerprint.

general_manager.chat.planned.catalog.normalize_match_text

normalize_match_text(value)

Normalize free text into comparable case-insensitive words.

general_manager.chat.planned.catalog.load_manager_catalog

load_manager_catalog(source, schema_index)

Load a catalog without changing the live chat schema index.

Validation intentionally trusts only schema_index membership. Catalog metadata can improve ranking, but cannot expose a hidden manager.

Planned task and validation types

Plans are JSON-compatible mappings. validate_plan(payload) returns a frozen ValidatedPlan for one to six read roots, or a mutation plan with no tasks; invalid shapes raise PlanValidationError before application data access. validate_dynamic_children(parent, payload, existing_tasks) validates at most two non-recursive children and enforces globally unique task IDs and compatible dependencies.

general_manager.chat.planned.models.TaskStatus module-attribute

TaskStatus = Literal[
    "pending",
    "running",
    "resolved",
    "blocked",
    "budget_exhausted",
]

general_manager.chat.planned.models.TerminalReason module-attribute

TerminalReason = Literal[
    "invalid_plan",
    "manager_unresolved",
    "dependency_blocked",
    "budget_exhausted",
    "deadline_exceeded",
    "provider_failed",
    "synthesis_failed",
]

general_manager.chat.planned.models.RequirementKind module-attribute

RequirementKind = Literal[
    "schema", "path", "query", "calculation"
]

general_manager.chat.planned.models.RoutingFeature module-attribute

RoutingFeature = Literal[
    "has_dependency",
    "requires_calculation",
    "multiple_queries",
]

general_manager.chat.planned.models.PlanIntent module-attribute

PlanIntent = Literal['read', 'mutation']

general_manager.chat.planned.models.CALCULATION_OPERATIONS module-attribute

CALCULATION_OPERATIONS = (
    "count",
    "sum",
    "average",
    "minimum",
    "maximum",
    "difference",
    "ratio",
    "percentage",
)

general_manager.chat.planned.models.EvidenceRequirement dataclass

One explicit piece of evidence needed to resolve a planned task.

general_manager.chat.planned.models.PlannedTask dataclass

A validated root or bounded dynamic child task.

general_manager.chat.planned.models.ValidatedPlan dataclass

An immutable plan accepted by the planned-chat validator.

general_manager.chat.planned.validation.MAX_ROOT_TASKS module-attribute

MAX_ROOT_TASKS = 6

general_manager.chat.planned.validation.MAX_CHILDREN_PER_ROOT module-attribute

MAX_CHILDREN_PER_ROOT = 2

general_manager.chat.planned.validation.MAX_ROOT_DEPENDENCY_DEPTH module-attribute

MAX_ROOT_DEPENDENCY_DEPTH = 1

general_manager.chat.planned.validation.PlanValidationError

Bases: ValueError

Private validation detail with a stable public invalid_plan reason.

general_manager.chat.planned.validation.validate_plan

validate_plan(payload)

Validate one complete JSON plan before any application data access.

general_manager.chat.planned.validation.validate_dynamic_children

validate_dynamic_children(parent, payload, existing_tasks)

Validate at most two non-recursive children owned by one root.

Planned resolution, evidence, and calculations

ManagerResolver ranks up to five managers that are already present in the schema index. resolve(query, anchors=()) returns deterministic ManagerCandidate records and may use anchors to prefer managers connected by an exposed relation path.

general_manager.chat.planned.resolver.AUDIT_MATCH_SOURCES module-attribute

AUDIT_MATCH_SOURCES = MappingProxyType(
    {
        "exact manager name": "exact_name",
        "exact catalog alias": "exact_alias",
        "catalog domain": "catalog_domain",
        "catalog aliases": "catalog_alias",
        "catalog use_when": "catalog_use_when",
        "schema description": "schema_description",
        "schema fields": "schema_field",
        "schema filters": "schema_filter",
        "schema relations": "schema_relation",
    }
)

general_manager.chat.planned.resolver.ManagerCandidate dataclass

One compact, locally ranked manager candidate.

general_manager.chat.planned.resolver.ManagerResolver

Rank only managers present in the live chat schema index.

resolve

resolve(query, anchors=())

Return up to five candidates in the specified lexicographic order.

EvidenceRecord snapshots JSON payloads and read-only provenance. An EvidenceStore owns turn-local records and links them to compatible EvidenceRequirement objects. canonical_call_identity() produces stable canonical JSON for a tool name and its arguments. calculate_evidence() allows only the operations in CALCULATION_OPERATIONS and returns the derived value as another immutable evidence record. Add that returned record to an EvidenceStore explicitly when it should participate in later requirement resolution; the calculation helper does not persist it itself.

general_manager.chat.planned.evidence.EvidenceError

Bases: ValueError

Base class for invalid evidence or evidence links.

general_manager.chat.planned.evidence.InvalidEvidenceError

Bases: EvidenceError

Raised when an evidence record cannot be represented safely.

general_manager.chat.planned.evidence.DuplicateEvidenceError

Bases: EvidenceError

Raised when a store receives an evidence ID it already contains.

general_manager.chat.planned.evidence.EvidenceNotFoundError

Bases: EvidenceError

Raised when an evidence link references an unknown record.

general_manager.chat.planned.evidence.IncompatibleEvidenceError

Bases: EvidenceError

Raised when a requirement cannot be satisfied by an evidence record.

general_manager.chat.planned.evidence.EvidenceLinkError module-attribute

EvidenceLinkError = IncompatibleEvidenceError

general_manager.chat.planned.evidence.EvidenceKind module-attribute

EvidenceKind = Literal[
    "schema", "path", "query", "calculation"
]

general_manager.chat.planned.evidence.EvidenceRecord dataclass

One immutable snapshot of successful, structured evidence.

create classmethod

create(
    evidence_id,
    task_id,
    kind,
    call_identity,
    provenance,
    payload,
)

Create a record by serializing a detached JSON payload.

payload

payload()

Decode and return a fresh payload snapshot on every access.

general_manager.chat.planned.evidence.EvidenceStore

Turn-local evidence collection with explicit requirement linking.

Records remain in memory until the owning turn ends. Methods return the immutable records they add or link; this store does not persist evidence.

add

add(record, *, requirement=None)

Add and return a record, optionally linking it to a requirement.

general_manager.chat.planned.evidence.canonical_call_identity

canonical_call_identity(name, args)

Return canonical JSON for one tool name and its JSON arguments.

general_manager.chat.planned.calculations.CalculationError

Bases: ValueError

Raised when a requested calculation cannot be safely evaluated.

general_manager.chat.planned.calculations.CalculationOperand dataclass

A structured path into one query evidence payload.

general_manager.chat.planned.calculations.calculate

calculate(operation, operands)

Evaluate one of the eight named operations using Decimal arithmetic.

general_manager.chat.planned.calculations.calculate_evidence

calculate_evidence(
    evidence_id,
    task_id,
    operation,
    operands,
    store,
    *,
    provenance=None,
    call_identity=None
)

Compute a value from query evidence and return derived evidence.

Planned orchestration helpers

The lower-level planned modules are transport-neutral and are useful when an application owns a custom chat transport or deterministic test harness. Normal applications should configure CHAT["planned"] and use the existing transport routes. plan_request() performs at most one correction and one fallback attempt; complete_provider_round() accepts exactly one terminal DoneEvent and either text or one tool call; synthesize_answer() references only eligible resolved evidence.

general_manager.chat.planned.budget.RoundBudget

Track every planned provider request in one global ledger.

Global-only calls (planner and synthesizer) use :meth:consume_global. Executor calls use :meth:consume_subtree, which atomically charges both the global and owning-root ledgers.

consume_global

consume_global()

Charge one planner/synthesizer request to the global budget.

consume_subtree

consume_subtree(root_id)

Charge one executor request globally and to its owning root.

consume

consume(root_id=None)

Charge one request, using global-only or executor semantics.

general_manager.chat.planned.budget.RoundBudgetExhausted

Bases: RuntimeError

Raised when a provider request would exceed a hard round limit.

general_manager.chat.planned.budget.BudgetExhaustedError module-attribute

BudgetExhaustedError = RoundBudgetExhausted

general_manager.chat.planned.planner.InvalidPlanError

Bases: ValueError

Stable terminal failure for an unusable planner result.

general_manager.chat.planned.planner.PlanningResult dataclass

A validated plan and all known usage from its provider attempts.

general_manager.chat.planned.planner.plan_request async

plan_request(
    user_text, messages, settings, budget, catalog_summary
)

Request, correct once, then fall back once to a validated plan.

general_manager.chat.planned.provider_calls.InvalidProviderRoundError

Bases: ValueError

Raised when a provider stream cannot represent one planned round.

general_manager.chat.planned.provider_calls.ProviderRoundResult dataclass

The only valid outcomes of one planned provider request.

general_manager.chat.planned.provider_calls.complete_provider_round async

complete_provider_round(
    provider, messages, tools, timeout_seconds
)

Buffer one bounded provider stream without changing legacy iteration.

Planned orchestration deliberately accepts either text or one tool call, never both. The timeout is supplied by the calling stage after it caps the request to its remaining deadline.

general_manager.chat.planned.routing.ExecutorRole module-attribute

ExecutorRole = Literal[
    "simple_executor", "complex_executor"
]

general_manager.chat.planned.routing.select_executor_role

select_executor_role(
    task, *, unique_manager, path_depth, prior_failure
)

Choose the simple executor only when every approved fact is simple.

general_manager.chat.planned.synthesis.SynthesisFailedError

Bases: ValueError

Stable terminal failure when no grounded synthesis is available.

general_manager.chat.planned.synthesis.SynthesisResult dataclass

One grounded answer and its eligible evidence references.

general_manager.chat.planned.synthesis.synthesize_answer async

synthesize_answer(
    user_text, resolved_evidence, coverage, settings, budget
)

Return one grounded answer, then use the fallback profile exactly once.

general_manager.chat.planned.events.PLANNED_PUBLIC_MESSAGES module-attribute

PLANNED_PUBLIC_MESSAGES = {
    "invalid_plan": "I could not prepare a safe plan for that request.",
    "manager_unresolved": "I could not resolve the required application data.",
    "dependency_blocked": "A required part of the request could not be completed.",
    "budget_exhausted": "The request reached its execution limit.",
    "deadline_exceeded": "The request reached its time limit.",
    "provider_failed": "The provider could not complete the request.",
    "synthesis_failed": "I could not produce a grounded answer from the available data.",
    "rate_limited": "Chat rate limit exceeded. Try again later.",
}

general_manager.chat.planned.events.planned_done_event

planned_done_event(usage, *, resolved, total, unresolved)

Build the sole terminal event for a grounded complete or partial turn.

general_manager.chat.planned.events.planned_error_event

planned_error_event(reason)

Build the sole terminal event when no grounded answer exists.

general_manager.chat.planned.events.planned_tool_call_event

planned_tool_call_event(task_id, call_id, name, args)

Build an actual planned tool-call event with its owning task ID.

general_manager.chat.planned.events.planned_tool_result_event

planned_tool_result_event(task_id, call_id, name, result)

Build the matching actual planned tool-result event.

general_manager.chat.planned.scheduler.SchedulerCallbacks dataclass

Existing chat integration seams, replaceable in deterministic tests.

general_manager.chat.planned.scheduler.PlannedCoverage dataclass

Sanitized resolved/unresolved task coverage for synthesis and done.

general_manager.chat.planned.scheduler.PlannedExecutionResult dataclass

Private completed turn state; it never becomes a public event directly.

general_manager.chat.planned.scheduler.PreparedPlannedTurn dataclass

Planner output plus accounting retained through Task 7 mutation fallback.

mutation_plan property

mutation_plan

Expose the original mutation plan for Task 7's unchanged legacy path.

general_manager.chat.planned.scheduler.prepare_planned_turn async

prepare_planned_turn(
    user_text,
    messages,
    settings,
    catalog_summary,
    *,
    planner=plan_request,
    resolver=None,
    clock=None,
    callbacks=None,
    scope=None
)

Plan once and preserve all planner usage for later terminal accounting.

general_manager.chat.planned.scheduler.iter_planned_read_events async

iter_planned_read_events(
    prepared,
    *,
    scope,
    conversation,
    messages,
    callbacks=None,
    clock=None
)

Yield planned public events, ending in one done or one error.

This accepts only validated read plans. Task 7 reads mutation_plan and sends it through its untouched legacy loop before this iterator is entered.