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
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""Durable Harness: a Session with task scheduling, conversations, and inspection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable, Coroutine, Mapping
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import Any, Literal, cast
|
|
8
|
+
|
|
9
|
+
from ..context import Context, with_abort_signal, without_abort_signal
|
|
10
|
+
from ..entries import ResetEntry
|
|
11
|
+
from ..ids import ConversationId, EntryId, SubmissionId, TaskId
|
|
12
|
+
from ..session.session import SessionImpl, _now_ms, contained
|
|
13
|
+
from ..session.transaction import Transaction, TransactionScope
|
|
14
|
+
from ..types import (
|
|
15
|
+
ROOT_CONVERSATION_ID,
|
|
16
|
+
ConversationOwnership,
|
|
17
|
+
ConversationRecord,
|
|
18
|
+
EntryRecord,
|
|
19
|
+
Page,
|
|
20
|
+
Storage,
|
|
21
|
+
SubmissionRecord,
|
|
22
|
+
TaskOutcome,
|
|
23
|
+
TaskRecord,
|
|
24
|
+
)
|
|
25
|
+
from .agent import AgentDoc, configure, create_agent, resolve_agent, resolve_settings
|
|
26
|
+
from .compaction import create_compaction
|
|
27
|
+
from .inbox import InboxDoc, QueueModes, withdraw_queued_inputs
|
|
28
|
+
from .live import LiveDoc, settle_scheduler_outcome, start_generation
|
|
29
|
+
from .provider import ProviderDoc
|
|
30
|
+
from .registry import BUILTIN_TASKS
|
|
31
|
+
from .scheduler import TaskScheduler
|
|
32
|
+
from .submissions import AbortResult, Submissions
|
|
33
|
+
from .task_graph import TaskGraphView
|
|
34
|
+
from .types import (
|
|
35
|
+
Agent,
|
|
36
|
+
ContextView,
|
|
37
|
+
ConversationHandle,
|
|
38
|
+
HarnessInspection,
|
|
39
|
+
RegistryReader,
|
|
40
|
+
RegistrySnapshot,
|
|
41
|
+
SettledSubmissionRecord,
|
|
42
|
+
SettledTask,
|
|
43
|
+
Submission,
|
|
44
|
+
)
|
|
45
|
+
from .usage import UsageDoc, UsageState, add_usage_state
|
|
46
|
+
from .util import closed_error, scan_all
|
|
47
|
+
from .view import ConversationViews
|
|
48
|
+
|
|
49
|
+
_SCAN_PAGE_SIZE = 256
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _ownership(options: Mapping[str, Any]) -> ConversationOwnership:
|
|
53
|
+
"""Host callers choose ownership explicitly; a conversation handle never infers it."""
|
|
54
|
+
|
|
55
|
+
ownership = options.get("ownership")
|
|
56
|
+
if not isinstance(ownership, Mapping):
|
|
57
|
+
raise TypeError("Conversation creation requires explicit ownership")
|
|
58
|
+
return cast(ConversationOwnership, ownership)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _safe_report(on_report: Callable[[BaseException], Any] | None) -> Callable[[BaseException], None]:
|
|
62
|
+
def report(error: BaseException) -> None:
|
|
63
|
+
def call() -> Any:
|
|
64
|
+
return None if on_report is None else on_report(error)
|
|
65
|
+
|
|
66
|
+
contained(call, lambda _error: None)
|
|
67
|
+
|
|
68
|
+
return report
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def safe_now(now: Callable[[], int] | None, report: Callable[[BaseException], None]) -> Callable[[], int]:
|
|
72
|
+
if now is None:
|
|
73
|
+
return _now_ms
|
|
74
|
+
reported = False
|
|
75
|
+
|
|
76
|
+
def clock() -> int:
|
|
77
|
+
nonlocal reported
|
|
78
|
+
try:
|
|
79
|
+
return now()
|
|
80
|
+
except Exception as error:
|
|
81
|
+
if not reported:
|
|
82
|
+
reported = True
|
|
83
|
+
report(error)
|
|
84
|
+
return _now_ms()
|
|
85
|
+
|
|
86
|
+
return clock
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BoundConversation:
|
|
90
|
+
"""Invocation-bound conversation handle for tasks and tools."""
|
|
91
|
+
|
|
92
|
+
__slots__ = ("id", "_harness", "_binding")
|
|
93
|
+
|
|
94
|
+
def __init__(self, id: ConversationId, harness: Harness, binding: Mapping[str, Any]) -> None:
|
|
95
|
+
self.id = id
|
|
96
|
+
self._harness = harness
|
|
97
|
+
self._binding = binding
|
|
98
|
+
|
|
99
|
+
def _bind(self, context: Context) -> Context:
|
|
100
|
+
return _bind_context(self._binding, context)
|
|
101
|
+
|
|
102
|
+
async def submit(self, submission: Any, context: Context) -> Any:
|
|
103
|
+
cast(Callable[[], None], self._binding["check"])()
|
|
104
|
+
# An invocation-bound handle accepts only input submissions; tools use ordinary transaction
|
|
105
|
+
# writes for passive entries (spec section 7.3).
|
|
106
|
+
if not isinstance(submission, Mapping):
|
|
107
|
+
raise TypeError("A conversation handle accepts only input submissions")
|
|
108
|
+
kind = submission.get("type")
|
|
109
|
+
if kind is not None and kind != "input":
|
|
110
|
+
raise TypeError("A conversation handle accepts only input submissions")
|
|
111
|
+
if kind is None and "content" not in submission:
|
|
112
|
+
raise TypeError("A conversation handle accepts only input submissions")
|
|
113
|
+
admitted = await self._harness.submit(self.id, submission, self._bind(context))
|
|
114
|
+
return BoundSubmission(admitted, self._binding)
|
|
115
|
+
|
|
116
|
+
async def abort(self, context: Context, options: Mapping[str, Any] | None = None) -> None:
|
|
117
|
+
cast(Callable[[], None], self._binding["check"])()
|
|
118
|
+
await self._harness._tasks.abort_conversation(
|
|
119
|
+
self.id, (options or {}).get("background") is True, self._bind(context)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def wait_for_idle(self, context: Context) -> None:
|
|
123
|
+
cast(Callable[[], None], self._binding["check"])()
|
|
124
|
+
await self._harness._tasks.wait_for_idle(self.id, self._bind(context))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class BoundSubmission:
|
|
128
|
+
"""Invocation-bound submission handle for tasks and tools."""
|
|
129
|
+
|
|
130
|
+
__slots__ = ("id", "_submission", "_binding")
|
|
131
|
+
|
|
132
|
+
def __init__(self, submission: Submission, binding: Mapping[str, Any]) -> None:
|
|
133
|
+
self.id = submission.id
|
|
134
|
+
self._submission = submission
|
|
135
|
+
self._binding = binding
|
|
136
|
+
|
|
137
|
+
def _bind(self, context: Context) -> Context:
|
|
138
|
+
return _bind_context(self._binding, context)
|
|
139
|
+
|
|
140
|
+
async def status(self, context: Context) -> SubmissionRecord:
|
|
141
|
+
cast(Callable[[], None], self._binding["check"])()
|
|
142
|
+
return await self._submission.status(self._bind(context))
|
|
143
|
+
|
|
144
|
+
async def wait(self, context: Context) -> SettledSubmissionRecord:
|
|
145
|
+
cast(Callable[[], None], self._binding["check"])()
|
|
146
|
+
return await self._submission.wait(self._bind(context))
|
|
147
|
+
|
|
148
|
+
async def abort(self, context: Context) -> AbortResult:
|
|
149
|
+
cast(Callable[[], None], self._binding["check"])()
|
|
150
|
+
return await self._submission.abort(self._bind(context))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ConversationHandleImpl:
|
|
154
|
+
"""Stateless handle for one conversation, bound to the Harness that returned it."""
|
|
155
|
+
|
|
156
|
+
__slots__ = ("id", "_harness")
|
|
157
|
+
|
|
158
|
+
def __init__(self, id: ConversationId, harness: Harness) -> None:
|
|
159
|
+
self.id = id
|
|
160
|
+
self._harness = harness
|
|
161
|
+
|
|
162
|
+
async def agent(self, context: Context) -> Agent:
|
|
163
|
+
return await self._harness.resolve_agent(self.id, None, context)
|
|
164
|
+
|
|
165
|
+
async def configure(self, change: Mapping[str, Any], context: Context) -> None:
|
|
166
|
+
async def apply(tx: Transaction) -> None:
|
|
167
|
+
await configure(tx, self.id, change)
|
|
168
|
+
|
|
169
|
+
await self._harness.commit(apply, context)
|
|
170
|
+
|
|
171
|
+
async def submit(self, submission: Any, context: Context) -> Any:
|
|
172
|
+
return await self._harness.submit(self.id, submission, context)
|
|
173
|
+
|
|
174
|
+
async def commit(self, change: Callable[[Transaction], Any], context: Context) -> Any:
|
|
175
|
+
return await self._harness.commit_with(change, context, TransactionScope(conversation_id=self.id))
|
|
176
|
+
|
|
177
|
+
async def context(self, context: Context, options: Mapping[str, Any] | None = None) -> ContextView:
|
|
178
|
+
return await self._harness.context(self.id, context, (options or {}).get("at"))
|
|
179
|
+
|
|
180
|
+
async def entries(
|
|
181
|
+
self,
|
|
182
|
+
query: Mapping[str, Any],
|
|
183
|
+
limit: int,
|
|
184
|
+
cursor: Any,
|
|
185
|
+
context: Context,
|
|
186
|
+
) -> Page[EntryRecord, Any]:
|
|
187
|
+
bounded = {"conversationId": self.id, **dict(query)}
|
|
188
|
+
return await self._harness.read_on_line(
|
|
189
|
+
lambda: self._harness.storage.scan_entries(cast(Any, bounded), limit, cursor, context)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def fork(self, at: EntryId, options: Mapping[str, Any], context: Context) -> ConversationHandleImpl:
|
|
193
|
+
return await self._harness._fork(self.id, at, options, context)
|
|
194
|
+
|
|
195
|
+
async def abort(self, context: Context, options: Mapping[str, Any] | None = None) -> None:
|
|
196
|
+
self._harness.resume()
|
|
197
|
+
await self._harness._tasks.abort_conversation(
|
|
198
|
+
self.id, (options or {}).get("background") is True, context
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def wait_for_idle(self, context: Context) -> None:
|
|
202
|
+
self._harness.resume()
|
|
203
|
+
await self._harness._tasks.wait_for_idle(self.id, context)
|
|
204
|
+
|
|
205
|
+
async def compact(self, instructions: str | None, context: Context) -> TaskId:
|
|
206
|
+
self._harness.resume()
|
|
207
|
+
value: dict[str, Any] = {"reason": "manual"}
|
|
208
|
+
if instructions is not None:
|
|
209
|
+
value["instructions"] = instructions
|
|
210
|
+
return cast(
|
|
211
|
+
TaskId,
|
|
212
|
+
await self.commit(lambda tx: create_compaction(tx, self.id, cast(Any, value)), context),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
async def reset(self, handoff: str | None, context: Context) -> None:
|
|
216
|
+
entry: dict[str, Any] = {"kind": ResetEntry.kind, "head": "self"}
|
|
217
|
+
if handoff is not None:
|
|
218
|
+
entry["model"] = [{"role": "user", "content": handoff, "timestamp": self._harness.now}]
|
|
219
|
+
await self.submit({"type": "write", "entry": entry}, context)
|
|
220
|
+
|
|
221
|
+
async def view_state(self, context: Context) -> Any:
|
|
222
|
+
return await self._harness._views.state(self.id, context)
|
|
223
|
+
|
|
224
|
+
async def watch(self, context: Context) -> Any:
|
|
225
|
+
return await self._harness._views.watch(self.id, context)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Harness(SessionImpl):
|
|
229
|
+
"""Durable agent harness over one Session."""
|
|
230
|
+
|
|
231
|
+
def __init__(self, raw_storage: Storage, options: Mapping[str, Any], context: Context) -> None:
|
|
232
|
+
report = _safe_report(options.get("on_report"))
|
|
233
|
+
now = safe_now(options.get("now"), report)
|
|
234
|
+
settings_option = options.get("settings")
|
|
235
|
+
super().__init__(
|
|
236
|
+
raw_storage,
|
|
237
|
+
now,
|
|
238
|
+
lambda: _settings_value(settings_option, "document_retention_ms"),
|
|
239
|
+
)
|
|
240
|
+
self._options = dict(options)
|
|
241
|
+
self._report_sink = report
|
|
242
|
+
self._views = ConversationViews(self, self.storage)
|
|
243
|
+
self._graph = TaskGraphView(self, self.storage)
|
|
244
|
+
self._tasks = TaskScheduler(
|
|
245
|
+
session=self,
|
|
246
|
+
storage=self.storage,
|
|
247
|
+
registry=cast(RegistryReader, options.get("registry")),
|
|
248
|
+
models=options.get("models"),
|
|
249
|
+
agent=lambda id, snapshot, call_context: self.resolve_agent(id, snapshot, call_context),
|
|
250
|
+
settings=lambda: resolve_settings(settings_option),
|
|
251
|
+
env=lambda id, call_context: self.build_env(id, call_context),
|
|
252
|
+
now=now,
|
|
253
|
+
report=report,
|
|
254
|
+
settle_outcome=lambda tx, record, outcome: self._settle_outcome(tx, record, cast(Any, outcome)),
|
|
255
|
+
withdraw_inputs=lambda tx, conversation_id: self._withdraw_inputs(tx, conversation_id),
|
|
256
|
+
conversation=lambda id, binding, call_context: self._bound_conversation(
|
|
257
|
+
id, binding, call_context
|
|
258
|
+
),
|
|
259
|
+
context=without_abort_signal(context),
|
|
260
|
+
)
|
|
261
|
+
self._settings_option = settings_option
|
|
262
|
+
self._submissions = Submissions(
|
|
263
|
+
session=self,
|
|
264
|
+
now=now,
|
|
265
|
+
queue_modes=lambda: _queue_modes(settings_option),
|
|
266
|
+
resume=self._tasks.resume,
|
|
267
|
+
start_run=self._start_run,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# ── Construction ───────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
async def open(
|
|
274
|
+
cls,
|
|
275
|
+
storage: Storage,
|
|
276
|
+
options: Mapping[str, Any],
|
|
277
|
+
context: Context,
|
|
278
|
+
) -> Harness:
|
|
279
|
+
"""Open a Harness over storage; the registry may keep changing while it runs."""
|
|
280
|
+
|
|
281
|
+
signal = context.abort_signal
|
|
282
|
+
if signal is not None:
|
|
283
|
+
signal.raise_if_aborted()
|
|
284
|
+
snapshot = cast(RegistryReader, options["registry"]).snapshot()
|
|
285
|
+
missing = [
|
|
286
|
+
task.definition.name
|
|
287
|
+
for task in BUILTIN_TASKS
|
|
288
|
+
if snapshot.task(task.definition.name) is None
|
|
289
|
+
]
|
|
290
|
+
if missing:
|
|
291
|
+
names = ", ".join(missing)
|
|
292
|
+
raise RuntimeError(
|
|
293
|
+
f"Registry lacks built-in tasks {names}; create it with create_registry()"
|
|
294
|
+
)
|
|
295
|
+
harness = cls(storage, options, context)
|
|
296
|
+
try:
|
|
297
|
+
await harness._tasks.open(context)
|
|
298
|
+
except BaseException:
|
|
299
|
+
try:
|
|
300
|
+
await harness.close(without_abort_signal(context))
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
raise
|
|
304
|
+
return harness
|
|
305
|
+
|
|
306
|
+
# ── Harness surface ────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
def report(self, error: BaseException) -> None:
|
|
309
|
+
self._report_sink(error)
|
|
310
|
+
|
|
311
|
+
def close(self, context: Context) -> Coroutine[Any, Any, None]:
|
|
312
|
+
"""A Harness seals admission synchronously, before it signals and joins its handlers."""
|
|
313
|
+
|
|
314
|
+
self._begin_close()
|
|
315
|
+
|
|
316
|
+
async def wait() -> None:
|
|
317
|
+
await self._wait_close(context)
|
|
318
|
+
|
|
319
|
+
return wait()
|
|
320
|
+
|
|
321
|
+
async def before_close(self) -> None:
|
|
322
|
+
await self._tasks.join()
|
|
323
|
+
|
|
324
|
+
def _closed_error(self) -> Exception:
|
|
325
|
+
return cast(Exception, closed_error(self))
|
|
326
|
+
|
|
327
|
+
async def conversation_created(self, tx: Transaction, record: ConversationRecord) -> None:
|
|
328
|
+
await tx.doc(LiveDoc, record["id"])
|
|
329
|
+
await tx.doc(InboxDoc, record["id"])
|
|
330
|
+
await tx.doc(UsageDoc, record["id"])
|
|
331
|
+
await tx.doc(ProviderDoc, record["id"])
|
|
332
|
+
await tx.doc(AgentDoc, record["id"])
|
|
333
|
+
await create_agent(tx, record)
|
|
334
|
+
custom = self._options.get("conversation_created") or self._options.get("conversationCreated")
|
|
335
|
+
if custom is not None:
|
|
336
|
+
result = custom(tx, record)
|
|
337
|
+
if hasattr(result, "__await__"):
|
|
338
|
+
await cast(Any, result)
|
|
339
|
+
|
|
340
|
+
async def resolve_agent(
|
|
341
|
+
self,
|
|
342
|
+
id: ConversationId,
|
|
343
|
+
snapshot: RegistrySnapshot | None,
|
|
344
|
+
context: Context,
|
|
345
|
+
) -> Agent:
|
|
346
|
+
registry = snapshot or cast(RegistryReader, self._options["registry"]).snapshot()
|
|
347
|
+
state = await self.snapshot(AgentDoc, id, context)
|
|
348
|
+
settings = resolve_settings(self._settings_option)
|
|
349
|
+
return resolve_agent(state, registry, settings, self.report)
|
|
350
|
+
|
|
351
|
+
async def build_env(self, id: ConversationId, context: Context) -> Any:
|
|
352
|
+
build = self._options.get("env")
|
|
353
|
+
if build is None:
|
|
354
|
+
return None
|
|
355
|
+
state = await self.snapshot(AgentDoc, id, context)
|
|
356
|
+
target: dict[str, Any] = {"conversation_id": id, "read": self}
|
|
357
|
+
if state is not None and "cwd" in state:
|
|
358
|
+
target["cwd"] = state["cwd"]
|
|
359
|
+
result = build(target, context)
|
|
360
|
+
if hasattr(result, "__await__"):
|
|
361
|
+
return await cast(Any, result)
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
def resume(self) -> None:
|
|
365
|
+
self.assert_usable()
|
|
366
|
+
self._tasks.resume()
|
|
367
|
+
|
|
368
|
+
async def get_task(self, id: TaskId, context: Context) -> TaskRecord | None:
|
|
369
|
+
return await self.read_on_line(lambda: self.storage.task(id, context))
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def scheduler_index_sizes(self) -> dict[str, int]:
|
|
373
|
+
"""Internal, for tests: sizes of the scheduler's ownership indexes."""
|
|
374
|
+
|
|
375
|
+
return self._tasks.index_sizes
|
|
376
|
+
|
|
377
|
+
async def inspect(self, context: Context) -> HarnessInspection:
|
|
378
|
+
async def read() -> HarnessInspection:
|
|
379
|
+
scheduling, tasks = await self._tasks.inspect(
|
|
380
|
+
cast(RegistryReader, self._options["registry"]).snapshot()
|
|
381
|
+
)
|
|
382
|
+
submissions: list[SubmissionRecord] = []
|
|
383
|
+
|
|
384
|
+
async def scan(status: Literal["queued", "placed"], cursor: Any) -> Any:
|
|
385
|
+
return await self.storage.scan_submissions(
|
|
386
|
+
{"status": status}, _SCAN_PAGE_SIZE, cursor, context
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
for status in ("queued", "placed"):
|
|
390
|
+
submissions.extend(await scan_all(partial(scan, status)))
|
|
391
|
+
submissions.sort(key=lambda record: record["id"])
|
|
392
|
+
return {"scheduling": cast(Any, scheduling), "tasks": tasks, "submissions": submissions}
|
|
393
|
+
|
|
394
|
+
return await self.read_queued_before_close(read)
|
|
395
|
+
|
|
396
|
+
async def abort_task(self, id: TaskId, context: Context) -> str:
|
|
397
|
+
return await self._tasks.abort(id, context)
|
|
398
|
+
|
|
399
|
+
async def wait_for_task(self, id: TaskId, context: Context) -> SettledTask:
|
|
400
|
+
self._tasks.resume()
|
|
401
|
+
return await self._tasks.wait_for_task(id, context)
|
|
402
|
+
|
|
403
|
+
async def wait_for_idle(self, context: Context) -> None:
|
|
404
|
+
self._tasks.resume()
|
|
405
|
+
await self._tasks.wait_for_idle(None, context)
|
|
406
|
+
|
|
407
|
+
async def root(
|
|
408
|
+
self, context: Context, options: Mapping[str, Any] | None = None
|
|
409
|
+
) -> ConversationHandleImpl:
|
|
410
|
+
self.assert_usable()
|
|
411
|
+
values = options or {}
|
|
412
|
+
|
|
413
|
+
async def create(tx: Transaction) -> ConversationId:
|
|
414
|
+
if await tx.conversation(ROOT_CONVERSATION_ID) is not None:
|
|
415
|
+
return ROOT_CONVERSATION_ID
|
|
416
|
+
record = await tx.create_root_conversation()
|
|
417
|
+
await self._apply_creation(tx, record["id"], values)
|
|
418
|
+
return record["id"]
|
|
419
|
+
|
|
420
|
+
return ConversationHandleImpl(await self.commit(create, context), self)
|
|
421
|
+
|
|
422
|
+
async def conversation(self, id: ConversationId, context: Context) -> ConversationHandleImpl | None:
|
|
423
|
+
self.assert_usable()
|
|
424
|
+
record = await self.read_on_line(lambda: self.storage.conversation(id, context))
|
|
425
|
+
return None if record is None else ConversationHandleImpl(record["id"], self)
|
|
426
|
+
|
|
427
|
+
async def create_conversation(
|
|
428
|
+
self, options: Mapping[str, Any], context: Context
|
|
429
|
+
) -> ConversationHandleImpl:
|
|
430
|
+
ownership = _ownership(options)
|
|
431
|
+
return await self._create(
|
|
432
|
+
options,
|
|
433
|
+
context,
|
|
434
|
+
lambda tx: tx.create_conversation({"ownership": ownership}),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
async def _fork(
|
|
438
|
+
self,
|
|
439
|
+
parent_id: ConversationId,
|
|
440
|
+
at: EntryId,
|
|
441
|
+
options: Mapping[str, Any],
|
|
442
|
+
context: Context,
|
|
443
|
+
) -> ConversationHandleImpl:
|
|
444
|
+
ownership = _ownership(options)
|
|
445
|
+
return await self._create(
|
|
446
|
+
options,
|
|
447
|
+
context,
|
|
448
|
+
lambda tx: tx.fork_conversation(parent_id, at, {"ownership": ownership}),
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
async def _create(
|
|
452
|
+
self,
|
|
453
|
+
options: Mapping[str, Any],
|
|
454
|
+
context: Context,
|
|
455
|
+
make: Callable[[Transaction], Awaitable[ConversationRecord]],
|
|
456
|
+
) -> ConversationHandleImpl:
|
|
457
|
+
self.assert_usable()
|
|
458
|
+
|
|
459
|
+
async def create(tx: Transaction) -> ConversationId:
|
|
460
|
+
record = await make(tx)
|
|
461
|
+
await self._apply_creation(tx, record["id"], options)
|
|
462
|
+
return record["id"]
|
|
463
|
+
|
|
464
|
+
return ConversationHandleImpl(await self.commit(create, context), self)
|
|
465
|
+
|
|
466
|
+
async def _apply_creation(
|
|
467
|
+
self, tx: Transaction, conversation_id: ConversationId, options: Mapping[str, Any]
|
|
468
|
+
) -> None:
|
|
469
|
+
"""Apply the conveniences' ``agent`` change and ``init`` inside the creating commit."""
|
|
470
|
+
|
|
471
|
+
agent = options.get("agent")
|
|
472
|
+
if agent is not None:
|
|
473
|
+
await configure(tx, conversation_id, agent)
|
|
474
|
+
init = options.get("init")
|
|
475
|
+
if init is not None:
|
|
476
|
+
result = init(tx, conversation_id)
|
|
477
|
+
if hasattr(result, "__await__"):
|
|
478
|
+
await cast(Any, result)
|
|
479
|
+
|
|
480
|
+
# ── Submissions ────────────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
async def submit(
|
|
483
|
+
self, conversation_id: ConversationId, submission: Any, context: Context
|
|
484
|
+
) -> Submission:
|
|
485
|
+
return await self._submissions.submit(conversation_id, submission, context)
|
|
486
|
+
|
|
487
|
+
async def submission(self, id: SubmissionId, context: Context) -> Submission | None:
|
|
488
|
+
return await self._submissions.get(id, context)
|
|
489
|
+
|
|
490
|
+
async def abort_submission(
|
|
491
|
+
self, id: SubmissionId, context: Context, conversation_id: ConversationId | None = None
|
|
492
|
+
) -> AbortResult | Literal["not_found"]:
|
|
493
|
+
return await self._submissions.abort(id, context, conversation_id)
|
|
494
|
+
|
|
495
|
+
async def _start_run(
|
|
496
|
+
self, tx: Transaction, conversation_id: ConversationId, live: Any, inputs: list[Any]
|
|
497
|
+
) -> None:
|
|
498
|
+
registry = cast(RegistryReader, self._options["registry"]).snapshot()
|
|
499
|
+
await start_generation(tx, conversation_id, live, inputs, registry)
|
|
500
|
+
|
|
501
|
+
# ── Internal scheduler callbacks ───────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
async def _settle_outcome(self, tx: Transaction, record: TaskRecord, outcome: TaskOutcome) -> None:
|
|
504
|
+
await settle_scheduler_outcome(tx, record, cast(Any, outcome))
|
|
505
|
+
|
|
506
|
+
async def _withdraw_inputs(self, tx: Transaction, conversation_id: ConversationId) -> None:
|
|
507
|
+
await withdraw_queued_inputs(tx, conversation_id)
|
|
508
|
+
|
|
509
|
+
async def _bound_conversation(
|
|
510
|
+
self,
|
|
511
|
+
id: ConversationId,
|
|
512
|
+
binding: Mapping[str, Any],
|
|
513
|
+
context: Context,
|
|
514
|
+
) -> ConversationHandle | None:
|
|
515
|
+
record = await self.read_on_line(lambda: self.storage.conversation(id, context))
|
|
516
|
+
return None if record is None else BoundConversation(id, self, binding)
|
|
517
|
+
|
|
518
|
+
# ── Not yet ported ─────────────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
async def usage(self, context: Context) -> UsageState:
|
|
521
|
+
"""Sum every conversation's committed ``pi.usage`` into the Session total."""
|
|
522
|
+
|
|
523
|
+
conversations = await self.read_on_line(
|
|
524
|
+
lambda: scan_all(
|
|
525
|
+
lambda cursor: self.storage.scan_conversations({}, _SCAN_PAGE_SIZE, cursor, context)
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
total = cast(UsageState, UsageDoc.definition.initial())
|
|
529
|
+
for record in conversations:
|
|
530
|
+
state = await self.snapshot(UsageDoc, record["id"], context)
|
|
531
|
+
if state is not None:
|
|
532
|
+
add_usage_state(total, state)
|
|
533
|
+
return total
|
|
534
|
+
|
|
535
|
+
async def task_graph(self, context: Context) -> Any:
|
|
536
|
+
"""Return the task-graph mount as a disposable read-only state."""
|
|
537
|
+
|
|
538
|
+
self.assert_usable()
|
|
539
|
+
return await self._graph.state(context)
|
|
540
|
+
|
|
541
|
+
async def watch_task_graph(self, context: Context) -> Any:
|
|
542
|
+
"""Return the task-graph mount through its serialized exact-frame watch."""
|
|
543
|
+
|
|
544
|
+
self.assert_usable()
|
|
545
|
+
return await self._graph.watch(context)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _bind_context(binding: Mapping[str, Any], context: Context) -> Context:
|
|
550
|
+
signal = binding["signal"]
|
|
551
|
+
return context if signal is None else with_abort_signal(signal, context)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _queue_modes(settings: Any) -> QueueModes:
|
|
555
|
+
resolved = resolve_settings(settings)
|
|
556
|
+
return QueueModes(
|
|
557
|
+
steering_mode=resolved.steering_mode,
|
|
558
|
+
follow_up_mode=resolved.follow_up_mode,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _settings_value(settings: Any, name: str) -> Any:
|
|
563
|
+
if settings is None:
|
|
564
|
+
return None
|
|
565
|
+
if isinstance(settings, Mapping):
|
|
566
|
+
return settings.get(name)
|
|
567
|
+
return getattr(settings, name, None)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
__all__ = ["Harness", "safe_now"]
|