## 2026-06-27 (chat memory: created_at -> created_ts, Qdrant storage convention) — 1.36.0 -> 1.37.0
State: keble-helpers 1.37.0 — align the durable chat-memory record's timestamp field with the
storage base it actually lands in. Breaking, no backward-compat (forward-only durable memory).
(Rebased over concurrent sibling merges 1.30.0–1.36.0 — those numbers were taken — so this ships as
1.37.0; orthogonal to the 1.36.0 AgenticEventEmitter rework.)

zma asked whether `ChatMemoryRecord.created_at` correctly maps keble's conventions. Finding: it
round-trips fine (no runtime bug) but uses `created_at`, which matches only the EXTERNAL Strapi
mirror (`StrapiSchemaBase.created_at`). The record is persisted INTO Qdrant by the host store, so
by the house convention (`typings/typing.py`) it should follow `QdrantSchemaBase.created_ts`
(Mongo/SQL objects use `SchemaBase.created`; Strapi-external uses `created_at`). Decision: rename
to `created_ts`.

Ai: in `keble_helpers/ai/chat_memory.py` renamed `ChatMemoryRecord.created_at` -> `created_ts`
(field + `episode()` classmethod param + body + the `since`/`until` recall docstring). Added an
explanatory comment tying the choice to `QdrantSchemaBase.created_ts` vs `SchemaBase.created` vs
Strapi's `created_at`. The camelCase wire alias becomes `createdTs` (consumed by keble-core
`IChatMemoryItemView`). Tests: `episode(created_ts=...)` round-trip, camelCase alias now asserts
`createdTs`, plus a NEW lock test (`created_ts` in model_fields, `created_at` NOT) so a future
agent can't silently revert the convention. 14 passed. Downstream coordinated in the same change:
keble-agentic-chat (2 construction sites), keble.backend store payload/index/API, keble-core type,
keble.frontend dep refresh.

## 2026-06-27 (AgenticEventEmitter: non-blocking detach-at-emit + drain-at-end) — 1.35.0 -> 1.36.0
State: keble-helpers 1.36.0 — the shared agentic event emitter is now non-blocking by design.
(Rebased over the concurrent 1.30.0–1.35.0 sibling merges — those versions were already taken — so
this emitter rework ships as 1.36.0. Consumers must bump the keble-helpers floor to >=1.36.0.)

zma: in `keble_positioning.aload_fresh_items` the item-record/relationship events were emitted with
an inline `await ...aemit(...)` during round processing; emission should never block the data path —
"await all async tasks at the end, not during processing". Cross-repo, clean/slim, code-breaking (no
backward-compat), best-effort failures → Sentry, full mock+IRL coverage. Detach EVERYTHING incl.
keble-task lifecycle; correctness preserved by drain placement.

Ai: reworked `keble_helpers/ai/events.py::AgenticEventEmitter`:
- `aemit` is now NON-BLOCKING — it schedules ONE background task wrapping the whole ordered callback
  chain (intra-event order preserved by the sequential chain coroutine) and returns immediately.
- new `adrain()` awaits all outstanding emit tasks at a workflow boundary, inspecting results with
  `asyncio.gather(..., return_exceptions=True)` (no raise) and routing each non-cancelled failure to
  an injected error sink, else `logger.error`. Loops to a fixpoint so re-entrant emits-during-drain
  are joined; no try/except anywhere.
- new package-neutral `EmissionError` (event + `SkipValidation[BaseException]`) and
  `EmissionErrorHook` type; new `bind_error_hook(hook)` injects the sink (backend wires Sentry; the
  package never imports an observability SDK). Exported from `ai/__init__` and the package root.
- private `_tasks: dict[Task, event]` + `_on_error` (`PrivateAttr`); `build`'s existing-instance
  identity passthrough is now load-bearing (keeps the bound hook + in-flight registry).
- Consumer contract: every caller MUST reach an `adrain()` boundary before reading a callback side
  effect and before its unit of work returns (segmenting run-end, positioning round-end, keble-task
  lifecycle + `astart` finally). Side-effect notes added on every changed block.
- Tests (`tests/test_ai_events.py`): non-blocking dispatch, intra-event order after drain, failure→hook
  without raising, chain-stops-at-first-failure, re-entrant-emit fixpoint, empty-callbacks no-op. The
  two old tests that relied on synchronous/ propagating `aemit` were rewritten to the new contract.

## 2026-06-27 (factory helpers onto owning types) — 1.34.0 -> 1.35.0
State: keble-helpers 1.35.0 — part of the cross-repo "factory/default/conversion
belongs on the owned type" refactor (clean, no backward-compat shims). Rebased onto
origin/main 1.34.0 (1.30.0-1.34.0: deps-base, tool_args, tool-provider, utils consolidation, identity enums); renumbered from
1.34.0 to 1.35.0 to clear the collision.

zma: study misplaced free helpers across positioning/segmenting/backend and move each
build/default/convert/guard helper onto the type it owns; full census; bump+publish.

Ai: added two owned-type factories so backend can drop the free helpers:
- `Money.build_from_cents(*, cents, currency)` (typings/typing.py) — exact integer
  std-cents -> Money; replaces backend `_money` in `app/schemas/token_conversion.py`.
- `AgenticActionStatus.build_from_progress_report(*, progress_report)` (ai/events.py)
  — canonical ProgressReport -> lifecycle status (FAILED>SUCCEEDED>PROGRESSED>STARTED);
  replaces backend `_event_status` in `app/core/task_room_progress.py`. ProgressReport
  is a TYPE_CHECKING-only import (no runtime/package-ordering cycle). Additive only;
  new unit tests `tests/test_typings/test_money_build_from_cents.py` and
  `tests/test_ai_action_status_from_progress.py` (7 passing). NOTE: the third census
  candidate `_build_failure_event_emitter` stays backend-local by design — it needs the
  backend redis runtime and a keble-task `TaskMongoObject`, so moving it would leak
  those concerns into this low-level lib.

## 2026-06-27 (unify agentic identity vocabulary) — 1.33.0 -> 1.34.0
State: keble-helpers 1.34.0 (rebased onto 1.33.0) — new single source of truth for cross-repo agentic identity strings.

zma: keble-positioning hardcoded `POSITIONING_STUDY_ARCHETYPE`, `POSITIONING_STUDY_PROVIDER_ID`,
`POSITIONING_TASK_TYPE`. Bad fashion + cross-repo drift risk (task_type shadowed the backend `TaskType`
enum). Decision (after weighing a protocol-first alternative): unify ALL three categories — task type,
archetype name, provider id — as typed enums in keble-helpers, since helpers already hosts the sub-agent
framework (`SubAgentArchetypeDescriptor` et al.), the `Marketplace` domain enum, and per-package
`AgenticActionEventSource` members. Single-source-of-truth over package-ownership; helpers now learns
every domain archetype name (accepted trade-off: a new archetype needs a helpers release).

Ai: added `keble_helpers/ai/identities.py` with three `str, Enum`s exported from the package root via
`ai/__init__.py`:
- `KebleTaskType` — every task-type routing string (POSITIONING, SEGMENTING_STUDY, DISCOVERY, CHAT,
  BOOTSTRAP, AMZ_PRODUCT_REPORT, DECISION_FOR_AMZ_PRODUCT_REPORT, SUBAGENT_WORKFLOW,
  SCOPED_AGENT_SESSION). Backend `TaskType` aliases this; `Settings.*_TASK_TYPE` defaults derive from it.
- `SubAgentArchetypeName` — every archetype id (positioning_study, segmenting_study, bootstrap_amz_report,
  product_discovery, product_niche, general_research, quarantine_digest). Backend binding dispatch +
  package descriptors + keble-core mirror reference these.
