No project description provided
Project description
Keble Task
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.
Version 2.22.0 Update
- Added the cross-repo Keble pytest marker vocabulary and strict marker config.
- Default tests no longer fail at collection when
tests/.envis absent: Mongo/Redis-backed tests skip at fixture setup whenMONGO_DB_URIorREDIS_URIis not configured. - The live LLM agent-tool test now requires both
RUN_LLM_LIVE=1andLIVE_LLM_*credentials, so full local pytest does not spend model tokens by accident. - Added
AGENTS.md,CLAUDE.md, andpyrightconfig.jsonfor future test workers. - Removed the legacy custom classifier rejected by package upload validation.
Testing
Default fast test command:
uv run pytest -m "not live and not slow and not eval and not local_stack"
Run all non-live tests with local Mongo/Redis configured:
uv run pytest -q
Run the live LLM canary:
RUN_LLM_LIVE=1 uv run pytest -m "live and llm"
Run static syntax/type checking:
npx --yes pyright .
Version 2.21.0 Update
Task agent tool metadata now names
keble_helpers.AgentToolRegistrationConfig directly. Package code and tests use
AgentToolApprovalMode for approval semantics; do not reintroduce bool approval
aliases in task provider contracts.
Build packaging explicitly excludes local agent scratch directories
(.claude/, .worktrees/, .wt-discovery/) so private symlinks never enter
release artifacts.
Version 2.18.0 Update
Code-breaking agent-schema naming + placement refactor (no backward-compat
aliases), mirroring the shipped keble-positioning convention. Re-rebased onto
the settled origin/main 2.17.0 (the AgenticActionEventSource SCREAMING_SNAKE
realignment + the no-emit-without-drain static guard) and bumped above main to
2.18.0:
schemas.pyis now thekeble_task.schemasPACKAGE (schemas/__init__.pykeeps all prior exports identical) so agent tool I/O schemas can live in the newschemas/for_agent.py.- Renames:
TaskSummaryView->TaskSummaryForAgent,TaskQueryToolsConfig->TaskAgentQueryToolsConfig,TaskMutationToolsConfig->TaskAgentMutationToolsConfig(all three now DEFINED inschemas/for_agent.py). - Registrars moved to a new
keble_task.agent.toolssubpackage (behavior only, zeroBaseModel):tools/query.py::register_query_toolsandtools/mutation.py::register_mutation_tools. The old bareregister/register_action_toolsnames and theagent/registry.py/agent/query_registry.pymodules are REMOVED. - New convention guard
tests/schemas/test_for_agent.py.
Agentic schema convention (ForAgent + agent/tools/)
Every schema lands in exactly one bucket; the bucket dictates its NAME and its
LOCATION. This is enforced by tests/schemas/test_for_agent.py.
| Bucket | What it is | Name | Lives in |
|---|---|---|---|
| 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/ |
| 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 |
| Mutation action payload | the typed @agent.tool mutation input |
unchanged | keble_task/actions.py (external action module) |
| Persisted / CRUD / event | *Base/*Update/*MongoObject/*Event |
unchanged | schemas/ |
The registrars under agent/tools/ (mutation.py, query.py) are BEHAVIOR
ONLY — they import contracts from schemas/ (and actions.py) and define zero
BaseModel classes. Do NOT reintroduce *View/*Display names for agent tool
payloads.
Version 2.12.0 Update
Added TaskClient.apublic_list_indexable(task_types, stages, limit) -> list[TaskPublicRef]:
a lean, projected ({_id, updated}) public read backing consumer-side SEO sitemaps —
filters sharing_scope=PUBLIC + task_type $in + stage $in, newest-updated first,
capped by limit, no childs/redis. Backed by the new
sharing_scope_task_type_stage_updated_desc_idx (the package owns its indexes via
aensure_task_indexes). Also completed the latent apublic_get_multi overload contract
(stages/title_contains are now honored, mirroring aowner_get_multi).
Package Line
- Package version:
2.22.0 - Python baseline:
>=3.13,<3.14 - Runtime ownership:
TaskBase.languageis the canonical, task-owned output/report language; report-generating handlers and child-task creation readtask.languageinstead of parsing domain-specific metadata blobs (optional for back-compat)- flat task lists return root tasks unless
parent_taskis explicitly filtered TaskRelationType.MAPPEDrecords viewport-map edges without changing parentageTaskClient.aapply_actions(...)creates related tasks through a pure generic action layer; feature packages own their own typed toolsTaskClient.astart(...)is the single task-start entrypoint- uncaught handler exceptions are finalized into terminal
FAILURE - start locks are always cleared before the run returns
- package events use direct
keble_helpers.AgenticActionEventJSON; backend transports them without room-specific wrapper schemas TaskClientcan emit canonical task lifecycle events after persistedPROCESSING,SUCCESS,FAILURE, and timeout stage transitions- downstream TypeScript room consumers should use
keble-core 0.1.32+direct-event builders rather than task-owned workspace snapshots
Version 2.5.1 Update
- Hardened the agentic
mutate_task_workspacetool against ID hallucination:CreateRelatedTaskAction.parent_task_idandfrom_task_idsnow carry model-facingField(description=...)guidance to leave them unset (attach under the current task) unless real ids are provided — never invent ids. - The tool boundary (
agent/registry.py) now maps model-fixable action-validation errors (ClientSideInvalidParams/ClientSideMissingParams/ServerSideInvalidParamsfromaapply_actions, e.g. a hallucinatedparent_task_id) topydantic_ai.ModelRetry, so the model self-corrects instead of aborting the run. Wiring/infra faults still propagate. - Added a deterministic
FunctionModelself-correction test (badparentTaskId-> ModelRetry -> recover) and made the live LLM prompt explicit about omitting the ids. - Package metadata synced to
2.5.1acrosspyproject.toml,pyproject.poetry.toml,uv.lock.
Version 2.5.0 Update
- Added
TaskBase.language: Optional[Language]— a typed, task-owned output/report language so the generic task framework is the single source of truth for the language a handler should produce output in, instead of consumers digging it out of domain-specific metadata dicts. TaskClient.acreate(..., language=...)threads the value onto the created task.- Optional with a
Nonedefault, so legacy callers and legacy persisted documents still validate and load. - Package metadata synced to
2.5.0acrosspyproject.toml,pyproject.poetry.toml,uv.lock.
Version 2.4.23 Update
- Added owner/public root-list indexes —
CRUDTask.aensure_task_indexesnow also createsowner_parent_task_created_desc_idxandsharing_scope_parent_task_created_desc_idxsoaowner_get_multi/apublic_get_multiroot lists filterowner/sharing_scopeon an index instead of residually after aparent_taskscan (created atainit/startup). CREATE_RELATED_TASKis now self-healing: standalone Mongo has no multi-document transactions, so if a relation write fails after the child task is created, a saga compensation (_arollback_created_related_task) deletes the orphaned child and any partial relation rows, then re-raises — the workspace never keeps a child without its relations.- All package tests are pyright-clean (
pyright keble_taskandpyright testsboth 0 errors); removed stale trackeddist/keble_task-0.0.0-*build artifacts (dist/stays gitignored). - Package metadata synced to
2.4.23acrosspyproject.toml,pyproject.poetry.toml,uv.lock.
Version 2.4.22 Update
build_task_tree(...)now fails hard on corrupted persisted references instead of silently shrinking trees: a child whoseparent_taskis absent from the loaded root tree or a missing requested root raiseskeble_exceptions.ObjectNotFound, and a parent cycle raiseskeble_exceptions.DataIntegrityCompromised. The sibling sort was simplified to use the non-Optional persistedcreateddatetime (notry/except). Behavior change: list/tree endpoints surface corruption as typed errors rather than partial results.- The gated live agent-tool test guards its OpenAI-model imports with
pytest.importorskipso collection never fails where the openai extra is absent, and thetestdependency group now includespydantic-ai-slim[openai]souv run pytestcan run it whenLIVE_LLM_*creds are present. - Package metadata synced to
2.4.22acrosspyproject.toml,pyproject.poetry.toml, anduv.lock.
Version 2.4.18 Update
TaskCostAggregateResponse.build(...)now takescosts: Sequence[TaskCostBase](read-only/covariant) so CRUD'slist[TaskCostMongoObject]is accepted without a list-invariance type error, and folds rows through new typedTaskCostAggregateBucket.empty()/accumulate()methods instead of an untypeddict[str, Any]accumulator — bucket math lives on the type that owns it.- The task collection now has tree/stage read indexes via
CRUDTask.aensure_task_indexes(...)(root_task+created,parent_task+created,stage+created), wired intoTaskClient.aensure_indexes(...)/ainit(...)so root-tree, root-list, and retry/timeout sweeps are index-backed (idempotent). TaskMetadata = dict[str, JsonValue]is the single strongly-typed contract for free-form task metadata;TaskBase.metadataand theTaskClientmetadata params use it instead of a baredict.- The agent registrar uses
AgentToolRegistrationConfigdirectly, removing the old local config shadowing that causedToolFuncContextassignment errors. - Added tests for timeout lifecycle, the processing guard, zero-usage cost rollup,
parent+child cost rollup by root, and a gated live-LLM
mutate_task_workspaceagent-tool run (LIVE_LLM_*creds intests/.env).
Version 2.4.16 Update
TaskCostCreate.from_task_usage(...)is the canonical cost-row constructor; it denormalizes owner and task type from the persisted task row.TaskCostAggregateResponse.build(...)owns in-memory response aggregation from already-filtered rows.- Runtime task-cost create/list/aggregate paths no longer create Mongo
indexes; callers must use
TaskClient.ainit(...)or startup wiring. TaskHandlerRequestinheritsAgentDbDeps, sousage_recordertravels with the same DB-rooted request object without making the task package own pricing or persistence policy outside cost rows.
Version 2.4.15 Update
TaskCostFilterBaseis now the single source of truth for task-cost read filters shared by list and aggregate requests.TaskCostListRequestowns only pagination fields on top of the shared filter contract.TaskCostAggregateRequestowns only aggregation controls on top of the shared filter contract.- Task-cost tag filters remain all-tags filters: every requested tag must be present on the cost row.
- Package metadata is synchronized across
pyproject.toml,pyproject.poetry.toml, anduv.lock.
Version 2.4.14 Update
TaskClient.ainit(amongo=...)exposes the package-owned Mongo index setup as a backend startup hook over existingaensure_indexes(...).
Version 2.4.13 Update
- Added separate task-cost storage through
TaskCostBase,TaskCostCreate,TaskCostMongoObject,TaskCostListRequest, andTaskCostAggregateRequest. TaskClient.acreate_task_cost(...)now denormalizes root task, owner, and task type from the persisted task row before writing the cost row.TaskClient.alist_task_costs(...)andTaskClient.aaggregate_task_costs(...)provide indexed admin reporting with total/hour/day/week/month buckets and optional tag fan-out.- Aggregation uses
RunUsage, per-millionMoneyrates, and caller-providedExchangeRateInUsdvalues while preserving sub-cent Decimal token costs. TaskClient.aensure_indexes(...)creates public-id, relation, and task-cost indexes from one startup-friendly entrypoint.
Version 2.4.12 Update
keble-tasknow declarestenacity>=9,<10.0.0directly because the task runtime imports it for handler retry.TaskClient.acreate_task_relations(...)rejects duplicate relation edge payloads before inserting any row.TaskClient.aapply_actions(...)rejects duplicatefrom_task_idsbefore creating the child task, preventing partial child-task persistence when an action would generate duplicate relation edges.- Previously skipped start/retry/timeout tests now exercise the real async
handler path with
ExtendedAsyncRedis.
Version 2.4.11 Update
- Added
TaskEventType.TASK_STAGE_CHANGEDas the package-owned lifecycle event for task stage transitions. - Added
TaskLifecycleEventPayload(task=TaskMongoObject)andTaskLifecycleEvent, reusing the persisted task schema instead of adding UI-specific state payloads. TaskClient.astart(...),aon_task_processing(...),aon_task_success(...),aon_task_failure(...), andaon_task_timeout(...)can emit lifecycle events after DB updates reload the final task row.- Consumers should use lifecycle events for room task state, while
CREATE_RELATED_TASKevents remain the canonical child/relation creation events.
Version 2.4.8 Update
CreateRelatedTaskActionrelation rows now persistaction.metadataonTaskRelationCreate.metadatainstead of writing{}.TaskActionCreatedRelationremains a slim public DTO but now includes relation metadata so downstream event consumers can render edge context.- No new relation schema was added;
TaskRelationBase.metadatais the canonical extension point for task-action relation annotations.
Version 2.4.9 Update
- Relation metadata now uses the package-owned
TaskRelationMetadataJSON-safe contract before persistence and event emission. CreateRelatedTaskAction.metadata,TaskRelationBase.metadata, andTaskActionCreatedRelation.metadatanormalize BSON ObjectIds, datetimes, and Pydantic models into JSON-compatible values.- Unsupported runtime objects are rejected at the metadata boundary instead of
leaking into SSE or TypeScript
JsonObjectconsumers.
Version 2.4.10 Update
- Unsupported relation metadata now raises
ValueErrorinside the JSON fallback so Pydantic wraps model construction failures asValidationError. - Regression coverage protects both
CreateRelatedTaskAction.metadataandTaskRelationCreate.metadatafrom surfacing rawTypeError.
Installation
pip install keble-task
Core Concepts
TaskClient
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).
astart(...) now owns the terminal failure boundary for task execution:
- it loads the task and checks the Redis start lock
- it moves the task to
PROCESSING - it runs the configured handler with retry support
- if the handler still raises, it writes a terminal
FAILURErow unless the handler already finalized the task - it always clears the Redis start lock before returning
Task Tree And Relations
Task trees and task relations are intentionally separate:
root_taskscopes the whole workspace.parent_taskstores the one canonical creator/trigger edge.TaskRelationMongoObjectstores extra lineage edges such as multi-sourceREDUCEDchildren and viewport-mapMAPPEDchildren.
Flat task list APIs now return only root tasks by default. Use include_childs=True
for task trees, parent_task=<id> for direct child listing, and relation APIs for
non-tree edges.
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.
Task Room Resolution
Task-room identity is intentionally generic and task-tree-only:
TaskClient.aresolve_task_room(...)accepts any owner-visible task id.- It returns:
root_task_idrequested_task_id
- The package does not know feature focus shape, selection metadata, or chat state.
- Backend/frontend layers should use the returned root task id as room identity and keep feature-specific metadata outside this package.
Task Room Graph Context
TaskRoomGraphContext is the compact agent-facing room graph. It is generic:
task ids, parent edges, task type/title/subtitle, and sidecar MAPPED /
REDUCED relation edges. It does not include positioning ids, grid ids, UX
selection, or frontend state.
context = await task_client.aget_task_room_graph_context(
amongo=amongo,
owner=owner,
root_task_id=root_task_id,
focused_task_id=focused_task_id,
)
prompt_text = context.to_prompt_text()
relations = await task_client.acreate_task_relations(
amongo=amongo,
objs_in=[
TaskRelationCreate(
root_task=root.id,
from_task_id=source_a.id,
to_task_id=child.id,
relation_type=TaskRelationType.REDUCED,
),
TaskRelationCreate(
root_task=root.id,
from_task_id=source_b.id,
to_task_id=child.id,
relation_type=TaskRelationType.REDUCED,
),
TaskRelationCreate(
root_task=root.id,
from_task_id=source_a.id,
to_task_id=mapped_view_task.id,
relation_type=TaskRelationType.MAPPED,
),
],
)
root_relations = await task_client.alist_task_relations_by_root(
amongo=amongo,
root_task=root.id,
)
Task Cost Tracking
Task costs are stored in a dedicated collection and are denormalized from the
task row at creation time. The task collection stays focused on execution state,
while cost reports can filter by task, root task, owner, task type, tags, and
occurred_at windows.
result = await agent.run(prompt, deps=deps)
await task_client.acreate_task_cost(
amongo=amongo,
task_id=task.id,
tags=["positioning", "classify_cells"],
run_usage=result.usage,
token_rates_per_million=TaskCostTokenRates(
input_tokens=Money(float_money=1.25, currency=Currency.USD),
output_tokens=Money(float_money=10.00, currency=Currency.USD),
),
additional_cost=Money(float_money=0.01, currency=Currency.USD),
seconds=12,
retry=max(task.attempts - 1, 0),
metadata={"model": "gateway/openai:gpt-5.2"},
)
Task Handler
For packages that want to support keble-task, implementing a task handler function is required rather than creating a TaskClient instance.
Task Lifecycle Events
ProgressTask is not the current task-handler transport. Handlers receive a
TaskHandlerRequest, read DB clients and cross-cutting context directly from that
request, and return TaskHandlerResponse | None.
Task lifecycle and domain progress are emitted through the shared
AgenticEventEmitter on request.event_emitter. The emitter is intentionally
detached: aemit() schedules callbacks and the task runtime drains before the
execution boundary returns.
Schemas
The package provides several schemas for working with tasks:
TaskStage
An enum representing the different stages of a task:
class TaskStage(str, Enum):
PENDING = "PENDING" # Task is created but not yet started
PROCESSING = "PROCESSING" # Task is currently being processed
SUCCESS = "SUCCESS" # Task completed successfully
FAILURE = "FAILURE" # Task failed to complete
Tasks in PENDING or PROCESSING stages can be started or restarted.
SharingScope
An enum defining the visibility scope of a task:
class SharingScope(str, Enum):
PRIVATE = "PRIVATE" # Only accessible by the owner
PUBLIC = "PUBLIC" # Accessible by anyone
TaskBase
The base schema for task data:
class TaskBase(SchemaBase):
# Error information
error: Optional[str] = None
exception_type: Optional[TaskExceptionType] = None
# Current task stage
stage: TaskStage
# Task type identifier
task_type: str
# Optional progress tracking key
progress_key: Optional[str]
# Display information
title: Optional[str]
subtitle: Optional[str]
image: Optional[str]
# Custom metadata for the task
metadata: Optional[dict]
# Access control
sharing_scope: SharingScope = SharingScope.PRIVATE
status: Status = Status.ACTIVE
owner: str
# Timestamp tracking
started_ts: Optional[Timestamp] = None
success_ts: Optional[Timestamp] = None
failure_ts: Optional[Timestamp] = None
# Token management
expected_token: int # Expected tokens to consume
consumed_token: int = 0 # Actual tokens consumed
# Retry and timeout handling
attempts: int = 0 # Number of attempts so far
timeout_mins: int = 120 # Timeout in minutes
The TaskBase provides two helpful properties:
unfinshed_timeout: ReturnsTrueif the task has timed out but is still inPENDINGorPROCESSINGstageallow_to_retry: ReturnsTrueif the task has fewer than 3 attempts and is in a stage that allows starting
TaskUpdate
A schema for the fields an owner may patch post-creation. Unset fields are not
written (aowner_update dumps with exclude_unset), so a partial patch never
clobbers other columns. image reuses the exact same URL validation as creation
(TaskBase.validate_image_url), so an edit can never store an image creation
would have rejected.
class TaskUpdate(BaseModel):
sharing_scope: Optional[SharingScope] = None
title: Optional[str] = None
image: Optional[str] = None # owner thumbnail; full http(s) URL only
TaskMongoObject
The MongoDB document representation of a task:
class TaskMongoObject(MongoObjectBase, TaskBase):
pass
This combines TaskBase with MongoDB-specific functionality from MongoObjectBase.
TaskRelationMongoObject
The MongoDB document representation of extra lineage between tasks in one workspace:
class TaskRelationType(str, Enum):
MAPPED = "MAPPED"
REDUCED = "REDUCED"
class TaskRelationBase(SchemaBase):
root_task: ObjectId
from_task_id: ObjectId
to_task_id: ObjectId
relation_type: TaskRelationType
metadata: dict[str, Any] = Field(default_factory=dict)
class TaskRelationMongoObject(MongoObjectBase, TaskRelationBase):
pass
Relation rows do not modify root_task, parent_task, or task status. They are sidecar lineage edges for backend/frontend composition.
Generic Task Actions
Use TaskActions when an agent or backend API needs task-owned mutations without feature-specific side effects.
out = await task_client.aapply_actions(
amongo=amongo,
current_task=source_task,
payload=TaskActions(
message="Create a related child task.",
actions=[
CreateRelatedTaskAction(
relation_type=TaskRelationType.MAPPED,
task_type="POSITIONING",
title="Related workspace",
metadata={"source": "agent"},
)
],
),
extended_aredis=extended_aredis,
)
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.
Action results now expose slim DTOs instead of full Mongo rows:
TaskActionCreatedTaskcarriestask_id, root/parent ids, task type, stage, title, and progress key.TaskActionCreatedRelationcarries relation id, root/source/target ids, and relation type.- Internal
TaskMongoObjectandTaskRelationMongoObjectremain the persistence models, but tool/action payloads should not return them directly.
Agent Tool Registration
Task owns the pydantic-ai registrar for generic task workspace mutations. Backend should register this package tool instead of defining a manual duplicate in backend chat code.
from keble_task.agent import TaskAgentDeps, register_mutation_tools
agent = Agent[TaskAgentDeps, Any](...)
register_mutation_tools(
agent,
task_client=task_client,
)
Deps shape:
TaskAgentDepsinheritskeble_db.AgentDbDeps; Mongo/Redis are not separate tool args.- Task runtime state is under
ctx.deps.task. - The tool delegates to
TaskClient.aapply_actions(...), so task parentage, generic relations, and task action events stay package-owned.
Native Chat Tool Provider (query tools)
The package also owns the READ/QUERY registrar (register_query_tools: owner-scoped
list_tasks / get_task) and, since 2.7.0, ships it as a NATIVE
keble_helpers.ChatToolProviderProtocol provider so hosts no longer need a
generic adapter around the registrar:
from keble_task.agent import TaskQueryChatToolProvider
provider = TaskQueryChatToolProvider(
task_client=task_client,
tools_config=None, # optional TaskAgentQueryToolsConfig | dict overrides
)
# A chat host composes providers declaratively; register attaches the tools.
provider.register(agent=agent, context=None)
Contract notes:
provider.provider_idis EXACTLY"task_query"— it is recorded in room diagnostics and mapped to a user-readable label by the frontend; do not change it.- All deps are captured at construction;
register(*, agent, context)ignorescontext, which the cross-repo protocol explicitly permits.
Task Handler Dependencies
The retired TaskResources bag is no longer part of the active handler API.
TaskHandlerRequest inherits keble_db.AgentDbDeps, so handlers read clients and
cross-cutting context directly from the request:
request.amongo
request.extended_aredis
request.aneo4j
request.qdrant_client
request.event_emitter
request.usage_recorder
The event_emitter field is the canonical, single-source channel for task
lifecycle and package-owned domain events. Package handlers emit typed
AgenticActionEvent values through this emitter; the task runtime drains it
before returning from lifecycle boundaries so owner-list and room listeners see
deterministic terminal state.
TaskHandlerRequest
A schema for task handling requests passed to task handlers:
class TaskHandlerRequest(AgentDbDeps):
model_config = PydanticModelConfig.default(arbitrary_types_allowed=True)
task: TaskMongoObject # The task to be processed
metadata: TaskMetadata | None = None
Handlers return TaskHandlerResponse when the current process completed the
task. Handlers return None only when completion is intentionally delegated to a
separate process; in that case the runtime leaves the task in PROCESSING until
the delegate finalizes it.
TaskHandlerResponse
A schema for responses returned from task handlers:
class TaskHandlerResponse(BaseModel):
model_config = PydanticModelConfig.default()
task: TaskMongoObject # The processed task
success: bool # Whether the task was successful
consuming_token: int # Actual tokens consumed
exception_type: Optional[TaskExceptionType] = None # Type of exception if failed
error: Optional[str] = None # Error message if failed
TokenConsumptionType
An enum representing token consumption actions:
class TokenConsumptionType(str, Enum):
CONSUME = "CONSUME" # Consume tokens
RECOVER = "RECOVER" # Recover (return) tokens
TokenConsumptionPayload
A schema for token consumption operations:
class TokenConsumptionPayload(BaseModel):
model_config = PydanticModelConfig.default(arbitrary_types_allowed=True)
consumption_type: TokenConsumptionType # Whether to consume or recover tokens
owner: str # The owner of the tokens
token: int # The amount of tokens to consume or recover
task_id: ObjectId | None = None
amongo: AsyncIOMotorClient
extended_aredis: ExtendedAsyncRedis
aneo4j: Neo4jAsyncDriver | None = None
metadata: TaskMetadata | None = None
TaskExceptionType
An enum representing different types of task exceptions:
class TaskExceptionType(str, Enum):
UNKNOWN = "UNKNOWN" # Unknown error
FAILED_TO_START = "FAILED_TO_START" # Task failed to start
NO_SUFFICIENT_DATA = "NO_SUFFICIENT_DATA" # Insufficient data to process task
TIMEOUT = "TIMEOUT" # Task timed out
TaskException
Base exception class for task-related errors:
class TaskException(KebleException):
def __init__(
self,
*,
exception_type: TaskExceptionType = TaskExceptionType.UNKNOWN,
error: Optional[str] = None,
):
self.exception_type = exception_type
self.error = error
# Inherits from KebleException
Subclasses of TaskException:
- TaskNoSufficientDataException: Raised when there is insufficient data to process a task
- TaskFailedToStartException: Raised when a task fails to start
- TaskTimeoutException: Raised when a task times out
- TaskUnknownException: Raised for unknown errors
Difficulty
A utility enum for representing task difficulty levels:
class Difficulty(str, Enum):
EASY = "EASY"
MEDIUM = "MEDIUM"
HARD = "HARD"
Examples
Backend API: Creating a TaskClient
import asyncio
from keble_task import (
TaskClient,
TaskHandlerRequest,
TaskHandlerResponse,
TokenConsumptionPayload,
TokenConsumptionType,
TaskExceptionType,
TaskStage,
)
from keble_helpers import AgenticEventEmitter, SharingScope
from motor.motor_asyncio import AsyncIOMotorClient
from keble_db import ExtendedAsyncRedis
# Define a token consumption handler (synchronous or asynchronous)
def token_consumption_handler(payload: TokenConsumptionPayload) -> bool:
# Access DB clients directly from the payload; there is no resources bag.
amongo = payload.amongo
extended_aredis = payload.extended_aredis
# Implement logic to handle token consumption or recovery
if payload.consumption_type == TokenConsumptionType.CONSUME:
print(f"Consuming {payload.token} tokens for {payload.owner}")
else: # TokenConsumptionType.RECOVER
print(f"Recovering {payload.token} tokens for {payload.owner}")
return True
# Alternatively, you can define an asynchronous token consumption handler
async def async_token_consumption_handler(payload: TokenConsumptionPayload) -> bool:
amongo = payload.amongo
extended_aredis = payload.extended_aredis
if payload.consumption_type == TokenConsumptionType.CONSUME:
print(f"Async consuming {payload.token} tokens for {payload.owner}")
else: # TokenConsumptionType.RECOVER
print(f"Async recovering {payload.token} tokens for {payload.owner}")
return True
# Define an async task handler
async def task_handler(request: TaskHandlerRequest) -> TaskHandlerResponse:
task = request.task
amongo = request.amongo
extended_aredis = request.extended_aredis
# Process the task with direct request clients and return the final response.
result = await process_task(task, amongo=amongo, extended_aredis=extended_aredis)
return TaskHandlerResponse(
task=task,
success=result.success,
consuming_token=result.tokens_used,
exception_type=None if result.success else TaskExceptionType.UNKNOWN,
error=None if result.success else result.error,
)
# Initialize MongoDB and Redis connections
async def setup():
# Initialize MongoDB and Redis connections
amongo_client = AsyncIOMotorClient("mongodb://localhost:27017")
extended_aredis = ExtendedAsyncRedis("redis://localhost:6379")
# Create TaskClient instance
task_client = TaskClient(
token_consumption_handler=token_consumption_handler, # Or async_token_consumption_handler
task_handler=task_handler,
mongo_database="my_database", # Optional, defaults to "__keble_task__"
task_collection="tasks" # Optional, defaults to "__keble_task__task__"
)
return task_client, amongo_client, extended_aredis
# Create a new task
async def create_task(task_client, amongo):
task = await task_client.acreate(
amongo=amongo,
expected_token=5, # Expected tokens to consume
owner="user123", # Owner ID
task_type="image_processing", # Type of task
title="Process Image", # Optional title
metadata={"image_url": "https://example.com/image.jpg"}, # Optional metadata
progress_key="process_image_123", # Optional progress key
image=None, # Optional image URL
subtitle=None, # Optional subtitle
timeout_mins=120, # Optional timeout in minutes (default 120)
sharing_scope=SharingScope.PRIVATE, # Optional sharing scope
stage=TaskStage.PENDING, # Optional initial stage
attempts=0, # Optional initial attempts
consumed_token=0 # Optional initial consumed tokens
)
return task
# Start a task and route lifecycle events through the shared emitter
async def start_task(task_client, amongo, extended_aredis, task_id):
event_emitter = AgenticEventEmitter()
await task_client.astart(
amongo=amongo,
extended_aredis=extended_aredis,
task_id=task_id,
task_lifecycle_event_emitter=event_emitter,
)
return event_emitter
# Get a task by ID
async def get_task(task_client, amongo, task_id):
task = await task_client.aget(
amongo=amongo,
task_id=task_id,
task_type=None, # Optional filter by task type
include_childs=True, # Optional: return TaskMongoObjectExtended with childs
)
return task
# Get a task by owner and ID
async def get_owner_task(task_client, amongo, owner, task_id):
task = await task_client.aowner_get(
amongo=amongo,
owner=owner,
task_id=task_id,
task_type=None, # Optional filter by task type
sharing_scope=None, # Optional filter by sharing scope
include_childs=True, # Optional: return TaskMongoObjectExtended with childs
)
return task
# Get multiple tasks
async def get_multiple_tasks(task_client, amongo, extended_aredis):
tasks = await task_client.aget_multi(
amongo=amongo,
extended_aredis=extended_aredis,
skip=0,
limit=10,
task_types=["image_processing", "text_processing"], # Optional filter by task types
include_childs=True, # Optional: return a tree (roots + childs)
root_task=None, # Optional: filter to a specific root tree
parent_task=None, # Optional: list direct childs of a parent task
)
return tasks
# Get multiple tasks for an owner
async def get_owner_multiple_tasks(task_client, amongo, extended_aredis, owner):
tasks = await task_client.aowner_get_multi(
amongo=amongo,
extended_aredis=extended_aredis,
owner=owner,
skip=0,
limit=10,
task_types=["image_processing"], # Optional filter by task types
sharing_scopes=[SharingScope.PRIVATE], # Optional filter by sharing scopes
include_childs=True, # Optional: return a tree (roots + childs)
root_task=None, # Optional: filter to a specific root tree
parent_task=None, # Optional: list direct childs of a parent task
)
return tasks
# Run example
async def main():
task_client, amongo, extended_aredis = await setup()
# Create a task
task = await create_task(task_client, amongo)
print(f"Created task with ID: {task.id}")
# Start task with lifecycle events
await start_task(task_client, amongo, extended_aredis, task.id)
print("Started task with lifecycle events")
# Get task by ID
retrieved_task = await get_task(task_client, amongo, task.id)
print(f"Retrieved task: {retrieved_task.stage}")
# Get task by owner
owner_task = await get_owner_task(task_client, amongo, "user123", task.id)
print(f"Retrieved owner task: {owner_task.stage}")
# Get multiple tasks
tasks = await get_multiple_tasks(task_client, amongo, extended_aredis)
print(f"Retrieved {len(tasks)} tasks")
# Get multiple owner tasks
owner_tasks = await get_owner_multiple_tasks(task_client, amongo, extended_aredis, "user123")
print(f"Retrieved {len(owner_tasks)} owner tasks")
if __name__ == "__main__":
asyncio.run(main())
Package Supporting Keble Task: Implementing a Task Handler
If you're developing a package that wants to support keble-task, you only need to provide a task handler function:
from keble_task import TaskHandlerRequest, TaskHandlerResponse, TaskExceptionType
async def my_package_task_handler(
request: TaskHandlerRequest,
) -> TaskHandlerResponse | None:
task = request.task
# Return None only when this handler intentionally delegates completion to
# another process. The task runtime leaves the task PROCESSING in that case.
if task.task_type != "my_package_task_type":
return None
result = await process_task(
task,
amongo=request.amongo,
extended_aredis=request.extended_aredis,
event_emitter=request.event_emitter,
)
return TaskHandlerResponse(
task=task,
success=result.success,
consuming_token=result.tokens_used,
exception_type=None if result.success else TaskExceptionType.UNKNOWN,
error=None if result.success else result.error,
)
async def process_task(task, *, amongo, extended_aredis, event_emitter):
# Implement your task processing logic here
# ...
return result
Error Handling
The package uses keble_exceptions.KebleException which will be thrown in various error scenarios. Specific task exceptions include:
TaskFailedToStartException: When a task fails to startTaskNoSufficientDataException: When there is insufficient data to process a taskTaskTimeoutException: When a task exceeds its timeout durationTaskUnknownException: For general unexpected errors
Always handle these exceptions appropriately in your implementation.
Task Lifecycle
- Creation: Tasks are created with
acreate()method - Starting: Tasks are started with
astart()method - Processing: Tasks are processed by the task handler
- Completion: The handler returns
TaskHandlerResponseand the runtime finalizes success or failure.
The package automatically handles retries, timeouts, and token consumption based on the configuration provided.
Lifecycle And Progress Events
The active progress path is event-based:
- Lifecycle:
TaskClientemitsKEBLE_TASK / TASK_STAGE_CHANGEDafter persisted stage transitions. - Domain progress: Package handlers emit their own typed
AgenticActionEventpayloads throughrequest.event_emitter. - Drain boundary:
aemit()schedules callbacks;adrain()is the deterministic side-effect boundary. - Frontend delivery: Backend bridges the same event envelope to owner-list and room websocket listeners.
ProgressTask may still appear in historical compatibility notes, but it is not
the current task-handler progress transport.
Breaking API Changes in Version 0.0.5
This version introduces significant breaking changes from the previous version:
-
All Functions are Now Async: All functions have been converted to asynchronous with an 'a' prefix (e.g.,
acreate,astart,aget) -
Direct Resource Parameters: Functions no longer accept a
TaskResourcesobject. Handler requests and token payloads carryamongo,extended_aredis, and optional graph/vector clients directly. -
Async Handlers: Task handlers should now be implemented as async functions. Token consumption handlers can be either synchronous or asynchronous.
-
MongoDB and Redis Requirements: The package now requires the async versions of MongoDB (AsyncIOMotorClient) and Redis clients.
-
Progress Tracking: Current progress uses
AgenticEventEmitterand typed package events; olderProgressTaskexamples are historical only. -
Token Consumption Handler: The parameter
token_consumption_handlernow supports both synchronous and asynchronous implementations.
If you're upgrading from a previous version, you'll need to update all your code to follow these new conventions.
Dependency Documentation
This repo no longer stores copied dependency README.md snapshots under a local readme/ directory.
Read dependency documentation from the maintained source repos so local docs do not drift from released packages.
Mongo Startup Indexes
Backend startup should call TaskClient.ainit(amongo=...). The method delegates
to the existing package-owned task, task-relation, and task-cost index setup so
APIs and workers do not create indexes inside hot read/write paths.
2.4.17 Task-Cost Source Aggregation
TaskCostCreate.from_task_usage(...) is the canonical constructor for durable
cost rows. It copies owner, task_type, and root_task from the stored task
context and accepts first-class usage source without duplicating it as a tag.
TaskCostAggregateResponse.build(...) now groups by TOTAL, TAG, SOURCE,
or TASK_TYPE through TaskCostAggregateGroupBy. List and aggregate reads
continue to share TaskCostFilterBase.to_mongo_filters(), and runtime
create/list/aggregate paths do not create indexes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keble_task-2.22.0.tar.gz.
File metadata
- Download URL: keble_task-2.22.0.tar.gz
- Upload date:
- Size: 324.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dbc1f2b0f30ee2a5e28f9fc2109558243a7cbc3097565cceff9666afe6b375d6
|
|
| MD5 |
8f648fb62d8e388260d4aa75ed08d008
|
|
| BLAKE2b-256 |
c898bbf73e78ab1c457dd39f356f776c4caa234086d9821c945417efc23734b4
|
File details
Details for the file keble_task-2.22.0-py3-none-any.whl.
File metadata
- Download URL: keble_task-2.22.0-py3-none-any.whl
- Upload date:
- Size: 65.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d2d9572c25758a96f567bc3df5385d3497775621e3a523c5989d38a3d9ff2d6
|
|
| MD5 |
dbe79aa1a02f13d7576b71ee77d5779c
|
|
| BLAKE2b-256 |
7c85842badccea1d14d4ee56b7c134d631d245e6f0d087671e9e7b060647caf3
|