Skip to main content

No project description provided

Project description

keble-agentic-chat

keble-agentic-chat owns the generic, host-neutral chat runtime used by Keble services that run Pydantic-AI agents. Version 1.0.x is a breaking storage and API release: timeline entries are the only public read-side contract, and raw provider messages are private resume state only.

Resume-created pending action bridge (3.41.1): every deferred PAUSE that persists pending client/server action rows must also persist chat.pending_resume. Fresh user turns use the user row as the origin; RESUME turns that immediately ask for the next browser confirmation carry the prior origin metadata forward. This keeps ChatAction rows and ChatPendingResume.pending_action_ids aligned so backend resolve guards do not reject valid guided-bootstrap clicks with "chat has no pending resume state".

Testing

Default fast tests are offline and exclude live, slow, eval, and local-stack storage tests:

uv run pytest -m "not live and not slow and not eval and not local_stack"

The shared marker vocabulary matches the rest of Keble:

  • unit: deterministic Python, schema, runtime, and fake-model tests.
  • contract: recorded or fixture-based external contract tests.
  • integration: real local dependency tests.
  • live: real external service calls.
  • eval: AI quality or retrieval quality tests.
  • db, mongo, qdrant, db_stack, local_stack: database and stack layers.
  • llm, embedding, keepa, keepa_contract, keepa_live: external model/API layers.

Storage-backed tests that use MongoDB, Redis, or Qdrant fixtures are marked integration, db, and local_stack during collection and require:

RUN_INTEGRATION=1 uv run pytest -m integration

Real LLM canaries are marked live and llm, and require:

RUN_LLM_LIVE=1 uv run pytest -m "live and llm"

Before finishing Python changes, run:

uv run pytest -m "not live and not slow and not eval and not local_stack"
npx --yes pyright .

Side effect if changes:

  • tests/conftest.py::pytest_collection_modifyitems owns test-layer markers for this package.
  • Default local and CI runs stay offline unless a worker explicitly enables RUN_INTEGRATION=1 or RUN_LLM_LIVE=1.
  • Backend/package consumers can rely on the same marker names used by keble-db.testing and keble-keepa.testing.

Agent-core contract collapse (3.39.0): general session tools and provider manifests name keble_helpers.AgentToolRegistrationConfig, AgentToolDescriptor, AgentContext, and AgentLifecycleResult directly. Current-line code should use the typed approval vocabulary and must not reintroduce the retired alias models.

Owner-list chat run liveness (3.40.0): ChatRunActionType and ChatRunStatusChangedPayload define the package-owned workspace event CHAT_RUN_STATUS_CHANGED. Backend task-list sockets forward it to CHAT subscribers so the frontend sidebar can render active chat runs without polling.

Agent-core identity projection (3.39.0): this package bundles the helper-owned provider/agent identity vocabulary. Provider origins derive from KebleProviderId.origin; package code should not hand-map provider/source strings.

Client-tool primitives (3.38.0): generic browser client tools now include Select, MultiSelect, Number, and Money payload/result pairs beside Confirm and TextInput. Payload objects own semantic result validation through validate_result(...) so stored browser results stay compact while domain packages can reject stale option values, out-of-range numbers, and cross-currency money answers before persistence.

Engine status (2.3.0): LangGraphChatRuntime (runtime/langgraph/runtime.py) is the sole turn engine — pydantic-ai executes, LangGraph controls flow. It exposes arun_turn / aresolve_action / astream_turn / astream_resolve_action over a compiled graph + a ChatHistoryStore, with streaming, thinking, interrupt/resume (Mongo checkpointer), compaction, per-run cost, and ChatMemoryStore recall/remember.