- `SubAgentProviderId` — every provider id (positioning_archetypes, segmenting_archetypes,
  amz_report_query, keepa_product_query). `compose_subagent_providers` dedup key.
Framework fields stay `str` (no keble-agentic-chat change); `str, Enum` members satisfy them, so values
are typed at the edges only. Tests (`tests/test_ai_identities.py`, 8 cases): lock every wire value,
uniqueness per enum, root-export presence, str-compat. 336 tests collect clean.

## 2026-06-27 (consolidate shared general utilities — canonical home) — 1.32.0 -> 1.33.0
State: keble-helpers 1.33.0 — keble-helpers becomes the single home for domain-free utilities that
had drifted across positioning/segmenting/products/task/agentic-chat/rag/backend. Rebased over the
concurrent 1.31.0 tool_args + 1.32.0 tool-provider merges (took 1.33.0; 1.30.0–1.32.0 already used).

zma: clean-first/slim-first/breaking sweep — every repo had grown its own `utc_now()` (and inline
`datetime.now(timezone.utc)`), dedupe, chunk, normalize-text, `maybe_await`. Consolidate the generic
ones into keble-helpers with full coverage; keep `hash_string` untouched (MD5 cache-key vs SHA256
error-fingerprint are different functions, and a helpers->exceptions cycle blocks sharing).

Ai: added canonical utilities + tests:
- `datetime.py`: `utc_now()` (canonical aware-UTC now) + `ensure_aware_utc(value)` (naive->assume-UTC
  else astimezone; absorbs agentic-chat's private `_as_aware_utc` + keble-task's inline normalize).
