monkeybot 2.1.1__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.
- monkeybot/__init__.py +3 -0
- monkeybot/cli/__init__.py +3 -0
- monkeybot/cli/__main__.py +8 -0
- monkeybot/cli/audio_io.py +8 -0
- monkeybot/cli/gateway_manager.py +17 -0
- monkeybot/cli/main.py +22 -0
- monkeybot/cli/push_to_talk.py +12 -0
- monkeybot/cli/realtime_client.py +13 -0
- monkeybot/core/__init__.py +19 -0
- monkeybot/core/attachments/__init__.py +22 -0
- monkeybot/core/attachments/catalog.py +62 -0
- monkeybot/core/attachments/config.py +52 -0
- monkeybot/core/attachments/freeze.py +158 -0
- monkeybot/core/attachments/resolve.py +70 -0
- monkeybot/core/attachments/store.py +180 -0
- monkeybot/core/attachments/text.py +72 -0
- monkeybot/core/attachments/tools.py +54 -0
- monkeybot/core/bootstrap.py +242 -0
- monkeybot/core/config/__init__.py +71 -0
- monkeybot/core/config/realtime_config.py +150 -0
- monkeybot/core/config/runtime_env.py +262 -0
- monkeybot/core/config/settings.py +341 -0
- monkeybot/core/config/validation.py +249 -0
- monkeybot/core/config/yaml_loader.py +45 -0
- monkeybot/core/context/__init__.py +781 -0
- monkeybot/core/context/campaign_context.py +8 -0
- monkeybot/core/context/common.py +14 -0
- monkeybot/core/context/curator.py +255 -0
- monkeybot/core/context/epoch.py +226 -0
- monkeybot/core/context/memory_prompt.py +222 -0
- monkeybot/core/context/tool_output_policy.py +270 -0
- monkeybot/core/context/tool_result_ingress.py +290 -0
- monkeybot/core/context/tool_shapers.py +361 -0
- monkeybot/core/hooks/__init__.py +261 -0
- monkeybot/core/llm/__init__.py +4 -0
- monkeybot/core/llm/provider.py +296 -0
- monkeybot/core/llm/realtime_provider.py +203 -0
- monkeybot/core/llm/usage.py +57 -0
- monkeybot/core/logging_utils.py +24 -0
- monkeybot/core/mcp/__init__.py +1 -0
- monkeybot/core/mcp/mcp_client.py +1215 -0
- monkeybot/core/mcp/ports_mcp.py +109 -0
- monkeybot/core/memory/__init__.py +24 -0
- monkeybot/core/memory/hook.py +413 -0
- monkeybot/core/memory/index_format.py +104 -0
- monkeybot/core/memory/integrity.py +180 -0
- monkeybot/core/memory/organizer.py +270 -0
- monkeybot/core/memory/storage_ops.py +139 -0
- monkeybot/core/memory/subsystem.py +91 -0
- monkeybot/core/messages/__init__.py +16 -0
- monkeybot/core/messages/convert_provider.py +41 -0
- monkeybot/core/messages/tool_integrity.py +262 -0
- monkeybot/core/messages/transform_context.py +84 -0
- monkeybot/core/path_safety.py +11 -0
- monkeybot/core/persistence/__init__.py +17 -0
- monkeybot/core/persistence/backends.py +236 -0
- monkeybot/core/persistence/db.py +28 -0
- monkeybot/core/persistence/durable_runs.py +286 -0
- monkeybot/core/persistence/firestore.py +658 -0
- monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
- monkeybot/core/persistence/history.py +156 -0
- monkeybot/core/persistence/postgres.py +895 -0
- monkeybot/core/persistence/runs.py +76 -0
- monkeybot/core/persistence/scheduled_loops.py +435 -0
- monkeybot/core/persistence/session_turn_locks.py +94 -0
- monkeybot/core/persistence/sqlite.py +218 -0
- monkeybot/core/persistence/sqlite_backend.py +74 -0
- monkeybot/core/persistence/thread_summary.py +61 -0
- monkeybot/core/persistence/transcript.py +194 -0
- monkeybot/core/persistence/usage.py +149 -0
- monkeybot/core/prompts/__init__.py +1 -0
- monkeybot/core/prompts/harness_prompt.py +197 -0
- monkeybot/core/prompts/prompt.py +215 -0
- monkeybot/core/runtime/__init__.py +1 -0
- monkeybot/core/runtime/context_budget.py +267 -0
- monkeybot/core/runtime/events.py +819 -0
- monkeybot/core/runtime/input_admission.py +154 -0
- monkeybot/core/runtime/loop.py +2374 -0
- monkeybot/core/runtime/provider_stream_mapper.py +159 -0
- monkeybot/core/runtime/realtime_loop.py +654 -0
- monkeybot/core/runtime/utterance_buffer.py +179 -0
- monkeybot/core/subagents/__init__.py +1 -0
- monkeybot/core/subagents/subagent_proto.py +331 -0
- monkeybot/core/subagents/subagent_worker.py +441 -0
- monkeybot/core/subagents/worker_pool.py +403 -0
- monkeybot/core/testing/__init__.py +1 -0
- monkeybot/core/testing/mocks_provider.py +86 -0
- monkeybot/core/testing/mocks_realtime_provider.py +137 -0
- monkeybot/core/tools/__init__.py +1 -0
- monkeybot/core/tools/core_tool_executor.py +1548 -0
- monkeybot/core/tools/inspector.py +226 -0
- monkeybot/core/tools/loop_inspector.py +45 -0
- monkeybot/core/tools/patch.py +480 -0
- monkeybot/core/tools/permission.py +284 -0
- monkeybot/core/tools/sandbox_executor.py +255 -0
- monkeybot/core/tools/spill_inventory.py +35 -0
- monkeybot/core/tools/terminal.py +381 -0
- monkeybot/core/tools/text_normalize.py +25 -0
- monkeybot/core/tools/types.py +33 -0
- monkeybot/core/tools/workspace_service.py +710 -0
- monkeybot/core/tools/workspace_tools.py +116 -0
- monkeybot/core/types/__init__.py +1 -0
- monkeybot/core/types/content_blocks.py +644 -0
- monkeybot/core/types/interfaces.py +156 -0
- monkeybot/core/types/types_tools.py +29 -0
- monkeybot/core/workspace/__init__.py +8 -0
- monkeybot/core/workspace/factory.py +45 -0
- monkeybot/core/workspace/gcs.py +130 -0
- monkeybot/core/workspace/local.py +162 -0
- monkeybot/core/workspace/protocol.py +45 -0
- monkeybot/core/workspace/s3.py +151 -0
- monkeybot/core/workspace_layout.py +27 -0
- monkeybot/gateway/__init__.py +1 -0
- monkeybot/gateway/bootstrap.py +18 -0
- monkeybot/gateway/main.py +47 -0
- monkeybot/gateway/realtime/__init__.py +31 -0
- monkeybot/gateway/realtime/app.py +321 -0
- monkeybot/gateway/realtime/deps.py +52 -0
- monkeybot/gateway/realtime/errors.py +81 -0
- monkeybot/gateway/realtime/guardrails.py +88 -0
- monkeybot/gateway/realtime/manager.py +77 -0
- monkeybot/gateway/realtime/metrics.py +144 -0
- monkeybot/gateway/realtime/routes.py +864 -0
- monkeybot/gateway/realtime/session.py +232 -0
- monkeybot/gateway/realtime/wire.py +412 -0
- monkeybot/gateway/realtime_main.py +49 -0
- monkeybot/gateway/sse/__init__.py +1 -0
- monkeybot/gateway/sse/app.py +733 -0
- monkeybot/gateway/sse/loop_port.py +31 -0
- monkeybot/gateway/sse/models.py +177 -0
- monkeybot/gateway/sse/reply_body.py +91 -0
- monkeybot/gateway/sse/routes.py +1101 -0
- monkeybot/gateway/sse/scheduler_routes.py +200 -0
- monkeybot/gateway/sse/scheduler_wiring.py +96 -0
- monkeybot/gateway/sse/session_bus.py +226 -0
- monkeybot/gateway/sse/sse.py +46 -0
- monkeybot/gateway/sse/workspace_layout.py +7 -0
- monkeybot/observability/__init__.py +220 -0
- monkeybot/observability/_state.py +10 -0
- monkeybot/observability/instrumentation.py +153 -0
- monkeybot/observability/propagation.py +65 -0
- monkeybot/observability/spans.py +455 -0
- monkeybot/providers/__init__.py +19 -0
- monkeybot/providers/_openai_compat.py +450 -0
- monkeybot/providers/_utils.py +473 -0
- monkeybot/providers/bedrock.py +145 -0
- monkeybot/providers/claude.py +125 -0
- monkeybot/providers/gemini.py +677 -0
- monkeybot/providers/gemini_live.py +398 -0
- monkeybot/providers/huggingface.py +129 -0
- monkeybot/providers/nvidia.py +104 -0
- monkeybot/providers/ollama.py +152 -0
- monkeybot/providers/openai.py +127 -0
- monkeybot/providers/pricing.py +60 -0
- monkeybot/providers/sampling.py +44 -0
- monkeybot/providers/vertex_claude.py +148 -0
- monkeybot/scaffold/__init__.py +33 -0
- monkeybot/scheduler/__init__.py +13 -0
- monkeybot/scheduler/__main__.py +4 -0
- monkeybot/scheduler/engine.py +333 -0
- monkeybot/scheduler/http_invoker.py +61 -0
- monkeybot/scheduler/interval.py +77 -0
- monkeybot/scheduler/tick_result.py +34 -0
- monkeybot/scheduler/worker.py +87 -0
- monkeybot/subagents/__init__.py +1 -0
- monkeybot/subagents/worker/__init__.py +1 -0
- monkeybot/subagents/worker/__main__.py +22 -0
- monkeybot/web_search/__init__.py +82 -0
- monkeybot/web_search/backends/__init__.py +5 -0
- monkeybot/web_search/backends/duckduckgo.py +32 -0
- monkeybot/web_search/backends/firecrawl.py +43 -0
- monkeybot/web_search/backends/tavily.py +45 -0
- monkeybot/web_search/protocol.py +25 -0
- monkeybot/web_search/tool.py +56 -0
- monkeybot-2.1.1.dist-info/METADATA +318 -0
- monkeybot-2.1.1.dist-info/RECORD +178 -0
- monkeybot-2.1.1.dist-info/WHEEL +4 -0
- monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Durable subprocess run tracking in SQLite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import cast
|
|
10
|
+
|
|
11
|
+
import aiosqlite
|
|
12
|
+
|
|
13
|
+
_SUBAGENT_COLUMNS: tuple[str, ...] = (
|
|
14
|
+
"run_id",
|
|
15
|
+
"parent_run_id",
|
|
16
|
+
"script",
|
|
17
|
+
"envelope_json",
|
|
18
|
+
"status",
|
|
19
|
+
"result_json",
|
|
20
|
+
"error_json",
|
|
21
|
+
"started_at",
|
|
22
|
+
"finished_at",
|
|
23
|
+
"scratch_dir",
|
|
24
|
+
"worker_id",
|
|
25
|
+
"claimed_at",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class SubagentRunRow:
|
|
31
|
+
"""One row from the ``subagent_runs`` table."""
|
|
32
|
+
|
|
33
|
+
run_id: str
|
|
34
|
+
parent_run_id: str | None
|
|
35
|
+
script: str
|
|
36
|
+
envelope_json: str
|
|
37
|
+
status: str
|
|
38
|
+
result_json: str | None
|
|
39
|
+
error_json: str | None
|
|
40
|
+
started_at: int
|
|
41
|
+
finished_at: int | None
|
|
42
|
+
scratch_dir: str
|
|
43
|
+
worker_id: str | None = None
|
|
44
|
+
claimed_at: int | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SubagentEnvelope:
|
|
49
|
+
"""Minimal envelope persisted with each subagent run."""
|
|
50
|
+
|
|
51
|
+
task: str
|
|
52
|
+
context: str
|
|
53
|
+
memory_storage_uri: str
|
|
54
|
+
parent_run_id: str
|
|
55
|
+
model: str = "gemini-2.5-flash"
|
|
56
|
+
traceparent: str | None = None
|
|
57
|
+
agent_md: str | None = None
|
|
58
|
+
subagent_type: str | None = None
|
|
59
|
+
|
|
60
|
+
def to_json(self) -> str:
|
|
61
|
+
"""Serialize envelope to JSON text."""
|
|
62
|
+
data = asdict(self)
|
|
63
|
+
if data.get("traceparent") is None:
|
|
64
|
+
data.pop("traceparent", None)
|
|
65
|
+
if data.get("agent_md") is None:
|
|
66
|
+
data.pop("agent_md", None)
|
|
67
|
+
if data.get("subagent_type") is None:
|
|
68
|
+
data.pop("subagent_type", None)
|
|
69
|
+
return json.dumps(data, sort_keys=True)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_json(cls, raw: str) -> SubagentEnvelope:
|
|
73
|
+
"""Deserialize JSON text; raises ``ValueError`` on invalid input."""
|
|
74
|
+
try:
|
|
75
|
+
data = json.loads(raw)
|
|
76
|
+
except json.JSONDecodeError as exc:
|
|
77
|
+
raise ValueError("Invalid envelope JSON") from exc
|
|
78
|
+
if not isinstance(data, dict):
|
|
79
|
+
raise ValueError("Envelope JSON must be an object")
|
|
80
|
+
base_required = {"task", "context", "parent_run_id"}
|
|
81
|
+
missing = base_required - data.keys()
|
|
82
|
+
if missing:
|
|
83
|
+
raise ValueError(f"Envelope missing fields: {sorted(missing)}")
|
|
84
|
+
from monkeybot.core.subagents.subagent_proto import _memory_storage_uri_from_dict
|
|
85
|
+
|
|
86
|
+
uri = _memory_storage_uri_from_dict(data)
|
|
87
|
+
model = data.get("model", "gemini-2.5-flash")
|
|
88
|
+
if not isinstance(model, str):
|
|
89
|
+
raise ValueError("model must be a string when present")
|
|
90
|
+
for key in ("task", "context", "parent_run_id"):
|
|
91
|
+
if not isinstance(data[key], str):
|
|
92
|
+
raise ValueError(f"{key} must be a string")
|
|
93
|
+
traceparent = data.get("traceparent")
|
|
94
|
+
if traceparent is not None and not isinstance(traceparent, str):
|
|
95
|
+
raise ValueError("traceparent must be a string when present")
|
|
96
|
+
agent_md = data.get("agent_md")
|
|
97
|
+
if agent_md is not None and not isinstance(agent_md, str):
|
|
98
|
+
raise ValueError("agent_md must be a string when present")
|
|
99
|
+
subagent_type = data.get("subagent_type")
|
|
100
|
+
if subagent_type is not None and not isinstance(subagent_type, str):
|
|
101
|
+
raise ValueError("subagent_type must be a string when present")
|
|
102
|
+
return cls(
|
|
103
|
+
task=data["task"],
|
|
104
|
+
context=data["context"],
|
|
105
|
+
memory_storage_uri=uri.strip(),
|
|
106
|
+
parent_run_id=data["parent_run_id"],
|
|
107
|
+
model=model,
|
|
108
|
+
traceparent=traceparent.strip() if isinstance(traceparent, str) and traceparent.strip() else None,
|
|
109
|
+
agent_md=agent_md.strip() if isinstance(agent_md, str) and agent_md.strip() else None,
|
|
110
|
+
subagent_type=(
|
|
111
|
+
subagent_type.strip()
|
|
112
|
+
if isinstance(subagent_type, str) and subagent_type.strip()
|
|
113
|
+
else None
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _tuple_to_run_row(row: tuple[object, ...]) -> SubagentRunRow:
|
|
119
|
+
d = dict(zip(_SUBAGENT_COLUMNS, row, strict=True))
|
|
120
|
+
return SubagentRunRow(
|
|
121
|
+
run_id=str(d["run_id"]),
|
|
122
|
+
parent_run_id=str(d["parent_run_id"]) if d["parent_run_id"] is not None else None,
|
|
123
|
+
script=str(d["script"]),
|
|
124
|
+
envelope_json=str(d["envelope_json"]),
|
|
125
|
+
status=str(d["status"]),
|
|
126
|
+
result_json=str(d["result_json"]) if d["result_json"] is not None else None,
|
|
127
|
+
error_json=str(d["error_json"]) if d["error_json"] is not None else None,
|
|
128
|
+
started_at=int(cast(int, d["started_at"])),
|
|
129
|
+
finished_at=int(cast(int, d["finished_at"])) if d["finished_at"] is not None else None,
|
|
130
|
+
scratch_dir=str(d["scratch_dir"]),
|
|
131
|
+
worker_id=str(d["worker_id"]) if d["worker_id"] is not None else None,
|
|
132
|
+
claimed_at=int(cast(int, d["claimed_at"])) if d["claimed_at"] is not None else None,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class SQLiteRunStore:
|
|
137
|
+
"""Persist lifecycle rows for subprocess subagents."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, conn: aiosqlite.Connection) -> None:
|
|
140
|
+
self._conn = conn
|
|
141
|
+
|
|
142
|
+
async def record_pending(
|
|
143
|
+
self,
|
|
144
|
+
run_id: str,
|
|
145
|
+
parent_run_id: str | None,
|
|
146
|
+
script: str,
|
|
147
|
+
envelope: SubagentEnvelope,
|
|
148
|
+
scratch_dir: Path,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Insert a ``pending`` row for worker-pool consumption."""
|
|
151
|
+
await self._record_run("pending", run_id, parent_run_id, script, envelope, scratch_dir)
|
|
152
|
+
|
|
153
|
+
async def record_started(
|
|
154
|
+
self,
|
|
155
|
+
run_id: str,
|
|
156
|
+
parent_run_id: str | None,
|
|
157
|
+
script: str,
|
|
158
|
+
envelope: SubagentEnvelope,
|
|
159
|
+
scratch_dir: Path,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Insert a ``running`` row with envelope metadata."""
|
|
162
|
+
await self._record_run("running", run_id, parent_run_id, script, envelope, scratch_dir)
|
|
163
|
+
|
|
164
|
+
async def _record_run(
|
|
165
|
+
self,
|
|
166
|
+
status: str,
|
|
167
|
+
run_id: str,
|
|
168
|
+
parent_run_id: str | None,
|
|
169
|
+
script: str,
|
|
170
|
+
envelope: SubagentEnvelope,
|
|
171
|
+
scratch_dir: Path,
|
|
172
|
+
) -> None:
|
|
173
|
+
now_ms = int(time.time() * 1000)
|
|
174
|
+
await self._conn.execute(
|
|
175
|
+
"""
|
|
176
|
+
INSERT INTO subagent_runs(
|
|
177
|
+
run_id, parent_run_id, script, envelope_json,
|
|
178
|
+
status, result_json, error_json, started_at, finished_at, scratch_dir,
|
|
179
|
+
worker_id, claimed_at
|
|
180
|
+
)
|
|
181
|
+
VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, NULL, ?, NULL, NULL)
|
|
182
|
+
""",
|
|
183
|
+
(
|
|
184
|
+
run_id,
|
|
185
|
+
parent_run_id,
|
|
186
|
+
script,
|
|
187
|
+
envelope.to_json(),
|
|
188
|
+
status,
|
|
189
|
+
now_ms,
|
|
190
|
+
str(scratch_dir),
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
await self._conn.commit()
|
|
194
|
+
|
|
195
|
+
async def claim(self, run_id: str, worker_id: str) -> bool:
|
|
196
|
+
"""Atomically transition ``pending`` -> ``running``; return True if this caller won."""
|
|
197
|
+
now_ms = int(time.time() * 1000)
|
|
198
|
+
cursor = await self._conn.execute(
|
|
199
|
+
"""
|
|
200
|
+
UPDATE subagent_runs
|
|
201
|
+
SET status = 'running',
|
|
202
|
+
worker_id = ?,
|
|
203
|
+
claimed_at = ?
|
|
204
|
+
WHERE run_id = ? AND status = 'pending'
|
|
205
|
+
""",
|
|
206
|
+
(worker_id, now_ms, run_id),
|
|
207
|
+
)
|
|
208
|
+
await self._conn.commit()
|
|
209
|
+
return int(cursor.rowcount) == 1
|
|
210
|
+
|
|
211
|
+
async def reset_stale_claims(self, stale_after_ms: int) -> int:
|
|
212
|
+
"""Reset ``running`` rows with stale ``claimed_at`` back to ``pending``."""
|
|
213
|
+
cutoff = int(time.time() * 1000) - stale_after_ms
|
|
214
|
+
cursor = await self._conn.execute(
|
|
215
|
+
"""
|
|
216
|
+
UPDATE subagent_runs
|
|
217
|
+
SET status = 'pending',
|
|
218
|
+
worker_id = NULL,
|
|
219
|
+
claimed_at = NULL
|
|
220
|
+
WHERE status = 'running'
|
|
221
|
+
AND claimed_at IS NOT NULL
|
|
222
|
+
AND claimed_at < ?
|
|
223
|
+
""",
|
|
224
|
+
(cutoff,),
|
|
225
|
+
)
|
|
226
|
+
await self._conn.commit()
|
|
227
|
+
return int(cursor.rowcount)
|
|
228
|
+
|
|
229
|
+
async def record_completed(self, run_id: str, result_json: str) -> None:
|
|
230
|
+
"""Mark run completed with payload."""
|
|
231
|
+
now_ms = int(time.time() * 1000)
|
|
232
|
+
await self._conn.execute(
|
|
233
|
+
"""
|
|
234
|
+
UPDATE subagent_runs
|
|
235
|
+
SET status = 'completed',
|
|
236
|
+
result_json = ?,
|
|
237
|
+
finished_at = ?,
|
|
238
|
+
error_json = NULL
|
|
239
|
+
WHERE run_id = ?
|
|
240
|
+
""",
|
|
241
|
+
(result_json, now_ms, run_id),
|
|
242
|
+
)
|
|
243
|
+
await self._conn.commit()
|
|
244
|
+
|
|
245
|
+
async def record_failed(self, run_id: str, error: str) -> None:
|
|
246
|
+
"""Mark run failed with JSON ``{\"message\": ...}`` error."""
|
|
247
|
+
now_ms = int(time.time() * 1000)
|
|
248
|
+
err_payload = json.dumps({"message": error})
|
|
249
|
+
await self._conn.execute(
|
|
250
|
+
"""
|
|
251
|
+
UPDATE subagent_runs
|
|
252
|
+
SET status = 'failed',
|
|
253
|
+
error_json = ?,
|
|
254
|
+
finished_at = ?
|
|
255
|
+
WHERE run_id = ?
|
|
256
|
+
""",
|
|
257
|
+
(err_payload, now_ms, run_id),
|
|
258
|
+
)
|
|
259
|
+
await self._conn.commit()
|
|
260
|
+
|
|
261
|
+
async def pending_runs(self) -> list[SubagentRunRow]:
|
|
262
|
+
"""Return ``pending`` runs oldest-first (worker pool claim candidates)."""
|
|
263
|
+
columns = ", ".join(_SUBAGENT_COLUMNS)
|
|
264
|
+
cursor = await self._conn.execute(
|
|
265
|
+
f"""
|
|
266
|
+
SELECT {columns} FROM subagent_runs
|
|
267
|
+
WHERE status = 'pending'
|
|
268
|
+
ORDER BY started_at ASC
|
|
269
|
+
"""
|
|
270
|
+
)
|
|
271
|
+
rows = await cursor.fetchall()
|
|
272
|
+
await cursor.close()
|
|
273
|
+
return [_tuple_to_run_row(tuple(row)) for row in rows]
|
|
274
|
+
|
|
275
|
+
async def get_run(self, run_id: str) -> SubagentRunRow | None:
|
|
276
|
+
"""Return full row or ``None``."""
|
|
277
|
+
columns = ", ".join(_SUBAGENT_COLUMNS)
|
|
278
|
+
cursor = await self._conn.execute(
|
|
279
|
+
f"SELECT {columns} FROM subagent_runs WHERE run_id = ?",
|
|
280
|
+
(run_id,),
|
|
281
|
+
)
|
|
282
|
+
row = await cursor.fetchone()
|
|
283
|
+
await cursor.close()
|
|
284
|
+
if row is None:
|
|
285
|
+
return None
|
|
286
|
+
return _tuple_to_run_row(tuple(row))
|