AgenticChat (2.3.0) is the thin per-scope SSOT holder (runtime/agentic_chat.py) — NOT the removed 2.x god-engine. One instance per chat scope owns the durable store + cooperative run-control, and builds the per-turn engine via abuild_runtime(graph=..., memory=..., compactor=...) -> LangGraphChatRuntime. It satisfies the framework-neutral keble_helpers.ChatScopeRuntimeProtocol, so hosts (and future agent packages) type against that contract without importing this package. Surface: .store, arequest_interrupt(...), abuild_runtime(...). The v1 timeline-first AgenticChat turn engine + its legacy|langgraph flag were removed in 2.2.0; the long AgenticChat(...) snippets further down are historical (they used the removed engine's constructor) — the new class takes only store= + run_control_stale_seconds=. service.py holds only the canonical ChatActionResolutionError. The store classes (InMemoryAgenticChatStore / MongoAgenticChatStore / RedisCachedAgenticChatStore) are unchanged.

Memory contracts moved to keble-helpers (2.7.0): ChatMemoryRecord, ChatMemoryKind, and ChatMemoryStoreProtocol are owned by framework-neutral keble_helpers (1.16.0) so other packages produce/consume the same records without importing this engine. runtime/langgraph/memory.py re-exports them (ChatMemoryStore stays the engine-side alias) and keeps NoOpChatMemoryStore. Recall scoping is locked: cross-chat per (owner, scope_type); scope_id/chat_id are write-side provenance metadata, not recall filters. Per-turn episodes are built with ChatMemoryRecord.episode(...) (no free-string kinds). Half-resolved action batches raise the typed ChatActionResolutionError (2.7.0) instead of a raw ValueError.

Version 3.35.0 Client-tool envelope helper adoption

request_client_action now builds ChatAction.request through keble_helpers.build_client_action_request_from_raw(...) (helpers 1.39.1). The runtime stays domain-neutral: host packages still own marketplace/report/etc. payload schemas, while this package validates only the common {tool_type, payload} envelope shape.

This is an additive Stage B migration step. Stored action request shape remains the existing snake-case raw dict; helpers readers accept both snake/camel during the downstream backend, keble-core, and frontend migration.

Version 3.35.1 Helpers 1.39.2 alias-fix refresh

Private wheel refresh only: keble-agentic-chat now bundles keble-helpers 1.39.2, where typed client-tool payload builders preserve camel-case nested payload aliases. Runtime request shape and chat behavior are unchanged from 3.35.0.

Version 3.36.1 Helpers 1.40.0 identity projection refresh

Private wheel refresh only: keble-agentic-chat now bundles keble-helpers 1.40.0, which adds KebleProviderId.for_provider(...). Runtime request shape and chat behavior are unchanged from 3.36.0; this release exists so positioning, segmenting, task, and decision packages can consume the same helper wheel during Stage B Phase 4.

Version 3.38.0 Generic Client-Tool Primitives

keble_agentic_chat.schemas.client_primitives now exports:

  • ClientToolOption
  • SelectClientToolPayload / SelectClientToolResult
  • MultiSelectClientToolPayload / MultiSelectClientToolResult
  • NumberClientToolPayload / NumberClientToolResult
  • MoneyClientToolPayload / MoneyClientToolResult

Side effect if changes:

  • AMZ guided bootstrap and future frontend client tools can render standard select, multi-select, number, and money questions without bespoke result schemas.
  • Host packages should call each payload's validate_result(...) before accepting a browser result, because the stored result intentionally carries only the compact answer.
  • The Money primitive reuses keble_helpers.typings.Money; do not introduce a second {amount, currency} shape in agentic-chat.

Agentic schema convention (3.33.0)

GENUINE pydantic-ai tool I/O schemas — the typed input to / return projection of an @agent.tool, and the tool-registration config for such tools — are named with the *ForAgent suffix (configs with an Agent infix, e.g. *AgentToolsConfig) and live in one module, keble_agentic_chat/schemas/for_agent.py. The convention guard tests/test_schemas/test_for_agent.py enforces both the naming and the placement.

This package is a MIXED BAG: most *View classes are NOT tool payloads and keep their names and locations:

  • BackgroundSessionStatusForAgent (schemas/for_agent.py) — the RETURN projection of the host-neutral session-status tools in runtime/general_tools.py and the SubAgentBinding protocol. Renamed from BackgroundSessionStatusView in 3.31.0 (re-landed on main in 3.33.0).
  • GeneralAgentToolsConfig (schemas/for_agent.py) — tool-registration config for register_general_tools. Renamed from GeneralToolsConfig in 3.31.0 (re-landed on main in 3.33.0).
  • KEPT as read models (NOT tool payloads): ChatHistoryView, ChatRunStatusView, ChatActionDisplay, and SubAgentJobView — the jobs-panel SSOT carried on ChatHistoryView.subagent_jobs and live events, a display projection rather than a tool I/O contract.

This rename is code-breaking with NO aliases: consumers importing BackgroundSessionStatusView / GeneralToolsConfig must switch to the new names from keble_agentic_chat.schemas.for_agent (both are also re-exported from the package root and keble_agentic_chat.schemas).

Version 3.18.1 Provider-Guided Async Waits

SUBAGENT_DELEGATION_PROMPT_RULES now tells parent agents to inspect the delegate tool result and provider-owned guidance before ending a turn. If a provider returns a child session that must finish before final success, the parent must make exactly one await_background_sessions call with the returned full session_id values before giving the final answer. Otherwise the previous "report started and end" behavior still applies.

This keeps async monitoring single-source: generated delegate tools provide the typed child references, await_background_sessions is the only sanctioned wait, and providers such as AMZ can require honest completion without adding a second wait tool or backend-specific prompt branch.

Version 3.16.8 Scoped-Agent ToolScope Enforcement

adrive_scoped_agent(...) now verifies the freshly built child agent's function tools against AgentRegistration.tool_scope.tool_names before the generated conclude tool is mounted. A SCOPED_AGENT with provider/family metadata but no materialized tool_names allowlist now fails with SubAgentToolScopeViolation instead of treating the scope as advisory.

Hosts that build scoped child agents must resolve provider/family scopes into concrete tool names from the same provider manifests they compose. This keeps SYNC and ASYNC scoped-agent runs from silently exposing mutation, delegation, or unregistered tools.

Version 3.16.7 Subagent Prompt Export Cleanup

The old BACKGROUND_SESSION_PROMPT_RULES public name is removed. The canonical prompt discipline is now SUBAGENT_DELEGATION_PROMPT_RULES, owned by subagents/delegate_tools.py alongside generated delegate_* tools. Runtime status tools still expose check_background_sessions, update_background_session, cancel_background_session, and await_background_sessions for durable stored session views, but they no longer own delegation prompt copy.

Version 3.16.6 Sync Runner Cleanup

The generated delegate tool path no longer reads model fields through getattr, and SyncSubAgentRunner now owns its digest cap/evidence merge helpers as class methods. This keeps the SYNC scoped-agent path single-owner without changing the typed spawn spec or AgentLifecycleResult contract.

Version 3.16.5 Scoped-Agent Host Context

adrive_scoped_agent(...) now passes SubAgentRunContext.host_context into Agent.run(..., deps=...) on every turn. Backend-built SCOPED_AGENT factories can therefore use the same typed dependency object as parent console tools without separate context injection or package-specific global state.

Version 3.16.4 Sync Scoped-Agent Runner

subagents/sync_runner.py now delegates SYNC scoped-agent execution to the bounded adrive_scoped_agent(...) driver instead of running a one-turn agent.run(...) path. SYNC delegates therefore share the generated typed conclude tool, output-model validation, deterministic-check retry, and archetype budgets with ASYNC scoped sessions.

The runner formats the typed AgentLifecycleResult as a capped JSON digest and drains child evidence onto the parent delegate tool call through the canonical EvidenceRecorder.attach() path, so in-turn helpers such as quarantine_digest can surface their source chips on the visible action row.

Version 3.16.3 Bounded Scoped-Agent Driver

subagents/scoped_agent_driver.py now has a real bounded SCOPED_AGENT driver instead of only the initial prompt helper. adrive_scoped_agent(...) builds the archetype agent, mounts a generated conclude tool, carries message history between turns, honors stop and queued-instruction hooks, enforces the archetype turn/wall/token budgets, and returns a typed AgentLifecycleResult.

The generated conclude schema exposes status, verdict, narrative, and the archetype output fields. ACHIEVED cannot be recorded unless the output model validates; deterministic acceptance checks run after the tool call and a failed check clears the conclusion before retrying with the failed criterion. This makes scoped-agent success depend on the archetype contract rather than a parent-authored prose goal.

Version 3.16.2 Workflow Checkpointer Compilation

subagents/workflow_driver.py now compiles raw LangGraph StateGraph builders with the host-supplied checkpointer before invoking or resuming a WORKFLOW archetype. Already compiled graphs and the graph-like backend adapter objects still pass through unchanged.

This closes the resumability part of the WORKFLOW driver contract: provider packages may return an uncompiled StateGraph, and the host controls durable thread storage through the shared checkpointer keyed by run.session_id.

Version 3.16.1 Workflow Interrupt/Resume Driver Completion

subagents/workflow_driver.py now handles the workflow control-flow contract expected by typed WORKFLOW archetypes:

  • initial invocation uses the validated archetype input model and stable thread_id = run.session_id;
  • LangGraph-style __interrupt__ values carrying EscalationQuestion are persisted through run.escalation_gate.araise(...) and return an ESCALATED conclusion;
  • aresume_workflow(...) sends Command(resume=<EscalationAnswer JSON>) and validates the resumed terminal output through the archetype output model and deterministic acceptance checks.

The driver still accepts the existing graph-like objects used by backend workflow adapters, so this patch tightens the framework contract without reopening any free-text child-goal path.

Version 3.16.0 Removed Legacy Free-Text Session Contracts

New child work is created only through generated delegate_* subagent tools backed by typed archetype input models and SubAgentSpawnSpec. The old free-text session creation surface and autonomous goal driver are deleted from the public package API, not shimmed.

  • schemas/subagent.py is the single child-session schema module.
  • SubAgentSpawnSpec forbids extra fields, so a model or host cannot sneak in a goal escape hatch.
  • SubAgentBinding is the only host protocol for async subagent sessions, supervision verbs, and escalation answers.
  • register_general_tools(...) and GeneralChatToolProvider expose only status/supervision helpers for already-started sessions: check_background_sessions, update_background_session, cancel_background_session, await_background_sessions, and bounded sleep.
  • register_memory_tools(...) and MemoryToolProvider (adapters/memory.py) expose the host ChatMemoryStoreProtocol to the model as explicit create_memories / search_memories / update_memory tools (on top of the engine's implicit recall/remember). search_memories passes include_shared=True so cross-owner SHARED conclusions are reusable; the host injects the store + per-run owner/scope/chat accessors (same pattern as get_binding). The host maps the three tool names to user-safe labels — raw names never reach the UI.
  • Implicit per-turn recall (3.25.0) renders a STRUCTURED, caveated context block (_recall_context_block): the agent sees auto_memory_search_query (the raw user message, which may be an imperfect retrieval query) + the recalled memories in full, and is told to fall back to search_memories/create_memories/update_memory when the auto-search missed. Each user turn also persists a REFERENCE of what was recalled on the USER ChatTimelineEntry.metadata via AutoMemoryRecall (the single auto_memory_recall key, search_query + {memory_id, score} refs — NO text, so an edited/deleted memory is never duplicated or left stale). Hosts read it back with AutoMemoryRecall.from_metadata(...) and resolve ids to current text from the store for a "what did the agent recall" view. When memory is DISABLED (no store wired → the NoOpChatMemoryStore), recall is suppressed entirely (3.26.0): no caveat block and no auto_memory_recall metadata, so a memory-less host never shows a caveat naming unmounted tools.
  • Synchronous helper work goes through SyncSubAgentRunner and registered SCOPED_AGENT archetypes, not a separate quarantine module.

Backend or provider packages that need new background work must register a AgentRegistration and let SubAgentDelegationToolProvider generate one typed delegate tool for that archetype.

Version 3.14.2 Snake-Case Delegate Field Names

Generated delegate tools keep Python field names as the model-facing contract even when the archetype input model has camel-case API aliases. The delegate still validates the final payload against the archetype input model by field name before building SubAgentSpawnSpec, so backend providers can keep camel-case wire schemas without leaking those aliases into LLM tool calls.

Version 3.14.1 Flat Delegate Tool Schemas

Generated delegate_<archetype> tools expose the archetype input model fields directly as tool parameters. A scoped agent that requires context also gets flattened user_request_verbatim, context_facts, and acceptance_criteria parameters, so pydantic-ai can apply field-level schema guidance and ModelRetry to the exact archetype contract.

Version 3.39.0 Agent Contracts

Current-line agent contracts used by backend/provider packages:

  • schemas/subagent.py owns typed spawn specs, escalation question/answer schemas, lifecycle-result usage, and generalized session notice shapes.
  • subagents/spec.py and subagents/registry.py add AgentRegistration, AcceptanceContract, SubAgentRunContext, SubAgentRegistry, and compose_subagent_providers(...) with manifest drift and duplicate-name checks.
  • SubAgentDelegationToolProvider mounts generated delegate_<archetype> tools under ChatProviderFamily.SUB_AGENT.
  • SubAgentBinding is the host protocol for typed async subagent starts, supervision verbs, and escalation answers.
  • scoped_agent_driver, sync_runner, and workflow_driver provide the first typed driver seams. Since 3.15.0, the legacy general-tools creation surface is removed; new delegation work must use archetypes, not free-text goals.

Version 3.13.1 Evidence Recorder Dedupe And Cap

EvidenceRecorder.attach() is the single evidence write path. It dedupes by AgenticEvidenceItem.canonical_key() and caps each tool-call bucket at eight chips, so take() and drain_all() always return clean lists. Helper evidence drains from the child recorder and attaches to the parent recorder, which owns the policy.

from keble_agentic_chat import AgenticChat, RedisCachedAgenticChatStore

chat = AgenticChat(store=RedisCachedAgenticChatStore(...), run_control_stale_seconds=900.0)
runtime = chat.abuild_runtime(graph=compiled_scope_graph, memory=memory_store)
async for event in runtime.astream_turn(owner=..., scope_type="TASK", scope_id=..., chat_id=..., user_input=...,
                                         server_progress_text="Reviewing setup request."):  # optional "fake thinking"
    ...  # token / thinking / final; server_progress_text (2.5.0, 1.x parity) streams
    ...  # as the FIRST thinking delta + persists in the run's THINKING row, so chat-
    ...  # completions providers with no native reasoning still show immediate feedback
await chat.arequest_interrupt(owner=..., scope_type="TASK", scope_id=..., chat_id=...)  # cooperative stop

Tool composition (2.4.0; contract tightened 3.28.0): compose_tool_providers(*, agent, providers) (runtime/tool_providers.py) is the canonical handler that attaches domain tools onto a scope agent. A host wraps each domain's register_*_tools as a small provider satisfying keble_helpers.ChatToolProviderProtocol (provider_id: ChatToolProviderId + first-class manifest + register(*, agent)), then hands an ordered list to this function — so every host/scope wires tools identically, the way abuild_runtime is the single store→runtime point. Composition asserts manifest↔registration parity (both drift directions), asserts manifest.provider_id is provider_id, and rejects a duplicate provider id in one scope. It returns the ordered ChatToolProviderIds for room diagnostics. The dead context parameter was removed in 3.28.0 (every host passed None; every provider captures deps at construction). Requires keble-helpers>=1.32.0.

from keble_agentic_chat import compose_tool_providers

result = compose_tool_providers(agent=agent, providers=[positioning_provider, task_provider])
# result.provider_ids == [ChatToolProviderId.POSITIONING_QUERY, ChatToolProviderId.TASK_QUERY]
# tools now attached to `agent` in that order

Version 3.4.0 session notices (push)

The push half of the session lifecycle: when a child session reaches a terminal status or escalates, the host posts a SessionNotice to the PARENT chat (AgenticChat.apost_session_notice - a REBASED write that can never race a live turn into failure). The next user-driven turn prepends all unconsumed notices as a leading context block ("Background session updates since your last turn…") and its persist clears EXACTLY the consumed ids (ChatHistoryPatch.clear_session_notice_ids - id-scoped so a notice posted mid-turn survives for the next turn). Notices are idempotent by notice_id (the child session id); a worker retry re-posting the same fact updates in place. A notice NEVER auto-triggers a turn — cost stays user-driven. ChatHistoryBase.session_notices carries the unconsumed set (additive). NoOpChatMemoryStore now documents the recall-consistency contract and its degradation is pinned visible by tests.

Version 3.3.0 durable session registry and status verbs

Phases 1-2 of the agentic stack hardening program (compaction-safe child identity plus steerable/cancellable background work):

  1. Durable session registry — new checkpointed background_sessions graph channel (NEVER cleared by a fresh turn) + BackgroundSessionTracker seeded by the agent node each run; session identity now survives provider-message compaction. check_background_sessions answers TERMINAL sessions from the registry without a binding call and stamps last_checked_at; BackgroundSessionStatusView gains additive created_at/last_checked_at. Wire via register_general_tools(..., get_tracker=...) + DefaultTurnGraphProvider(session_tracker=...).
  2. Prompt discipline — exported SUBAGENT_DELEGATION_PROMPT_RULES (never auto-poll; statuses in history are ALWAYS stale; never truncate ids; no duplicate starts); the start/check/sleep tool descriptions now match (the old copy actively instructed poll-with-sleep loops).
  3. Session verbs (BREAKING for binding implementers) - SubAgentBinding owns aupdate_subagent_session, acancel_subagent_session, and await_subagent_sessions; matching status tools are mounted by register_general_tools(...).
  4. Run liveness - ChatRunStatusView + LangGraphChatRuntime.aget_run_status for reload-surviving "generating…" state and targeted interrupts.

Version 3.2.0 chat history concurrency correctness

Four guarantees added after a live incident (a background session's completion line killed the parent turn; a leaked run lock burned an autonomous session to a lying MAX_TURNS in milliseconds; every concurrent room open left twin history documents):

  1. Rebased persist_apersist / _aabandon_stale_pending / aacquire / arelease retry ChatHistoryRevisionConflict through store.protocol.apatch_chat_history_rebased, re-deriving the patch from the latest snapshot. Out-of-band timeline appends (worker THINKING / completion lines) are commutative with runtime patches, so a mid-turn append can no longer discard the turn. Hosts appending out-of-band keep doing exactly what they did; the engine now always rebase-wins too.
  2. Crash-safe run lock — every runtime command body runs inside HeldRunLock; an exception or an abandoned stream generator releases the lock immediately (run-id-guarded arelease), instead of wedging the chat for the 900s stale TTL. The success path is unchanged: the final persist still clears the lock atomically with the turn's rows.
  3. Busy turns fail loudChatTurnResult.competing_run_id is set ONLY when a command ran nothing because another run held the lock. Host drivers must treat that as a typed contention failure instead of counting no-op turns toward a budget.
  4. Atomic scope chataget_or_create_scope_chat is one find_one_and_update upsert under a new UNIQUE (owner, scope_type, scope_id) index; MongoAgenticChatStore.ainit first dedupes legacy twins (deletes ONLY empty twins, keeps content, logs loud and skips the unique index while any scope still holds multiple content-bearing documents). The in-memory store holds one lock across find+create.

New dependency: tenacity>=9 (transient lost-race retries only — never control flow).

Historical Background-Session Note

Older releases had a model-facing, prose-driven child-session creation API. That path is intentionally removed in 3.16.0. Current code keeps only the read-side BackgroundSessionStatusView name so downstream renderers do not need an unrelated UI rename; the executable contract is typed subagent delegation.

Version 2.9.0 backend tool-call timeline cards

(2.9.1: the NON-streamed agent node derives cards from result.new_messages() instead of attaching event_stream_handler — attaching one to Agent.run(...) forces a streamed provider request and breaks models without stream support. Output tools are excluded by name via the agent's output schema.)

Auto-executed backend tools are no longer invisible to the chat UI. Every agent run observes its provider event stream (event_stream_handler, both run and run_stream paths) and records each finished function-tool call as one terminal ChatAction(kind=SERVER_TOOL_CALL) (shared keble_helpers.AgenticActionKind, requires keble-helpers>=1.17.0):

  1. Created directly SUCCEEDED (ToolReturnPart) or FAILED (retry prompt) with the deterministic identity tool-call:<tool_call_id> — never pending, never resolvable, and excluded from the deferred pending batch contract.
  2. The deferred client-action registrar tool, host-excluded names (tool_action_excluded_tools, e.g. a sleep poll loop), and tool_call_ids replayed by a deferred resume are skipped, so approval/client tools never duplicate as cards.
  3. Rows ride the new tool_actions graph channel into _apersist (ordered thinking -> tool cards -> pending actions -> assistant) and stream live as the new ChatStreamEntryAdded event (custom channel kind STREAM_TOOL_ACTION), reusing the SAME entry id so live upserts and the refetched timeline converge.
  4. The persisted action carries only safe typed metadata: the stable tool_name, the terminal status, and optional host display copy via tool_call_formatter (returning None hides a call). Raw args/results are never stored; the frontend owns localized labels per tool name.

Version 1.1.0 unified actions + background subtasks

Two changes land together:

  1. Unified action contract. ChatAction now inherits keble_helpers.AgenticClientActionBase, and ChatActionKind/ChatActionStatus/ ChatActionProgress are aliases of the shared AgenticActionKind/ AgenticActionStatus/AgenticActionProgress. Server approvals, browser client tools, and self-served subagent decisions are one contract — no chat-local status or progress enum exists anymore. Requires keble-helpers>=1.13.0.
  2. Background subtasks + self-serving subagents. A chat agent can spawn a child task (e.g. an Amazon report) that runs in the background without blocking the conversation, then poll it. See protocols.py (BackgroundTaskBinding, SelfServingResolver), schemas/subtask.py (SpawnedSubtaskRef, SubtaskStatusView, SelfServeContext), runtime/general_tools.py (register_general_tools: spawn_subtask/check_background_tasks/ check_subtasks/bounded sleep), and runtime/self_serving.py (adrive_self_serving, bounded by MAX_SELF_SERVE_ITERATIONS). The package stays host-neutral: the backend owns the worker, persistence, and watchdog. A domain task is no longer guaranteed to be a ROOT task, and a client-tool invoke is no longer guaranteed to raise a human prompt.

Version 1.0.25 deferred action cleanup

  • Removed the obsolete single-action deferred-result converter after the runtime moved to batch-owned resume aggregation.
  • The existing batch converter remains the only provider resume path for aresolve_action(...) and astream_resolve_action(...), keeping pending_resume.pending_action_ids as the single source of truth.

Version 1.0.24 deferred action batch resume

  • Action resolution now treats pending_resume.pending_action_ids as one provider-owned deferred batch. Resolving one action updates that timeline row and keeps pending_resume intact while any sibling action remains pending.
  • When the last pending action in the batch resolves, aresolve_action(...) and astream_resolve_action(...) resume Pydantic-AI with one aggregate DeferredToolResults containing every server approval and browser client-tool result from that provider turn.
  • Hosts still resolve one visible action at a time. Multiple simultaneous client tools are allowed when the model has enough current domain state, but they are never a hard-coded runtime requirement.

Version 1.0.22 current Pydantic-AI stream API

  • The package now requires pydantic-ai-slim >=1.102.0 and consumes streamed provider snapshots through StreamedRunResult.stream_response(...).
  • Streamed chat still derives the terminal flag from ModelResponse.state, so the public ChatStream* sequence remains unchanged while the deprecated stream_responses() API is no longer called.
  • The existing AgenticChat._stream_run_usage(...) normalizer remains in place for usage-accounting safety across host environments.

Version 1.0.21 streamed usage compatibility

  • Streamed usage accounting now normalizes Pydantic-AI StreamedRunResult.usage whether the installed Pydantic-AI version exposes it as a RunUsage value or as a zero-argument method.
  • This keeps backend task-cost recording from turning successful streamed chat turns into ERROR timeline rows on Docker runtimes pinned to older Pydantic-AI minor versions.

Version 1.4.0 LangGraph orchestration substrate

keble_agentic_chat.runtime.langgraph adds the first integration where LangGraph owns chat orchestration and each pydantic-ai Agent run is a graph node. Read runtime/langgraph/protocols.py for the interface-first adapter map (node↔agent run, interrupt↔client-tool=AgenticClientActionBase, checkpointer↔durable thread keyed by chat id, stop↔ChatRunControl).

  • build_agentic_turn_graph(...) builds the generic agent -> pause -> apply turn graph; make_agent_node / make_pause_node / make_apply_node are composable factories so host graphs (e.g. the AMZ bootstrap) reuse the agent and pause nodes and supply their own draft-aware apply + finalize nodes.
  • make_agent_node(agent_for_state=...) rebuilds the agent per turn from channel state — needed when the system prompt derives from evolving domain state.
  • apply_resolutions_to_actions(...) is the shared resolution-stamp SSOT (action_with_resolution) so host apply nodes never re-implement resolution.
  • Adds langgraph (+ langgraph-checkpoint); langgraph transitively requires langchain-core. Host runtimes select the engine via a legacy|langgraph flag (legacy default until parity is proven).

Version 1.3.0 broadened provider-thinking persistence

  • AgenticChat(persist_provider_thinking=True) (default) persists provider thinking from any model into the run-owned THINKING timeline rows, not only OpenAI Responses summaries. Hosts that want summary-only behaviour for privacy pass persist_provider_thinking=False.
  • A single _thinking_part_source(part) classifier drives both the streamed and non-streamed paths: OpenAI Responses summaries always persist (PROVIDER_REASONING_SUMMARY); other displayable thinking (Anthropic, DeepSeek, …) persists as PROVIDER_THINKING only when the flag is on; OpenAI raw chain-of-thought (reserved part ids or raw_content) is never surfaced.
  • Non-streamed turns (arun_turn / aresolve_action / arun_internal_turn) now flush final-message thinking into the thinking:<run_id> row, so reasoning models no longer show a transient "thinking…" with empty history. Streamed turns persist incrementally as before (no double-write).

Version 1.0.20 chat thinking follow-up

  • ChatLiveReady now carries scopeType and scopeId, matching the frontend live-frame contract without adding a parallel ready-frame schema.
  • AgenticChat.astream_turn(...) accepts optional backend-authored server_progress_text. The runtime appends it after the user row under the same run-owned THINKING timeline entry and streams a newline-terminated THINKING_DELTA before provider text.
  • keble-helpers >=1.12.16 is required so clean Python 3.13 installs keep shared helper imports and Aliyun OSS compatibility stable.

Version 1.0.18 usage accounting

  • Chat turn, internal turn, action-resolution, and streaming paths accept an optional UsageAccountingRecorderProtocol.
  • After a Pydantic-AI run or completed stream, the runtime emits one package-neutral LLM token event with RunUsage, model/provider metadata, elapsed seconds, and chat tags.
  • The package does not price usage or persist cost rows; backend services own pricing and storage.

Version 1.0.16 resume text safety

  • AgenticChat accepts an optional resume_text_sanitizer hook that runs after provider-history role validation and before model replay.
  • The sanitizer only rewrites provider-authored text parts. Tool calls, tool returns, user prompts, and pending action identity remain unchanged.
  • Hosts can now remove legacy technical diagnostics from old compacted or assistant provider text without adding a new stored schema.

Version 1.0.15 provider resume safety

  • Native compaction now trims kept provider tails to a valid tool-call boundary. A retained tool-return or retry prompt must still have its matching assistant tool call in the compacted tail.
  • Provider-history resume now runs the same validation when loading existing stored chats, so old compacted histories with orphan tool rows are repaired before the next model request.
  • Action-resolution resume preserves the final pending assistant tool call only for the provider call that is about to attach the matching deferred result.
  • This prevents OpenAI-compatible providers from receiving orphan tool role messages after summary replacement.
  • Public timeline rows remain unchanged; the fix is isolated to private Pydantic-AI provider-message persistence and replay.

Version 1.0.14 compaction tail safety

  • Initial compaction-tail repair release. Superseded by 1.0.15, which also repairs already-persisted provider histories during resume loading.

Version 1.0.13 action progress update

  • ChatAction.progress is the canonical stored progress surface for browser/client-tool and backend task work associated with one timeline action.
  • ChatActionProgressUpdate owns action matching by action_id first and stored progress_key second, then updates the existing action row instead of creating a parallel progress history.
  • Stores expose aapply_action_progress(...), returning the updated timeline entry for live ENTRY_UPDATED frames. Mongo, Redis-cache, and memory stores all delegate through the same patch builder.
  • Terminal progress maps back to ChatActionStatus.SUCCEEDED or ChatActionStatus.FAILED when the progress report reaches a known terminal state.

Version 1.0.10 update

  • Packages the pending-resume audit clarification that keeps timeline actions and pending_action_ids as the canonical resume source of truth.
  • No storage shape, timeline API, or provider runtime behavior changes are introduced in this release.

Runtime Contract

AgenticChat runs normal user turns and resumes pending actions:

service: AgenticChat[MyDeps, ChatAssistantText, str] = AgenticChat(
    store=store,
    output_type=ChatAssistantText,
)

result = await service.arun_turn(
    agent=agent,
    owner=owner,
    scope_type="task_room",
    scope_id=task_id,
    chat_id=chat.id,
    user_input=ChatUserInput(text="Continue the setup."),
    deps=deps,
)

Custom assistant output schemas do not inherit a package base class. Pass an explicit text adapter so the runtime can append assistant timeline rows:

service: AgenticChat[MyDeps, MyOutput, str] = AgenticChat(
    store=store,
    output_type=MyOutput,
    assistant_text_adapter=lambda output: output.answer,
)

Pending server approvals and browser-resolved tools both use the same action resolution API:

await service.aresolve_action(
    agent=agent,
    owner=owner,
    scope_type="task_room",
    scope_id=task_id,
    chat_id=chat.id,
    resolution=ChatActionResolution(
        action_id=action_id,
        status="APPROVED",
    ),
    deps=deps,
)

Hosts with a live transport should use astream_resolve_action(...) for the same browser/server action resumes. It persists the accepted/rejected action before provider execution, emits transient text/reasoning deltas while the resume runs, and finishes with the same persisted ChatTurnResult contract as aresolve_action(...).

If one provider response requested multiple client tools or server approvals, the runtime keeps the private pending-resume state until every action in that batch has a result. The frontend or host app should still send one ChatActionResolution per user click; the final resolution in the batch is the only one that resumes the provider, and it resumes with all deferred results.

When a provider emits normal assistant text in the same response as deferred tool requests, the runtime appends the assistant row before the action rows. This keeps the visible transcript in the same order the model produced it:

USER
ASSISTANT
ACTION

Hosts that need to advance a deterministic workflow after a tool resolution can pass resume_prompt to aresolve_action(...) or run arun_internal_turn(...). Internal turns are stored only in private provider history and do not add synthetic USER rows to the public timeline; only the assistant/action result is appended.

Browser tools are registered as deferred client actions. The model tool name is configurable, but defaults to request_client_tool for model familiarity:

register_client_action_tool(
    agent,
    validate_client_action_request=validate_client_action,
)

For the two universal browser interactions — a YES/NO confirm and a free-text / textarea question with an editable default — the package ships generic, host-agnostic primitive schemas in keble_agentic_chat.schemas.client_primitives (re-exported from the package root): ConfirmClientToolPayload / ConfirmClientToolResult and TextInputClientToolPayload / TextInputClientToolResult. These are reusable building blocks, not new agent tools or a new registrar — a host embeds the primitive payload inside its own typed client-action payload (or the generic request_client_action payload dict) and parses the primitive result out of ChatActionResolution.result. The camelCase wire aliases are the cross-repo contract mirrored by keble-core and the frontend forms:

confirm  payload {prompt, defaultAnswer, confirmLabel?, cancelLabel?} -> result {confirmed}
text     payload {prompt, defaultValue, multiline, placeholder?, maxLength?} -> result {text}

Streaming uses the same timeline/action runtime as normal turns. astream_turn and astream_resolve_action set run_control.mode="STREAM", emit ChatStreamTextDelta rows while the provider stream is active, may emit transient ChatStreamReasoningSummaryDelta rows for provider-marked safe reasoning summaries, and always finish with one ChatStreamFinal containing the canonical ChatTurnResult. Reasoning summaries are never persisted in the public timeline and must not carry raw chain-of-thought or provider raw_content. arequest_interrupt(...) only targets active stream runs and clears run control with an interrupted final result instead of persisting a stale assistant row. Streaming must always call the provider stream directly; do not implement it as a final-only wrapper around arun_turn(...).

Active run control is a private lock, but the public history view exposes runtimeState.isCommandRunning and runtimeState.mode so hosts can disable inputs and poll during live recovery. Duplicate arun_turn(...), aresolve_action(...), arun_internal_turn(...), and astream_turn(...) calls return the currently persisted chat state while a non-stale command owns the lock. run_control_stale_seconds clears abandoned locks using run_control.started_at or the chat updated timestamp, then allows the new command to proceed without appending a synthetic ERROR row. Hosts that need to recover a live request before constructing expensive runtime dependencies should call aget_active_run_result_or_clear_stale(...); it uses the same package-owned stale-lock policy as normal commands. If another command wins the lock between the public pre-check and command preparation, the error boundary returns that competing active result instead of clearing the lock or writing an ERROR row.

Runtime cleanup is also run-id scoped. Interrupted streams and runtime error boundaries re-read the latest stored chat before clearing run_control; if a newer command owns the lock, the runtime returns that active state and leaves the newer lock untouched. Runtime mode and timeline row kind are exported enums inside Python while preserving the same uppercase JSON values.

Native Auto-Compaction

Native compaction is opt-in. Pass both a ChatCompactionPolicy and a host-owned compactor; otherwise the runtime behaves as it did before. Compaction replaces only private provider_messages; it never deletes public timeline rows, rewrites actions, or changes action ids/statuses/results. When compaction runs, the runtime appends a visible SUMMARY timeline row before the user message that triggered compaction.

async def compact_chat_context(
    request: ChatCompactionRequest,
    deps: MyDeps,
) -> ChatCompactionResult:
    """Summarize old chat context with a host-owned model or service."""

    summary = await deps.summary_agent.asummarize(request)
    return ChatCompactionResult(
        summary_text=summary.text,
        metadata={"summary_model": summary.model_name},
    )


service = AgenticChat(
    store=store,
    output_type=ChatAssistantText,
    compaction_policy=ChatCompactionPolicy(
        max_provider_messages=40,
        keep_recent_provider_messages=12,
    ),
    compactor=compact_chat_context,
)

Compaction triggers when an enabled threshold is exceeded: provider-message count, serialized provider-message character count, or optional latest provider input-token usage. The compacted private history is built with Pydantic-AI typed messages: one SystemPromptPart summary request plus the configured recent raw provider tail. If a new user message supersedes unresolved actions, the runtime abandons those actions first and does not keep the unsafe deferred-tool tail.

Action resolution never compacts before resume because Pydantic-AI needs the exact deferred tool-call history. Native compaction is also disabled when a custom history_builder is installed, because the host already owns provider history construction. Pydantic-AI history processors are useful request-time transforms, but they are not enough for native compaction because they do not persist compacted provider state or create visible SUMMARY audit rows through ChatHistoryPatch.

Storage Contract

ChatHistoryMongoObject persists one v1.0 document:

  • schema_version
  • owner, scope_type, scope_id, title
  • revision
  • timeline
  • provider_messages
  • pending_resume
  • run_control

The public ChatHistoryView exposes only id, scope fields, title, timeline, and the safe runtime_state summary. Do not add duplicate read-side fields for tool approvals, browser tools, pending tool state, model messages, raw run ids, or compaction internals.

pending_resume.request_raw is an audit/debug snapshot of the provider deferred request. Runtime resume decisions must use the canonical timeline actions plus pending_resume.pending_action_ids; do not treat request_raw as a second source of truth for action identity or status.

All stores implement one optimistic patch method:

await store.apatch_chat_history(
    owner=owner,
    scope_type=scope_type,
    scope_id=scope_id,
    chat_id=chat_id,
    patch=ChatHistoryPatch(
        expected_revision=chat.revision,
        append_timeline=[entry],
        update_actions=[resolved_action],
    ),
)

Memory and Mongo stores both raise ChatHistoryRevisionConflict when the expected revision is stale. Redis is a cache wrapper only; cached pre-1.0 documents are ignored and replaced from the canonical store.

Breaking Migration Note

There is no 0.8.x storage migration layer. Old documents or cache payloads that contain these removed fields are invalid for v1.0:

  • tool_approval_request
  • tool_calls
  • client_tool_request
  • client_tool_calls
  • pending_tool_requests
  • pending_tool_state
  • timeline_entries
  • model_messages
  • turns
  • tool_descriptions
  • stream_control

Hosts should create new v1.0 chat rows or run their own explicit migration outside this package.

3.17.0 LangGraph Workflow Contract

WORKFLOW graph factories must return a real LangGraph StateGraph or CompiledStateGraph. The driver compiles raw graphs with the host checkpointer, passes thread_id on both fresh and resumed invocations, and resumes with Command(resume=<EscalationAnswer JSON>).

Duck-typed adapters with only ainvoke are rejected because they bypass checkpoint ownership and durable resume. SCOPED_AGENT escalation is opt-in by descriptor; non-FORBIDDEN scoped archetypes receive one escalate_to_parent tool and hosts resume them through the existing steering channel.

Verification

Use Python 3.13 and the package uv environment:

uv run pytest -q
uv run pyright

Mongo Startup Indexes

Hosts should call store.ainit(amongo=...) during startup. The Mongo store creates the owner/scope_type/scope_id/updated desc index used by scoped chat history listing, the Redis store delegates to its durable store, and the memory store is a no-op for the same contract. The startup method is part of the store protocol so host apps can initialize every concrete store through one public API.

1.0.17 Persisted Thinking Timeline

keble-agentic-chat 1.0.17 adds durable THINKING timeline rows and canonical ChatStreamThinkingDelta / ChatLiveThinkingDelta frames. Thinking text is provider-safe summary or host-authored progress copy only; raw chain-of-thought must never enter timeline rows, stream frames, provider messages, or compatibility reasoning-summary deltas.

Use ChatTimelineEntryTextAppend with store.aappend_timeline_entry_text(...) to upsert one newline-delimited thinking row by stable entry_id, such as thinking:<run_id> or thinking:<progress_key>. REASONING_SUMMARY_DELTA remains as a compatibility frame during migration, but new consumers should render THINKING_DELTA and persisted ChatTimelineEntryKind.THINKING.

1.0.19 Usage Access Compatibility

Agentic chat usage accounting continues to emit one package-neutral LLM event for normal and streamed turns when a host recorder is provided. Runtime code now reads Pydantic-AI usage through the current result.usage property instead of the deprecated method form.

1.0.23 Result Usage Normalization

Normal and streamed chat accounting now delegates usage normalization to UsageAccountingEvent.from_result_usage(...) / UsageAccountingEvent.normalize_run_usage(...). This keeps chat-specific code focused on chat runtime state while the helper package owns compatibility for Pydantic-AI value-style and method-style usage APIs.

1.0.26 Pending Action Progress State

ChatActionProgressUpdate may attach progress to a pending action, but terminal progress no longer changes PENDING to SUCCEEDED or FAILED. User/browser confirmation remains the only way to resolve pending client tools.

Terminal progress can still close actions that have already entered the accepted execution path through APPROVED or SUBMITTED; rejected, denied, and abandoned actions keep their original lifecycle state.

Version 3.18.0 Update

Scoped-agent runs now mount the shared tool-resilience capability. Unexpected tool exceptions are returned to the model as safe recoverable tool results so a child can try another tool or conclude honestly; exhausted children can return a typed non-empty NOT_ACHIEVED conclusion when the host opts in.

WORKFLOW runs stream LangGraph node events through the optional workflow_node_event_sink on SubAgentRunContext, and topology is derived from compiled graphs via workflow_topology_from_graph(...). Providers should not hand-author parallel topology metadata.

3.39.0 Agent Lifecycle Contract Collapse

keble-agentic-chat 3.39.0 uses the helper-owned agent-core projection as the only current-line lifecycle contract:

  1. AgentRegistration.descriptor is an AgentDescriptor.
  2. Terminal child results are AgentLifecycleResult(status, verdict, errorCode, result, narrative, concludedAt).
  3. Escalations use EscalationQuestion.audience.
  4. Spawn/run context uses AgentContext.
  5. Descriptor-owned escalation tools mount through register_agent_escalation_tools(...).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

keble_agentic_chat-3.42.0.tar.gz (673.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

keble_agentic_chat-3.42.0-py3-none-any.whl (180.2 kB view details)

Uploaded Python 3

File details

Details for the file keble_agentic_chat-3.42.0.tar.gz.

File metadata

  • Download URL: keble_agentic_chat-3.42.0.tar.gz
  • Upload date:
  • Size: 673.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for keble_agentic_chat-3.42.0.tar.gz
Algorithm Hash digest
SHA256 3611d46e4c2a36663ef07ea6b93fb6821c638fd37512d4e4a8e8bd27dcafaf80
MD5 dda39bd6afbedfb0cc3d3b102652f6af
BLAKE2b-256 e6b0de4deaa8aad46be1e8b183a0b76bce7e5359844e1cd38401e96b5a0101bc

See more details on using hashes here.

File details

Details for the file keble_agentic_chat-3.42.0-py3-none-any.whl.

File metadata

  • Download URL: keble_agentic_chat-3.42.0-py3-none-any.whl
  • Upload date:
  • Size: 180.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for keble_agentic_chat-3.42.0-py3-none-any.whl
Algorithm Hash digest
SHA256 55de83ff531359c5ff4cba6359446793d882c7a6553b62eb7a81876410d585a4
MD5 b47be5dc4aa31da95b76675b81af3d71
BLAKE2b-256 ce65b863b3fd7332034c525574afe9ed7ed97f48b7565c991015e179f687419a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page