memex-python 0.13.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.
- memex/__init__.py +336 -0
- memex/_time.py +26 -0
- memex/_uuid.py +62 -0
- memex/bulk.py +138 -0
- memex/commands.py +75 -0
- memex/envelope.py +69 -0
- memex/errors.py +51 -0
- memex/factories.py +97 -0
- memex/graph.py +30 -0
- memex/integrity.py +317 -0
- memex/intent.py +318 -0
- memex/models.py +271 -0
- memex/query.py +435 -0
- memex/reducer.py +151 -0
- memex/replay.py +144 -0
- memex/retrieval.py +266 -0
- memex/schemas.py +67 -0
- memex/serialization.py +47 -0
- memex/stats.py +71 -0
- memex/store.py +222 -0
- memex/task.py +361 -0
- memex/transplant.py +480 -0
- memex_python-0.13.0.dist-info/METADATA +150 -0
- memex_python-0.13.0.dist-info/RECORD +26 -0
- memex_python-0.13.0.dist-info/WHEEL +4 -0
- memex_python-0.13.0.dist-info/licenses/LICENSE +190 -0
memex/store.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""``MemexStore`` — an optional stateful, OO facade over the functional core.
|
|
2
|
+
|
|
3
|
+
The functional API (``apply_command`` / ``get_items`` / ...) is the backbone and
|
|
4
|
+
stays pure. ``MemexStore`` holds the three graph states, rebinds them on each
|
|
5
|
+
mutation, and returns the emitted lifecycle events — convenient for agents and
|
|
6
|
+
daemons that don't want to thread ``state =`` through every call.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import bulk, integrity, query, retrieval, serialization, stats
|
|
14
|
+
from .factories import create_edge, create_memory_item
|
|
15
|
+
from .graph import GraphState, create_graph_state
|
|
16
|
+
from .integrity import Contradiction, StaleItem
|
|
17
|
+
from .intent import (
|
|
18
|
+
Intent,
|
|
19
|
+
IntentLifecycleEvent,
|
|
20
|
+
IntentState,
|
|
21
|
+
apply_intent_command,
|
|
22
|
+
create_intent,
|
|
23
|
+
create_intent_state,
|
|
24
|
+
get_intents,
|
|
25
|
+
)
|
|
26
|
+
from .models import Edge, MemoryItem, MemoryLifecycleEvent, ScoredItem
|
|
27
|
+
from .reducer import apply_command
|
|
28
|
+
from .retrieval import SupportNode
|
|
29
|
+
from .stats import GraphStats
|
|
30
|
+
from .task import (
|
|
31
|
+
Task,
|
|
32
|
+
TaskLifecycleEvent,
|
|
33
|
+
TaskState,
|
|
34
|
+
apply_task_command,
|
|
35
|
+
create_task,
|
|
36
|
+
create_task_state,
|
|
37
|
+
get_tasks,
|
|
38
|
+
)
|
|
39
|
+
from .transplant import ImportReport, MemexExport, export_slice, import_slice
|
|
40
|
+
|
|
41
|
+
__all__ = ["MemexStore"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MemexStore:
|
|
45
|
+
"""A mutable container over the Memory / Intent / Task graphs."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
mem: GraphState | None = None,
|
|
50
|
+
intents: IntentState | None = None,
|
|
51
|
+
tasks: TaskState | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self.mem = mem if mem is not None else create_graph_state()
|
|
54
|
+
self.intents = intents if intents is not None else create_intent_state()
|
|
55
|
+
self.tasks = tasks if tasks is not None else create_task_state()
|
|
56
|
+
|
|
57
|
+
# -- memory mutations ---------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def apply(self, cmd: Any) -> list[MemoryLifecycleEvent]:
|
|
60
|
+
result = apply_command(self.mem, cmd)
|
|
61
|
+
self.mem = result.state
|
|
62
|
+
return result.events
|
|
63
|
+
|
|
64
|
+
def create(self, **kwargs: Any) -> MemoryItem:
|
|
65
|
+
item = create_memory_item(**kwargs)
|
|
66
|
+
self.apply({"type": "memory.create", "item": item})
|
|
67
|
+
return item
|
|
68
|
+
|
|
69
|
+
def add(self, item: MemoryItem) -> MemoryItem:
|
|
70
|
+
self.apply({"type": "memory.create", "item": item})
|
|
71
|
+
return item
|
|
72
|
+
|
|
73
|
+
def update(self, item_id: str, partial: dict[str, Any], author: str, reason: str | None = None) -> list[MemoryLifecycleEvent]:
|
|
74
|
+
return self.apply({"type": "memory.update", "item_id": item_id, "partial": partial, "author": author, "reason": reason})
|
|
75
|
+
|
|
76
|
+
def retract(self, item_id: str, author: str, reason: str | None = None) -> list[MemoryLifecycleEvent]:
|
|
77
|
+
return self.apply({"type": "memory.retract", "item_id": item_id, "author": author, "reason": reason})
|
|
78
|
+
|
|
79
|
+
def add_edge(self, **kwargs: Any) -> Edge:
|
|
80
|
+
edge = create_edge(**kwargs)
|
|
81
|
+
self.apply({"type": "edge.create", "edge": edge})
|
|
82
|
+
return edge
|
|
83
|
+
|
|
84
|
+
# -- memory queries -----------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def items(self, filter: Any = None, options: Any = None) -> list[MemoryItem]:
|
|
87
|
+
return query.get_items(self.mem, filter, options)
|
|
88
|
+
|
|
89
|
+
def item(self, id: str) -> MemoryItem | None:
|
|
90
|
+
return query.get_item_by_id(self.mem, id)
|
|
91
|
+
|
|
92
|
+
def scored(self, weights: Any, options: Any = None) -> list[ScoredItem]:
|
|
93
|
+
return query.get_scored_items(self.mem, weights, options)
|
|
94
|
+
|
|
95
|
+
def edges(self, filter: Any = None) -> list[Edge]:
|
|
96
|
+
return query.get_edges(self.mem, filter)
|
|
97
|
+
|
|
98
|
+
def parents(self, item_id: str) -> list[MemoryItem]:
|
|
99
|
+
return query.get_parents(self.mem, item_id)
|
|
100
|
+
|
|
101
|
+
def children(self, item_id: str) -> list[MemoryItem]:
|
|
102
|
+
return query.get_children(self.mem, item_id)
|
|
103
|
+
|
|
104
|
+
def related(self, item_id: str, direction: str = "both") -> list[MemoryItem]:
|
|
105
|
+
return query.get_related_items(self.mem, item_id, direction)
|
|
106
|
+
|
|
107
|
+
def smart_retrieve(self, **kwargs: Any) -> list[ScoredItem]:
|
|
108
|
+
return retrieval.smart_retrieve(self.mem, **kwargs)
|
|
109
|
+
|
|
110
|
+
def support_tree(self, item_id: str) -> SupportNode | None:
|
|
111
|
+
return retrieval.get_support_tree(self.mem, item_id)
|
|
112
|
+
|
|
113
|
+
def support_set(self, item_id: str) -> list[MemoryItem]:
|
|
114
|
+
return retrieval.get_support_set(self.mem, item_id)
|
|
115
|
+
|
|
116
|
+
def stats(self) -> GraphStats:
|
|
117
|
+
return stats.get_stats(self.mem)
|
|
118
|
+
|
|
119
|
+
# -- integrity ----------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def mark_contradiction(self, a: str, b: str, author: str, meta: dict[str, Any] | None = None) -> list[MemoryLifecycleEvent]:
|
|
122
|
+
result = integrity.mark_contradiction(self.mem, a, b, author, meta)
|
|
123
|
+
self.mem = result.state
|
|
124
|
+
return result.events
|
|
125
|
+
|
|
126
|
+
def resolve_contradiction(self, winner: str, loser: str, author: str, reason: str | None = None) -> list[MemoryLifecycleEvent]:
|
|
127
|
+
result = integrity.resolve_contradiction(self.mem, winner, loser, author, reason)
|
|
128
|
+
self.mem = result.state
|
|
129
|
+
return result.events
|
|
130
|
+
|
|
131
|
+
def mark_alias(self, a: str, b: str, author: str, meta: dict[str, Any] | None = None) -> list[MemoryLifecycleEvent]:
|
|
132
|
+
result = integrity.mark_alias(self.mem, a, b, author, meta)
|
|
133
|
+
self.mem = result.state
|
|
134
|
+
return result.events
|
|
135
|
+
|
|
136
|
+
def cascade_retract(self, item_id: str, author: str, reason: str | None = None) -> list[str]:
|
|
137
|
+
result = integrity.cascade_retract(self.mem, item_id, author, reason)
|
|
138
|
+
self.mem = result.state
|
|
139
|
+
return result.retracted
|
|
140
|
+
|
|
141
|
+
def contradictions(self) -> list[Contradiction]:
|
|
142
|
+
return integrity.get_contradictions(self.mem)
|
|
143
|
+
|
|
144
|
+
def stale_items(self) -> list[StaleItem]:
|
|
145
|
+
return integrity.get_stale_items(self.mem)
|
|
146
|
+
|
|
147
|
+
def aliases(self, item_id: str) -> list[MemoryItem]:
|
|
148
|
+
return integrity.get_aliases(self.mem, item_id)
|
|
149
|
+
|
|
150
|
+
def alias_group(self, item_id: str) -> list[MemoryItem]:
|
|
151
|
+
return integrity.get_alias_group(self.mem, item_id)
|
|
152
|
+
|
|
153
|
+
# -- bulk ---------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def apply_many(self, filter: Any, transform: Any, author: str, reason: str | None = None, options: Any = None) -> list[MemoryLifecycleEvent]:
|
|
156
|
+
result = bulk.apply_many(self.mem, filter, transform, author, reason, options)
|
|
157
|
+
self.mem = result.state
|
|
158
|
+
return result.events
|
|
159
|
+
|
|
160
|
+
def bulk_adjust_scores(self, criteria: Any, delta: Any, author: str, reason: str | None = None) -> list[MemoryLifecycleEvent]:
|
|
161
|
+
result = bulk.bulk_adjust_scores(self.mem, criteria, delta, author, reason)
|
|
162
|
+
self.mem = result.state
|
|
163
|
+
return result.events
|
|
164
|
+
|
|
165
|
+
def decay_importance(self, older_than_ms: int, factor: float, author: str, reason: str | None = None) -> list[MemoryLifecycleEvent]:
|
|
166
|
+
result = bulk.decay_importance(self.mem, older_than_ms, factor, author, reason)
|
|
167
|
+
self.mem = result.state
|
|
168
|
+
return result.events
|
|
169
|
+
|
|
170
|
+
# -- intent graph -------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def apply_intent(self, cmd: Any) -> list[IntentLifecycleEvent]:
|
|
173
|
+
result = apply_intent_command(self.intents, cmd)
|
|
174
|
+
self.intents = result.state
|
|
175
|
+
return result.events
|
|
176
|
+
|
|
177
|
+
def create_intent(self, **kwargs: Any) -> Intent:
|
|
178
|
+
intent = create_intent(**kwargs)
|
|
179
|
+
self.apply_intent({"type": "intent.create", "intent": intent})
|
|
180
|
+
return intent
|
|
181
|
+
|
|
182
|
+
def get_intents(self, filter: Any = None) -> list[Intent]:
|
|
183
|
+
return get_intents(self.intents, filter)
|
|
184
|
+
|
|
185
|
+
# -- task graph ---------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def apply_task(self, cmd: Any) -> list[TaskLifecycleEvent]:
|
|
188
|
+
result = apply_task_command(self.tasks, cmd)
|
|
189
|
+
self.tasks = result.state
|
|
190
|
+
return result.events
|
|
191
|
+
|
|
192
|
+
def create_task(self, **kwargs: Any) -> Task:
|
|
193
|
+
task = create_task(**kwargs)
|
|
194
|
+
self.apply_task({"type": "task.create", "task": task})
|
|
195
|
+
return task
|
|
196
|
+
|
|
197
|
+
def get_tasks(self, filter: Any = None) -> list[Task]:
|
|
198
|
+
return get_tasks(self.tasks, filter)
|
|
199
|
+
|
|
200
|
+
# -- transplant ---------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def export_slice(self, **kwargs: Any) -> MemexExport:
|
|
203
|
+
return export_slice(self.mem, self.intents, self.tasks, **kwargs)
|
|
204
|
+
|
|
205
|
+
def import_slice(self, slice: MemexExport | dict[str, Any], **kwargs: Any) -> ImportReport:
|
|
206
|
+
result = import_slice(self.mem, self.intents, self.tasks, slice, **kwargs)
|
|
207
|
+
self.mem = result.mem_state
|
|
208
|
+
self.intents = result.intent_state
|
|
209
|
+
self.tasks = result.task_state
|
|
210
|
+
return result.report
|
|
211
|
+
|
|
212
|
+
# -- serialization (memory graph) --------------------------------------
|
|
213
|
+
|
|
214
|
+
def to_json(self) -> serialization.SerializedGraphState:
|
|
215
|
+
return serialization.to_json(self.mem)
|
|
216
|
+
|
|
217
|
+
def dumps(self, pretty: bool = False) -> str:
|
|
218
|
+
return serialization.stringify(self.mem, pretty)
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def loads(cls, json_str: str) -> MemexStore:
|
|
222
|
+
return cls(mem=serialization.parse(json_str))
|
memex/task.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Task graph — units of work tied to intents, with a status machine.
|
|
2
|
+
|
|
3
|
+
``pending → running → completed`` (``running → failed → running`` retry,
|
|
4
|
+
``cancel`` from any non-terminal state). Invalid transitions raise
|
|
5
|
+
``InvalidTaskTransitionError``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Annotated, Any, Literal, NamedTuple
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
|
14
|
+
|
|
15
|
+
from ._uuid import uuid7
|
|
16
|
+
from .errors import MemexError
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"TaskStatus",
|
|
20
|
+
"Task",
|
|
21
|
+
"TaskState",
|
|
22
|
+
"TaskCommand",
|
|
23
|
+
"TaskLifecycleEvent",
|
|
24
|
+
"TaskFilter",
|
|
25
|
+
"TaskResult",
|
|
26
|
+
"TaskNotFoundError",
|
|
27
|
+
"DuplicateTaskError",
|
|
28
|
+
"InvalidTaskTransitionError",
|
|
29
|
+
"create_task_state",
|
|
30
|
+
"create_task",
|
|
31
|
+
"apply_task_command",
|
|
32
|
+
"get_tasks",
|
|
33
|
+
"get_task_by_id",
|
|
34
|
+
"get_tasks_by_intent",
|
|
35
|
+
"get_child_tasks",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
TaskStatus = Literal["pending", "running", "completed", "failed", "cancelled"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Task(BaseModel):
|
|
42
|
+
model_config = ConfigDict(frozen=True)
|
|
43
|
+
|
|
44
|
+
id: str
|
|
45
|
+
intent_id: str
|
|
46
|
+
parent_id: str | None = None
|
|
47
|
+
|
|
48
|
+
action: str
|
|
49
|
+
label: str | None = None
|
|
50
|
+
|
|
51
|
+
status: TaskStatus
|
|
52
|
+
priority: float = Field(ge=0, le=1)
|
|
53
|
+
|
|
54
|
+
context: dict[str, Any] | None = None
|
|
55
|
+
result: dict[str, Any] | None = None
|
|
56
|
+
error: str | None = None
|
|
57
|
+
|
|
58
|
+
input_memory_ids: list[str] | None = None
|
|
59
|
+
output_memory_ids: list[str] | None = None
|
|
60
|
+
|
|
61
|
+
agent_id: str | None = None
|
|
62
|
+
attempt: int | None = None
|
|
63
|
+
|
|
64
|
+
meta: dict[str, Any] | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class TaskState:
|
|
69
|
+
tasks: dict[str, Task] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_task_state() -> TaskState:
|
|
73
|
+
return TaskState(tasks={})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_task(
|
|
77
|
+
*,
|
|
78
|
+
intent_id: str,
|
|
79
|
+
action: str,
|
|
80
|
+
priority: float,
|
|
81
|
+
id: str | None = None,
|
|
82
|
+
parent_id: str | None = None,
|
|
83
|
+
label: str | None = None,
|
|
84
|
+
status: TaskStatus | None = None,
|
|
85
|
+
context: dict[str, Any] | None = None,
|
|
86
|
+
result: dict[str, Any] | None = None,
|
|
87
|
+
error: str | None = None,
|
|
88
|
+
input_memory_ids: list[str] | None = None,
|
|
89
|
+
output_memory_ids: list[str] | None = None,
|
|
90
|
+
agent_id: str | None = None,
|
|
91
|
+
attempt: int | None = None,
|
|
92
|
+
meta: dict[str, Any] | None = None,
|
|
93
|
+
) -> Task:
|
|
94
|
+
return Task(
|
|
95
|
+
id=id if id is not None else uuid7(),
|
|
96
|
+
intent_id=intent_id,
|
|
97
|
+
parent_id=parent_id,
|
|
98
|
+
action=action,
|
|
99
|
+
label=label,
|
|
100
|
+
status=status if status is not None else "pending",
|
|
101
|
+
priority=priority,
|
|
102
|
+
context=context,
|
|
103
|
+
result=result,
|
|
104
|
+
error=error,
|
|
105
|
+
input_memory_ids=input_memory_ids,
|
|
106
|
+
output_memory_ids=output_memory_ids,
|
|
107
|
+
agent_id=agent_id,
|
|
108
|
+
attempt=attempt if attempt is not None else 0,
|
|
109
|
+
meta=meta,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Commands
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TaskCreate(BaseModel):
|
|
119
|
+
type: Literal["task.create"] = "task.create"
|
|
120
|
+
task: Task
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TaskUpdate(BaseModel):
|
|
124
|
+
type: Literal["task.update"] = "task.update"
|
|
125
|
+
task_id: str
|
|
126
|
+
partial: dict[str, Any]
|
|
127
|
+
author: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TaskStart(BaseModel):
|
|
131
|
+
type: Literal["task.start"] = "task.start"
|
|
132
|
+
task_id: str
|
|
133
|
+
agent_id: str | None = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TaskComplete(BaseModel):
|
|
137
|
+
type: Literal["task.complete"] = "task.complete"
|
|
138
|
+
task_id: str
|
|
139
|
+
result: dict[str, Any] | None = None
|
|
140
|
+
output_memory_ids: list[str] | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TaskFail(BaseModel):
|
|
144
|
+
type: Literal["task.fail"] = "task.fail"
|
|
145
|
+
task_id: str
|
|
146
|
+
error: str
|
|
147
|
+
retryable: bool | None = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TaskCancel(BaseModel):
|
|
151
|
+
type: Literal["task.cancel"] = "task.cancel"
|
|
152
|
+
task_id: str
|
|
153
|
+
reason: str | None = None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
TaskCommand = Annotated[
|
|
157
|
+
TaskCreate | TaskUpdate | TaskStart | TaskComplete | TaskFail | TaskCancel,
|
|
158
|
+
Field(discriminator="type"),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
_TASK_COMMAND_ADAPTER: TypeAdapter[TaskCommand] = TypeAdapter(TaskCommand)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
TaskEventType = Literal[
|
|
165
|
+
"task.created",
|
|
166
|
+
"task.updated",
|
|
167
|
+
"task.started",
|
|
168
|
+
"task.completed",
|
|
169
|
+
"task.failed",
|
|
170
|
+
"task.cancelled",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TaskLifecycleEvent(BaseModel):
|
|
175
|
+
namespace: Literal["task"] = "task"
|
|
176
|
+
type: TaskEventType
|
|
177
|
+
task: Task
|
|
178
|
+
cause_type: str
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TaskResult(NamedTuple):
|
|
182
|
+
state: TaskState
|
|
183
|
+
events: list[TaskLifecycleEvent]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Errors
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TaskNotFoundError(MemexError):
|
|
192
|
+
def __init__(self, task_id: str) -> None:
|
|
193
|
+
super().__init__(f"Task not found: {task_id}")
|
|
194
|
+
self.task_id = task_id
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class DuplicateTaskError(MemexError):
|
|
198
|
+
def __init__(self, task_id: str) -> None:
|
|
199
|
+
super().__init__(f"Task already exists: {task_id}")
|
|
200
|
+
self.task_id = task_id
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class InvalidTaskTransitionError(MemexError):
|
|
204
|
+
def __init__(self, task_id: str, from_status: str, to_status: str) -> None:
|
|
205
|
+
super().__init__(f"Invalid task transition: {task_id} from {from_status} to {to_status}")
|
|
206
|
+
self.task_id = task_id
|
|
207
|
+
self.from_status = from_status
|
|
208
|
+
self.to_status = to_status
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Reducer
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _event(task: Task, event_type: TaskEventType, cause_type: str) -> TaskLifecycleEvent:
|
|
217
|
+
return TaskLifecycleEvent(type=event_type, task=task, cause_type=cause_type)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def apply_task_command(state: TaskState, cmd: TaskCommand | dict[str, Any]) -> TaskResult:
|
|
221
|
+
command = cmd if isinstance(cmd, BaseModel) else _TASK_COMMAND_ADAPTER.validate_python(cmd)
|
|
222
|
+
|
|
223
|
+
match command:
|
|
224
|
+
case TaskCreate(task=task):
|
|
225
|
+
if task.id in state.tasks:
|
|
226
|
+
raise DuplicateTaskError(task.id)
|
|
227
|
+
tasks = {**state.tasks, task.id: task}
|
|
228
|
+
return TaskResult(TaskState(tasks), [_event(task, "task.created", "task.create")])
|
|
229
|
+
|
|
230
|
+
case TaskUpdate(task_id=task_id, partial=partial):
|
|
231
|
+
existing = state.tasks.get(task_id)
|
|
232
|
+
if existing is None:
|
|
233
|
+
raise TaskNotFoundError(task_id)
|
|
234
|
+
update = {k: v for k, v in partial.items() if k not in ("id", "status")}
|
|
235
|
+
updated = existing.model_copy(update=update)
|
|
236
|
+
tasks = {**state.tasks, task_id: updated}
|
|
237
|
+
return TaskResult(TaskState(tasks), [_event(updated, "task.updated", "task.update")])
|
|
238
|
+
|
|
239
|
+
case TaskStart(task_id=task_id, agent_id=agent_id):
|
|
240
|
+
existing = state.tasks.get(task_id)
|
|
241
|
+
if existing is None:
|
|
242
|
+
raise TaskNotFoundError(task_id)
|
|
243
|
+
if existing.status not in ("pending", "failed"):
|
|
244
|
+
raise InvalidTaskTransitionError(task_id, existing.status, "running")
|
|
245
|
+
updated = existing.model_copy(update={
|
|
246
|
+
"status": "running",
|
|
247
|
+
"agent_id": agent_id if agent_id is not None else existing.agent_id,
|
|
248
|
+
"attempt": (existing.attempt or 0) + 1,
|
|
249
|
+
})
|
|
250
|
+
tasks = {**state.tasks, task_id: updated}
|
|
251
|
+
return TaskResult(TaskState(tasks), [_event(updated, "task.started", "task.start")])
|
|
252
|
+
|
|
253
|
+
case TaskComplete(task_id=task_id, result=result, output_memory_ids=output_memory_ids):
|
|
254
|
+
existing = state.tasks.get(task_id)
|
|
255
|
+
if existing is None:
|
|
256
|
+
raise TaskNotFoundError(task_id)
|
|
257
|
+
if existing.status != "running":
|
|
258
|
+
raise InvalidTaskTransitionError(task_id, existing.status, "completed")
|
|
259
|
+
updated = existing.model_copy(update={
|
|
260
|
+
"status": "completed",
|
|
261
|
+
"result": result if result is not None else existing.result,
|
|
262
|
+
"output_memory_ids": output_memory_ids if output_memory_ids is not None else existing.output_memory_ids,
|
|
263
|
+
})
|
|
264
|
+
tasks = {**state.tasks, task_id: updated}
|
|
265
|
+
return TaskResult(TaskState(tasks), [_event(updated, "task.completed", "task.complete")])
|
|
266
|
+
|
|
267
|
+
case TaskFail(task_id=task_id, error=error):
|
|
268
|
+
existing = state.tasks.get(task_id)
|
|
269
|
+
if existing is None:
|
|
270
|
+
raise TaskNotFoundError(task_id)
|
|
271
|
+
if existing.status != "running":
|
|
272
|
+
raise InvalidTaskTransitionError(task_id, existing.status, "failed")
|
|
273
|
+
updated = existing.model_copy(update={"status": "failed", "error": error})
|
|
274
|
+
tasks = {**state.tasks, task_id: updated}
|
|
275
|
+
return TaskResult(TaskState(tasks), [_event(updated, "task.failed", "task.fail")])
|
|
276
|
+
|
|
277
|
+
case TaskCancel(task_id=task_id):
|
|
278
|
+
existing = state.tasks.get(task_id)
|
|
279
|
+
if existing is None:
|
|
280
|
+
raise TaskNotFoundError(task_id)
|
|
281
|
+
if existing.status in ("completed", "cancelled"):
|
|
282
|
+
raise InvalidTaskTransitionError(task_id, existing.status, "cancelled")
|
|
283
|
+
updated = existing.model_copy(update={"status": "cancelled"})
|
|
284
|
+
tasks = {**state.tasks, task_id: updated}
|
|
285
|
+
return TaskResult(TaskState(tasks), [_event(updated, "task.cancelled", "task.cancel")])
|
|
286
|
+
|
|
287
|
+
case _: # pragma: no cover
|
|
288
|
+
raise TypeError(f"Unknown task command: {command!r}")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# Query
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TaskFilter(BaseModel):
|
|
297
|
+
intent_id: str | None = None
|
|
298
|
+
action: str | None = None
|
|
299
|
+
status: TaskStatus | None = None
|
|
300
|
+
statuses: list[TaskStatus] | None = None
|
|
301
|
+
agent_id: str | None = None
|
|
302
|
+
min_priority: float | None = None
|
|
303
|
+
has_input_memory_id: str | None = None
|
|
304
|
+
has_output_memory_id: str | None = None
|
|
305
|
+
parent_id: str | None = None
|
|
306
|
+
is_root: bool | None = None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _coerce_task_filter(f: TaskFilter | dict[str, Any] | None) -> TaskFilter | None:
|
|
310
|
+
if f is None or isinstance(f, TaskFilter):
|
|
311
|
+
return f
|
|
312
|
+
return TaskFilter.model_validate(f)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def get_tasks(state: TaskState, filter: TaskFilter | dict[str, Any] | None = None) -> list[Task]:
|
|
316
|
+
f = _coerce_task_filter(filter)
|
|
317
|
+
if f is None:
|
|
318
|
+
return list(state.tasks.values())
|
|
319
|
+
|
|
320
|
+
results: list[Task] = []
|
|
321
|
+
for task in state.tasks.values():
|
|
322
|
+
if f.intent_id is not None and task.intent_id != f.intent_id:
|
|
323
|
+
continue
|
|
324
|
+
if f.action is not None and task.action != f.action:
|
|
325
|
+
continue
|
|
326
|
+
if f.status is not None and task.status != f.status:
|
|
327
|
+
continue
|
|
328
|
+
if f.statuses is not None and task.status not in f.statuses:
|
|
329
|
+
continue
|
|
330
|
+
if f.agent_id is not None and task.agent_id != f.agent_id:
|
|
331
|
+
continue
|
|
332
|
+
if f.min_priority is not None and task.priority < f.min_priority:
|
|
333
|
+
continue
|
|
334
|
+
if f.has_input_memory_id is not None:
|
|
335
|
+
if not task.input_memory_ids or f.has_input_memory_id not in task.input_memory_ids:
|
|
336
|
+
continue
|
|
337
|
+
if f.has_output_memory_id is not None:
|
|
338
|
+
if not task.output_memory_ids or f.has_output_memory_id not in task.output_memory_ids:
|
|
339
|
+
continue
|
|
340
|
+
if f.parent_id is not None and task.parent_id != f.parent_id:
|
|
341
|
+
continue
|
|
342
|
+
if f.is_root is not None:
|
|
343
|
+
has_parent = task.parent_id is not None
|
|
344
|
+
if f.is_root and has_parent:
|
|
345
|
+
continue
|
|
346
|
+
if not f.is_root and not has_parent:
|
|
347
|
+
continue
|
|
348
|
+
results.append(task)
|
|
349
|
+
return results
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def get_task_by_id(state: TaskState, id: str) -> Task | None:
|
|
353
|
+
return state.tasks.get(id)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def get_tasks_by_intent(state: TaskState, intent_id: str) -> list[Task]:
|
|
357
|
+
return get_tasks(state, {"intent_id": intent_id})
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def get_child_tasks(state: TaskState, parent_id: str) -> list[Task]:
|
|
361
|
+
return get_tasks(state, {"parent_id": parent_id})
|