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_secondsdefaults to60and limits the wait for the first provider event. Ollama, OpenAI, and Anthropic also pass it to their SDK client.stream_timeout_secondsdefaults to30and 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_mutation_executed module-attribute ¶
chat_mutation_executed = 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 ¶
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.
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.
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.
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.
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.
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_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 ¶
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.
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.