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 keepsChatActionrows andChatPendingResume.pending_action_idsaligned 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_modifyitemsowns test-layer markers for this package.- Default local and CI runs stay offline unless a worker explicitly enables
RUN_INTEGRATION=1orRUN_LLM_LIVE=1. - Backend/package consumers can rely on the same marker names used by
keble-db.testingandkeble-keepa.testing.
Agent-core contract collapse (3.39.0): general session tools and provider manifests name
keble_helpers.AgentToolRegistrationConfig,AgentToolDescriptor,AgentContext, andAgentLifecycleResultdirectly. Current-line code should use the typedapprovalvocabulary and must not reintroduce the retired alias models.
Owner-list chat run liveness (3.40.0):
ChatRunActionTypeandChatRunStatusChangedPayloaddefine the package-owned workspace eventCHAT_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, andMoneypayload/result pairs besideConfirmandTextInput. Payload objects own semantic result validation throughvalidate_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 exposesarun_turn/aresolve_action/astream_turn/astream_resolve_actionover a compiled graph + aChatHistoryStore, with streaming, thinking, interrupt/resume (Mongo checkpointer), compaction, per-run cost, andChatMemoryStorerecall/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 viaabuild_runtime(graph=..., memory=..., compactor=...) -> LangGraphChatRuntime. It satisfies the framework-neutralkeble_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-firstAgenticChatturn engine + itslegacy|langgraphflag were removed in 2.2.0; the longAgenticChat(...)snippets further down are historical (they used the removed engine's constructor) — the new class takes onlystore=+run_control_stale_seconds=.service.pyholds only the canonicalChatActionResolutionError. The store classes (InMemoryAgenticChatStore/MongoAgenticChatStore/RedisCachedAgenticChatStore) are unchanged.Memory contracts moved to keble-helpers (2.7.0):
ChatMemoryRecord,ChatMemoryKind, andChatMemoryStoreProtocolare owned by framework-neutralkeble_helpers(1.16.0) so other packages produce/consume the same records without importing this engine.runtime/langgraph/memory.pyre-exports them (ChatMemoryStorestays the engine-side alias) and keepsNoOpChatMemoryStore. Recall scoping is locked: cross-chat per(owner, scope_type);scope_id/chat_idare write-side provenance metadata, not recall filters. Per-turn episodes are built withChatMemoryRecord.episode(...)(no free-string kinds). Half-resolved action batches raise the typedChatActionResolutionError(2.7.0) instead of a rawValueError.
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:
ClientToolOptionSelectClientToolPayload/SelectClientToolResultMultiSelectClientToolPayload/MultiSelectClientToolResultNumberClientToolPayload/NumberClientToolResultMoneyClientToolPayload/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
Moneyprimitive reuseskeble_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 inruntime/general_tools.pyand theSubAgentBindingprotocol. Renamed fromBackgroundSessionStatusViewin 3.31.0 (re-landed on main in 3.33.0).GeneralAgentToolsConfig(schemas/for_agent.py) — tool-registration config forregister_general_tools. Renamed fromGeneralToolsConfigin 3.31.0 (re-landed on main in 3.33.0).- KEPT as read models (NOT tool payloads):
ChatHistoryView,ChatRunStatusView,ChatActionDisplay, andSubAgentJobView— the jobs-panel SSOT carried onChatHistoryView.subagent_jobsand 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 carryingEscalationQuestionare persisted throughrun.escalation_gate.araise(...)and return anESCALATEDconclusion; aresume_workflow(...)sendsCommand(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.pyis the single child-session schema module.SubAgentSpawnSpecforbids extra fields, so a model or host cannot sneak in agoalescape hatch.SubAgentBindingis the only host protocol for async subagent sessions, supervision verbs, and escalation answers.register_general_tools(...)andGeneralChatToolProviderexpose only status/supervision helpers for already-started sessions:check_background_sessions,update_background_session,cancel_background_session,await_background_sessions, and boundedsleep.register_memory_tools(...)andMemoryToolProvider(adapters/memory.py) expose the hostChatMemoryStoreProtocolto the model as explicitcreate_memories/search_memories/update_memorytools (on top of the engine's implicit recall/remember).search_memoriespassesinclude_shared=Trueso cross-owner SHARED conclusions are reusable; the host injects the store + per-run owner/scope/chat accessors (same pattern asget_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 seesauto_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 tosearch_memories/create_memories/update_memorywhen the auto-search missed. Each user turn also persists a REFERENCE of what was recalled on the USERChatTimelineEntry.metadataviaAutoMemoryRecall(the singleauto_memory_recallkey,search_query+{memory_id, score}refs — NO text, so an edited/deleted memory is never duplicated or left stale). Hosts read it back withAutoMemoryRecall.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 → theNoOpChatMemoryStore), recall is suppressed entirely (3.26.0): no caveat block and noauto_memory_recallmetadata, so a memory-less host never shows a caveat naming unmounted tools. - Synchronous helper work goes through
SyncSubAgentRunnerand registeredSCOPED_AGENTarchetypes, 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.pyowns typed spawn specs, escalation question/answer schemas, lifecycle-result usage, and generalized session notice shapes.subagents/spec.pyandsubagents/registry.pyaddAgentRegistration,AcceptanceContract,SubAgentRunContext,SubAgentRegistry, andcompose_subagent_providers(...)with manifest drift and duplicate-name checks.SubAgentDelegationToolProvidermounts generateddelegate_<archetype>tools underChatProviderFamily.SUB_AGENT.SubAgentBindingis the host protocol for typed async subagent starts, supervision verbs, and escalation answers.scoped_agent_driver,sync_runner, andworkflow_driverprovide 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'sregister_*_toolsas a small provider satisfyingkeble_helpers.ChatToolProviderProtocol(provider_id: ChatToolProviderId+ first-classmanifest+register(*, agent)), then hands an ordered list to this function — so every host/scope wires tools identically, the wayabuild_runtimeis the single store→runtime point. Composition assertsmanifest↔registration parity (both drift directions), assertsmanifest.provider_id is provider_id, and rejects a duplicate provider id in one scope. It returns the orderedChatToolProviderIds for room diagnostics. The deadcontextparameter was removed in 3.28.0 (every host passedNone; every provider captures deps at construction). Requireskeble-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):
- Durable session registry — new checkpointed
background_sessionsgraph channel (NEVER cleared by a fresh turn) +BackgroundSessionTrackerseeded by the agent node each run; session identity now survives provider-message compaction.check_background_sessionsanswers TERMINAL sessions from the registry without a binding call and stampslast_checked_at;BackgroundSessionStatusViewgains additivecreated_at/last_checked_at. Wire viaregister_general_tools(..., get_tracker=...)+DefaultTurnGraphProvider(session_tracker=...). - 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). - Session verbs (BREAKING for binding implementers) -
SubAgentBindingownsaupdate_subagent_session,acancel_subagent_session, andawait_subagent_sessions; matching status tools are mounted byregister_general_tools(...). - Run liveness -
ChatRunStatusView+LangGraphChatRuntime.aget_run_statusfor 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):
- Rebased persist —
_apersist/_aabandon_stale_pending/aacquire/areleaseretryChatHistoryRevisionConflictthroughstore.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. - Crash-safe run lock — every runtime command body runs inside
HeldRunLock; an exception or an abandoned stream generator releases the lock immediately (run-id-guardedarelease), 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. - Busy turns fail loud —
ChatTurnResult.competing_run_idis 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. - Atomic scope chat —
aget_or_create_scope_chatis onefind_one_and_updateupsert under a new UNIQUE(owner, scope_type, scope_id)index;MongoAgenticChatStore.ainitfirst 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):
- Created directly SUCCEEDED (
ToolReturnPart) or FAILED (retry prompt) with the deterministic identitytool-call:<tool_call_id>— never pending, never resolvable, and excluded from the deferredpendingbatch contract. - The deferred client-action registrar tool, host-excluded names
(
tool_action_excluded_tools, e.g. asleeppoll loop), and tool_call_ids replayed by a deferred resume are skipped, so approval/client tools never duplicate as cards. - Rows ride the new
tool_actionsgraph channel into_apersist(ordered thinking -> tool cards -> pending actions -> assistant) and stream live as the newChatStreamEntryAddedevent (custom channel kindSTREAM_TOOL_ACTION), reusing the SAME entry id so live upserts and the refetched timeline converge. - The persisted action carries only safe typed metadata: the stable
tool_name, the terminal status, and optional host display copy viatool_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:
- Unified action contract.
ChatActionnow inheritskeble_helpers.AgenticClientActionBase, andChatActionKind/ChatActionStatus/ChatActionProgressare aliases of the sharedAgenticActionKind/AgenticActionStatus/AgenticActionProgress. Server approvals, browser client tools, and self-served subagent decisions are one contract — no chat-local status or progress enum exists anymore. Requireskeble-helpers>=1.13.0. - 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/boundedsleep), andruntime/self_serving.py(adrive_self_serving, bounded byMAX_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(...)andastream_resolve_action(...), keepingpending_resume.pending_action_idsas the single source of truth.
Version 1.0.24 deferred action batch resume
- Action resolution now treats
pending_resume.pending_action_idsas one provider-owned deferred batch. Resolving one action updates that timeline row and keepspending_resumeintact while any sibling action remains pending. - When the last pending action in the batch resolves,
aresolve_action(...)andastream_resolve_action(...)resume Pydantic-AI with one aggregateDeferredToolResultscontaining 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.0and consumes streamed provider snapshots throughStreamedRunResult.stream_response(...). - Streamed chat still derives the terminal flag from
ModelResponse.state, so the publicChatStream*sequence remains unchanged while the deprecatedstream_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.usagewhether the installed Pydantic-AI version exposes it as aRunUsagevalue 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 genericagent -> pause -> applyturn graph;make_agent_node/make_pause_node/make_apply_nodeare composable factories so host graphs (e.g. the AMZ bootstrap) reuse the agent and pause nodes and supply their own draft-aware apply +finalizenodes.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 requireslangchain-core. Host runtimes select the engine via alegacy|langgraphflag (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-ownedTHINKINGtimeline rows, not only OpenAI Responses summaries. Hosts that want summary-only behaviour for privacy passpersist_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 asPROVIDER_THINKINGonly when the flag is on; OpenAI raw chain-of-thought (reserved part ids orraw_content) is never surfaced. - Non-streamed turns (
arun_turn/aresolve_action/arun_internal_turn) now flush final-message thinking into thethinking:<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
ChatLiveReadynow carriesscopeTypeandscopeId, matching the frontend live-frame contract without adding a parallel ready-frame schema.AgenticChat.astream_turn(...)accepts optional backend-authoredserver_progress_text. The runtime appends it after the user row under the same run-ownedTHINKINGtimeline entry and streams a newline-terminatedTHINKING_DELTAbefore provider text.keble-helpers >=1.12.16is 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
AgenticChataccepts an optionalresume_text_sanitizerhook 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-returnor 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
toolrole 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.progressis the canonical stored progress surface for browser/client-tool and backend task work associated with one timeline action.ChatActionProgressUpdateowns action matching byaction_idfirst and storedprogress_keysecond, then updates the existing action row instead of creating a parallel progress history.- Stores expose
aapply_action_progress(...), returning the updated timeline entry for liveENTRY_UPDATEDframes. Mongo, Redis-cache, and memory stores all delegate through the same patch builder. - Terminal progress maps back to
ChatActionStatus.SUCCEEDEDorChatActionStatus.FAILEDwhen 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_idsas 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_versionowner,scope_type,scope_id,titlerevisiontimelineprovider_messagespending_resumerun_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_requesttool_callsclient_tool_requestclient_tool_callspending_tool_requestspending_tool_statetimeline_entriesmodel_messagesturnstool_descriptionsstream_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:
AgentRegistration.descriptoris anAgentDescriptor.- Terminal child results are
AgentLifecycleResult(status, verdict, errorCode, result, narrative, concludedAt). - Escalations use
EscalationQuestion.audience. - Spawn/run context uses
AgentContext. - Descriptor-owned escalation tools mount through
register_agent_escalation_tools(...).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3611d46e4c2a36663ef07ea6b93fb6821c638fd37512d4e4a8e8bd27dcafaf80
|
|
| MD5 |
dda39bd6afbedfb0cc3d3b102652f6af
|
|
| BLAKE2b-256 |
e6b0de4deaa8aad46be1e8b183a0b76bce7e5359844e1cd38401e96b5a0101bc
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55de83ff531359c5ff4cba6359446793d882c7a6553b62eb7a81876410d585a4
|
|
| MD5 |
b47be5dc4aa31da95b76675b81af3d71
|
|
| BLAKE2b-256 |
ce65b863b3fd7332034c525574afe9ed7ed97f48b7565c991015e179f687419a
|