- NEW `collections.py`: `dedupe_preserve_order` (hashable identity, from rag+segmenting),
  `dedupe_keep_order` (key/`.key`-attr/identity, from positioning), `chunked` (canonical batch,
  replaces removed `slice_to_list`), `ensure_list` (typed raise), and `DedupeHelpers` (moved verbatim
  from keble-products: keyed/json/string dedupe + `merge_money_ranges` over keble-helpers' `MoneyRange`).
- NEW `text.py`: `normalize_semantic_text` (from segmenting). NEW `async_utils.py`: `maybe_await`
  (from positioning).
- `common.py`: REMOVED `slice_to_list` (zero non-doc callers); `__init__` re-exports the new surface and
  drops `slice_to_list`.
- Tests: `test_datetime.py`, `test_collections.py`, `test_text.py`, `test_async_utils.py`; updated
  `test_import_contract.py` to pin the new exports and assert `slice_to_list` is gone. Full suite: 359 passed.
Consumers repoint to these in their own branches (Phase 2/3); no new dependency edges (all already depend
on keble-helpers; keble-helpers depends on none of them, so no cycle).

## 2026-06-27 (chat tool-provider contract unification) — 1.31.0 -> 1.32.0
State: keble-helpers 1.32.0 — the cross-repo chat tool-provider contract becomes typed and
self-describing (foundation of a coordinated ~10-repo breaking refactor). Rebased over the
concurrent 1.31.0 tool_args helper merge (took 1.32.0; 1.30.0/1.31.0 already used).

zma: The chat tool-provider interface was implicit/ambiguous: `provider_id` was a magic
string, the backend carried a redundant `mount_id`, and `register(*, agent, context)` forced a
dead `context` param (every host passed `None`, every impl `del context`). Clean-first,
slim-first, codebreaking (no compat shim), full coverage.

Ai: tightened the contract at its authority, keble-helpers:
- NEW `ai/chat_tool_provider_id.py::ChatToolProviderId(str, Enum)` — the SINGLE closed,
  cross-repo set of provider ids (task_query, positioning_query/_mutation, positioning,
  segmenting_query, memory, general, client_action, subagent_delegation, keepa_product_query,
  amz_report_query/_setup/_start, guided_create_registry, shuffle_discovery, decision/_mutation/
  _evidence, offload_read, search_image). `(str, Enum)` → wire values unchanged (no Redis/lang
  migration); keble-core mirrors it 1:1.
- `ChatToolProviderProtocol`: `provider_id -> ChatToolProviderId`; `manifest` is now a
  FIRST-CLASS required member (was enforced only at composition time); `register(*, agent)` —
  `context` dropped.
- `ChatToolProviderManifest.provider_id: ChatToolProviderId` (was `str`). `ChatProviderFamily`
  already exported.
- Tests: contract conformance for the agent-only register + required manifest + id↔manifest
  parity; manifest rejects unknown ids + json wire-value stability round-trip. 21 passed.

Side effects (downstream, same coordinated release): keble-agentic-chat `compose_tool_providers`
(drop context + hardening), every product provider package, keble.backend mount layer, keble-core
TS mirror, keble.frontend lang map. Consumers must bump the keble-helpers floor to >=1.32.0.

## 2026-06-27 (canonical agentic-tool id helpers — *_or_model_retry) — 1.30.0 -> 1.31.0
State: keble-helpers 1.31.0 — one drift-free home for model-supplied id validation in
pydantic-ai tools (clean-first, no backward-compat). Superset of the unpublished 1.30.0
"unify agent deps base" commit (44e8cdd); took 1.31.0 to respect that pending version label.

zma: `_parse_object_id` (raw model id -> `ModelRetry` on bad value) is copy-pasted across
keble-positioning / keble-segmenting / keble-task / keble-amz-product-report (with a
copy-paste bug in segmenting: both grid tools hardcoded "grid_id"). Make it a shared
keble-helpers AGENTS-module utility. Core principle: HTTP `ClientSideInvalidParams` /
worker `ServerSideInvalidParams` are correct and stay UNTOUCHED, but mean nothing to an
agent — at an agentic-tool boundary a bad/missing id must surface as `ModelRetry` with a
useful, field-specific message. Both failure modes retry: invalid format, AND valid id
but no object found.

Ai: new `keble_helpers/ai/tool_args.py` exporting two helpers (re-exported via
`ai/__init__.py` -> root `from .ai import *`):
- `parse_object_id_or_model_retry(raw, *, field_name)` — `ObjectId.is_valid` gate, else
  `ModelRetry(f"{field_name} is not a valid id: {raw!r}")`; returns `bson.ObjectId`.
- `require_object_or_model_retry(obj, *, field_name, raw)` — `None` load -> a DISTINCT
  `ModelRetry("no object found for ...")` (so the model can tell malformed-id from
  not-found recovery); else narrows `T | None` -> `T`.
`Side effect if changes` note names all four consuming agent packages. Scope = agent
layer only; no HTTP/worker code touched. Tests: `tests/test_ai_tool_args.py` — valid
parse, malformed -> ModelRetry (field+value+"not a valid id"), passthrough, None ->
not-found ModelRetry asserting the two messages never collide. 4 passed.
## 2026-06-27 (usage-accounting metadata: covariant Mapping input contract) — 1.29.0 -> 1.29.1
State: keble-helpers 1.29.1 — fix the dict-invariance type error at the source of the contract.

zma: Pylance error in keble-segmenting — `dict[str, str]` not assignable to `metadata` of
`UsageAccountingEvent.from_result_usage` (`UsageAccountingMetadata = dict[str, UsageAccountingMetadataValue]`,
and `dict` is invariant in its value type). Fix clean-first/slim-first at the root, no backward-compat
shim, with full coverage. Evidence the fix belongs here: backend already carried a `cast(Any, ...)`
workaround for this exact invariance.

Ai: split the input contract from the stored shape in `keble_helpers/ai/usage_accounting.py`:
- `UsageAccountingMetadata` is now a covariant `Mapping[str, UsageAccountingMetadataValue]` (input alias),
  so caller-typed `dict[str, str]` variables assign cleanly. (Pylance itself recommends Mapping here.)
- `UsageAccountingEvent.metadata` stays a concrete `dict[str, UsageAccountingMetadataValue]` (stored shape).
- New `UsageAccountingEvent.build_metadata(metadata)` staticmethod normalizes the optional input Mapping
  into the concrete stored dict (None -> {}, else `dict(metadata)`); the two `metadata or {}` sites in
  `from_run_usage` / `from_unit_count` now call it. `from_result_usage` / `from_request_usage` delegate.
- Added `Side effect if changes:` note naming all consumers. Consumers keble-keepa / keble-keepa-analysis /
  keble-graphrag need no change/rebuild (Mapping is strictly wider than dict, source-compatible).
- Tests: new dict[str,str]-variable covariance case (statically locks the contract), non-str value
  round-trip, and `build_metadata(None) == {}` + detached-copy assertions. 11 passed.

## 2026-06-25 (embedding batch-cap helper — generic items) — 1.28.0 -> 1.29.0
State: extend aembed_in_batches to image embedding (same provider cap).

zma: the image embedding path is still unbatched (graphrag image branch + backend aembed_images)
and can hit the SAME embed-v-4-0 96 cap on image-heavy studies. Reuse the batching helper for images.

Ai: generalized `aembed_in_batches` element type from `Sequence[str]` to `Sequence[T]` (generic) so it
batches text OR image inputs (`str | AnyHttpUrl`) through the identical cap contract — backward
compatible (kept the `texts` keyword; str callers unchanged). Existing unit tests still pass.

## 2026-06-25 (embedding batch-cap helper) — 1.27.0 -> 1.28.0
State: keble-helpers 1.28.0 — adds the canonical batched-embedding helper.

zma: Production crash on /positioning — Azure Cohere `embed-v-4-0` rejects >96 texts/request
with a permanent 400 ("total number of texts must be at most 96 - received 97"). No batching
existed anywhere in the embedding path. zma wants ONE shared, tested batching helper that all
list-embed call sites (chat memory store, GraphRAG, RAG ingest) route through.

Ai: Added `keble_helpers/ai/embedding_batching.py::aembed_in_batches(texts, batch_size,
aembed_chunk)` — embedder-agnostic (takes an async chunk-embedder callable, so the package keeps
NO pydantic-ai/vendor dependency). It splits `texts` into contiguous <= `batch_size` windows,
awaits the chunk embedder per window IN ORDER, and concatenates so result[i] maps to texts[i];
returns [] for empty input and asserts a positive cap. Exported from `keble_helpers.ai` and the
package root. Unit tests cover empty / 1 / exactly-cap / cap+1 (the 97 case) / 200 / non-positive
cap, asserting no chunk ever exceeds the cap and order is preserved.

## 2026-06-24 (image guard -> keep URL, not bytes)
State: keble-helpers 1.27.0 — light keep-URL guard counterpart to the 1.26.0 bytes path

zma: The 1.26.0 bytes rollout over-corrected a TRANSIENT image-download episode. Inlining every
ImageUrl as `BinaryContent` made the dimension-discovery request structurally HEAVY (~45 product
images in one structured-output call), so the model call now dies with a generic
`openai.APITimeoutError("Request timed out.")` — which is NOT in `is_image_url_model_recoverable_error`,
so the strip-and-retry net never fires → positioning hangs. The gateway is healthy for light calls.
Owner direction: validate the image, but if it PASSES the guard keep it as a URL (light), never bytes.

Ai: added module-level `aguard_image_prompt_urls(prompt, *, checker=None)` in `ai/image_runtime.py`
(exported from `ai/__init__` + package root). It is the LIGHT counterpart to
`arealize_image_prompt_urls`: it reuses the EXISTING `ImagePromptChecker.afilter_prompt_images`
(reachability preflight + provider-safe image-count budget) and returns the prompt with only HEALTHY
images KEPT AS `ImageUrl` (bad/over-budget dropped) — zero `BinaryContent`. With no checker a shared
`enabled=False` default applies only the count budget (no network probe). The provider still
downloads the kept URLs; a slow URL that times out is recovered text-only by the existing
`arun_with_image_fallback` (its matcher already catches "downloading image"). Strengthened the
`arealize_image_prompt_urls` docstring to mark it the heavy bytes path and point LLM URL paths at the
guard (prevents a future agent re-introducing the regression). 3 new tests (reachability keep/drop,
budget cap no-network, package-root export) + 25 existing image-runtime tests green. No new dependency.

## 2026-06-24 (image prefetch -> BinaryContent)
State: keble-helpers 1.26.0 — multimodal prompts can now send image BYTES, not URLs

zma: Fix the live "stuck positioning" bug at its root — keble-segmenting was feeding raw
Amazon-CDN `ImageUrl` to the model, OpenAI downloaded them server-side and TIMED OUT
(`ModelHTTPError 400 "Timed out while downloading image ..."`), poisoning dimension discovery.
The owner's established fashion is to prefetch image bytes and send `BinaryContent` message-by-
message (keble-amz `adownload_image` + gather-degrade). keble-helpers owns the reusable primitive.

Ai: added `ImagePromptChecker.arealize_prompt_images()` + module-level `arealize_image_prompt_urls()`
in `ai/image_runtime.py`: walk a prompt list, fetch each `ImageUrl` to bytes in-memory (aiohttp +
tenacity retry, loop-local semaphore, image-count budget reused from preflight), swap to
`BinaryContent(data, media_type)` (media type from response Content-Type, else URL extension, else
image/jpeg), and DEGRADE (drop) any URL that fails/times out via `gather(return_exceptions=True)` —
never raises on one bad image. Also extended `is_image_url_model_recoverable_error` to match the
provider "downloading image" timeout shape so the existing strip-and-retry net is a real fallback.
New `image_fetch_timeout_secs` (default 10s) on `ImagePromptChecker`. Exported the new helper. 5 new
tests (real local HTTP server: realize+order, degrade-drop, count budget, passthrough/empty,
classifier) + 25 existing image-runtime tests still green. No new dependency (aiohttp already a dep).

## 2026-06-20 (finalize merge)
State: keble-helpers 1.25.0 — Phase 1A merged to main + tagged

zma (continuation worker): finalize the Phase 1A merge — branch `feature/generic-agentic-memory` was fast-forwardable; verify the ff brings only the intended `ai:` memory commits, merge to main in an isolated worktree, push the annotated tag (1.25.0 already on PyPI — do not re-publish).

Ai: synced the dangling `uv.lock` 1.24.0->1.25.0 entry (committed `0f54d6d`, the bump's lockfile half was left unstaged), verified the fast-forward from `origin/main` brings ONLY the two `ai:` memory commits (`623fd9a` contract + `0f54d6d` lock), fast-forwarded `origin/main` to `0f54d6d`, and created+pushed annotated tag `keble-helpers-v1.25.0` -> `623fd9a`. Did NOT re-publish (1.25.0 already on PyPI). Remote main + tag confirmed via `git ls-remote`.

## 2026-06-20 15:08
State: keble-helpers 1.25.0 — generic agentic three-store memory contract (Track 1 Phase 1A)

zma: Track 1 (FOUNDATION, interface-first) of the generic-agentic-memory plan. Phase 1A keble-helpers: additively extend the framework-neutral chat-memory contract so backend can evolve its store into a Qdrant+Mongo+Neo4j multi-store and discovery/niche can recall shared cross-owner conclusions, WITHOUT breaking the 7 existing chat-memory tests. Publish before Track 2 codes against it.

Ai: Additive to `keble_helpers.ai.chat_memory`:
- New `MemoryLinkKind` (REPORT|MEMORY|OTHER) and `MemoryLinkRole` (FINAL|SUBMARKET|WRONG_CONFIG) UPPERCASE enums + `MemoryLink{kind, ref_id, role}` typed provenance edge (camelCase wire aliases). All exported from the package root.
- `ChatMemoryRecord` gains optional `owner: str|None=None`, `links: list[MemoryLink]|None=None`, `parent_memory_id: str|None=None` (defaults preserve every existing constructor).
- `ChatMemoryStoreProtocol.arecall` gains keyword-only DEFAULTED filters `kinds`, `since`, `until`, `include_shared=False`, so existing chat call sites compile unchanged and chat recall stays owner-private; only opt-in callers get shared/time/kind filtering.
- New protocol method `aupdate(*, owner, scope_type, memory_id, text=None, metadata=None) -> bool` (the mutation half of the agentic `update_memory` tool; owner+scope gated).
Bumped metadata to 1.25.0, built + published to PyPI via `uv publish`, and bundled the wheel into `keble.backend/dependencies`. Tests: 13 passed (extended conformance stores with the new params/aupdate + new additive-field, enum, alias, recall-filter, and aupdate-behavior cases).

## 2026-06-12 21:16
State: keble-helpers 1.24.0 — Phase 3 framework-neutral subagent primitives

zma: Execute Round-4 subagent architecture plan, Phase 3 helpers: add subagent archetype contracts, remove the old background-session provider family, and expose the shared primitives.

Ai: Added `keble_helpers.ai.subagent` with typed archetype kind/mode/outcome enums, escalation policy, budgets, tool scope, `DelegationBrief`, archetype/provider manifests, and the provider protocol. `SubAgentArchetypeDescriptor` rejects SYNC archetypes that can escalate; `SubAgentProviderManifest.assert_covers` detects both declaration drift directions. Replaced `ChatProviderFamily.BACKGROUND_SESSION` with `SUB_AGENT`, exported the new contracts, bumped package metadata to 1.24.0, and documented the coordinated breaking enum change.

## 2026-06-12 20:33
State: keble-helpers 1.23.0 — Phase 1 evidence dedupe identity

zma: Execute Round-4 subagent architecture plan, Phase 1 first: add shared evidence canonical key so repeated report chips dedupe by artifact, not label.

Ai: Added `AgenticEvidenceItem.canonical_key()` using `kind|url|sorted(ref)` while excluding display `label`; added tests for relabeled same-ref equality and different-ref inequality. Bumped package metadata/lock to 1.23.0 and documented the evidence identity contract.

## 2026-06-12 (Round 3 Phase 1)
State: keble-helpers 1.21.0 — provider-scoped manifest family

zma: Round-3 composition hard contract — diagnostics/composition records need to group providers structurally without string-matching ids.

Ai: `ChatProviderFamily(str, Enum)` (DOMAIN_TOOL | GENERAL | CLIENT_ACTION | BACKGROUND_SESSION) + additive `ChatToolProviderManifest.family` (defaults DOMAIN_TOOL for existing manifests). Exported. Tests: default + JSON round-trip.

## 2026-06-12 13:10
State: 1.20.0 — typed agentic evidence + tree-rollup usage dimension

zma: Round-2 program Phase 3 (evidence chips on tool cards) + Phase 5 cost rollup.

Ai: NEW `keble_helpers/ai/evidence.py`: `AgenticEvidenceKind` (LINK/PRODUCT/DOCUMENT/FILE) + `AgenticEvidenceItem` (optional safe label, url, compact `ref` domain keys). `AgenticClientActionBase.evidence: list[AgenticEvidenceItem]` (additive, defaults empty — legacy payloads validate unchanged) so every host action persists/streams chips with zero new wire surfaces. `UsageAccountingTagSet.root_task_id` (additive dimension, renders/parses like the others) for tree-level "what did this report cost" rollups via the existing TAG aggregation. Tests: 300 passed (round-trip, additive default, dimension round-trip).

## 2026-06-11 (feature/server-tool-call-kind) — 1.17.0: AgenticActionKind.SERVER_TOOL_CALL
State: `1.16.0 -> 1.17.0`.

zma: testing the /chat console — backend agentic tool calls are invisible in the room (only browser client tools render). Decision: each auto-executed backend tool call becomes its own persisted ACTION timeline card, so the shared kind enum needs a fourth family.

Ai: added `AgenticActionKind.SERVER_TOOL_CALL` (auto-executed backend tool call recorded for the visible timeline; created directly terminal SUCCEEDED/FAILED, never pending, never resolvable) with docstring step 4 and README family list update. New `test_server_tool_call_kind_is_available` builds a terminal `AgenticClientActionBase` of the new kind. No schema/field changes — purely additive enum member consumed by keble-agentic-chat 2.9.0 (capture), keble-core 0.30.0 (TS mirror), keble.backend (ws mapping), keble.frontend (localized cards). Suite 287 passed.

## 2026-06-10 (feature/chat-memory-contract) — 1.16.0: chat memory contracts promoted from keble-agentic-chat
State: `1.15.0 -> 1.16.0`.

zma: investigated two outside agents' audit advice; among the valid points, `ChatMemoryRecord`/`ChatMemoryStore` were still package-local in keble-agentic-chat (its own docstring promised promotion to keble-helpers "in a later release"). Decision: promote the EXISTING shape faithfully (no field rewrite), add a well-known-kinds enum, and document the locked recall-scoping semantics (cross-chat per owner+scope_type; scope_id/chat_id are provenance-only).

Ai: added `keble_helpers/ai/chat_memory.py` — `ChatMemoryKind(str, Enum)` (EPISODE/FACT/PREFERENCE/SUMMARY well-known kinds; `ChatMemoryRecord.kind` stays `str` so hosts can define custom kinds), `ChatMemoryRecord` (faithful promotion of the keble-agentic-chat shape: memory_id/kind/text/score/metadata/created_at, camelCase aliases via `PydanticModelConfig.default(populate_by_name=True)`, plus an `episode(...)` classmethod replacing free-string `kind="EPISODE"` constructions), and `@runtime_checkable ChatMemoryStoreProtocol` (`arecall`/`aremember` signatures copied EXACTLY from the engine's `ChatMemoryStore`). Exported via `ai/__init__.py` → package root. README gains a "Chat memory contracts" cross-repo-standard section documenting the locked scoping contract. New `tests/test_ai_chat_memory.py` (7 tests: root export, enum+custom kind, episode classmethod, camelCase wire shape, runtime-checkable conformance incl. async round-trip, negative case, no langgraph/keble_agentic_chat import leak). Full suite 290 passed. keble-agentic-chat 2.7.0 re-exports these so existing import paths keep working.

## 2026-06-09 (feature/agentic-chat-tool-provider) — 1.15.0: ChatToolProviderProtocol
State: `1.14.0 -> 1.15.0`.

zma: after reviewing two outside agents' advice (mostly stale parked-checkout reads), the one valuable + genuinely-absent thing is a seam for any domain to "offer its tools" to the new `AgenticChat` chat scopes. Backend currently hardcodes each domain's `register_*_tools` inline per scope builder. Decision: tools-only seam via backend thin adapters; keble-helpers holds the framework-neutral INTERFACE next to `ChatScopeRuntimeProtocol`.

Ai: added `keble_helpers/ai/chat_tool_provider.py::ChatToolProviderProtocol` — a `@runtime_checkable` framework-neutral Protocol for "a domain tool provider for one chat scope": `provider_id: str` (stable id for logs/room diagnostics) + `register(*, agent: Any, context: Any) -> None` (attach the domain's tools to the scope agent). All types `Any` so no LangGraph/pydantic-ai leak — keble-helpers stays dependency-free; the canonical compose-handler is `keble_agentic_chat.compose_tool_providers` (2.4.0). Exported via `ai/__init__.py` (picked up by the package-root `from .ai import *`). README documents it as the cross-repo standard (deps/data stay explicit per scope; only tool composition is unified). New `tests/test_ai_chat_tool_provider.py` proves export, runtime-checkable conformance, register wiring, the negative case, and that the neutral module pulls in neither `langgraph` nor `keble_agentic_chat`.

## 2026-06-09 (feature/agentic-chat-scope-protocol) — 1.14.0: ChatScopeRuntimeProtocol
State: `1.13.0 -> 1.14.0`.

zma: introduce a clean package-owned `AgenticChat` (in keble-agentic-chat) as the single source of truth for agentic-chat + LangGraph integration (owns store + run-control + per-turn runtime factory); keble-helpers should hold the framework-neutral INTERFACE so other agents can respect the contract without depending on keble-agentic-chat. keble-helpers is public — publish via uv.

Ai: added `keble_helpers/ai/chat_scope.py::ChatScopeRuntimeProtocol` — a `@runtime_checkable` framework-neutral Protocol for "a per-scope agentic-chat runtime holder": `store` (Any — the durable chat-history store) + `async arequest_interrupt(*, owner, scope_type, scope_id, chat_id, reason=None) -> bool` (cooperative stop). No LangGraph/pydantic-ai types leak (store/chat_id are `Any`), so keble-helpers stays dependency-free; the concrete impl is `keble_agentic_chat.AgenticChat`. Exported via `ai/__init__.py` (picked up by the package-root `from .ai import *`). README documents it as the cross-repo standard future agentic-chat hosts must respect. New `tests/test_ai_chat_scope.py` proves export + runtime-checkable conformance (+ negative case). pyright 0 errors; ai test subset green.

## 2026-06-07 (feature/unified-agentic-action-contract)
State: branch `feature/unified-agentic-action-contract`; `keble-helpers` `1.12.18` -> `1.13.0`.

zma:
- Client-side tool actions were NOT unified onto the keble-helpers agentic base: `keble_agentic_chat.ChatAction` inherited only `ChatValueBase`, and `ChatActionStatus` / `ChatActionProgress` duplicated the shared event status / progress shapes. Raising a client tool (or a server approval, or a self-served subagent decision) is still an action and must ride one base.
- Decision: Shared base + unify enums (collapse `ChatActionStatus` into the shared status enum), accepting a coordinated breaking change across packages, keble-core, and frontend.

Ai:
- `ai/events.py`: replaced the 4-member `AgenticActionEventStatus` with a single superset `AgenticActionStatus(str, Enum)` carrying both lifecycle (`STARTED`/`PROGRESSED`/`SUCCEEDED`/`FAILED`) and resolution (`PENDING`/`APPROVED`/`DENIED`/`SUBMITTED`/`REJECTED`/`ABANDONED`) members; kept `AgenticActionEventStatus = AgenticActionStatus` as a backward alias so every existing `AgenticActionEventStatus.*` reference and the `started/progressed/succeeded/failed` factories keep working. Added `KEBLE_AGENTIC_CHAT` and `KEBLE_BACKEND` to `AgenticActionEventSource`.
- New `ai/client_action.py`: canonical package-neutral contract — `AgenticActionKind` (`SERVER_TOOL_APPROVAL`/`CLIENT_TOOL`/`SERVER_SELF_SERVED`), `AgenticActionProgress` (renamed home of `ChatActionProgress`, keeps `.queued()`/`.to_action_status()`), and `AgenticClientActionBase` (the base `keble_agentic_chat.ChatAction` will inherit).
- Exported the new names via `ai/__init__.py` (picked up by top-level `from .ai import *`).
- Tests: added `tests/test_ai_client_action.py` (status alias identity, client-action validation, unknown-status rejection, self-served kind, progress→status mapping). `tests/test_ai_events.py` still green. 11 passed.
- README: added "Unified Agentic Action Contract" section and noted the `AgenticActionEventStatus` alias.



zma:
- Unify agentic event/action/progress handling across packages onto the existing keble-helpers base.
- keble-helpers should own the unified interfaces/schemas (Base for inheritance) so task, segmenting, positioning, and amz-product-report all utilize them at once.

Ai:
- `ai/events.py`: added `AgenticActionEventSource(str, Enum)` (KEBLE_TASK / KEBLE_SEGMENTING / KEBLE_POSITIONING / KEBLE_AMZ_PRODUCT_REPORT) as the single source of truth for the emitter identity; retyped `AgenticActionEvent.source` from a free `str` to this enum (str-enum so the wire string is unchanged).
- Added inheritable lifecycle `cls` factories `AgenticActionEvent.started/progressed/succeeded/failed(...)` (delegating to a private `_build`) so packages stop hand-building envelopes and inheriting wrong status defaults.
- Exported `AgenticActionEventSource` via `ai/__init__.py`; updated `tests/test_ai_events.py` (enum source, unknown-string rejection, factory status correctness); README event section refreshed.
- Built `dist/keble_helpers-1.12.18-py3-none-any.whl`; PyPI publish remains credential-blocked, so consumers keep using the bundled wheel via `keble.backend/dependencies` + `requirements.keble.txt`.

## 2026-05-23 10:10 CST
State: branch `hotfix/version-recent-changes`; `keble-helpers` line moving from `1.12.9` to `1.12.10`.

zma:
- Version all recently changed Keble repos, frontend, backend, and production with patch bumps and annotated tags.
- Build and publish public packages where allowed, but do not accidentally publish classified repos.

Ai:
- Bumped `keble-helpers` metadata to `1.12.10` so the helper publication-blocker documentation has a package-scoped release tag.
- Built `dist/keble_helpers-1.12.10-py3-none-any.whl`; `uv publish` failed because no PyPI credentials or OIDC trusted-publishing token are available in this shell.
- Kept helper runtime schemas unchanged; service repos must keep consuming bundled wheels until credentialed publication succeeds.

## 2026-05-10 19:37 CST
State: branch `fix/agent-base-deps-release`; code unchanged, release publication still externally blocked.

zma:
- Implement the follow-up cleanup plan for `AgentBaseDeps` release handling.
- Do not leave the helper publication gap implicit.

Ai:
- Rebuilt `keble-helpers 1.12.9` with `uv build` and confirmed the wheel artifact is valid.
- Checked the configured package index; visible `keble-helpers` releases still stop at `1.12.1`.
- Retried `uv publish dist/keble_helpers-1.12.9-py3-none-any.whl`; publish failed because no PyPI credentials or OIDC trusted-publishing token are available in this shell.
- Documented the blocker in README; backend must keep using the bundled `1.12.9` wheel until credentialed publication happens.

## 2026-05-10 18:38 CST
State: branch `fix/bootstrap-agent-base-deps`; `keble-helpers` bumped to `1.12.9`.

zma:
- Add shared agent base deps with marketplace and language for cross-package agent runtimes.

Ai:
- Added `AgentBaseDeps`, root exports, README documentation, focused tests, and built `dist/keble_helpers-1.12.9-py3-none-any.whl`.
- PyPI publish was attempted but blocked by missing credentials in this environment.

# AiAgentChangesLogs

````markdown
Update `AiAgentChangesLogs.md` in project root directory on each completion. Include timestamp, project state, my prompt and your changes. Be simple when you are simply fixing a small bug. Be concise and conclusive when you are updating large portion of the codebase. Sample format: 
```markdown
## 2026-01-01 14:00
State: <the state of the project linked to this section>

<My github username>: <a summary of my prompt, shorten it if too long>
<!-- you may want to use git to check my git username, since there are multiple people working in the same branch -->

Ai: <a summary on what have you changed? Especially the mechanism, dependencies, schemas, etc...>
```
````

## 2026-05-01 13:00 CST
State: branch `fix/image-prompt-invalid-format`; `keble-helpers` bumped to `1.12.7`.

zma:
- investigate invalid image failures in deep-analysis worker runs
- reuse the shared `ImagePromptChecker` instead of backend-local validation
- support provider `invalid_image_format` fallback and stricter model image format preflight

Ai:
- extended image preflight with supported MIME/extension policy and `UNSUPPORTED_FORMAT`
- captured response `Content-Type` in `ImagePreflightDecision`
- added broad recoverable image-provider error matching for invalid/unsupported image messages
- updated image runtime regressions and rebuilt the helper wheel for backend consumption

## 2026-04-23 13:59 CST
State: branch `refactor/milestone-rebaseline-docs`; `keble-helpers` improve docs now define helpers as shared schemas/protocols only.

zma:
- Implement the milestone rebaseline plan.
- Clarify helpers should not own backend queue dispatch or publication policy.

Ai:
- prepended `improve.README.md` with the current helper contract source of truth
- documented helpers as owners of queue-neutral envelope/protocol schemas, action events, and `.build(...)` primitives
- clarified Celery app, ledger persistence, retries, and enqueue policy stay in backend
- left code unchanged

## 2026-04-23 11:44 CST
State: branch `refactor/agent-deps-build-progress`; `keble-helpers 1.12.6` replaces registrar/callback normalizers with schema-owned builds.

zma:
- Replace the recent `normalize_*` tool-config helper pattern with compact `Class.build(...)` schemas.
- Keep helpers from growing top-level utility functions for every registrar config.
- Apply the same shape to callback emitter construction.

Ai:
- replaced `normalize_agent_tool_config(...)` with `AgentToolConfig.build(...)`
- replaced `normalize_agentic_event_emitter(...)` with `AgenticEventEmitter.build(...)`
- removed the normalizers from helper exports
- updated README, improve plan, and tests to use schema-owned construction
- bumped manifests to `1.12.6`
- verification:
  - `uv run pytest tests/test_ai_tools.py tests/test_ai_events.py -q` -> `8 passed`
  - `uv run pytest tests -q` -> `246 passed, 2 failed` because `tiktoken`
    attempted to fetch `cl100k_base.tiktoken` and the proxy/remote connection
    returned chunked/SSL EOF errors
  - `uv build --wheel` -> `dist/keble_helpers-1.12.6-py3-none-any.whl`

## 2026-04-23 09:35 CST
State: branch `feature/agentic-tool-registrars`; `keble-helpers 1.12.5` starts the package-owned pydantic-ai tool registrar cleanup.

zma:
- Implement the agentic tool layer cleanup plan.
- Move shared pydantic-ai tool metadata to helpers.
- Keep package registrars, not backend, responsible for tool definitions.

Ai:
- added `keble_helpers.ai.tools` with `AgentToolConfig` and `normalize_agent_tool_config(...)`
- exported the shared tool config through `keble_helpers.ai` and package root
- documented the helper-owned tool metadata boundary and prepended the improve plan
- bumped manifests to `1.12.5`
- verification:
  - `uv run pytest tests/test_ai_tools.py tests/test_ai_events.py -q` -> `7 passed`
  - `uv build --wheel` -> `dist/keble_helpers-1.12.5-py3-none-any.whl`

## 2026-04-22 23:33 CST
State: branch `feature/shared-agentic-events`; `keble-helpers 1.12.4` owns shared agentic action event primitives.

zma:
- Implement the shared event protocol needed by task, segmenting, positioning, and backend.
- Keep it package-neutral and avoid callback swallowing.

Ai:
- added `keble_helpers.ai.events` with `AgenticActionEvent`, `AgenticActionEventStatus`, `AgenticEventEmitter`, callback type, and normalizer
- exported the event primitives through `keble_helpers.ai` and package root
- documented the helper event contract and prepended the improve plan
- bumped `pyproject.toml` and `pyproject.poetry.toml` to `1.12.4`
- built `dist/keble_helpers-1.12.4-py3-none-any.whl`
- verification:
  - `uv run pytest tests/test_ai_events.py -q` -> `4 passed`
  - `uv run pytest tests/test_ai_events.py tests/test_queued.py -q` -> `7 passed`
  - `uv build --wheel`
  - `python -m py_compile keble_helpers/ai/events.py`

## 2026-03-20 13:41 CST
State: branch `keble-helpers-1.12.1` is code-complete, pushed, and build-verified; publish was attempted from this environment but PyPI rejected it because no valid authentication was configured.

zma:
- continue until the helper release is actually done
- do not stop before the publish/release path is resolved

Ai:
- Verified the pushed `keble-helpers-1.12.1` branch still builds cleanly and checked both `uv publish` and `poetry publish` paths from the current environment.
- Confirmed the real remaining blocker is publish authentication, not packaging: `uv publish --dry-run dist/*` reported missing credentials / no trusted-publishing token, and `uvx --from poetry poetry publish --no-interaction` failed with PyPI `HTTP Error 403: Invalid or non-existent authentication information`.
- Left the code/build state unchanged after the failed upload attempt; the branch, commit, and built artifacts remain ready for a credentialed publish.

## 2026-03-20 10:16 CST
State: branch `keble-helpers-1.12.1`; helper-owned `pydantic-ai` wrapper typing is now additive, runtime-neutral, and release-built with `uv`.

zma:
- examine `keble-helpers` `arun_with_image_fallback` / `ImagePromptChecker`
- use `TypeVar` for agent wrapper functions and tools so response typing is more certain
- bump git repo branch and package version so the helper can be built/published

Ai:
- Re-typed `keble_helpers.ai.image_runtime` around a generic `SupportsAgentRun[...]` protocol so both `arun_with_image_fallback(...)` entrypoints now return concrete `AgentRunResult[OutputT]` instead of `Any`, without changing the preflight/retry behavior.
- Added additive `typed_tool(...)` and `typed_tool_plain(...)` helpers in `keble_helpers.ai.tooling` to preserve decorated callable typing while still registering tools through `pydantic-ai`.
- Exported the new helpers through `keble_helpers.ai` and root package imports, added focused tests in `tests/test_ai_tooling.py`, updated README usage docs, and aligned both manifests to `1.12.1`.
- Validation: `./.venv/bin/pytest -q tests/test_ai_image_runtime.py tests/test_ai_agent.py tests/test_ai_helpers.py tests/test_ai_tooling.py` -> `30 passed`; `uv build --out-dir dist` produced `keble_helpers-1.12.1.tar.gz` and `keble_helpers-1.12.1-py3-none-any.whl`.

## 2026-02-10 18:10
State: Added implementation and tests for Amazon product URL parsing in enum typings; no existing change log file was present, so this file was initialized.

zhenhao-ma: complete my parse_amazon_product_url function in AmazonMarketplace enum

Ai: Implemented `AmazonMarketplace.parse_amazon_product_url` with strict parsing for supported Amazon domains and common product URL path formats (`/dp/`, `/gp/product/`, `/gp/aw/d/`, `/exec/obidos/asin/`, `/product/`). Added new parser tests for valid and invalid URLs in `tests/test_typings/test_typing.py`. Verified with pytest (`9 parser tests passed`, `98 total typing tests passed`).

## 2026-02-10 18:16
State: Expanded Amazon product URL parser coverage and strengthened normalization behavior for domain/path cases.

zhenhao-ma: further update my functions based on your finding. and makesure you can handle domain with or without www. and case insensitively. and ensure returned asin are all in uppercase

Ai: Updated `AmazonMarketplace.parse_amazon_product_url` to improve real-world URL coverage and normalization. Domain matching now explicitly normalizes host case and strips repeated `www.` prefixes, then resolves marketplaces with length-sorted domain matching to avoid subset collisions (such as `amazon.com` vs `amazon.com.mx`). Expanded supported URL patterns (`/gp/aw/dp/`, `/gp/dp/`, `/o/asin/`, `/product-reviews/`, `/gp/offer-listing/`, `/gp/customer-reviews/`) and added a query fallback for `asin=`. ASIN output remains normalized to uppercase. Added new tests for mixed-case URLs, with/without `www`, subset domains, and added path/query variants. Verified with pytest (`15 parser tests passed`, `104 total typing tests passed`).

## 2026-02-10 20:40
State: Added AI prompt URL extraction helpers with image/webpage classification and dedicated test coverage.

zhenhao-ma: did my lib offers extract url extracter from prompt. since users imput prompt can contain different types of url (such as image or webpage), if there is none, add a function to ai/helpers.py

Ai: Added `extract_urls_from_prompt` in `keble_helpers/ai/helpers.py` and a typed schema `PromptUrlExtractionResult` (`all_urls`, `image_urls`, `webpage_urls`). Implemented URL parsing with deduplication, trailing punctuation cleanup, HTTP/HTTPS validation, and `is_image_url` classification by extension. Exported new helpers via `keble_helpers/ai/__init__.py`. Added tests in `tests/test_ai_helpers.py` and verified with pytest (`11 passed` across AI helper/agent/token-related tests).

## 2026-03-05 13:01
State: Global helper runtime loop-safety fix + multimodal resilience contract finalized.

zhenhao-ma: Replace local image preflight copies with shared helper runtime and fix persistent runtime instability.

Ai:
- Added loop-safe semaphore handling in `keble_helpers/ai/image_runtime.py` (semaphore is now event-loop-local to prevent cross-loop future errors).
- Added regression test `test_preflight_runtime_is_loop_safe_for_reuse`.
- Kept typed preflight + model image-404 fallback retry contract with `tenacity(reraise=True)`.
- Updated package version `1.11.4 -> 1.11.5` and rebuilt wheel.
- Updated `README.md` with a dedicated "Pydantic AI image runtime" section and usage contract.
- Validation:
  - `tests/test_ai_image_runtime.py` -> 8 passed.

## 2026-03-05 13:41
State: Helper image runtime cache hardening follow-up.

zhenhao-ma: Address reviewer concern on potential unbounded preflight cache growth.

Ai:
- Added bounded cache maintenance in `keble_helpers/ai/image_runtime.py`:
  - evict expired entries on each cache access,
  - cap cache size and evict oldest entry at capacity.
- Added test `test_preflight_runtime_evicts_expired_cache_entries`.
- Bumped package version `1.11.5 -> 1.11.6` and rebuilt wheel.
- Validation: `tests/test_ai_image_runtime.py` -> 9 passed.

## 2026-03-06 09:18
State: Configurable HTTP status policy for image preflight + release `1.11.7`.

zhenhao-ma: Make image preflight status filtering configurable (`only_accepts` / `rejects`), enforce mutually exclusive config, keep default reject behavior, then release and propagate strict status policy usage to consumers.

Ai:
- Updated `ImagePromptPreflightRuntime` in `keble_helpers/ai/image_runtime.py`:
  - added `only_accepts: list[int] | None`,
  - added `rejects: list[int] | None`,
  - raises `ValueError` when both are provided,
  - defaults to `rejects=[404]` when neither is provided.
- Wired status policy into preflight decision evaluation.
- Added test coverage in `tests/test_ai_image_runtime.py` for:
  - default policy behavior,
  - `only_accepts` behavior,
  - `rejects` behavior,
  - mutual-exclusion validation.
- Updated helper README usage docs for the new status-policy contract.
- Added `pytest-asyncio` to test dependencies and refreshed lockfile.
- Bumped version `1.11.6 -> 1.11.7`.
- Validation: `poetry run pytest -q` -> `228 passed`.
- Build + publish: `poetry build`, `poetry publish` completed for `keble-helpers 1.11.7`.
## 2026-03-06 09:22
State: Release hardening for strict status policy correctness on redirecting image URLs.

zhenhao-ma:
- Keep preflight status policy configurable and make strict acceptance reliable for consumer runtimes.

Ai:
- Updated preflight HTTP probe to avoid auto-following redirects, so 3xx status codes are evaluated by policy directly.
- Added redirect-focused regression test to ensure `only_accepts=[200,206]` rejects redirected URLs while permissive default still allows non-404 statuses.
- Updated README to document redirect-handling behavior.
- Bumped version to `1.11.8` for release.

## 2026-03-06 14:42 CST
State: branch keble-helpers-1.11.9

zhenhao-ma: continue issue.README investigation; centralize image prompt checker ownership in backend; remove misleading helper/runtime split; verify with pytest; append the new findings to updated.README.md.

Ai: Renamed the public helper concept to `ImagePromptChecker`, removed the legacy public `ImagePromptPreflightRuntime` surface, collapsed `arun_with_image_fallback(...)` to a single checker-owned policy path, added regression coverage for checker retry ownership, updated README, and bumped the package to `1.11.9`.

## 2026-03-07 17:41 CST
State: branch `keble-helpers-1.11.10`; helpers own canonical shared `CommerceEntityType`.

zhenhao-ma:
- Investigate the review feedback about the missing Phase 3 helper extraction.
- If correct, move canonical `CommerceEntityType` into helpers and align downstream repos.

Ai:
- Added full-superset `CommerceEntityType` to helper typings, exported it through `keble_helpers.typings` and root package imports, added README/test coverage, and bumped the package to `1.11.10`.

## 2026-03-07 18:26 CST
State: branch `keble-helpers-1.11.11`; helper-owned `CommerceEntityType` keeps acronym-preserving title formatting.

zhenhao-ma:
- Investigate the latest review feedback.
- Fix helper acronym formatting regression after the shared enum migration.
- Continue the positioning follow-up phases from the current landed state.

Ai:
- Replaced the plain title-casing implementation of `CommerceEntityType.upper_snake_to_title()` with acronym-aware formatting so `SKU` remains uppercase.
- Added regression coverage for `SKU` and `BRAND_MENTION`, updated README guidance, and bumped the package to `1.11.11`.

## 2026-04-21 12:45 CST
State: branch `feature/universal-queued-envelope`; helper package now owns the shared queue-neutral envelope/protocol contract and version `1.12.3`.

zma:
- before backend implementation, add the relevant improve plan for the shared queue contract
- support a multi-purpose broker design that can later carry non-task package jobs

Ai:
- added `keble_helpers.queued` with `QueuedEnvelope`, `QueuedEnvelopeProcessContext`, `QueuedEnvelopeProcessResult`, and `QueuedEnvelopeProcessorProtocol`
- exported the new types from `keble_helpers`
- documented backend-owned dispatch versus package-owned payload validation in README
- added tests for JSON serialization, result builders, and a protocol-compatible fake processor
- bumped `pyproject.toml` and `pyproject.poetry.toml` to `1.12.3`, refreshed `uv.lock`, and built `dist/keble_helpers-1.12.3-py3-none-any.whl`
- verification:
  - `uv run pytest tests/test_queued.py -q` -> `3 passed`
  - `uv build --wheel` -> built `keble_helpers-1.12.3-py3-none-any.whl`

## 2026-04-21 13:09 CST
State: branch `feature/universal-queued-envelope`; corrected the `keble-py` monorepo tag for helper release `1.12.3`.

zma:
- retag the helper release correctly because `keble-py` hosts multiple packages
- provide a short future-agent prompt so monorepo tags are not created incorrectly again

Ai:
- created the package-scoped annotated tag `keble-helpers-v1.12.3` on commit `84f6f5f`
- pushed `keble-helpers-v1.12.3` to origin
- deleted the incorrect generic tag `v1.12.3` locally and from origin
- left the current uncommitted `keble_helpers/queued.py` working-tree edit untouched

## 2026-04-30 10:44 CST
State: branch `fix/objectid-chat-serialization`; helper code unchanged, ObjectId boundary guidance documented.

zma:
- execute the ObjectId-safe chat tool approval fix
- document that helper ObjectId requires JSON-mode dumps at API/chat/tool boundaries
- do not mass-replace BSON ObjectId query internals

Ai:
- added the README ObjectId boundary rule explaining `keble_helpers.ObjectId`, BSON internals, and `model_dump(mode="json")`
- left helper runtime code and package version unchanged
## 2026-05-07 22:22
State: branch `fix/image-prompt-budget`; `keble-helpers` bumped to `1.12.8`.

zma: Fix positioning task creation failure where GraphRAG multimodal prompts can exceed the model image limit (`Too many images in request: 51, maximum allowed: 50`).

Ai: Added `ImagePromptChecker.max_images_per_prompt` with typed `IMAGE_COUNT_LIMIT` drop decisions, kept non-image prompt parts intact, and extended model-side recoverable error matching for provider image-count failures. Added focused tests for budget validation, disabled-preflight budgeting, and text-only fallback after too-many-images responses.

## 2026-05-10 18:41 CST
State: branch `fix/bootstrap-agent-base-deps`; `keble-helpers` bumped to `1.12.9`.

zma:
- Add a global helper-level `AgentBaseDeps` with `marketplace` and `language` so backend/private agent deps can inherit a shared runtime context.
- Keep marketplace available for tools but use language to guide user-facing chat output by default.

Ai:
- Added `keble_helpers.ai.AgentBaseDeps` with typed `Marketplace` and `Language` defaults, exported it through helper public surfaces, documented the agent runtime context, and added schema/export tests.
- Bumped helper metadata/lockfiles to `1.12.9` and built the wheel artifact.
- Attempted package publication, but the publish command was blocked by missing package-index credentials; the backend branch consumes the bundled wheel artifact.

## 2026-05-10 19:22 CST
State: branch `fix/helper-agent-base-deps-publish`; helper code unchanged, publication still blocked by credentials.

zma:
- Implement the follow-up cleanup and finish the helper publication path if possible.

Ai:
- Checked the configured package index and confirmed `keble-helpers==1.12.9` is not published there; latest visible version is `1.12.1`.
- Retried `uv publish dist/keble_helpers-1.12.9-py3-none-any.whl`, but trusted publishing and explicit credentials are unavailable in this environment.
- Left helper code/version unchanged; backend must keep using the bundled `1.12.9` wheel until a credentialed publish is available.

## 2026-05-25 00:40
State: branch `fix/task-cost-accounting`; helper package bumped to `1.12.11`.

zma:
- Add typed usage accounting event/protocol only, without Mongo, pricing, or task-cost persistence in helpers.

Ai:
- Added `UsageAccountingEvent`, source/unit enums, metadata aliases, recorder protocol, and callback type under `keble_helpers.ai`.
- Enforced token events with `RunUsage` and counted API/item/search events with positive `unit_count`.
- Exported the contract through helper public surfaces and added focused usage-accounting tests.
## 2026-05-25 12:12
State: branch `fix/task-cost-accounting-audit-hardening`; package `keble-helpers 1.12.14`.

zma:
- Keep usage accounting package-neutral, strongly typed, and free of backend/Mongo/pricing logic.

Ai:
- Added `UsageAccountingEvent.from_request_usage(...)` and `from_unit_count(...)` classmethod constructors alongside existing run-usage conversion.
- Preserved uppercase Enum source/unit contracts and recorder protocol ownership in helpers only.
- Added focused tests for request usage and counted unit event construction.

## 2026-05-25 12:54
State: branch `fix/chat-thinking-followup-hardening`; `keble-helpers` bumped to `1.12.16`.

zma:
- Fix host Python verification for current-line chat-thinking work without hiding the issue in backend service code.
- Keep compatibility logic in the owning helper package and preserve existing public exports.

Ai:
- Added Python 3.13 import compatibility dependencies for Aliyun OSS usage: `aliyun-python-sdk-core`, `legacy-cgi`, and `six`.
- Centralized the required Aliyun vendored-six module aliases in `keble_helpers.aliyun.oss` before importing `oss2`, preserving the existing `from keble_helpers import AliyunOss` contract.
- Added a public import contract test, refreshed helper lockfiles, built and published `keble-helpers 1.12.16`, and verified the full helper test suite.

## 2026-05-26 09:46 CST
State: branch `fix/task-cost-accounting-runtime-hardening`; `keble-helpers` bumped to `1.12.17`.

zma:
- Implement the task-cost runtime hardening plan and prevent Pydantic-AI result usage methods from leaking into persisted usage events.
- Keep usage accounting package-neutral and classmethod-owned, without Mongo, pricing, or task-cost persistence in helpers.

Ai:
- Added `UsageAccountingEvent.normalize_run_usage(...)` and `UsageAccountingEvent.from_result_usage(...)` so one helper-owned path handles value-style and method-style Pydantic-AI usage.
- Added regression coverage for both API shapes and rebuilt the helper wheel for backend/package consumption.

## 2026-06-12 04:40
State: 1.18.0 — progress converter + typed usage tags (agentic hardening program Phase 4)

zhenhao-ma: (program, approved) AgenticActionProgress and ProgressReport are field-twins with no converter (hosts hand-copied and drifted); usage tags are ad-hoc strings so cost cannot be grouped by provider/tool/session.

Ai: `AgenticActionProgress.from_progress_report(report)` — THE single converter between the two progress twins (progress_key documented as the binding contract); `UsageAccountingTagSet` {provider, tool, session_id, run_id, scope_type} rendering into the EXISTING `tags: list[str]` wire shape as `dim:value` strings (keble-task's TAG group-by aggregates them with NO storage/enum change) with `parse()` round-trip; dimension order is a module-level constant (a pydantic class attr becomes ModelPrivateAttr). Tests: 3 new (field-complete conversion, round-trip with unknown-tag tolerance, empty-dim skip); suite 294 passed.

## 2026-06-12 07:50
State: 1.19.0 — declarative provider manifests + shared approval mode (program Phase 6)

zhenhao-ma: (program, approved) tool inventories drift across docstrings/prompts/frontend maps; approval policy is a per-package enum.

Ai: new `ai/provider_manifest.py`: `ChatToolKind` (QUERY/MUTATION/CLIENT_TOOL/SPAWN), `ChatToolSpec`, `ChatToolProviderManifest` (tool_names/assert_covers fail-loud + to_prompt_fragment), `compose_available_tools_prompt` (generated "Available tools" block), `AgentToolApprovalMode` (NEVER/REQUIRE_APPROVAL — the single approval vocabulary; replaces per-package policies) and `AgentToolConfig.resolve_requires_approval(mode)` so runtime flag and manifest derive from ONE call. Tests 3 new; suite 297 passed.
