pi-durable-python 1.3.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.
- pi_durable/__init__.py +320 -0
- pi_durable/context.py +328 -0
- pi_durable/delta.py +269 -0
- pi_durable/documents.py +366 -0
- pi_durable/entries.py +66 -0
- pi_durable/env/__init__.py +408 -0
- pi_durable/env/decode.py +48 -0
- pi_durable/env/line_scan.py +190 -0
- pi_durable/env/posix.py +1547 -0
- pi_durable/errors.py +89 -0
- pi_durable/harness/__init__.py +1 -0
- pi_durable/harness/agent.py +344 -0
- pi_durable/harness/compaction.py +673 -0
- pi_durable/harness/context.py +515 -0
- pi_durable/harness/define.py +111 -0
- pi_durable/harness/events.py +766 -0
- pi_durable/harness/generation.py +1079 -0
- pi_durable/harness/harness.py +570 -0
- pi_durable/harness/inbox.py +255 -0
- pi_durable/harness/live.py +374 -0
- pi_durable/harness/output.py +467 -0
- pi_durable/harness/prompt.py +288 -0
- pi_durable/harness/provider.py +78 -0
- pi_durable/harness/registry.py +136 -0
- pi_durable/harness/scheduler.py +1977 -0
- pi_durable/harness/submissions.py +357 -0
- pi_durable/harness/task_graph.py +334 -0
- pi_durable/harness/tool.py +1365 -0
- pi_durable/harness/types.py +772 -0
- pi_durable/harness/usage.py +88 -0
- pi_durable/harness/util.py +204 -0
- pi_durable/harness/validation.py +460 -0
- pi_durable/harness/view.py +324 -0
- pi_durable/ids.py +63 -0
- pi_durable/json.py +186 -0
- pi_durable/providers/__init__.py +362 -0
- pi_durable/providers/anthropic.py +603 -0
- pi_durable/providers/base.py +97 -0
- pi_durable/providers/json_parse.py +117 -0
- pi_durable/providers/openai.py +574 -0
- pi_durable/providers/request.py +35 -0
- pi_durable/providers/retry.py +127 -0
- pi_durable/providers/stream.py +183 -0
- pi_durable/providers/transcript.py +224 -0
- pi_durable/providers/usage.py +58 -0
- pi_durable/py.typed +0 -0
- pi_durable/search.py +115 -0
- pi_durable/session/__init__.py +5 -0
- pi_durable/session/drafts.py +762 -0
- pi_durable/session/forks.py +109 -0
- pi_durable/session/observation.py +374 -0
- pi_durable/session/session.py +1193 -0
- pi_durable/session/transaction.py +1088 -0
- pi_durable/storage/__init__.py +5 -0
- pi_durable/storage/jsonl/__init__.py +15 -0
- pi_durable/storage/jsonl/storage.py +958 -0
- pi_durable/storage/memory.py +889 -0
- pi_durable/storage/scan.py +57 -0
- pi_durable/storage/sqlite/__init__.py +26 -0
- pi_durable/storage/sqlite/database.py +62 -0
- pi_durable/storage/sqlite/migrations.py +191 -0
- pi_durable/storage/sqlite/posix.py +314 -0
- pi_durable/storage/sqlite/schema.py +97 -0
- pi_durable/storage/sqlite/storage.py +1130 -0
- pi_durable/tasks.py +89 -0
- pi_durable/testing/__init__.py +41 -0
- pi_durable/testing/assertions.py +44 -0
- pi_durable/testing/env_conformance.py +828 -0
- pi_durable/testing/runner.py +64 -0
- pi_durable/testing/storage_conformance.py +2008 -0
- pi_durable/testing/types.py +99 -0
- pi_durable/tool_search/__init__.py +159 -0
- pi_durable/tools/__init__.py +81 -0
- pi_durable/tools/_values.py +43 -0
- pi_durable/tools/bash.py +250 -0
- pi_durable/tools/edit.py +214 -0
- pi_durable/tools/edit_diff.py +452 -0
- pi_durable/tools/env.py +19 -0
- pi_durable/tools/file_mutation_queue.py +68 -0
- pi_durable/tools/image.py +248 -0
- pi_durable/tools/image_processor.py +77 -0
- pi_durable/tools/path_utils.py +111 -0
- pi_durable/tools/read.py +506 -0
- pi_durable/tools/write.py +71 -0
- pi_durable/truncate.py +185 -0
- pi_durable/types.py +1297 -0
- pi_durable_python-1.3.0.dist-info/METADATA +276 -0
- pi_durable_python-1.3.0.dist-info/RECORD +90 -0
- pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
- pi_durable_python-1.3.0.dist-info/licenses/LICENSE +21 -0
pi_durable/types.py
ADDED
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
"""Core durable records, queries, and the Storage protocol.
|
|
2
|
+
|
|
3
|
+
Records are persisted JSON objects with camelCase fields, exactly as in the reference package.
|
|
4
|
+
Python type declarations are ``TypedDict`` values so storage, JSONL, and SQLite carry the same
|
|
5
|
+
shape without a mapping layer. This module deliberately does not use postponed annotations:
|
|
6
|
+
``TypedDict`` computes ``__required_keys__``/``__optional_keys__`` at class creation, and PEP 563
|
|
7
|
+
string annotations would make every field appear required at runtime.
|
|
8
|
+
|
|
9
|
+
Entry records are the immutable value of the transcript. :func:`entry_draft` validates a draft
|
|
10
|
+
and :func:`entry_record` assigns its identity, resolving ``head: "self"``; both return deep-frozen
|
|
11
|
+
JSON values that cannot be mutated after construction.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
16
|
+
from typing import (
|
|
17
|
+
TYPE_CHECKING,
|
|
18
|
+
Any,
|
|
19
|
+
Generic,
|
|
20
|
+
Literal,
|
|
21
|
+
Never,
|
|
22
|
+
NotRequired,
|
|
23
|
+
Protocol,
|
|
24
|
+
TypeAlias,
|
|
25
|
+
TypedDict,
|
|
26
|
+
TypeVar,
|
|
27
|
+
cast,
|
|
28
|
+
overload,
|
|
29
|
+
runtime_checkable,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from .context import Context
|
|
33
|
+
from .ids import (
|
|
34
|
+
ROOT_CONVERSATION_ID,
|
|
35
|
+
ConversationId,
|
|
36
|
+
DocumentId,
|
|
37
|
+
EntryId,
|
|
38
|
+
Id,
|
|
39
|
+
Seq,
|
|
40
|
+
SubmissionId,
|
|
41
|
+
TaskId,
|
|
42
|
+
require_positive_int,
|
|
43
|
+
)
|
|
44
|
+
from .json import JsonObject, JsonValue, copy_json, freeze_json
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from .harness.types import ContextView, TaskRuntime
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"ROOT_CONVERSATION_ID",
|
|
51
|
+
"CheckpointInfo",
|
|
52
|
+
"CommitChange",
|
|
53
|
+
"CommitPublication",
|
|
54
|
+
"ContextEdit",
|
|
55
|
+
"ConversationId",
|
|
56
|
+
"ConversationOwnership",
|
|
57
|
+
"ConversationQuery",
|
|
58
|
+
"ConversationRecord",
|
|
59
|
+
"Cursor",
|
|
60
|
+
"DocumentAddress",
|
|
61
|
+
"DocumentCommitChange",
|
|
62
|
+
"DocumentContent",
|
|
63
|
+
"DocumentCopySource",
|
|
64
|
+
"DocumentCreate",
|
|
65
|
+
"DocumentId",
|
|
66
|
+
"DocumentPoint",
|
|
67
|
+
"DocumentQuery",
|
|
68
|
+
"DocumentReader",
|
|
69
|
+
"DocumentRecord",
|
|
70
|
+
"DocumentObserver",
|
|
71
|
+
"DocumentSemantics",
|
|
72
|
+
"DocumentWatch",
|
|
73
|
+
"EntryDraft",
|
|
74
|
+
"EntryId",
|
|
75
|
+
"EntryQuery",
|
|
76
|
+
"EntryRecord",
|
|
77
|
+
"EntryWithHead",
|
|
78
|
+
"Id",
|
|
79
|
+
"JoinPolicy",
|
|
80
|
+
"JsonObject",
|
|
81
|
+
"JsonValue",
|
|
82
|
+
"Page",
|
|
83
|
+
"PhaseHandler",
|
|
84
|
+
"RunningTask",
|
|
85
|
+
"ScanOrder",
|
|
86
|
+
"Seq",
|
|
87
|
+
"Session",
|
|
88
|
+
"SessionEnd",
|
|
89
|
+
"Storage",
|
|
90
|
+
"StorageWrite",
|
|
91
|
+
"StoredDocument",
|
|
92
|
+
"SubmissionCreate",
|
|
93
|
+
"SubmissionId",
|
|
94
|
+
"SubmissionQuery",
|
|
95
|
+
"SubmissionRecord",
|
|
96
|
+
"SubmissionSettlement",
|
|
97
|
+
"TableCommitChange",
|
|
98
|
+
"TaskId",
|
|
99
|
+
"TaskOptions",
|
|
100
|
+
"TaskOutcome",
|
|
101
|
+
"TaskOutcomeError",
|
|
102
|
+
"TaskOwnership",
|
|
103
|
+
"TaskQuery",
|
|
104
|
+
"TaskRecord",
|
|
105
|
+
"TaskState",
|
|
106
|
+
"Tx",
|
|
107
|
+
"TypedEntry",
|
|
108
|
+
"TypedEntryDraft",
|
|
109
|
+
"WatchEnd",
|
|
110
|
+
"WatchEnded",
|
|
111
|
+
"WatchHandle",
|
|
112
|
+
"entry_draft",
|
|
113
|
+
"entry_record",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
MessageRecord: TypeAlias = dict[str, JsonValue]
|
|
117
|
+
"""One model-facing message. The pi-ai adapter supplies its concrete shape."""
|
|
118
|
+
Op: TypeAlias = list[Any]
|
|
119
|
+
"""One Chord delta operation, represented as its JSON array."""
|
|
120
|
+
JsonValueT_co = TypeVar("JsonValueT_co", covariant=True)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ── Identity, conversation, and entry records ──────────────────────────────
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ConversationParent(TypedDict):
|
|
127
|
+
conversationId: ConversationId
|
|
128
|
+
at: EntryId
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ConversationOwner(TypedDict):
|
|
132
|
+
conversationId: ConversationId
|
|
133
|
+
taskId: TaskId
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ConversationRecord(TypedDict):
|
|
137
|
+
id: ConversationId
|
|
138
|
+
parent: NotRequired[ConversationParent]
|
|
139
|
+
owner: NotRequired[ConversationOwner]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class OwnerlessConversation(TypedDict):
|
|
143
|
+
kind: Literal["ownerless"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TaskOwnedConversation(TypedDict):
|
|
147
|
+
kind: Literal["task"]
|
|
148
|
+
taskId: TaskId
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
ConversationOwnership: TypeAlias = OwnerlessConversation | TaskOwnedConversation
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ConversationOwnedTask(TypedDict):
|
|
155
|
+
kind: Literal["conversation"]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
TaskOwnership: TypeAlias = ConversationOwnedTask | TaskOwnedConversation
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class OmitContextEdit(TypedDict):
|
|
162
|
+
target: EntryId
|
|
163
|
+
action: Literal["omit"]
|
|
164
|
+
messages: NotRequired[Never]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ReplaceContextEdit(TypedDict):
|
|
168
|
+
target: EntryId
|
|
169
|
+
action: Literal["replace"]
|
|
170
|
+
messages: list[MessageRecord]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
ContextEdit: TypeAlias = OmitContextEdit | ReplaceContextEdit
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class EntryRecord(TypedDict):
|
|
177
|
+
id: EntryId
|
|
178
|
+
conversationId: ConversationId
|
|
179
|
+
kind: str
|
|
180
|
+
model: NotRequired[list[MessageRecord]]
|
|
181
|
+
data: NotRequired[JsonValue]
|
|
182
|
+
head: NotRequired[EntryId]
|
|
183
|
+
edits: NotRequired[list[ContextEdit]]
|
|
184
|
+
byTaskId: NotRequired[TaskId]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class EntryDraft(TypedDict):
|
|
188
|
+
kind: str
|
|
189
|
+
model: NotRequired[list[MessageRecord]]
|
|
190
|
+
data: NotRequired[JsonValue]
|
|
191
|
+
head: NotRequired[EntryId | Literal["self"]]
|
|
192
|
+
edits: NotRequired[list[ContextEdit]]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class EntryWithHead(TypedDict):
|
|
196
|
+
"""An entry that is also a head marker, so its ``head`` is the range's lower bound."""
|
|
197
|
+
|
|
198
|
+
id: EntryId
|
|
199
|
+
conversationId: ConversationId
|
|
200
|
+
kind: str
|
|
201
|
+
model: NotRequired[list[MessageRecord]]
|
|
202
|
+
data: NotRequired[JsonValue]
|
|
203
|
+
head: EntryId
|
|
204
|
+
edits: NotRequired[list[ContextEdit]]
|
|
205
|
+
byTaskId: NotRequired[TaskId]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
D = TypeVar("D", bound=JsonValue)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TypedEntry(TypedDict, Generic[D]):
|
|
212
|
+
"""An entry record whose ``data`` is narrowed to ``D`` when present."""
|
|
213
|
+
|
|
214
|
+
id: EntryId
|
|
215
|
+
conversationId: ConversationId
|
|
216
|
+
kind: str
|
|
217
|
+
model: NotRequired[list[MessageRecord]]
|
|
218
|
+
data: NotRequired[D]
|
|
219
|
+
head: NotRequired[EntryId]
|
|
220
|
+
edits: NotRequired[list[ContextEdit]]
|
|
221
|
+
byTaskId: NotRequired[TaskId]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TypedEntryDraft(TypedDict, Generic[D]):
|
|
225
|
+
"""Entry draft content of a typed kind; the token supplies ``kind``."""
|
|
226
|
+
|
|
227
|
+
model: NotRequired[list[MessageRecord]]
|
|
228
|
+
data: NotRequired[D]
|
|
229
|
+
head: NotRequired[EntryId | Literal["self"]]
|
|
230
|
+
edits: NotRequired[list[ContextEdit]]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── Submission records ──────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class SubmissionRecordBase(TypedDict):
|
|
237
|
+
id: SubmissionId
|
|
238
|
+
conversationId: ConversationId
|
|
239
|
+
requestId: NotRequired[str]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class InputQueuedRecord(SubmissionRecordBase):
|
|
243
|
+
type: Literal["input"]
|
|
244
|
+
status: Literal["queued"]
|
|
245
|
+
entry: NotRequired[Never]
|
|
246
|
+
answer: NotRequired[Never]
|
|
247
|
+
reason: NotRequired[Never]
|
|
248
|
+
detail: NotRequired[Never]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class InputPlacedRecord(SubmissionRecordBase):
|
|
252
|
+
type: Literal["input"]
|
|
253
|
+
status: Literal["placed"]
|
|
254
|
+
entry: EntryId
|
|
255
|
+
answer: NotRequired[Never]
|
|
256
|
+
reason: NotRequired[Never]
|
|
257
|
+
detail: NotRequired[Never]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class InputDoneRecord(SubmissionRecordBase):
|
|
261
|
+
type: Literal["input"]
|
|
262
|
+
status: Literal["done"]
|
|
263
|
+
entry: EntryId
|
|
264
|
+
answer: EntryId
|
|
265
|
+
reason: NotRequired[Never]
|
|
266
|
+
detail: NotRequired[Never]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class InputUnansweredRecord(SubmissionRecordBase):
|
|
270
|
+
type: Literal["input"]
|
|
271
|
+
status: Literal["unanswered"]
|
|
272
|
+
entry: NotRequired[EntryId]
|
|
273
|
+
reason: str
|
|
274
|
+
detail: NotRequired[JsonValue]
|
|
275
|
+
answer: NotRequired[Never]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class WriteQueuedRecord(SubmissionRecordBase):
|
|
279
|
+
type: Literal["write"]
|
|
280
|
+
status: Literal["queued"]
|
|
281
|
+
entry: NotRequired[Never]
|
|
282
|
+
answer: NotRequired[Never]
|
|
283
|
+
reason: NotRequired[Never]
|
|
284
|
+
detail: NotRequired[Never]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class WriteDoneRecord(SubmissionRecordBase):
|
|
288
|
+
type: Literal["write"]
|
|
289
|
+
status: Literal["done"]
|
|
290
|
+
entry: EntryId
|
|
291
|
+
answer: NotRequired[Never]
|
|
292
|
+
reason: NotRequired[Never]
|
|
293
|
+
detail: NotRequired[Never]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class WriteUnansweredRecord(SubmissionRecordBase):
|
|
297
|
+
type: Literal["write"]
|
|
298
|
+
status: Literal["unanswered"]
|
|
299
|
+
reason: str
|
|
300
|
+
detail: NotRequired[JsonValue]
|
|
301
|
+
entry: NotRequired[Never]
|
|
302
|
+
answer: NotRequired[Never]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
SubmissionRecord: TypeAlias = (
|
|
306
|
+
InputQueuedRecord
|
|
307
|
+
| InputPlacedRecord
|
|
308
|
+
| InputDoneRecord
|
|
309
|
+
| InputUnansweredRecord
|
|
310
|
+
| WriteQueuedRecord
|
|
311
|
+
| WriteDoneRecord
|
|
312
|
+
| WriteUnansweredRecord
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class SubmissionCreateBase(TypedDict):
|
|
317
|
+
conversationId: ConversationId
|
|
318
|
+
requestId: NotRequired[str]
|
|
319
|
+
id: NotRequired[Never]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class InputQueuedCreate(SubmissionCreateBase):
|
|
323
|
+
type: Literal["input"]
|
|
324
|
+
status: Literal["queued"]
|
|
325
|
+
entry: NotRequired[Never]
|
|
326
|
+
answer: NotRequired[Never]
|
|
327
|
+
reason: NotRequired[Never]
|
|
328
|
+
detail: NotRequired[Never]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class InputPlacedCreate(SubmissionCreateBase):
|
|
332
|
+
type: Literal["input"]
|
|
333
|
+
status: Literal["placed"]
|
|
334
|
+
entry: EntryId
|
|
335
|
+
answer: NotRequired[Never]
|
|
336
|
+
reason: NotRequired[Never]
|
|
337
|
+
detail: NotRequired[Never]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class InputDoneCreate(SubmissionCreateBase):
|
|
341
|
+
type: Literal["input"]
|
|
342
|
+
status: Literal["done"]
|
|
343
|
+
entry: EntryId
|
|
344
|
+
answer: EntryId
|
|
345
|
+
reason: NotRequired[Never]
|
|
346
|
+
detail: NotRequired[Never]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class InputUnansweredCreate(SubmissionCreateBase):
|
|
350
|
+
type: Literal["input"]
|
|
351
|
+
status: Literal["unanswered"]
|
|
352
|
+
entry: NotRequired[EntryId]
|
|
353
|
+
reason: str
|
|
354
|
+
detail: NotRequired[JsonValue]
|
|
355
|
+
answer: NotRequired[Never]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class WriteQueuedCreate(SubmissionCreateBase):
|
|
359
|
+
type: Literal["write"]
|
|
360
|
+
status: Literal["queued"]
|
|
361
|
+
entry: NotRequired[Never]
|
|
362
|
+
answer: NotRequired[Never]
|
|
363
|
+
reason: NotRequired[Never]
|
|
364
|
+
detail: NotRequired[Never]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class WriteDoneCreate(SubmissionCreateBase):
|
|
368
|
+
type: Literal["write"]
|
|
369
|
+
status: Literal["done"]
|
|
370
|
+
entry: EntryId
|
|
371
|
+
answer: NotRequired[Never]
|
|
372
|
+
reason: NotRequired[Never]
|
|
373
|
+
detail: NotRequired[Never]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class WriteUnansweredCreate(SubmissionCreateBase):
|
|
377
|
+
type: Literal["write"]
|
|
378
|
+
status: Literal["unanswered"]
|
|
379
|
+
reason: str
|
|
380
|
+
detail: NotRequired[JsonValue]
|
|
381
|
+
entry: NotRequired[Never]
|
|
382
|
+
answer: NotRequired[Never]
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
SubmissionCreate: TypeAlias = (
|
|
386
|
+
InputQueuedCreate
|
|
387
|
+
| InputPlacedCreate
|
|
388
|
+
| InputDoneCreate
|
|
389
|
+
| InputUnansweredCreate
|
|
390
|
+
| WriteQueuedCreate
|
|
391
|
+
| WriteDoneCreate
|
|
392
|
+
| WriteUnansweredCreate
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class DoneSubmissionSettlement(TypedDict):
|
|
397
|
+
status: Literal["done"]
|
|
398
|
+
answer: EntryId
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class UnansweredSubmissionSettlement(TypedDict):
|
|
402
|
+
status: Literal["unanswered"]
|
|
403
|
+
reason: str
|
|
404
|
+
detail: NotRequired[JsonValue]
|
|
405
|
+
answer: NotRequired[Never]
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
SubmissionSettlement: TypeAlias = DoneSubmissionSettlement | UnansweredSubmissionSettlement
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ── Task records ────────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
JoinPolicy: TypeAlias = Literal["failFast", "allSettled"]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class TaskOutcomeError(TypedDict):
|
|
417
|
+
message: str
|
|
418
|
+
detail: NotRequired[JsonValue]
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
R = TypeVar("R")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class TaskOutcomeCompleted(TypedDict, Generic[R]):
|
|
425
|
+
status: Literal["completed"]
|
|
426
|
+
result: R
|
|
427
|
+
error: NotRequired[Never]
|
|
428
|
+
reason: NotRequired[Never]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class TaskOutcomeFailed(TypedDict, Generic[R]):
|
|
432
|
+
status: Literal["failed"]
|
|
433
|
+
error: TaskOutcomeError
|
|
434
|
+
result: NotRequired[R]
|
|
435
|
+
reason: NotRequired[Never]
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class TaskOutcomeAborted(TypedDict, Generic[R]):
|
|
439
|
+
status: Literal["aborted"]
|
|
440
|
+
reason: NotRequired[str]
|
|
441
|
+
result: NotRequired[R]
|
|
442
|
+
error: NotRequired[Never]
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class TaskOutcomeOrphaned(TypedDict):
|
|
446
|
+
status: Literal["orphaned"]
|
|
447
|
+
reason: str
|
|
448
|
+
result: NotRequired[Never]
|
|
449
|
+
error: NotRequired[Never]
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class TaskOutcomeFaulted(TypedDict):
|
|
453
|
+
status: Literal["faulted"]
|
|
454
|
+
error: TaskOutcomeError
|
|
455
|
+
result: NotRequired[Never]
|
|
456
|
+
reason: NotRequired[Never]
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
TaskOutcome: TypeAlias = (
|
|
460
|
+
TaskOutcomeCompleted[Any]
|
|
461
|
+
| TaskOutcomeFailed[Any]
|
|
462
|
+
| TaskOutcomeAborted[Any]
|
|
463
|
+
| TaskOutcomeOrphaned
|
|
464
|
+
| TaskOutcomeFaulted
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
S = TypeVar("S")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class TaskStatePending(TypedDict, Generic[S]):
|
|
471
|
+
status: Literal["pending"]
|
|
472
|
+
checkpoint: S
|
|
473
|
+
outcome: NotRequired[Never]
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class TaskStateRunning(TypedDict, Generic[S]):
|
|
477
|
+
status: Literal["running"]
|
|
478
|
+
checkpoint: S
|
|
479
|
+
outcome: NotRequired[Never]
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class TaskStateWaiting(TypedDict, Generic[S]):
|
|
483
|
+
status: Literal["waiting"]
|
|
484
|
+
checkpoint: S
|
|
485
|
+
on: list[TaskId]
|
|
486
|
+
policy: JoinPolicy
|
|
487
|
+
outcome: NotRequired[Never]
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class TaskStateCompleting(TypedDict, Generic[S]):
|
|
491
|
+
status: Literal["completing"]
|
|
492
|
+
outcome: TaskOutcome
|
|
493
|
+
checkpoint: NotRequired[Never]
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class TaskStateTerminal(TypedDict, Generic[S]):
|
|
497
|
+
status: Literal["terminal"]
|
|
498
|
+
outcome: TaskOutcome
|
|
499
|
+
checkpoint: NotRequired[Never]
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
TaskState: TypeAlias = (
|
|
503
|
+
TaskStatePending[Any]
|
|
504
|
+
| TaskStateRunning[Any]
|
|
505
|
+
| TaskStateWaiting[Any]
|
|
506
|
+
| TaskStateCompleting[Any]
|
|
507
|
+
| TaskStateTerminal[Any]
|
|
508
|
+
)
|
|
509
|
+
NextTaskState: TypeAlias = TaskStateRunning[Any] | TaskStateWaiting[Any] | TaskStateTerminal[Any]
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class TaskRecordBase(TypedDict):
|
|
513
|
+
id: TaskId
|
|
514
|
+
conversationId: ConversationId
|
|
515
|
+
kind: str
|
|
516
|
+
version: int
|
|
517
|
+
input: JsonValue
|
|
518
|
+
owner: NotRequired[TaskId]
|
|
519
|
+
background: bool
|
|
520
|
+
abortRequested: bool
|
|
521
|
+
abortReason: NotRequired[Literal["restart"]]
|
|
522
|
+
abandonOnRestart: NotRequired[Literal[True]]
|
|
523
|
+
startedAt: NotRequired[int]
|
|
524
|
+
endedAt: NotRequired[int]
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class LiveTaskRecord(TaskRecordBase):
|
|
528
|
+
state: TaskStatePending[Any] | TaskStateRunning[Any] | TaskStateWaiting[Any]
|
|
529
|
+
memos: NotRequired[dict[str, JsonValue]]
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class SettledTaskRecord(TaskRecordBase):
|
|
533
|
+
state: TaskStateCompleting[Any] | TaskStateTerminal[Any]
|
|
534
|
+
memos: NotRequired[Never]
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
TaskRecord: TypeAlias = LiveTaskRecord | SettledTaskRecord
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class RunningTaskRecord(TaskRecordBase):
|
|
541
|
+
state: TaskStateRunning[Any]
|
|
542
|
+
memos: NotRequired[dict[str, JsonValue]]
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
RunningTask: TypeAlias = RunningTaskRecord
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
PhaseHandler: TypeAlias = Callable[[RunningTask, "TaskRuntime", Context], Awaitable[None]]
|
|
549
|
+
"""Runs one checkpoint phase; it must commit durable progress through ``TaskRuntime.commit()``."""
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class TaskOptions(TypedDict):
|
|
553
|
+
ownership: TaskOwnership
|
|
554
|
+
conversationId: NotRequired[ConversationId]
|
|
555
|
+
background: NotRequired[bool]
|
|
556
|
+
abandonOnRestart: NotRequired[bool]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# ── Documents ───────────────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class SessionDocumentSemantics(TypedDict):
|
|
563
|
+
scope: Literal["session"]
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
class LatestConversationSemantics(TypedDict):
|
|
567
|
+
scope: Literal["conversation"]
|
|
568
|
+
history: Literal["latest"]
|
|
569
|
+
fork: Literal["current", "initial"]
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class RewindableConversationSemantics(TypedDict):
|
|
573
|
+
scope: Literal["conversation"]
|
|
574
|
+
history: Literal["rewindable"]
|
|
575
|
+
fork: Literal["asOf", "current", "initial"]
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class TaskDocumentSemantics(TypedDict):
|
|
579
|
+
scope: Literal["task"]
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
DocumentSemantics: TypeAlias = (
|
|
583
|
+
SessionDocumentSemantics
|
|
584
|
+
| LatestConversationSemantics
|
|
585
|
+
| RewindableConversationSemantics
|
|
586
|
+
| TaskDocumentSemantics
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class CheckpointInfo(TypedDict):
|
|
591
|
+
deltasSinceBase: int
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class SessionDocumentScope(TypedDict):
|
|
595
|
+
kind: Literal["session"]
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
class ConversationDocumentScope(TypedDict):
|
|
599
|
+
kind: Literal["conversation"]
|
|
600
|
+
conversationId: ConversationId
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class TaskDocumentScope(TypedDict):
|
|
604
|
+
kind: Literal["task"]
|
|
605
|
+
taskId: TaskId
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
DocumentScope: TypeAlias = SessionDocumentScope | ConversationDocumentScope | TaskDocumentScope
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class DocumentRecordBase(TypedDict):
|
|
612
|
+
id: DocumentId
|
|
613
|
+
kind: str
|
|
614
|
+
key: NotRequired[str]
|
|
615
|
+
createdAt: Seq
|
|
616
|
+
retiredAt: NotRequired[Seq]
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class SessionDocumentRecord(DocumentRecordBase):
|
|
620
|
+
scope: SessionDocumentScope
|
|
621
|
+
history: NotRequired[Never]
|
|
622
|
+
fork: NotRequired[Never]
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class LatestConversationDocumentRecord(DocumentRecordBase):
|
|
626
|
+
scope: ConversationDocumentScope
|
|
627
|
+
history: Literal["latest"]
|
|
628
|
+
fork: Literal["current", "initial"]
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class RewindableConversationDocumentRecord(DocumentRecordBase):
|
|
632
|
+
scope: ConversationDocumentScope
|
|
633
|
+
history: Literal["rewindable"]
|
|
634
|
+
fork: Literal["asOf", "current", "initial"]
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class TaskDocumentRecord(DocumentRecordBase):
|
|
638
|
+
scope: TaskDocumentScope
|
|
639
|
+
history: NotRequired[Never]
|
|
640
|
+
fork: NotRequired[Never]
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
DocumentRecord: TypeAlias = (
|
|
644
|
+
SessionDocumentRecord
|
|
645
|
+
| LatestConversationDocumentRecord
|
|
646
|
+
| RewindableConversationDocumentRecord
|
|
647
|
+
| TaskDocumentRecord
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class DocumentCreateBase(TypedDict):
|
|
652
|
+
id: DocumentId
|
|
653
|
+
kind: str
|
|
654
|
+
key: NotRequired[str]
|
|
655
|
+
createdAt: NotRequired[Never]
|
|
656
|
+
retiredAt: NotRequired[Never]
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class SessionDocumentCreate(DocumentCreateBase):
|
|
660
|
+
scope: SessionDocumentScope
|
|
661
|
+
history: NotRequired[Never]
|
|
662
|
+
fork: NotRequired[Never]
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class LatestConversationDocumentCreate(DocumentCreateBase):
|
|
666
|
+
scope: ConversationDocumentScope
|
|
667
|
+
history: Literal["latest"]
|
|
668
|
+
fork: Literal["current", "initial"]
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class RewindableConversationDocumentCreate(DocumentCreateBase):
|
|
672
|
+
scope: ConversationDocumentScope
|
|
673
|
+
history: Literal["rewindable"]
|
|
674
|
+
fork: Literal["asOf", "current", "initial"]
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class TaskDocumentCreate(DocumentCreateBase):
|
|
678
|
+
scope: TaskDocumentScope
|
|
679
|
+
history: NotRequired[Never]
|
|
680
|
+
fork: NotRequired[Never]
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
DocumentCreate: TypeAlias = (
|
|
684
|
+
SessionDocumentCreate
|
|
685
|
+
| LatestConversationDocumentCreate
|
|
686
|
+
| RewindableConversationDocumentCreate
|
|
687
|
+
| TaskDocumentCreate
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class DocumentBaseContent(TypedDict):
|
|
692
|
+
version: int
|
|
693
|
+
kind: Literal["base"]
|
|
694
|
+
value: JsonObject
|
|
695
|
+
ops: NotRequired[Never]
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
class DocumentDeltaContent(TypedDict):
|
|
699
|
+
version: int
|
|
700
|
+
kind: Literal["delta"]
|
|
701
|
+
ops: list[Op]
|
|
702
|
+
value: NotRequired[Never]
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
DocumentContent: TypeAlias = DocumentBaseContent | DocumentDeltaContent
|
|
706
|
+
DocumentPoint: TypeAlias = Seq | Literal["current"]
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class DocumentCopySource(TypedDict):
|
|
710
|
+
id: DocumentId
|
|
711
|
+
at: DocumentPoint
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class StoredDocument(TypedDict):
|
|
715
|
+
record: DocumentRecord
|
|
716
|
+
version: int
|
|
717
|
+
value: JsonObject
|
|
718
|
+
deltasSinceBase: int
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class DocumentAddress(TypedDict):
|
|
722
|
+
kind: str
|
|
723
|
+
scope: DocumentScope
|
|
724
|
+
key: NotRequired[str]
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class DocumentQuery(TypedDict):
|
|
728
|
+
scope: DocumentScope
|
|
729
|
+
at: DocumentPoint
|
|
730
|
+
kind: NotRequired[str]
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# ── Commits and storage writes ──────────────────────────────────────────────
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class ConversationWrite(TypedDict):
|
|
737
|
+
type: Literal["conversation"]
|
|
738
|
+
value: ConversationRecord
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class EntryWrite(TypedDict):
|
|
742
|
+
type: Literal["entry"]
|
|
743
|
+
value: EntryRecord
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class TaskWrite(TypedDict):
|
|
747
|
+
type: Literal["task"]
|
|
748
|
+
value: TaskRecord
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
class SubmissionWrite(TypedDict):
|
|
752
|
+
type: Literal["submission"]
|
|
753
|
+
value: SubmissionRecord
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
class DocumentCreateWrite(TypedDict):
|
|
757
|
+
type: Literal["document.create"]
|
|
758
|
+
record: DocumentCreate
|
|
759
|
+
content: DocumentBaseContent
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class DocumentCopyWrite(TypedDict):
|
|
763
|
+
type: Literal["document.copy"]
|
|
764
|
+
record: DocumentCreate
|
|
765
|
+
source: DocumentCopySource
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class DocumentChangeWrite(TypedDict):
|
|
769
|
+
type: Literal["document.change"]
|
|
770
|
+
id: DocumentId
|
|
771
|
+
content: DocumentContent
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
class DocumentRetireWrite(TypedDict):
|
|
775
|
+
type: Literal["document.retire"]
|
|
776
|
+
id: DocumentId
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
StorageWrite: TypeAlias = (
|
|
780
|
+
ConversationWrite
|
|
781
|
+
| EntryWrite
|
|
782
|
+
| TaskWrite
|
|
783
|
+
| SubmissionWrite
|
|
784
|
+
| DocumentCreateWrite
|
|
785
|
+
| DocumentCopyWrite
|
|
786
|
+
| DocumentChangeWrite
|
|
787
|
+
| DocumentRetireWrite
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
TableCommitChange: TypeAlias = ConversationWrite | EntryWrite | TaskWrite | SubmissionWrite
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
class DocumentCommittedChange(TypedDict):
|
|
794
|
+
type: Literal["document"]
|
|
795
|
+
record: DocumentRecord
|
|
796
|
+
conversationId: ConversationId | None
|
|
797
|
+
version: int | None
|
|
798
|
+
value: JsonObject | None
|
|
799
|
+
ops: list[Op]
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
class DocumentCopiedChange(TypedDict):
|
|
803
|
+
type: Literal["document.copy"]
|
|
804
|
+
record: DocumentRecord
|
|
805
|
+
conversationId: ConversationId
|
|
806
|
+
source: DocumentCopySource
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
DocumentCommitChange: TypeAlias = DocumentCommittedChange | DocumentCopiedChange
|
|
810
|
+
CommitChange: TypeAlias = TableCommitChange | DocumentCommitChange
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
class CommitPublication(TypedDict):
|
|
814
|
+
seq: Seq
|
|
815
|
+
changes: list[CommitChange]
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
class WatchEnded(TypedDict):
|
|
819
|
+
reason: Literal["stopped", "cancelled", "session_closed", "retired"]
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
class WatchListenerErrorEnd(TypedDict):
|
|
823
|
+
reason: Literal["listener_error"]
|
|
824
|
+
error: BaseException
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
class WatchSessionFailedEnd(TypedDict):
|
|
828
|
+
reason: Literal["session_failed"]
|
|
829
|
+
error: object
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
WatchEnd: TypeAlias = WatchEnded | WatchListenerErrorEnd | WatchSessionFailedEnd
|
|
833
|
+
"""Terminal result of one document watch."""
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class WatchHandle(Protocol, Generic[JsonValueT_co]):
|
|
837
|
+
"""Serialized exact-frame observation of an immutable value with bounded pending delivery."""
|
|
838
|
+
|
|
839
|
+
@property
|
|
840
|
+
def value(self) -> JsonValueT_co:
|
|
841
|
+
"""Acquisition revision before start; latest delivered immutable revision afterward."""
|
|
842
|
+
...
|
|
843
|
+
|
|
844
|
+
@property
|
|
845
|
+
def closed(self) -> asyncio.Future[WatchEnd]:
|
|
846
|
+
"""Settle when the watch terminates; an already-running callback remains caller-owned."""
|
|
847
|
+
...
|
|
848
|
+
|
|
849
|
+
def start(self, listener: Callable[[JsonValueT_co, list[Op], Context], Any]) -> None:
|
|
850
|
+
"""Install the sole asynchronous listener. Never invokes it inline."""
|
|
851
|
+
...
|
|
852
|
+
|
|
853
|
+
def stop(self) -> asyncio.Future[WatchEnd]:
|
|
854
|
+
"""Idempotently stop future callbacks and return this watch's terminal result."""
|
|
855
|
+
...
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
DocumentWatch: TypeAlias = WatchHandle[JsonObject | None]
|
|
859
|
+
"""Serialized exact-frame observation of one document incarnation."""
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class DocumentObserver(Protocol):
|
|
863
|
+
"""Non-creating document watch acquisition shared by Session and later invocation APIs."""
|
|
864
|
+
|
|
865
|
+
async def watch_doc(self, token: Any, *args: Any) -> DocumentWatch | None: ...
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
# ── Queries and pages ───────────────────────────────────────────────────────
|
|
869
|
+
|
|
870
|
+
ScanOrder: TypeAlias = Literal["ascending", "descending"]
|
|
871
|
+
Cursor: TypeAlias = dict[str, JsonValue]
|
|
872
|
+
|
|
873
|
+
ItemT = TypeVar("ItemT")
|
|
874
|
+
CursorT = TypeVar("CursorT")
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
class Page(TypedDict, Generic[ItemT, CursorT]):
|
|
878
|
+
items: list[ItemT]
|
|
879
|
+
next: NotRequired[CursorT]
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
class ConversationQuery(TypedDict):
|
|
883
|
+
ownerConversationId: NotRequired[ConversationId]
|
|
884
|
+
ownerTaskId: NotRequired[TaskId]
|
|
885
|
+
order: NotRequired[ScanOrder]
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
class EntryQuery(TypedDict):
|
|
889
|
+
conversationId: ConversationId
|
|
890
|
+
minEntryId: NotRequired[EntryId]
|
|
891
|
+
maxEntryId: NotRequired[EntryId]
|
|
892
|
+
order: NotRequired[ScanOrder]
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class TaskQuery(TypedDict):
|
|
896
|
+
conversationId: NotRequired[ConversationId]
|
|
897
|
+
kind: NotRequired[str]
|
|
898
|
+
status: NotRequired[Literal["pending", "running", "waiting", "completing", "terminal"]]
|
|
899
|
+
abortRequested: NotRequired[bool]
|
|
900
|
+
background: NotRequired[bool]
|
|
901
|
+
order: NotRequired[ScanOrder]
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
class SubmissionQuery(TypedDict):
|
|
905
|
+
conversationId: NotRequired[ConversationId]
|
|
906
|
+
status: NotRequired[Literal["queued", "placed", "done", "unanswered"]]
|
|
907
|
+
order: NotRequired[ScanOrder]
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
class EntryLookup(TypedDict):
|
|
911
|
+
entry: EntryRecord
|
|
912
|
+
commitSeq: Seq
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# ── Storage protocol ────────────────────────────────────────────────────────
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
@runtime_checkable
|
|
919
|
+
class Storage(Protocol):
|
|
920
|
+
"""Atomic persistence boundary for Session records.
|
|
921
|
+
|
|
922
|
+
Storage trusts the owning Session to supply semantically valid records, references, ancestry,
|
|
923
|
+
and transitions. Implementations enforce atomicity, global ID ownership, immutable
|
|
924
|
+
conversation/entry creation, document record consistency, and detached values; the Session
|
|
925
|
+
serializes commits.
|
|
926
|
+
|
|
927
|
+
An error a method throws is final: it fails the Session, which nothing retries, so retry
|
|
928
|
+
transient failures inside the method. Two errors of a read fail only that read: a
|
|
929
|
+
:class:`~pi_durable.errors.StorageRequestError` for an invalid request, and a rejection
|
|
930
|
+
because its caller's context was aborted. Any error from :meth:`commit` or :meth:`mint_id`
|
|
931
|
+
is fatal.
|
|
932
|
+
"""
|
|
933
|
+
|
|
934
|
+
async def commit(self, writes: Sequence[StorageWrite], context: Context) -> Seq:
|
|
935
|
+
"""Atomically persist one batch and return its sequence."""
|
|
936
|
+
|
|
937
|
+
async def mint_id(self) -> int:
|
|
938
|
+
"""Return a fresh ID greater than every ID minted or stored before, also across reopen.
|
|
939
|
+
|
|
940
|
+
The value is unbranded; a caller applies :func:`~pi_durable.ids.id_from_number` at its
|
|
941
|
+
own trusted boundary.
|
|
942
|
+
"""
|
|
943
|
+
|
|
944
|
+
async def conversation(self, id: ConversationId, context: Context) -> ConversationRecord | None:
|
|
945
|
+
"""Look up one conversation by exact ID."""
|
|
946
|
+
|
|
947
|
+
async def scan_conversations(
|
|
948
|
+
self,
|
|
949
|
+
query: ConversationQuery,
|
|
950
|
+
limit: int,
|
|
951
|
+
cursor: Cursor | None,
|
|
952
|
+
context: Context,
|
|
953
|
+
) -> Page[ConversationRecord, Cursor]:
|
|
954
|
+
"""Scan conversations in ``query['order']`` (default ascending) by ID."""
|
|
955
|
+
|
|
956
|
+
@overload
|
|
957
|
+
async def entry(self, id: EntryId, context: Context) -> EntryLookup | None: ...
|
|
958
|
+
|
|
959
|
+
@overload
|
|
960
|
+
async def entry(
|
|
961
|
+
self,
|
|
962
|
+
conversation_id: ConversationId,
|
|
963
|
+
id: EntryId,
|
|
964
|
+
context: Context,
|
|
965
|
+
) -> EntryLookup | None: ...
|
|
966
|
+
|
|
967
|
+
async def entry(
|
|
968
|
+
self,
|
|
969
|
+
*args: Any,
|
|
970
|
+
context: Context | None = None,
|
|
971
|
+
**kwargs: Any,
|
|
972
|
+
) -> EntryLookup | None:
|
|
973
|
+
"""Look up one global entry, or one visible through a conversation's ancestry."""
|
|
974
|
+
|
|
975
|
+
async def find_latest_head_marker(
|
|
976
|
+
self,
|
|
977
|
+
conversation_id: ConversationId,
|
|
978
|
+
at_or_before_entry_id: EntryId | None,
|
|
979
|
+
context: Context,
|
|
980
|
+
) -> EntryWithHead | None:
|
|
981
|
+
"""Return the newest visible entry with a ``head`` at or below the optional cutoff."""
|
|
982
|
+
|
|
983
|
+
async def scan_entries(
|
|
984
|
+
self,
|
|
985
|
+
query: EntryQuery,
|
|
986
|
+
limit: int,
|
|
987
|
+
cursor: Cursor | None,
|
|
988
|
+
context: Context,
|
|
989
|
+
) -> Page[EntryRecord, Cursor]:
|
|
990
|
+
"""Scan the inclusive visible range in ``query['order']`` (default descending)."""
|
|
991
|
+
|
|
992
|
+
async def task(self, id: TaskId, context: Context) -> TaskRecord | None:
|
|
993
|
+
"""Look up the latest complete record for one task."""
|
|
994
|
+
|
|
995
|
+
async def scan_tasks(
|
|
996
|
+
self,
|
|
997
|
+
query: TaskQuery,
|
|
998
|
+
limit: int,
|
|
999
|
+
cursor: Cursor | None,
|
|
1000
|
+
context: Context,
|
|
1001
|
+
) -> Page[TaskRecord, Cursor]:
|
|
1002
|
+
"""Scan task records matching every supplied filter."""
|
|
1003
|
+
|
|
1004
|
+
async def submission(self, id: SubmissionId, context: Context) -> SubmissionRecord | None:
|
|
1005
|
+
"""Look up the latest complete record for one admitted submission."""
|
|
1006
|
+
|
|
1007
|
+
async def scan_submissions(
|
|
1008
|
+
self,
|
|
1009
|
+
query: SubmissionQuery,
|
|
1010
|
+
limit: int,
|
|
1011
|
+
cursor: Cursor | None,
|
|
1012
|
+
context: Context,
|
|
1013
|
+
) -> Page[SubmissionRecord, Cursor]:
|
|
1014
|
+
"""Scan submissions matching every supplied filter."""
|
|
1015
|
+
|
|
1016
|
+
async def submission_by_request(
|
|
1017
|
+
self,
|
|
1018
|
+
conversation_id: ConversationId,
|
|
1019
|
+
request_id: str,
|
|
1020
|
+
context: Context,
|
|
1021
|
+
) -> SubmissionRecord | None:
|
|
1022
|
+
"""Find a submission by its conversation-scoped host deduplication key."""
|
|
1023
|
+
|
|
1024
|
+
async def find_document(
|
|
1025
|
+
self,
|
|
1026
|
+
address: DocumentAddress,
|
|
1027
|
+
at: DocumentPoint,
|
|
1028
|
+
context: Context,
|
|
1029
|
+
) -> DocumentRecord | None:
|
|
1030
|
+
"""Resolve the incarnation occupying one exact logical address at the selected point."""
|
|
1031
|
+
|
|
1032
|
+
async def document(
|
|
1033
|
+
self,
|
|
1034
|
+
id: DocumentId,
|
|
1035
|
+
at: DocumentPoint,
|
|
1036
|
+
context: Context,
|
|
1037
|
+
) -> StoredDocument | None:
|
|
1038
|
+
"""Materialize one specific incarnation by ID at the selected point."""
|
|
1039
|
+
|
|
1040
|
+
async def scan_documents(
|
|
1041
|
+
self,
|
|
1042
|
+
query: DocumentQuery,
|
|
1043
|
+
limit: int,
|
|
1044
|
+
cursor: Cursor | None,
|
|
1045
|
+
context: Context,
|
|
1046
|
+
) -> Page[DocumentRecord, Cursor]:
|
|
1047
|
+
"""Scan incarnations alive in one exact scope at the selected point."""
|
|
1048
|
+
|
|
1049
|
+
def close(self, context: Context) -> Awaitable[None]:
|
|
1050
|
+
"""Release backend resources; all later operations must reject."""
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
# ── Public Session and Tx surfaces ─────────────────────────────────────────
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
class Tx(Protocol):
|
|
1057
|
+
"""Transaction surface available inside one Session commit callback."""
|
|
1058
|
+
|
|
1059
|
+
async def conversation(self, id: ConversationId) -> ConversationRecord | None: ...
|
|
1060
|
+
|
|
1061
|
+
async def entry(self, first: Any, second: Any = None) -> EntryRecord | None: ...
|
|
1062
|
+
|
|
1063
|
+
async def task(self, id: TaskId) -> TaskRecord | None: ...
|
|
1064
|
+
|
|
1065
|
+
async def scan_conversations(
|
|
1066
|
+
self,
|
|
1067
|
+
query: ConversationQuery,
|
|
1068
|
+
limit: int,
|
|
1069
|
+
cursor: Cursor | None = None,
|
|
1070
|
+
) -> Page[ConversationRecord, Cursor]: ...
|
|
1071
|
+
|
|
1072
|
+
async def scan_entries(
|
|
1073
|
+
self,
|
|
1074
|
+
query: EntryQuery,
|
|
1075
|
+
limit: int,
|
|
1076
|
+
cursor: Cursor | None = None,
|
|
1077
|
+
) -> Page[EntryRecord, Cursor]: ...
|
|
1078
|
+
|
|
1079
|
+
async def latest_head_marker(self, conversation_id: ConversationId) -> EntryWithHead | None: ...
|
|
1080
|
+
|
|
1081
|
+
async def scan_tasks(
|
|
1082
|
+
self,
|
|
1083
|
+
query: TaskQuery,
|
|
1084
|
+
limit: int,
|
|
1085
|
+
cursor: Cursor | None = None,
|
|
1086
|
+
) -> Page[TaskRecord, Cursor]: ...
|
|
1087
|
+
|
|
1088
|
+
async def submission_by_request(
|
|
1089
|
+
self, conversation_id: ConversationId, request_id: str
|
|
1090
|
+
) -> SubmissionRecord | None: ...
|
|
1091
|
+
|
|
1092
|
+
async def create_conversation(self, options: dict[str, Any]) -> ConversationRecord: ...
|
|
1093
|
+
|
|
1094
|
+
async def fork_conversation(
|
|
1095
|
+
self,
|
|
1096
|
+
parent_conversation_id: ConversationId,
|
|
1097
|
+
at: EntryId,
|
|
1098
|
+
options: dict[str, Any],
|
|
1099
|
+
) -> ConversationRecord: ...
|
|
1100
|
+
|
|
1101
|
+
async def append_entry(self, first: Any, second: Any, third: Any = None) -> EntryRecord: ...
|
|
1102
|
+
|
|
1103
|
+
async def create_task(self, task: Any, input: JsonValue, options: TaskOptions) -> TaskId: ...
|
|
1104
|
+
|
|
1105
|
+
async def create_submission(self, create: SubmissionCreate) -> SubmissionRecord: ...
|
|
1106
|
+
|
|
1107
|
+
def settle_submission(self, id: SubmissionId, settlement: SubmissionSettlement) -> None: ...
|
|
1108
|
+
|
|
1109
|
+
def place_submission(self, id: SubmissionId, entry: EntryId) -> None: ...
|
|
1110
|
+
|
|
1111
|
+
def set_task(self, value: TaskRecord) -> None: ...
|
|
1112
|
+
|
|
1113
|
+
async def doc(self, token: Any, *args: Any) -> Any: ...
|
|
1114
|
+
|
|
1115
|
+
async def retire_doc(self, token: Any, *args: Any) -> None: ...
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
class DocumentReader(Protocol):
|
|
1119
|
+
"""Read side of committed documents: ``Session.snapshot`` and ``snapshotAsOf``."""
|
|
1120
|
+
|
|
1121
|
+
async def snapshot(self, token: Any, *args: Any) -> JsonObject | None: ...
|
|
1122
|
+
|
|
1123
|
+
async def snapshot_as_of(self, token: Any, *args: Any) -> JsonObject | None: ...
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
class SessionEndClosed(TypedDict):
|
|
1127
|
+
"""The Session ended with ``close()``."""
|
|
1128
|
+
|
|
1129
|
+
reason: Literal["closed"]
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
class SessionEndFailed(TypedDict):
|
|
1133
|
+
"""The Session failed and closed itself; ``error`` is the first Storage error."""
|
|
1134
|
+
|
|
1135
|
+
reason: Literal["failed"]
|
|
1136
|
+
error: Any
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
SessionEnd: TypeAlias = SessionEndClosed | SessionEndFailed
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
class Session(Protocol):
|
|
1143
|
+
"""Owner of one mutation line, its records, and its tracked documents."""
|
|
1144
|
+
|
|
1145
|
+
async def commit(self, change: Any, context: Context) -> Any: ...
|
|
1146
|
+
|
|
1147
|
+
async def close(self, context: Context) -> None: ...
|
|
1148
|
+
|
|
1149
|
+
@property
|
|
1150
|
+
def closed(self) -> asyncio.Future[SessionEnd]: ...
|
|
1151
|
+
|
|
1152
|
+
def subscribe_commits(self, listener: Any) -> Any: ...
|
|
1153
|
+
|
|
1154
|
+
def subscribe_close(self, listener: Any) -> Any: ...
|
|
1155
|
+
|
|
1156
|
+
async def snapshot(self, token: Any, *args: Any) -> JsonObject | None: ...
|
|
1157
|
+
|
|
1158
|
+
async def document_state(self, token: Any, *args: Any) -> Any: ...
|
|
1159
|
+
|
|
1160
|
+
async def watch_doc(self, token: Any, *args: Any) -> Any: ...
|
|
1161
|
+
|
|
1162
|
+
async def snapshot_as_of(self, token: Any, *args: Any) -> JsonObject | None: ...
|
|
1163
|
+
|
|
1164
|
+
# Port addition: the Session-level read that `Conversation.context()` exposes. Upstream keeps
|
|
1165
|
+
# it in the Harness, which does not exist yet.
|
|
1166
|
+
async def context(
|
|
1167
|
+
self,
|
|
1168
|
+
conversation_id: ConversationId,
|
|
1169
|
+
context: Context,
|
|
1170
|
+
at: EntryId | None = None,
|
|
1171
|
+
) -> "ContextView": ...
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
# ── Entry construction ─────────────────────────────────────────────────────
|
|
1175
|
+
|
|
1176
|
+
_ENTRY_DRAFT_FIELDS = frozenset({"kind", "model", "data", "head", "edits"})
|
|
1177
|
+
_EDIT_FIELDS = frozenset({"target", "action", "messages"})
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def _require_mapping(value: object, label: str) -> Mapping[str, Any]:
|
|
1181
|
+
if not isinstance(value, Mapping):
|
|
1182
|
+
raise TypeError(f"{label} must be a mapping; got {type(value).__name__}")
|
|
1183
|
+
for key in value:
|
|
1184
|
+
if not isinstance(key, str):
|
|
1185
|
+
raise TypeError(f"{label} must have string keys; got {key!r}")
|
|
1186
|
+
return cast(Mapping[str, Any], value)
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _copy_messages(value: object, label: str) -> list[MessageRecord]:
|
|
1190
|
+
if not isinstance(value, list):
|
|
1191
|
+
raise TypeError(f"{label} must be a list of messages")
|
|
1192
|
+
messages: list[MessageRecord] = []
|
|
1193
|
+
for index, item in enumerate(value):
|
|
1194
|
+
message = _require_mapping(item, f"{label}[{index}]")
|
|
1195
|
+
role = message.get("role")
|
|
1196
|
+
if not isinstance(role, str) or not role:
|
|
1197
|
+
raise TypeError(f"{label}[{index}] must have a non-empty string role")
|
|
1198
|
+
messages.append(cast(MessageRecord, copy_json(message)))
|
|
1199
|
+
return messages
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def _copy_context_edits(value: object, label: str) -> list[ContextEdit]:
|
|
1203
|
+
if not isinstance(value, list):
|
|
1204
|
+
raise TypeError(f"{label} must be a list of context edits")
|
|
1205
|
+
edits: list[ContextEdit] = []
|
|
1206
|
+
for index, item in enumerate(value):
|
|
1207
|
+
edit = _require_mapping(item, f"{label}[{index}]")
|
|
1208
|
+
unknown = set(edit) - _EDIT_FIELDS
|
|
1209
|
+
if unknown:
|
|
1210
|
+
raise TypeError(f"{label}[{index}] has unknown field {sorted(unknown)[0]!r}")
|
|
1211
|
+
if "target" not in edit:
|
|
1212
|
+
raise TypeError(f"{label}[{index}] is missing target")
|
|
1213
|
+
if "action" not in edit:
|
|
1214
|
+
raise TypeError(f"{label}[{index}] is missing action")
|
|
1215
|
+
target = require_positive_int(edit["target"], f"{label}[{index}] target")
|
|
1216
|
+
action = edit["action"]
|
|
1217
|
+
if action == "omit":
|
|
1218
|
+
if "messages" in edit:
|
|
1219
|
+
raise TypeError(f"{label}[{index}] omit cannot carry messages")
|
|
1220
|
+
edits.append(cast(ContextEdit, freeze_json({"target": target, "action": "omit"})))
|
|
1221
|
+
elif action == "replace":
|
|
1222
|
+
if "messages" not in edit:
|
|
1223
|
+
raise TypeError(f"{label}[{index}] replace requires messages")
|
|
1224
|
+
messages = _copy_messages(edit["messages"], f"{label}[{index}].messages")
|
|
1225
|
+
edits.append(
|
|
1226
|
+
cast(
|
|
1227
|
+
ContextEdit,
|
|
1228
|
+
freeze_json({"target": target, "action": "replace", "messages": messages}),
|
|
1229
|
+
)
|
|
1230
|
+
)
|
|
1231
|
+
else:
|
|
1232
|
+
raise TypeError(f"{label}[{index}].action must be 'omit' or 'replace'")
|
|
1233
|
+
return edits
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def _copy_entry_head(value: object, label: str) -> EntryId | Literal["self"]:
|
|
1237
|
+
if value == "self":
|
|
1238
|
+
return "self"
|
|
1239
|
+
return cast(EntryId, require_positive_int(value, label))
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def entry_draft(value: Mapping[str, Any]) -> EntryDraft:
|
|
1243
|
+
"""Validate and freeze an entry draft.
|
|
1244
|
+
|
|
1245
|
+
Unknown fields, a missing or empty kind, malformed edits, non-JSON data, and non-message model
|
|
1246
|
+
entries are rejected with ``TypeError``. The returned value is a deep-frozen JSON object.
|
|
1247
|
+
"""
|
|
1248
|
+
|
|
1249
|
+
draft = _require_mapping(value, "Entry draft")
|
|
1250
|
+
unknown = set(draft) - _ENTRY_DRAFT_FIELDS
|
|
1251
|
+
if unknown:
|
|
1252
|
+
raise TypeError(f"Entry draft has unknown field {sorted(unknown)[0]!r}")
|
|
1253
|
+
kind = draft.get("kind")
|
|
1254
|
+
if not isinstance(kind, str) or not kind:
|
|
1255
|
+
raise TypeError("Entry kind must be a non-empty string")
|
|
1256
|
+
copied: dict[str, JsonValue] = {"kind": kind}
|
|
1257
|
+
if "model" in draft:
|
|
1258
|
+
copied["model"] = cast(JsonValue, _copy_messages(draft["model"], "Entry draft model"))
|
|
1259
|
+
if "data" in draft:
|
|
1260
|
+
copied["data"] = copy_json(draft["data"])
|
|
1261
|
+
if "head" in draft:
|
|
1262
|
+
copied["head"] = cast(JsonValue, _copy_entry_head(draft["head"], "Entry draft head"))
|
|
1263
|
+
if "edits" in draft:
|
|
1264
|
+
copied["edits"] = cast(JsonValue, _copy_context_edits(draft["edits"], "Entry draft edits"))
|
|
1265
|
+
return cast(EntryDraft, freeze_json(copied))
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def entry_record(
|
|
1269
|
+
entry_id: EntryId,
|
|
1270
|
+
conversation_id: ConversationId,
|
|
1271
|
+
draft: Mapping[str, Any],
|
|
1272
|
+
*,
|
|
1273
|
+
by_task_id: TaskId | None = None,
|
|
1274
|
+
) -> EntryRecord:
|
|
1275
|
+
"""Build an immutable entry record from a validated draft.
|
|
1276
|
+
|
|
1277
|
+
``head: "self"`` resolves to the newly assigned entry ID. The record copies by value, so
|
|
1278
|
+
neither the draft nor its model messages can change it afterwards.
|
|
1279
|
+
"""
|
|
1280
|
+
|
|
1281
|
+
entry_id = cast(EntryId, require_positive_int(entry_id, "entry ID"))
|
|
1282
|
+
conversation_id = cast(ConversationId, require_positive_int(conversation_id, "conversation ID"))
|
|
1283
|
+
validated = entry_draft(draft)
|
|
1284
|
+
record: dict[str, JsonValue] = {
|
|
1285
|
+
"id": entry_id,
|
|
1286
|
+
"conversationId": conversation_id,
|
|
1287
|
+
"kind": validated["kind"],
|
|
1288
|
+
}
|
|
1289
|
+
for field in ("model", "data", "edits"):
|
|
1290
|
+
if field in validated:
|
|
1291
|
+
record[field] = copy_json(validated[field])
|
|
1292
|
+
if "head" in validated:
|
|
1293
|
+
head = validated["head"]
|
|
1294
|
+
record["head"] = entry_id if head == "self" else head
|
|
1295
|
+
if by_task_id is not None:
|
|
1296
|
+
record["byTaskId"] = require_positive_int(by_task_id, "task ID")
|
|
1297
|
+
return cast(EntryRecord, freeze_json(record))
|