keble-task 2.22.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1083 @@
1
+ Metadata-Version: 2.4
2
+ Name: keble-task
3
+ Version: 2.22.0
4
+ Author-email: zhenhao-ma <bob0103779@gmail.com>
5
+ Requires-Python: <3.14,>=3.13
6
+ Requires-Dist: keble-db<2.0.0,>=1.7.1
7
+ Requires-Dist: keble-helpers<2.0.0,>=1.40.0
8
+ Requires-Dist: pydantic-ai-slim<2.0.0,>=1.41.0
9
+ Requires-Dist: tenacity<10.0.0,>=9
10
+ Provides-Extra: test
11
+ Requires-Dist: httpx[socks]<1.0.0,>=0.27; extra == 'test'
12
+ Requires-Dist: pydantic-ai-slim[openai]<2.0.0,>=1.41.0; extra == 'test'
13
+ Requires-Dist: pytest-asyncio<1.0.0,>=0; extra == 'test'
14
+ Requires-Dist: pytest<9.0.0,>=8; extra == 'test'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Keble Task
18
+
19
+ Keble Task is a package for managing asynchronous task execution with MongoDB storage and Redis locking. It provides a client for creating, tracking, and managing tasks with support for retries, timeouts, and token consumption.
20
+
21
+ ## Version 2.22.0 Update
22
+
23
+ - Added the cross-repo Keble pytest marker vocabulary and strict marker config.
24
+ - Default tests no longer fail at collection when `tests/.env` is absent:
25
+ Mongo/Redis-backed tests skip at fixture setup when `MONGO_DB_URI` or
26
+ `REDIS_URI` is not configured.
27
+ - The live LLM agent-tool test now requires both `RUN_LLM_LIVE=1` and
28
+ `LIVE_LLM_*` credentials, so full local pytest does not spend model tokens by
29
+ accident.
30
+ - Added `AGENTS.md`, `CLAUDE.md`, and `pyrightconfig.json` for future test
31
+ workers.
32
+ - Removed the legacy custom classifier rejected by package upload validation.
33
+
34
+ ## Testing
35
+
36
+ Default fast test command:
37
+
38
+ ```bash
39
+ uv run pytest -m "not live and not slow and not eval and not local_stack"
40
+ ```
41
+
42
+ Run all non-live tests with local Mongo/Redis configured:
43
+
44
+ ```bash
45
+ uv run pytest -q
46
+ ```
47
+
48
+ Run the live LLM canary:
49
+
50
+ ```bash
51
+ RUN_LLM_LIVE=1 uv run pytest -m "live and llm"
52
+ ```
53
+
54
+ Run static syntax/type checking:
55
+
56
+ ```bash
57
+ npx --yes pyright .
58
+ ```
59
+
60
+ ## Version 2.21.0 Update
61
+
62
+ Task agent tool metadata now names
63
+ `keble_helpers.AgentToolRegistrationConfig` directly. Package code and tests use
64
+ `AgentToolApprovalMode` for approval semantics; do not reintroduce bool approval
65
+ aliases in task provider contracts.
66
+
67
+ Build packaging explicitly excludes local agent scratch directories
68
+ (`.claude/`, `.worktrees/`, `.wt-discovery/`) so private symlinks never enter
69
+ release artifacts.
70
+
71
+ ## Version 2.18.0 Update
72
+
73
+ Code-breaking agent-schema naming + placement refactor (no backward-compat
74
+ aliases), mirroring the shipped `keble-positioning` convention. Re-rebased onto
75
+ the settled `origin/main` 2.17.0 (the `AgenticActionEventSource` SCREAMING_SNAKE
76
+ realignment + the no-emit-without-drain static guard) and bumped above main to
77
+ 2.18.0:
78
+
79
+ - `schemas.py` is now the `keble_task.schemas` PACKAGE (`schemas/__init__.py`
80
+ keeps all prior exports identical) so agent tool I/O schemas can live in the
81
+ new `schemas/for_agent.py`.
82
+ - Renames: `TaskSummaryView` -> `TaskSummaryForAgent`, `TaskQueryToolsConfig` ->
83
+ `TaskAgentQueryToolsConfig`, `TaskMutationToolsConfig` ->
84
+ `TaskAgentMutationToolsConfig` (all three now DEFINED in
85
+ `schemas/for_agent.py`).
86
+ - Registrars moved to a new `keble_task.agent.tools` subpackage (behavior only,
87
+ zero `BaseModel`): `tools/query.py::register_query_tools` and
88
+ `tools/mutation.py::register_mutation_tools`. The old bare `register` /
89
+ `register_action_tools` names and the `agent/registry.py` /
90
+ `agent/query_registry.py` modules are REMOVED.
91
+ - New convention guard `tests/schemas/test_for_agent.py`.
92
+
93
+ ## Agentic schema convention (`ForAgent` + `agent/tools/`)
94
+
95
+ Every schema lands in exactly one bucket; the bucket dictates its NAME and its
96
+ LOCATION. This is enforced by `tests/schemas/test_for_agent.py`.
97
+
98
+ | Bucket | What it is | Name | Lives in |
99
+ | --- | --- | --- | --- |
100
+ | Agent tool I/O | typed input to / return projection of a pydantic-ai `@agent.tool` | `*ForAgent` suffix | `schemas/for_agent.py` only — never in `agent/` |
101
+ | Agent tool config/enum | tool-registration config or approval/mode enum used only by agent tools | `*Agent…Config` / `*Agent…Policy` infix | `schemas/for_agent.py` |
102
+ | Mutation action payload | the typed `@agent.tool` mutation input | unchanged | `keble_task/actions.py` (external action module) |
103
+ | Persisted / CRUD / event | `*Base`/`*Update`/`*MongoObject`/`*Event` | unchanged | `schemas/` |
104
+
105
+ The registrars under `agent/tools/` (`mutation.py`, `query.py`) are BEHAVIOR
106
+ ONLY — they import contracts from `schemas/` (and `actions.py`) and define zero
107
+ `BaseModel` classes. Do NOT reintroduce `*View`/`*Display` names for agent tool
108
+ payloads.
109
+
110
+ ## Version 2.12.0 Update
111
+
112
+ Added `TaskClient.apublic_list_indexable(task_types, stages, limit) -> list[TaskPublicRef]`:
113
+ a lean, projected (`{_id, updated}`) public read backing consumer-side SEO sitemaps —
114
+ filters `sharing_scope=PUBLIC` + `task_type $in` + `stage $in`, newest-`updated` first,
115
+ capped by `limit`, no childs/redis. Backed by the new
116
+ `sharing_scope_task_type_stage_updated_desc_idx` (the package owns its indexes via
117
+ `aensure_task_indexes`). Also completed the latent `apublic_get_multi` overload contract
118
+ (`stages`/`title_contains` are now honored, mirroring `aowner_get_multi`).
119
+
120
+ ## Package Line
121
+
122
+ 1. Package version: `2.22.0`
123
+ 2. Python baseline: `>=3.13,<3.14`
124
+ 3. Runtime ownership:
125
+ - `TaskBase.language` is the canonical, task-owned output/report language;
126
+ report-generating handlers and child-task creation read `task.language`
127
+ instead of parsing domain-specific metadata blobs (optional for back-compat)
128
+ - flat task lists return root tasks unless `parent_task` is explicitly filtered
129
+ - `TaskRelationType.MAPPED` records viewport-map edges without changing parentage
130
+ - `TaskClient.aapply_actions(...)` creates related tasks through a pure generic action layer; feature packages own their own typed tools
131
+ - `TaskClient.astart(...)` is the single task-start entrypoint
132
+ - uncaught handler exceptions are finalized into terminal `FAILURE`
133
+ - start locks are always cleared before the run returns
134
+ - package events use direct `keble_helpers.AgenticActionEvent` JSON; backend
135
+ transports them without room-specific wrapper schemas
136
+ - `TaskClient` can emit canonical task lifecycle events after persisted
137
+ `PROCESSING`, `SUCCESS`, `FAILURE`, and timeout stage transitions
138
+ - downstream TypeScript room consumers should use `keble-core 0.1.32+`
139
+ direct-event builders rather than task-owned workspace snapshots
140
+
141
+ ## Version 2.5.1 Update
142
+
143
+ 1. Hardened the agentic `mutate_task_workspace` tool against ID hallucination:
144
+ `CreateRelatedTaskAction.parent_task_id` and `from_task_ids` now carry model-facing
145
+ `Field(description=...)` guidance to leave them unset (attach under the current task) unless
146
+ real ids are provided — never invent ids.
147
+ 2. The tool boundary (`agent/registry.py`) now maps model-fixable action-validation errors
148
+ (`ClientSideInvalidParams` / `ClientSideMissingParams` / `ServerSideInvalidParams` from
149
+ `aapply_actions`, e.g. a hallucinated `parent_task_id`) to `pydantic_ai.ModelRetry`, so the
150
+ model self-corrects instead of aborting the run. Wiring/infra faults still propagate.
151
+ 3. Added a deterministic `FunctionModel` self-correction test (bad `parentTaskId` -> ModelRetry
152
+ -> recover) and made the live LLM prompt explicit about omitting the ids.
153
+ 4. Package metadata synced to `2.5.1` across `pyproject.toml`, `pyproject.poetry.toml`, `uv.lock`.
154
+
155
+ ## Version 2.5.0 Update
156
+
157
+ 1. Added `TaskBase.language: Optional[Language]` — a typed, task-owned output/report language so
158
+ the generic task framework is the single source of truth for the language a handler should
159
+ produce output in, instead of consumers digging it out of domain-specific metadata dicts.
160
+ 2. `TaskClient.acreate(..., language=...)` threads the value onto the created task.
161
+ 3. Optional with a `None` default, so legacy callers and legacy persisted documents still validate
162
+ and load.
163
+ 4. Package metadata synced to `2.5.0` across `pyproject.toml`, `pyproject.poetry.toml`, `uv.lock`.
164
+
165
+ ## Version 2.4.23 Update
166
+
167
+ 1. Added owner/public root-list indexes — `CRUDTask.aensure_task_indexes` now also creates
168
+ `owner_parent_task_created_desc_idx` and `sharing_scope_parent_task_created_desc_idx` so
169
+ `aowner_get_multi` / `apublic_get_multi` root lists filter `owner` / `sharing_scope` on an
170
+ index instead of residually after a `parent_task` scan (created at `ainit`/startup).
171
+ 2. `CREATE_RELATED_TASK` is now self-healing: standalone Mongo has no multi-document
172
+ transactions, so if a relation write fails after the child task is created, a saga
173
+ compensation (`_arollback_created_related_task`) deletes the orphaned child and any partial
174
+ relation rows, then re-raises — the workspace never keeps a child without its relations.
175
+ 3. All package tests are pyright-clean (`pyright keble_task` and `pyright tests` both 0 errors);
176
+ removed stale tracked `dist/keble_task-0.0.0-*` build artifacts (`dist/` stays gitignored).
177
+ 4. Package metadata synced to `2.4.23` across `pyproject.toml`, `pyproject.poetry.toml`, `uv.lock`.
178
+
179
+ ## Version 2.4.22 Update
180
+
181
+ 1. `build_task_tree(...)` now **fails hard on corrupted persisted references** instead of
182
+ silently shrinking trees: a child whose `parent_task` is absent from the loaded root
183
+ tree or a missing requested root raises `keble_exceptions.ObjectNotFound`, and a parent
184
+ cycle raises `keble_exceptions.DataIntegrityCompromised`. The sibling sort was simplified
185
+ to use the non-Optional persisted `created` datetime (no `try/except`). Behavior change:
186
+ list/tree endpoints surface corruption as typed errors rather than partial results.
187
+ 2. The gated live agent-tool test guards its OpenAI-model imports with
188
+ `pytest.importorskip` so collection never fails where the openai extra is absent, and the
189
+ `test` dependency group now includes `pydantic-ai-slim[openai]` so `uv run pytest` can run
190
+ it when `LIVE_LLM_*` creds are present.
191
+ 3. Package metadata synced to `2.4.22` across `pyproject.toml`, `pyproject.poetry.toml`, and
192
+ `uv.lock`.
193
+
194
+ ## Version 2.4.18 Update
195
+
196
+ 1. `TaskCostAggregateResponse.build(...)` now takes `costs: Sequence[TaskCostBase]`
197
+ (read-only/covariant) so CRUD's `list[TaskCostMongoObject]` is accepted without a
198
+ list-invariance type error, and folds rows through new typed
199
+ `TaskCostAggregateBucket.empty()` / `accumulate()` methods instead of an untyped
200
+ `dict[str, Any]` accumulator — bucket math lives on the type that owns it.
201
+ 2. The task collection now has tree/stage read indexes via
202
+ `CRUDTask.aensure_task_indexes(...)` (`root_task+created`, `parent_task+created`,
203
+ `stage+created`), wired into `TaskClient.aensure_indexes(...)`/`ainit(...)` so
204
+ root-tree, root-list, and retry/timeout sweeps are index-backed (idempotent).
205
+ 3. `TaskMetadata = dict[str, JsonValue]` is the single strongly-typed contract for
206
+ free-form task metadata; `TaskBase.metadata` and the `TaskClient` metadata params
207
+ use it instead of a bare `dict`.
208
+ 4. The agent registrar uses `AgentToolRegistrationConfig` directly, removing
209
+ the old local config shadowing that caused `ToolFuncContext` assignment
210
+ errors.
211
+ 5. Added tests for timeout lifecycle, the processing guard, zero-usage cost rollup,
212
+ parent+child cost rollup by root, and a gated live-LLM `mutate_task_workspace`
213
+ agent-tool run (`LIVE_LLM_*` creds in `tests/.env`).
214
+
215
+ ## Version 2.4.16 Update
216
+
217
+ 1. `TaskCostCreate.from_task_usage(...)` is the canonical cost-row constructor;
218
+ it denormalizes owner and task type from the persisted task row.
219
+ 2. `TaskCostAggregateResponse.build(...)` owns in-memory response aggregation
220
+ from already-filtered rows.
221
+ 3. Runtime task-cost create/list/aggregate paths no longer create Mongo
222
+ indexes; callers must use `TaskClient.ainit(...)` or startup wiring.
223
+ 4. `TaskHandlerRequest` inherits `AgentDbDeps`, so `usage_recorder` travels with
224
+ the same DB-rooted request object without making the task package own pricing
225
+ or persistence policy outside cost rows.
226
+
227
+ ## Version 2.4.15 Update
228
+
229
+ 1. `TaskCostFilterBase` is now the single source of truth for task-cost read
230
+ filters shared by list and aggregate requests.
231
+ 2. `TaskCostListRequest` owns only pagination fields on top of the shared
232
+ filter contract.
233
+ 3. `TaskCostAggregateRequest` owns only aggregation controls on top of the
234
+ shared filter contract.
235
+ 4. Task-cost tag filters remain all-tags filters: every requested tag must be
236
+ present on the cost row.
237
+ 5. Package metadata is synchronized across `pyproject.toml`,
238
+ `pyproject.poetry.toml`, and `uv.lock`.
239
+
240
+ ## Version 2.4.14 Update
241
+
242
+ 1. `TaskClient.ainit(amongo=...)` exposes the package-owned Mongo index setup
243
+ as a backend startup hook over existing `aensure_indexes(...)`.
244
+
245
+ ## Version 2.4.13 Update
246
+
247
+ 1. Added separate task-cost storage through `TaskCostBase`, `TaskCostCreate`,
248
+ `TaskCostMongoObject`, `TaskCostListRequest`, and
249
+ `TaskCostAggregateRequest`.
250
+ 2. `TaskClient.acreate_task_cost(...)` now denormalizes root task, owner, and
251
+ task type from the persisted task row before writing the cost row.
252
+ 3. `TaskClient.alist_task_costs(...)` and
253
+ `TaskClient.aaggregate_task_costs(...)` provide indexed admin reporting with
254
+ total/hour/day/week/month buckets and optional tag fan-out.
255
+ 4. Aggregation uses `RunUsage`, per-million `Money` rates, and caller-provided
256
+ `ExchangeRateInUsd` values while preserving sub-cent Decimal token costs.
257
+ 5. `TaskClient.aensure_indexes(...)` creates public-id, relation, and task-cost
258
+ indexes from one startup-friendly entrypoint.
259
+
260
+ ## Version 2.4.12 Update
261
+
262
+ 1. `keble-task` now declares `tenacity>=9,<10.0.0` directly because the task
263
+ runtime imports it for handler retry.
264
+ 2. `TaskClient.acreate_task_relations(...)` rejects duplicate relation edge
265
+ payloads before inserting any row.
266
+ 3. `TaskClient.aapply_actions(...)` rejects duplicate `from_task_ids` before
267
+ creating the child task, preventing partial child-task persistence when an
268
+ action would generate duplicate relation edges.
269
+ 4. Previously skipped start/retry/timeout tests now exercise the real async
270
+ handler path with `ExtendedAsyncRedis`.
271
+
272
+ ## Version 2.4.11 Update
273
+
274
+ 1. Added `TaskEventType.TASK_STAGE_CHANGED` as the package-owned lifecycle
275
+ event for task stage transitions.
276
+ 2. Added `TaskLifecycleEventPayload(task=TaskMongoObject)` and
277
+ `TaskLifecycleEvent`, reusing the persisted task schema instead of adding
278
+ UI-specific state payloads.
279
+ 3. `TaskClient.astart(...)`, `aon_task_processing(...)`,
280
+ `aon_task_success(...)`, `aon_task_failure(...)`, and
281
+ `aon_task_timeout(...)` can emit lifecycle events after DB updates reload
282
+ the final task row.
283
+ 4. Consumers should use lifecycle events for room task state, while
284
+ `CREATE_RELATED_TASK` events remain the canonical child/relation creation
285
+ events.
286
+
287
+ ## Version 2.4.8 Update
288
+
289
+ 1. `CreateRelatedTaskAction` relation rows now persist `action.metadata` on
290
+ `TaskRelationCreate.metadata` instead of writing `{}`.
291
+ 2. `TaskActionCreatedRelation` remains a slim public DTO but now includes
292
+ relation metadata so downstream event consumers can render edge context.
293
+ 3. No new relation schema was added; `TaskRelationBase.metadata` is the
294
+ canonical extension point for task-action relation annotations.
295
+
296
+ ## Version 2.4.9 Update
297
+
298
+ 1. Relation metadata now uses the package-owned `TaskRelationMetadata`
299
+ JSON-safe contract before persistence and event emission.
300
+ 2. `CreateRelatedTaskAction.metadata`, `TaskRelationBase.metadata`, and
301
+ `TaskActionCreatedRelation.metadata` normalize BSON ObjectIds, datetimes,
302
+ and Pydantic models into JSON-compatible values.
303
+ 3. Unsupported runtime objects are rejected at the metadata boundary instead of
304
+ leaking into SSE or TypeScript `JsonObject` consumers.
305
+
306
+ ## Version 2.4.10 Update
307
+
308
+ 1. Unsupported relation metadata now raises `ValueError` inside the JSON
309
+ fallback so Pydantic wraps model construction failures as `ValidationError`.
310
+ 2. Regression coverage protects both `CreateRelatedTaskAction.metadata` and
311
+ `TaskRelationCreate.metadata` from surfacing raw `TypeError`.
312
+
313
+ ## Installation
314
+
315
+ ```bash
316
+ pip install keble-task
317
+ ```
318
+
319
+ ## Core Concepts
320
+
321
+ ### TaskClient
322
+
323
+ The `TaskClient` is the main entry point for backend APIs that need to create and manage tasks. All operations are asynchronous with 'a' prefix (e.g., `acreate`, `astart`, `aget`).
324
+
325
+ `astart(...)` now owns the terminal failure boundary for task execution:
326
+
327
+ 1. it loads the task and checks the Redis start lock
328
+ 2. it moves the task to `PROCESSING`
329
+ 3. it runs the configured handler with retry support
330
+ 4. if the handler still raises, it writes a terminal `FAILURE` row unless the handler already finalized the task
331
+ 5. it always clears the Redis start lock before returning
332
+
333
+ ### Task Tree And Relations
334
+
335
+ Task trees and task relations are intentionally separate:
336
+
337
+ 1. `root_task` scopes the whole workspace.
338
+ 2. `parent_task` stores the one canonical creator/trigger edge.
339
+ 3. `TaskRelationMongoObject` stores extra lineage edges such as multi-source `REDUCED` children and viewport-map `MAPPED` children.
340
+
341
+ Flat task list APIs now return only root tasks by default. Use `include_childs=True`
342
+ for task trees, `parent_task=<id>` for direct child listing, and relation APIs for
343
+ non-tree edges.
344
+
345
+ The package does not expose a workspace snapshot schema. Backend routes should fetch the existing task tree and relation rows separately, then decide how to compose them for frontend views.
346
+
347
+ ### Task Room Resolution
348
+
349
+ Task-room identity is intentionally generic and task-tree-only:
350
+
351
+ 1. `TaskClient.aresolve_task_room(...)` accepts any owner-visible task id.
352
+ 2. It returns:
353
+ - `root_task_id`
354
+ - `requested_task_id`
355
+ 3. The package does not know feature focus shape, selection metadata, or chat state.
356
+ 4. Backend/frontend layers should use the returned root task id as room identity
357
+ and keep feature-specific metadata outside this package.
358
+
359
+ ### Task Room Graph Context
360
+
361
+ `TaskRoomGraphContext` is the compact agent-facing room graph. It is generic:
362
+ task ids, parent edges, task type/title/subtitle, and sidecar `MAPPED` /
363
+ `REDUCED` relation edges. It does not include positioning ids, grid ids, UX
364
+ selection, or frontend state.
365
+
366
+ ```python
367
+ context = await task_client.aget_task_room_graph_context(
368
+ amongo=amongo,
369
+ owner=owner,
370
+ root_task_id=root_task_id,
371
+ focused_task_id=focused_task_id,
372
+ )
373
+ prompt_text = context.to_prompt_text()
374
+ ```
375
+
376
+ ```python
377
+ relations = await task_client.acreate_task_relations(
378
+ amongo=amongo,
379
+ objs_in=[
380
+ TaskRelationCreate(
381
+ root_task=root.id,
382
+ from_task_id=source_a.id,
383
+ to_task_id=child.id,
384
+ relation_type=TaskRelationType.REDUCED,
385
+ ),
386
+ TaskRelationCreate(
387
+ root_task=root.id,
388
+ from_task_id=source_b.id,
389
+ to_task_id=child.id,
390
+ relation_type=TaskRelationType.REDUCED,
391
+ ),
392
+ TaskRelationCreate(
393
+ root_task=root.id,
394
+ from_task_id=source_a.id,
395
+ to_task_id=mapped_view_task.id,
396
+ relation_type=TaskRelationType.MAPPED,
397
+ ),
398
+ ],
399
+ )
400
+
401
+ root_relations = await task_client.alist_task_relations_by_root(
402
+ amongo=amongo,
403
+ root_task=root.id,
404
+ )
405
+ ```
406
+
407
+ ### Task Cost Tracking
408
+
409
+ Task costs are stored in a dedicated collection and are denormalized from the
410
+ task row at creation time. The task collection stays focused on execution state,
411
+ while cost reports can filter by task, root task, owner, task type, tags, and
412
+ `occurred_at` windows.
413
+
414
+ ```python
415
+ result = await agent.run(prompt, deps=deps)
416
+
417
+ await task_client.acreate_task_cost(
418
+ amongo=amongo,
419
+ task_id=task.id,
420
+ tags=["positioning", "classify_cells"],
421
+ run_usage=result.usage,
422
+ token_rates_per_million=TaskCostTokenRates(
423
+ input_tokens=Money(float_money=1.25, currency=Currency.USD),
424
+ output_tokens=Money(float_money=10.00, currency=Currency.USD),
425
+ ),
426
+ additional_cost=Money(float_money=0.01, currency=Currency.USD),
427
+ seconds=12,
428
+ retry=max(task.attempts - 1, 0),
429
+ metadata={"model": "gateway/openai:gpt-5.2"},
430
+ )
431
+ ```
432
+
433
+ ### Task Handler
434
+
435
+ For packages that want to support keble-task, implementing a task handler function is required rather than creating a TaskClient instance.
436
+
437
+ ### Task Lifecycle Events
438
+
439
+ `ProgressTask` is not the current task-handler transport. Handlers receive a
440
+ `TaskHandlerRequest`, read DB clients and cross-cutting context directly from that
441
+ request, and return `TaskHandlerResponse | None`.
442
+
443
+ Task lifecycle and domain progress are emitted through the shared
444
+ `AgenticEventEmitter` on `request.event_emitter`. The emitter is intentionally
445
+ detached: `aemit()` schedules callbacks and the task runtime drains before the
446
+ execution boundary returns.
447
+
448
+ ## Schemas
449
+
450
+ The package provides several schemas for working with tasks:
451
+
452
+ ### TaskStage
453
+
454
+ An enum representing the different stages of a task:
455
+
456
+ ```python
457
+ class TaskStage(str, Enum):
458
+ PENDING = "PENDING" # Task is created but not yet started
459
+ PROCESSING = "PROCESSING" # Task is currently being processed
460
+ SUCCESS = "SUCCESS" # Task completed successfully
461
+ FAILURE = "FAILURE" # Task failed to complete
462
+ ```
463
+
464
+ Tasks in `PENDING` or `PROCESSING` stages can be started or restarted.
465
+
466
+ ### SharingScope
467
+
468
+ An enum defining the visibility scope of a task:
469
+
470
+ ```python
471
+ class SharingScope(str, Enum):
472
+ PRIVATE = "PRIVATE" # Only accessible by the owner
473
+ PUBLIC = "PUBLIC" # Accessible by anyone
474
+ ```
475
+
476
+ ### TaskBase
477
+
478
+ The base schema for task data:
479
+
480
+ ```python
481
+ class TaskBase(SchemaBase):
482
+ # Error information
483
+ error: Optional[str] = None
484
+ exception_type: Optional[TaskExceptionType] = None
485
+
486
+ # Current task stage
487
+ stage: TaskStage
488
+
489
+ # Task type identifier
490
+ task_type: str
491
+
492
+ # Optional progress tracking key
493
+ progress_key: Optional[str]
494
+
495
+ # Display information
496
+ title: Optional[str]
497
+ subtitle: Optional[str]
498
+ image: Optional[str]
499
+
500
+ # Custom metadata for the task
501
+ metadata: Optional[dict]
502
+
503
+ # Access control
504
+ sharing_scope: SharingScope = SharingScope.PRIVATE
505
+ status: Status = Status.ACTIVE
506
+ owner: str
507
+
508
+ # Timestamp tracking
509
+ started_ts: Optional[Timestamp] = None
510
+ success_ts: Optional[Timestamp] = None
511
+ failure_ts: Optional[Timestamp] = None
512
+
513
+ # Token management
514
+ expected_token: int # Expected tokens to consume
515
+ consumed_token: int = 0 # Actual tokens consumed
516
+
517
+ # Retry and timeout handling
518
+ attempts: int = 0 # Number of attempts so far
519
+ timeout_mins: int = 120 # Timeout in minutes
520
+ ```
521
+
522
+ The `TaskBase` provides two helpful properties:
523
+
524
+ - `unfinshed_timeout`: Returns `True` if the task has timed out but is still in `PENDING` or `PROCESSING` stage
525
+ - `allow_to_retry`: Returns `True` if the task has fewer than 3 attempts and is in a stage that allows starting
526
+
527
+ ### TaskUpdate
528
+
529
+ A schema for the fields an owner may patch post-creation. Unset fields are not
530
+ written (`aowner_update` dumps with `exclude_unset`), so a partial patch never
531
+ clobbers other columns. `image` reuses the exact same URL validation as creation
532
+ (`TaskBase.validate_image_url`), so an edit can never store an image creation
533
+ would have rejected.
534
+
535
+ ```python
536
+ class TaskUpdate(BaseModel):
537
+ sharing_scope: Optional[SharingScope] = None
538
+ title: Optional[str] = None
539
+ image: Optional[str] = None # owner thumbnail; full http(s) URL only
540
+ ```
541
+
542
+ ### TaskMongoObject
543
+
544
+ The MongoDB document representation of a task:
545
+
546
+ ```python
547
+ class TaskMongoObject(MongoObjectBase, TaskBase):
548
+ pass
549
+ ```
550
+
551
+ This combines `TaskBase` with MongoDB-specific functionality from `MongoObjectBase`.
552
+
553
+ ### TaskRelationMongoObject
554
+
555
+ The MongoDB document representation of extra lineage between tasks in one workspace:
556
+
557
+ ```python
558
+ class TaskRelationType(str, Enum):
559
+ MAPPED = "MAPPED"
560
+ REDUCED = "REDUCED"
561
+
562
+
563
+ class TaskRelationBase(SchemaBase):
564
+ root_task: ObjectId
565
+ from_task_id: ObjectId
566
+ to_task_id: ObjectId
567
+ relation_type: TaskRelationType
568
+ metadata: dict[str, Any] = Field(default_factory=dict)
569
+
570
+
571
+ class TaskRelationMongoObject(MongoObjectBase, TaskRelationBase):
572
+ pass
573
+ ```
574
+
575
+ Relation rows do not modify `root_task`, `parent_task`, or task status. They are sidecar lineage edges for backend/frontend composition.
576
+
577
+ ### Generic Task Actions
578
+
579
+ Use `TaskActions` when an agent or backend API needs task-owned mutations without feature-specific side effects.
580
+
581
+ ```python
582
+ out = await task_client.aapply_actions(
583
+ amongo=amongo,
584
+ current_task=source_task,
585
+ payload=TaskActions(
586
+ message="Create a related child task.",
587
+ actions=[
588
+ CreateRelatedTaskAction(
589
+ relation_type=TaskRelationType.MAPPED,
590
+ task_type="POSITIONING",
591
+ title="Related workspace",
592
+ metadata={"source": "agent"},
593
+ )
594
+ ],
595
+ ),
596
+ extended_aredis=extended_aredis,
597
+ )
598
+ ```
599
+
600
+ The task package creates the child task, validates root ownership, writes relation rows, and emits `TaskActionEvent` using `keble_helpers.AgenticActionEvent`. Positioning-specific mapped/reduced-child behavior belongs in backend-owned typed tools, not in `keble-task` hook registries.
601
+
602
+ Action results now expose slim DTOs instead of full Mongo rows:
603
+
604
+ 1. `TaskActionCreatedTask` carries `task_id`, root/parent ids, task type, stage, title, and progress key.
605
+ 2. `TaskActionCreatedRelation` carries relation id, root/source/target ids, and relation type.
606
+ 3. Internal `TaskMongoObject` and `TaskRelationMongoObject` remain the persistence models, but tool/action payloads should not return them directly.
607
+
608
+ ### Agent Tool Registration
609
+
610
+ Task owns the pydantic-ai registrar for generic task workspace mutations.
611
+ Backend should register this package tool instead of defining a manual duplicate
612
+ in backend chat code.
613
+
614
+ ```python
615
+ from keble_task.agent import TaskAgentDeps, register_mutation_tools
616
+
617
+ agent = Agent[TaskAgentDeps, Any](...)
618
+
619
+ register_mutation_tools(
620
+ agent,
621
+ task_client=task_client,
622
+ )
623
+ ```
624
+
625
+ Deps shape:
626
+ - `TaskAgentDeps` inherits `keble_db.AgentDbDeps`; Mongo/Redis are not separate tool args.
627
+ - Task runtime state is under `ctx.deps.task`.
628
+ - The tool delegates to `TaskClient.aapply_actions(...)`, so task parentage, generic relations, and task action events stay package-owned.
629
+
630
+ ### Native Chat Tool Provider (query tools)
631
+
632
+ The package also owns the READ/QUERY registrar (`register_query_tools`: owner-scoped
633
+ `list_tasks` / `get_task`) and, since 2.7.0, ships it as a NATIVE
634
+ `keble_helpers.ChatToolProviderProtocol` provider so hosts no longer need a
635
+ generic adapter around the registrar:
636
+
637
+ ```python
638
+ from keble_task.agent import TaskQueryChatToolProvider
639
+
640
+ provider = TaskQueryChatToolProvider(
641
+ task_client=task_client,
642
+ tools_config=None, # optional TaskAgentQueryToolsConfig | dict overrides
643
+ )
644
+
645
+ # A chat host composes providers declaratively; register attaches the tools.
646
+ provider.register(agent=agent, context=None)
647
+ ```
648
+
649
+ Contract notes:
650
+ - `provider.provider_id` is EXACTLY `"task_query"` — it is recorded in room
651
+ diagnostics and mapped to a user-readable label by the frontend; do not change it.
652
+ - All deps are captured at construction; `register(*, agent, context)` ignores
653
+ `context`, which the cross-repo protocol explicitly permits.
654
+
655
+ ### Task Handler Dependencies
656
+
657
+ The retired `TaskResources` bag is no longer part of the active handler API.
658
+ `TaskHandlerRequest` inherits `keble_db.AgentDbDeps`, so handlers read clients and
659
+ cross-cutting context directly from the request:
660
+
661
+ ```python
662
+ request.amongo
663
+ request.extended_aredis
664
+ request.aneo4j
665
+ request.qdrant_client
666
+ request.event_emitter
667
+ request.usage_recorder
668
+ ```
669
+
670
+ The `event_emitter` field is the canonical, single-source channel for task
671
+ lifecycle and package-owned domain events. Package handlers emit typed
672
+ `AgenticActionEvent` values through this emitter; the task runtime drains it
673
+ before returning from lifecycle boundaries so owner-list and room listeners see
674
+ deterministic terminal state.
675
+
676
+ ### TaskHandlerRequest
677
+
678
+ A schema for task handling requests passed to task handlers:
679
+
680
+ ```python
681
+ class TaskHandlerRequest(AgentDbDeps):
682
+ model_config = PydanticModelConfig.default(arbitrary_types_allowed=True)
683
+ task: TaskMongoObject # The task to be processed
684
+ metadata: TaskMetadata | None = None
685
+ ```
686
+
687
+ Handlers return `TaskHandlerResponse` when the current process completed the
688
+ task. Handlers return `None` only when completion is intentionally delegated to a
689
+ separate process; in that case the runtime leaves the task in `PROCESSING` until
690
+ the delegate finalizes it.
691
+
692
+ ### TaskHandlerResponse
693
+
694
+ A schema for responses returned from task handlers:
695
+
696
+ ```python
697
+ class TaskHandlerResponse(BaseModel):
698
+ model_config = PydanticModelConfig.default()
699
+ task: TaskMongoObject # The processed task
700
+ success: bool # Whether the task was successful
701
+ consuming_token: int # Actual tokens consumed
702
+ exception_type: Optional[TaskExceptionType] = None # Type of exception if failed
703
+ error: Optional[str] = None # Error message if failed
704
+ ```
705
+
706
+ ### TokenConsumptionType
707
+
708
+ An enum representing token consumption actions:
709
+
710
+ ```python
711
+ class TokenConsumptionType(str, Enum):
712
+ CONSUME = "CONSUME" # Consume tokens
713
+ RECOVER = "RECOVER" # Recover (return) tokens
714
+ ```
715
+
716
+ ### TokenConsumptionPayload
717
+
718
+ A schema for token consumption operations:
719
+
720
+ ```python
721
+ class TokenConsumptionPayload(BaseModel):
722
+ model_config = PydanticModelConfig.default(arbitrary_types_allowed=True)
723
+ consumption_type: TokenConsumptionType # Whether to consume or recover tokens
724
+ owner: str # The owner of the tokens
725
+ token: int # The amount of tokens to consume or recover
726
+ task_id: ObjectId | None = None
727
+ amongo: AsyncIOMotorClient
728
+ extended_aredis: ExtendedAsyncRedis
729
+ aneo4j: Neo4jAsyncDriver | None = None
730
+ metadata: TaskMetadata | None = None
731
+ ```
732
+
733
+ ### TaskExceptionType
734
+
735
+ An enum representing different types of task exceptions:
736
+
737
+ ```python
738
+ class TaskExceptionType(str, Enum):
739
+ UNKNOWN = "UNKNOWN" # Unknown error
740
+ FAILED_TO_START = "FAILED_TO_START" # Task failed to start
741
+ NO_SUFFICIENT_DATA = "NO_SUFFICIENT_DATA" # Insufficient data to process task
742
+ TIMEOUT = "TIMEOUT" # Task timed out
743
+ ```
744
+
745
+ ### TaskException
746
+
747
+ Base exception class for task-related errors:
748
+
749
+ ```python
750
+ class TaskException(KebleException):
751
+ def __init__(
752
+ self,
753
+ *,
754
+ exception_type: TaskExceptionType = TaskExceptionType.UNKNOWN,
755
+ error: Optional[str] = None,
756
+ ):
757
+ self.exception_type = exception_type
758
+ self.error = error
759
+ # Inherits from KebleException
760
+ ```
761
+
762
+ Subclasses of TaskException:
763
+
764
+ - **TaskNoSufficientDataException**: Raised when there is insufficient data to process a task
765
+ - **TaskFailedToStartException**: Raised when a task fails to start
766
+ - **TaskTimeoutException**: Raised when a task times out
767
+ - **TaskUnknownException**: Raised for unknown errors
768
+
769
+ ### Difficulty
770
+
771
+ A utility enum for representing task difficulty levels:
772
+
773
+ ```python
774
+ class Difficulty(str, Enum):
775
+ EASY = "EASY"
776
+ MEDIUM = "MEDIUM"
777
+ HARD = "HARD"
778
+ ```
779
+
780
+ ## Examples
781
+
782
+ ### Backend API: Creating a TaskClient
783
+
784
+ ```python
785
+ import asyncio
786
+ from keble_task import (
787
+ TaskClient,
788
+ TaskHandlerRequest,
789
+ TaskHandlerResponse,
790
+ TokenConsumptionPayload,
791
+ TokenConsumptionType,
792
+ TaskExceptionType,
793
+ TaskStage,
794
+ )
795
+ from keble_helpers import AgenticEventEmitter, SharingScope
796
+ from motor.motor_asyncio import AsyncIOMotorClient
797
+ from keble_db import ExtendedAsyncRedis
798
+
799
+ # Define a token consumption handler (synchronous or asynchronous)
800
+ def token_consumption_handler(payload: TokenConsumptionPayload) -> bool:
801
+ # Access DB clients directly from the payload; there is no resources bag.
802
+ amongo = payload.amongo
803
+ extended_aredis = payload.extended_aredis
804
+
805
+ # Implement logic to handle token consumption or recovery
806
+ if payload.consumption_type == TokenConsumptionType.CONSUME:
807
+ print(f"Consuming {payload.token} tokens for {payload.owner}")
808
+ else: # TokenConsumptionType.RECOVER
809
+ print(f"Recovering {payload.token} tokens for {payload.owner}")
810
+
811
+ return True
812
+
813
+ # Alternatively, you can define an asynchronous token consumption handler
814
+ async def async_token_consumption_handler(payload: TokenConsumptionPayload) -> bool:
815
+ amongo = payload.amongo
816
+ extended_aredis = payload.extended_aredis
817
+
818
+ if payload.consumption_type == TokenConsumptionType.CONSUME:
819
+ print(f"Async consuming {payload.token} tokens for {payload.owner}")
820
+ else: # TokenConsumptionType.RECOVER
821
+ print(f"Async recovering {payload.token} tokens for {payload.owner}")
822
+
823
+ return True
824
+
825
+ # Define an async task handler
826
+ async def task_handler(request: TaskHandlerRequest) -> TaskHandlerResponse:
827
+ task = request.task
828
+ amongo = request.amongo
829
+ extended_aredis = request.extended_aredis
830
+
831
+ # Process the task with direct request clients and return the final response.
832
+ result = await process_task(task, amongo=amongo, extended_aredis=extended_aredis)
833
+
834
+ return TaskHandlerResponse(
835
+ task=task,
836
+ success=result.success,
837
+ consuming_token=result.tokens_used,
838
+ exception_type=None if result.success else TaskExceptionType.UNKNOWN,
839
+ error=None if result.success else result.error,
840
+ )
841
+
842
+ # Initialize MongoDB and Redis connections
843
+ async def setup():
844
+ # Initialize MongoDB and Redis connections
845
+ amongo_client = AsyncIOMotorClient("mongodb://localhost:27017")
846
+ extended_aredis = ExtendedAsyncRedis("redis://localhost:6379")
847
+
848
+ # Create TaskClient instance
849
+ task_client = TaskClient(
850
+ token_consumption_handler=token_consumption_handler, # Or async_token_consumption_handler
851
+ task_handler=task_handler,
852
+ mongo_database="my_database", # Optional, defaults to "__keble_task__"
853
+ task_collection="tasks" # Optional, defaults to "__keble_task__task__"
854
+ )
855
+
856
+ return task_client, amongo_client, extended_aredis
857
+
858
+ # Create a new task
859
+ async def create_task(task_client, amongo):
860
+ task = await task_client.acreate(
861
+ amongo=amongo,
862
+ expected_token=5, # Expected tokens to consume
863
+ owner="user123", # Owner ID
864
+ task_type="image_processing", # Type of task
865
+ title="Process Image", # Optional title
866
+ metadata={"image_url": "https://example.com/image.jpg"}, # Optional metadata
867
+ progress_key="process_image_123", # Optional progress key
868
+ image=None, # Optional image URL
869
+ subtitle=None, # Optional subtitle
870
+ timeout_mins=120, # Optional timeout in minutes (default 120)
871
+ sharing_scope=SharingScope.PRIVATE, # Optional sharing scope
872
+ stage=TaskStage.PENDING, # Optional initial stage
873
+ attempts=0, # Optional initial attempts
874
+ consumed_token=0 # Optional initial consumed tokens
875
+ )
876
+ return task
877
+
878
+ # Start a task and route lifecycle events through the shared emitter
879
+ async def start_task(task_client, amongo, extended_aredis, task_id):
880
+ event_emitter = AgenticEventEmitter()
881
+
882
+ await task_client.astart(
883
+ amongo=amongo,
884
+ extended_aredis=extended_aredis,
885
+ task_id=task_id,
886
+ task_lifecycle_event_emitter=event_emitter,
887
+ )
888
+
889
+ return event_emitter
890
+
891
+ # Get a task by ID
892
+ async def get_task(task_client, amongo, task_id):
893
+ task = await task_client.aget(
894
+ amongo=amongo,
895
+ task_id=task_id,
896
+ task_type=None, # Optional filter by task type
897
+ include_childs=True, # Optional: return TaskMongoObjectExtended with childs
898
+ )
899
+ return task
900
+
901
+ # Get a task by owner and ID
902
+ async def get_owner_task(task_client, amongo, owner, task_id):
903
+ task = await task_client.aowner_get(
904
+ amongo=amongo,
905
+ owner=owner,
906
+ task_id=task_id,
907
+ task_type=None, # Optional filter by task type
908
+ sharing_scope=None, # Optional filter by sharing scope
909
+ include_childs=True, # Optional: return TaskMongoObjectExtended with childs
910
+ )
911
+ return task
912
+
913
+ # Get multiple tasks
914
+ async def get_multiple_tasks(task_client, amongo, extended_aredis):
915
+ tasks = await task_client.aget_multi(
916
+ amongo=amongo,
917
+ extended_aredis=extended_aredis,
918
+ skip=0,
919
+ limit=10,
920
+ task_types=["image_processing", "text_processing"], # Optional filter by task types
921
+ include_childs=True, # Optional: return a tree (roots + childs)
922
+ root_task=None, # Optional: filter to a specific root tree
923
+ parent_task=None, # Optional: list direct childs of a parent task
924
+ )
925
+ return tasks
926
+
927
+ # Get multiple tasks for an owner
928
+ async def get_owner_multiple_tasks(task_client, amongo, extended_aredis, owner):
929
+ tasks = await task_client.aowner_get_multi(
930
+ amongo=amongo,
931
+ extended_aredis=extended_aredis,
932
+ owner=owner,
933
+ skip=0,
934
+ limit=10,
935
+ task_types=["image_processing"], # Optional filter by task types
936
+ sharing_scopes=[SharingScope.PRIVATE], # Optional filter by sharing scopes
937
+ include_childs=True, # Optional: return a tree (roots + childs)
938
+ root_task=None, # Optional: filter to a specific root tree
939
+ parent_task=None, # Optional: list direct childs of a parent task
940
+ )
941
+ return tasks
942
+
943
+ # Run example
944
+ async def main():
945
+ task_client, amongo, extended_aredis = await setup()
946
+
947
+ # Create a task
948
+ task = await create_task(task_client, amongo)
949
+ print(f"Created task with ID: {task.id}")
950
+
951
+ # Start task with lifecycle events
952
+ await start_task(task_client, amongo, extended_aredis, task.id)
953
+ print("Started task with lifecycle events")
954
+
955
+ # Get task by ID
956
+ retrieved_task = await get_task(task_client, amongo, task.id)
957
+ print(f"Retrieved task: {retrieved_task.stage}")
958
+
959
+ # Get task by owner
960
+ owner_task = await get_owner_task(task_client, amongo, "user123", task.id)
961
+ print(f"Retrieved owner task: {owner_task.stage}")
962
+
963
+ # Get multiple tasks
964
+ tasks = await get_multiple_tasks(task_client, amongo, extended_aredis)
965
+ print(f"Retrieved {len(tasks)} tasks")
966
+
967
+ # Get multiple owner tasks
968
+ owner_tasks = await get_owner_multiple_tasks(task_client, amongo, extended_aredis, "user123")
969
+ print(f"Retrieved {len(owner_tasks)} owner tasks")
970
+
971
+ if __name__ == "__main__":
972
+ asyncio.run(main())
973
+ ```
974
+
975
+ ### Package Supporting Keble Task: Implementing a Task Handler
976
+
977
+ If you're developing a package that wants to support keble-task, you only need to provide a task handler function:
978
+
979
+ ```python
980
+ from keble_task import TaskHandlerRequest, TaskHandlerResponse, TaskExceptionType
981
+
982
+ async def my_package_task_handler(
983
+ request: TaskHandlerRequest,
984
+ ) -> TaskHandlerResponse | None:
985
+ task = request.task
986
+
987
+ # Return None only when this handler intentionally delegates completion to
988
+ # another process. The task runtime leaves the task PROCESSING in that case.
989
+ if task.task_type != "my_package_task_type":
990
+ return None
991
+
992
+ result = await process_task(
993
+ task,
994
+ amongo=request.amongo,
995
+ extended_aredis=request.extended_aredis,
996
+ event_emitter=request.event_emitter,
997
+ )
998
+
999
+ return TaskHandlerResponse(
1000
+ task=task,
1001
+ success=result.success,
1002
+ consuming_token=result.tokens_used,
1003
+ exception_type=None if result.success else TaskExceptionType.UNKNOWN,
1004
+ error=None if result.success else result.error,
1005
+ )
1006
+
1007
+ async def process_task(task, *, amongo, extended_aredis, event_emitter):
1008
+ # Implement your task processing logic here
1009
+ # ...
1010
+ return result
1011
+ ```
1012
+
1013
+ ## Error Handling
1014
+
1015
+ The package uses `keble_exceptions.KebleException` which will be thrown in various error scenarios. Specific task exceptions include:
1016
+
1017
+ - `TaskFailedToStartException`: When a task fails to start
1018
+ - `TaskNoSufficientDataException`: When there is insufficient data to process a task
1019
+ - `TaskTimeoutException`: When a task exceeds its timeout duration
1020
+ - `TaskUnknownException`: For general unexpected errors
1021
+
1022
+ Always handle these exceptions appropriately in your implementation.
1023
+
1024
+ ## Task Lifecycle
1025
+
1026
+ 1. **Creation**: Tasks are created with `acreate()` method
1027
+ 2. **Starting**: Tasks are started with `astart()` method
1028
+ 3. **Processing**: Tasks are processed by the task handler
1029
+ 4. **Completion**: The handler returns `TaskHandlerResponse` and the runtime finalizes success or failure.
1030
+
1031
+ The package automatically handles retries, timeouts, and token consumption based on the configuration provided.
1032
+
1033
+ ## Lifecycle And Progress Events
1034
+
1035
+ The active progress path is event-based:
1036
+
1037
+ 1. **Lifecycle**: `TaskClient` emits `KEBLE_TASK / TASK_STAGE_CHANGED` after persisted stage transitions.
1038
+ 2. **Domain progress**: Package handlers emit their own typed `AgenticActionEvent` payloads through `request.event_emitter`.
1039
+ 3. **Drain boundary**: `aemit()` schedules callbacks; `adrain()` is the deterministic side-effect boundary.
1040
+ 4. **Frontend delivery**: Backend bridges the same event envelope to owner-list and room websocket listeners.
1041
+
1042
+ `ProgressTask` may still appear in historical compatibility notes, but it is not
1043
+ the current task-handler progress transport.
1044
+
1045
+ ## Breaking API Changes in Version 0.0.5
1046
+
1047
+ This version introduces significant breaking changes from the previous version:
1048
+
1049
+ 1. **All Functions are Now Async**: All functions have been converted to asynchronous with an 'a' prefix (e.g., `acreate`, `astart`, `aget`)
1050
+
1051
+ 2. **Direct Resource Parameters**: Functions no longer accept a `TaskResources` object. Handler requests and token payloads carry `amongo`, `extended_aredis`, and optional graph/vector clients directly.
1052
+
1053
+ 3. **Async Handlers**: Task handlers should now be implemented as async functions. Token consumption handlers can be either synchronous or asynchronous.
1054
+
1055
+ 4. **MongoDB and Redis Requirements**: The package now requires the async versions of MongoDB (AsyncIOMotorClient) and Redis clients.
1056
+
1057
+ 5. **Progress Tracking**: Current progress uses `AgenticEventEmitter` and typed package events; older `ProgressTask` examples are historical only.
1058
+
1059
+ 6. **Token Consumption Handler**: The parameter `token_consumption_handler` now supports both synchronous and asynchronous implementations.
1060
+
1061
+ If you're upgrading from a previous version, you'll need to update all your code to follow these new conventions.
1062
+
1063
+ ## Dependency Documentation
1064
+
1065
+ This repo no longer stores copied dependency `README.md` snapshots under a local `readme/` directory.
1066
+ Read dependency documentation from the maintained source repos so local docs do not drift from released packages.
1067
+
1068
+ ## Mongo Startup Indexes
1069
+
1070
+ Backend startup should call `TaskClient.ainit(amongo=...)`. The method delegates
1071
+ to the existing package-owned task, task-relation, and task-cost index setup so
1072
+ APIs and workers do not create indexes inside hot read/write paths.
1073
+
1074
+ ## 2.4.17 Task-Cost Source Aggregation
1075
+
1076
+ `TaskCostCreate.from_task_usage(...)` is the canonical constructor for durable
1077
+ cost rows. It copies `owner`, `task_type`, and `root_task` from the stored task
1078
+ context and accepts first-class usage `source` without duplicating it as a tag.
1079
+
1080
+ `TaskCostAggregateResponse.build(...)` now groups by `TOTAL`, `TAG`, `SOURCE`,
1081
+ or `TASK_TYPE` through `TaskCostAggregateGroupBy`. List and aggregate reads
1082
+ continue to share `TaskCostFilterBase.to_mongo_filters()`, and runtime
1083
+ create/list/aggregate paths do not create indexes.