shikiru-cli 0.2.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.
- shikiru/__init__.py +1 -0
- shikiru/__main__.py +8 -0
- shikiru/agents/__init__.py +3 -0
- shikiru/agents/contracts.py +590 -0
- shikiru/agents/registry.py +87 -0
- shikiru/agents/sdk/__init__.py +17 -0
- shikiru/agents/sdk/reset_time.py +137 -0
- shikiru/agents/sdk/turn.py +335 -0
- shikiru/agents/settings.py +151 -0
- shikiru/agents/testing/__init__.py +26 -0
- shikiru/agents/testing/contract_suite.py +653 -0
- shikiru/agents/testing/resume.py +15 -0
- shikiru/bootstrap.py +1277 -0
- shikiru/cli/__init__.py +1 -0
- shikiru/cli/app.py +770 -0
- shikiru/cli/context.py +63 -0
- shikiru/cli/render.py +1475 -0
- shikiru/cli/report.py +156 -0
- shikiru/cli/session_prompt.py +63 -0
- shikiru/cli/view.py +131 -0
- shikiru/clock.py +41 -0
- shikiru/config/__init__.py +8 -0
- shikiru/config/defaults.toml +131 -0
- shikiru/config/loader.py +178 -0
- shikiru/config/settings.py +392 -0
- shikiru/dashboard/__init__.py +3 -0
- shikiru/dashboard/actions_list.py +799 -0
- shikiru/dashboard/activity.py +150 -0
- shikiru/dashboard/app.py +1765 -0
- shikiru/dashboard/blocker_evidence.py +122 -0
- shikiru/dashboard/blocker_rows.py +84 -0
- shikiru/dashboard/briefing_view.py +871 -0
- shikiru/dashboard/contract.py +1086 -0
- shikiru/dashboard/covered.py +32 -0
- shikiru/dashboard/detail.py +1209 -0
- shikiru/dashboard/detail_fold.py +244 -0
- shikiru/dashboard/detail_hint.py +68 -0
- shikiru/dashboard/details_list_view.py +150 -0
- shikiru/dashboard/diagnostics.py +80 -0
- shikiru/dashboard/diagnostics_sections.py +186 -0
- shikiru/dashboard/evidence_table.py +123 -0
- shikiru/dashboard/extra_details.py +214 -0
- shikiru/dashboard/fault_guidance.py +216 -0
- shikiru/dashboard/fold_bodies.py +276 -0
- shikiru/dashboard/help_keys.py +166 -0
- shikiru/dashboard/help_screen.py +204 -0
- shikiru/dashboard/interaction.py +17 -0
- shikiru/dashboard/ladder_editor.py +544 -0
- shikiru/dashboard/live_settings.py +62 -0
- shikiru/dashboard/motion.py +368 -0
- shikiru/dashboard/orchestration_fields.py +201 -0
- shikiru/dashboard/palette.py +1606 -0
- shikiru/dashboard/palette_app.py +89 -0
- shikiru/dashboard/palette_drafts.py +33 -0
- shikiru/dashboard/resolution.py +573 -0
- shikiru/dashboard/resolution_view.py +302 -0
- shikiru/dashboard/row_list.py +256 -0
- shikiru/dashboard/section_rows.py +173 -0
- shikiru/dashboard/services.py +665 -0
- shikiru/dashboard/session_history.py +402 -0
- shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
- shikiru/dashboard/task_picker.py +358 -0
- shikiru/dashboard/tick.py +59 -0
- shikiru/dashboard/timeline_filter.py +129 -0
- shikiru/dashboard/timeline_section.py +385 -0
- shikiru/dashboard/usage_body.py +269 -0
- shikiru/dashboard/views.py +2314 -0
- shikiru/dashboard/wizard.py +1389 -0
- shikiru/domain/__init__.py +1 -0
- shikiru/domain/acceptance.py +130 -0
- shikiru/domain/activity.py +28 -0
- shikiru/domain/attempts.py +333 -0
- shikiru/domain/briefing.py +257 -0
- shikiru/domain/commands.py +219 -0
- shikiru/domain/decisions.py +231 -0
- shikiru/domain/docker.py +262 -0
- shikiru/domain/engine_uptime.py +87 -0
- shikiru/domain/escalation.py +194 -0
- shikiru/domain/escalation_evidence.py +186 -0
- shikiru/domain/escalation_panel.py +144 -0
- shikiru/domain/events.py +71 -0
- shikiru/domain/evidence_export.py +89 -0
- shikiru/domain/faults.py +86 -0
- shikiru/domain/feedback.py +180 -0
- shikiru/domain/feedback_rounds.py +172 -0
- shikiru/domain/fencing.py +31 -0
- shikiru/domain/ladder.py +102 -0
- shikiru/domain/ladder_text.py +44 -0
- shikiru/domain/levels.py +44 -0
- shikiru/domain/motion.py +199 -0
- shikiru/domain/packet_evidence.py +377 -0
- shikiru/domain/pushes.py +47 -0
- shikiru/domain/quota_blocks.py +44 -0
- shikiru/domain/quota_waits.py +219 -0
- shikiru/domain/recovery.py +194 -0
- shikiru/domain/reports.py +498 -0
- shikiru/domain/results.py +217 -0
- shikiru/domain/runs.py +85 -0
- shikiru/domain/scheduling.py +231 -0
- shikiru/domain/status_tasks.py +40 -0
- shikiru/domain/steps.py +325 -0
- shikiru/domain/tasks.py +174 -0
- shikiru/domain/timeline.py +208 -0
- shikiru/domain/timeline_phrases.py +271 -0
- shikiru/domain/versions.py +18 -0
- shikiru/domain/workflow.py +372 -0
- shikiru/engine/__init__.py +0 -0
- shikiru/engine/acceptance.py +90 -0
- shikiru/engine/agents.py +61 -0
- shikiru/engine/cosmetic_decorator.py +99 -0
- shikiru/engine/daemon.py +409 -0
- shikiru/engine/dependency.py +37 -0
- shikiru/engine/escalation_filing.py +110 -0
- shikiru/engine/escalations.py +401 -0
- shikiru/engine/events.py +285 -0
- shikiru/engine/evidence.py +81 -0
- shikiru/engine/fault_reporter.py +144 -0
- shikiru/engine/feedback.py +577 -0
- shikiru/engine/feedback_audit.py +80 -0
- shikiru/engine/feedback_intake.py +95 -0
- shikiru/engine/feedback_snapshot.py +148 -0
- shikiru/engine/flow.py +87 -0
- shikiru/engine/handlers/__init__.py +33 -0
- shikiru/engine/handlers/base.py +148 -0
- shikiru/engine/handlers/engine.py +24 -0
- shikiru/engine/handlers/escalations.py +174 -0
- shikiru/engine/handlers/operator.py +81 -0
- shikiru/engine/handlers/tasks.py +237 -0
- shikiru/engine/history.py +128 -0
- shikiru/engine/integration.py +1234 -0
- shikiru/engine/integration_turns.py +337 -0
- shikiru/engine/layout_guard.py +362 -0
- shikiru/engine/lifecycle.py +60 -0
- shikiru/engine/monitor.py +317 -0
- shikiru/engine/notification_delivery.py +124 -0
- shikiru/engine/notification_feed.py +57 -0
- shikiru/engine/notifier.py +91 -0
- shikiru/engine/pull_request_closing.py +117 -0
- shikiru/engine/push_service.py +177 -0
- shikiru/engine/quota_probe_threads.py +79 -0
- shikiru/engine/quota_reconciler.py +210 -0
- shikiru/engine/quota_schedule.py +24 -0
- shikiru/engine/quota_waits.py +102 -0
- shikiru/engine/reconcile.py +91 -0
- shikiru/engine/recovery.py +284 -0
- shikiru/engine/retention.py +102 -0
- shikiru/engine/root_preparation.py +86 -0
- shikiru/engine/scheduler.py +320 -0
- shikiru/engine/session_guard.py +46 -0
- shikiru/engine/sessions.py +95 -0
- shikiru/engine/signalled.py +75 -0
- shikiru/engine/step_runner.py +140 -0
- shikiru/engine/supervisor.py +101 -0
- shikiru/engine/takeover.py +223 -0
- shikiru/engine/task_facts.py +46 -0
- shikiru/engine/tick_guard.py +95 -0
- shikiru/engine/titles.py +297 -0
- shikiru/engine/transitions.py +79 -0
- shikiru/engine/turn_context.py +206 -0
- shikiru/engine/turn_execution.py +30 -0
- shikiru/engine/turn_loop.py +230 -0
- shikiru/engine/turn_processes.py +79 -0
- shikiru/engine/turns.py +457 -0
- shikiru/errors.py +144 -0
- shikiru/helper/__init__.py +3 -0
- shikiru/helper/helper.py +182 -0
- shikiru/helper/models.py +11 -0
- shikiru/mcp/__init__.py +1 -0
- shikiru/mcp/server.py +196 -0
- shikiru/operator/__init__.py +33 -0
- shikiru/operator/attachment.py +24 -0
- shikiru/operator/commands.py +319 -0
- shikiru/operator/detectors.py +374 -0
- shikiru/operator/doctor.py +351 -0
- shikiru/operator/events.py +57 -0
- shikiru/operator/evidence_export.py +498 -0
- shikiru/operator/evidence_writer.py +78 -0
- shikiru/operator/lifecycle.py +294 -0
- shikiru/operator/plugin_checks.py +194 -0
- shikiru/operator/probe.py +287 -0
- shikiru/operator/queries.py +1601 -0
- shikiru/operator/services.py +36 -0
- shikiru/operator/sessions.py +378 -0
- shikiru/operator/strip_views.py +162 -0
- shikiru/operator/timeline.py +416 -0
- shikiru/operator/toml_text.py +62 -0
- shikiru/palette.py +18 -0
- shikiru/paths.py +91 -0
- shikiru/plugins/__init__.py +0 -0
- shikiru/plugins/agents/__init__.py +1 -0
- shikiru/plugins/agents/claude/__init__.py +5 -0
- shikiru/plugins/agents/claude/agent.py +251 -0
- shikiru/plugins/agents/claude/context.py +116 -0
- shikiru/plugins/agents/claude/headless.py +92 -0
- shikiru/plugins/agents/claude/hook.py +57 -0
- shikiru/plugins/agents/claude/model_catalog.py +105 -0
- shikiru/plugins/agents/claude/options.py +62 -0
- shikiru/plugins/agents/claude/turn.py +1018 -0
- shikiru/plugins/agents/codex/__init__.py +5 -0
- shikiru/plugins/agents/codex/agent.py +486 -0
- shikiru/plugins/agents/codex/headless.py +166 -0
- shikiru/plugins/agents/codex/home.py +14 -0
- shikiru/plugins/agents/codex/model_catalog.py +128 -0
- shikiru/plugins/agents/codex/options.py +94 -0
- shikiru/plugins/agents/codex/quota.py +195 -0
- shikiru/plugins/agents/codex/sandbox.py +31 -0
- shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
- shikiru/plugins/agents/codex/turn.py +767 -0
- shikiru/plugins/agents/codex/usage.py +106 -0
- shikiru/plugins/agents/shared_settings.py +62 -0
- shikiru/plugins/widgets/__init__.py +3 -0
- shikiru/plugins/widgets/dialogue.py +144 -0
- shikiru/plugins/widgets/disk.py +123 -0
- shikiru/plugins/widgets/docker.py +225 -0
- shikiru/plugins/widgets/escalations.py +402 -0
- shikiru/plugins/widgets/faults.py +204 -0
- shikiru/plugins/widgets/header.py +321 -0
- shikiru/plugins/widgets/queue.py +1140 -0
- shikiru/plugins/widgets/queue_hint.py +93 -0
- shikiru/plugins/widgets/resources.py +105 -0
- shikiru/plugins/widgets/strips.py +82 -0
- shikiru/plugins/widgets/usage.py +176 -0
- shikiru/private.py +94 -0
- shikiru/process.py +535 -0
- shikiru/prompts/__init__.py +3 -0
- shikiru/prompts/renderer.py +85 -0
- shikiru/prompts/templates/_base.md.j2 +17 -0
- shikiru/prompts/templates/_macros.md.j2 +61 -0
- shikiru/prompts/templates/conflicts.md.j2 +22 -0
- shikiru/prompts/templates/consult.md.j2 +5 -0
- shikiru/prompts/templates/continue.md.j2 +4 -0
- shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
- shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
- shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
- shikiru/prompts/templates/handoff.md.j2 +23 -0
- shikiru/prompts/templates/implement.md.j2 +13 -0
- shikiru/prompts/templates/plan.md.j2 +30 -0
- shikiru/prompts/templates/rework.md.j2 +13 -0
- shikiru/prompts/templates/triage.md.j2 +20 -0
- shikiru/prompts/templates/verify.md.j2 +29 -0
- shikiru/store/__init__.py +1 -0
- shikiru/store/commands.py +242 -0
- shikiru/store/database.py +297 -0
- shikiru/store/escalations.py +240 -0
- shikiru/store/events.py +213 -0
- shikiru/store/feedback.py +266 -0
- shikiru/store/migrations/001_initial.sql +63 -0
- shikiru/store/migrations/002_headless_turns.sql +31 -0
- shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
- shikiru/store/migrations/004_repo_base_ref.sql +3 -0
- shikiru/store/migrations/005_complexity_explicit.sql +3 -0
- shikiru/store/migrations/006_events_kind_index.sql +1 -0
- shikiru/store/migrations/007_notification_cursor.sql +1 -0
- shikiru/store/migrations/008_reported_usage.sql +1 -0
- shikiru/store/migrations/009_billing_declarations.sql +2 -0
- shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
- shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
- shikiru/store/migrations/013_push_targets.sql +7 -0
- shikiru/store/migrations/014_agent_block_time.sql +6 -0
- shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
- shikiru/store/migrations/016_feedback_rounds.sql +29 -0
- shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
- shikiru/store/migrations/__init__.py +1 -0
- shikiru/store/notification_cursor.py +29 -0
- shikiru/store/pushes.py +117 -0
- shikiru/store/quotas.py +153 -0
- shikiru/store/reader.py +310 -0
- shikiru/store/retention.py +92 -0
- shikiru/store/runs.py +227 -0
- shikiru/store/tasks.py +949 -0
- shikiru/telemetry/__init__.py +1 -0
- shikiru/telemetry/disk.py +167 -0
- shikiru/telemetry/docker.py +164 -0
- shikiru/telemetry/resources.py +296 -0
- shikiru/tmux/__init__.py +3 -0
- shikiru/tmux/client.py +637 -0
- shikiru/tmux/decorator.py +225 -0
- shikiru/tmux/layout.py +200 -0
- shikiru/tmux/session.py +381 -0
- shikiru/ui/__init__.py +9 -0
- shikiru/ui/age.py +45 -0
- shikiru/ui/bubbles.py +41 -0
- shikiru/ui/compact.py +1209 -0
- shikiru/ui/console.py +49 -0
- shikiru/ui/fold.py +50 -0
- shikiru/ui/gated_static.py +117 -0
- shikiru/ui/gauge.py +40 -0
- shikiru/ui/glyphs.py +109 -0
- shikiru/ui/keys.py +57 -0
- shikiru/ui/live_interval.py +42 -0
- shikiru/ui/popup.py +311 -0
- shikiru/ui/quota_status.py +26 -0
- shikiru/ui/recent_window.py +78 -0
- shikiru/ui/screen.py +57 -0
- shikiru/ui/section.py +193 -0
- shikiru/ui/session.py +65 -0
- shikiru/ui/size.py +17 -0
- shikiru/ui/splash.py +42 -0
- shikiru/ui/status.py +47 -0
- shikiru/ui/status_strip.py +149 -0
- shikiru/ui/stream.py +152 -0
- shikiru/ui/strip.py +332 -0
- shikiru/ui/theme.py +83 -0
- shikiru/ui/tokens.py +51 -0
- shikiru/view_log.py +126 -0
- shikiru/workspace/__init__.py +3 -0
- shikiru/workspace/base_refs.py +160 -0
- shikiru/workspace/caches.py +136 -0
- shikiru/workspace/facts.py +118 -0
- shikiru/workspace/git.py +601 -0
- shikiru/workspace/github.py +744 -0
- shikiru/workspace/messages.py +107 -0
- shikiru/workspace/reports.py +145 -0
- shikiru/workspace/roots.py +180 -0
- shikiru_cli-0.2.0.dist-info/METADATA +428 -0
- shikiru_cli-0.2.0.dist-info/RECORD +319 -0
- shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
- shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
- shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Row retention: the finished history that can be pruned from the state DB."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shikiru.domain.commands import CommandStatus
|
|
8
|
+
from shikiru.domain.escalation import EscalationStatus
|
|
9
|
+
from shikiru.domain.events import EventKind
|
|
10
|
+
from shikiru.domain.tasks import Status
|
|
11
|
+
from shikiru.store.database import format_timestamp
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import sqlite3 # noqa: TID251
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
_FINISHED = (Status.DONE.value, Status.CANCELLED.value)
|
|
19
|
+
_FAULT_KINDS = (EventKind.ENGINE_FAULT.value, EventKind.ENGINE_RECOVERED.value)
|
|
20
|
+
_NEWEST_EVENT = "id < (SELECT MAX(id) FROM events)"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StateRetention:
|
|
24
|
+
"""Selects and deletes rows past the retention horizon, never the ones still needed.
|
|
25
|
+
|
|
26
|
+
The newest event and command row always stay, so ids are never reused: a reused command id
|
|
27
|
+
would match the messages and events filed under the old one. Events not yet covered by the
|
|
28
|
+
notification cursor stay, so an urgent event is never pruned before it is delivered.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, connection: sqlite3.Connection) -> None:
|
|
32
|
+
"""Use `connection`, the engine's writer."""
|
|
33
|
+
self._connection = connection
|
|
34
|
+
|
|
35
|
+
def settled_task_ids(self, finished_before: datetime) -> list[str]:
|
|
36
|
+
"""Return finished tasks older than `finished_before` that still have events.
|
|
37
|
+
|
|
38
|
+
A task with an open escalation is not settled: its packet and evidence are still read.
|
|
39
|
+
"""
|
|
40
|
+
rows = self._connection.execute(
|
|
41
|
+
f"""
|
|
42
|
+
SELECT id FROM tasks
|
|
43
|
+
WHERE status IN ({_marks(_FINISHED)})
|
|
44
|
+
AND COALESCE(finished_at, created_at) < ?
|
|
45
|
+
AND EXISTS (SELECT 1 FROM events WHERE events.task_id = tasks.id)
|
|
46
|
+
AND NOT EXISTS (
|
|
47
|
+
SELECT 1 FROM escalations
|
|
48
|
+
WHERE escalations.task_id = tasks.id AND escalations.status = ?
|
|
49
|
+
)
|
|
50
|
+
ORDER BY seq
|
|
51
|
+
""",
|
|
52
|
+
(*_FINISHED, format_timestamp(finished_before), EscalationStatus.OPEN.value),
|
|
53
|
+
).fetchall()
|
|
54
|
+
return [row["id"] for row in rows]
|
|
55
|
+
|
|
56
|
+
def delete_task_events(self, task_ids: Sequence[str], *, delivered_up_to: int) -> int:
|
|
57
|
+
"""Delete the events of `task_ids` that were delivered; return how many went."""
|
|
58
|
+
deleted = 0
|
|
59
|
+
for task_id in task_ids:
|
|
60
|
+
deleted += self._connection.execute(
|
|
61
|
+
f"DELETE FROM events WHERE task_id = ? AND id <= ? AND {_NEWEST_EVENT}",
|
|
62
|
+
(task_id, delivered_up_to),
|
|
63
|
+
).rowcount
|
|
64
|
+
return deleted
|
|
65
|
+
|
|
66
|
+
def delete_engine_events(self, before: datetime, *, delivered_up_to: int) -> int:
|
|
67
|
+
"""Delete delivered engine-level events older than `before`, but not fault records.
|
|
68
|
+
|
|
69
|
+
A fault or its recovery stays: `status` derives the open fault streaks from them.
|
|
70
|
+
"""
|
|
71
|
+
return self._connection.execute(
|
|
72
|
+
f"""
|
|
73
|
+
DELETE FROM events
|
|
74
|
+
WHERE task_id IS NULL AND ts < ? AND id <= ? AND {_NEWEST_EVENT}
|
|
75
|
+
AND kind NOT IN ({_marks(_FAULT_KINDS)})
|
|
76
|
+
""",
|
|
77
|
+
(format_timestamp(before), delivered_up_to, *_FAULT_KINDS),
|
|
78
|
+
).rowcount
|
|
79
|
+
|
|
80
|
+
def delete_finished_commands(self, before: datetime) -> int:
|
|
81
|
+
"""Delete answered commands older than `before`; a pending command is never deleted."""
|
|
82
|
+
return self._connection.execute(
|
|
83
|
+
"""
|
|
84
|
+
DELETE FROM commands
|
|
85
|
+
WHERE status != ? AND ts < ? AND id < (SELECT MAX(id) FROM commands)
|
|
86
|
+
""",
|
|
87
|
+
(CommandStatus.PENDING.value, format_timestamp(before)),
|
|
88
|
+
).rowcount
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _marks(values: Sequence[str]) -> str:
|
|
92
|
+
return ", ".join("?" for _ in values)
|
shikiru/store/runs.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Agent session runs: one per task role's session lineage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shikiru.agents.contracts import (
|
|
8
|
+
UNKNOWN_BILLING_DECLARATION,
|
|
9
|
+
BillingDeclaration,
|
|
10
|
+
Usage,
|
|
11
|
+
)
|
|
12
|
+
from shikiru.domain.runs import Run, RunStatus
|
|
13
|
+
from shikiru.domain.tasks import Role
|
|
14
|
+
from shikiru.store.database import (
|
|
15
|
+
format_timestamp,
|
|
16
|
+
parse_required_timestamp,
|
|
17
|
+
parse_timestamp,
|
|
18
|
+
require_after_insert,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import sqlite3 # noqa: TID251
|
|
23
|
+
|
|
24
|
+
from shikiru.agents.contracts import Rung
|
|
25
|
+
from shikiru.clock import Clock
|
|
26
|
+
|
|
27
|
+
_RUN_COLUMNS = (
|
|
28
|
+
"id, task_id, role, agent, model, effort, session_id, resume_command, "
|
|
29
|
+
"last_message, status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, "
|
|
30
|
+
"cost_usd, cost_basis, context_used_pct, usage_reported, billing_mode, billing_source, "
|
|
31
|
+
"started_at, ended_at"
|
|
32
|
+
)
|
|
33
|
+
MAX_TAKEN_OVER_ROLE_COUNT = len(Role)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _row_to_run(row: sqlite3.Row) -> Run:
|
|
37
|
+
return Run(
|
|
38
|
+
id=row["id"],
|
|
39
|
+
task_id=row["task_id"],
|
|
40
|
+
role=Role(row["role"]),
|
|
41
|
+
agent=row["agent"],
|
|
42
|
+
model=row["model"],
|
|
43
|
+
effort=row["effort"],
|
|
44
|
+
session_id=row["session_id"],
|
|
45
|
+
resume_command=row["resume_command"],
|
|
46
|
+
last_message=row["last_message"],
|
|
47
|
+
status=RunStatus(row["status"]),
|
|
48
|
+
usage=Usage(
|
|
49
|
+
input_tokens=row["input_tokens"],
|
|
50
|
+
output_tokens=row["output_tokens"],
|
|
51
|
+
cache_read_tokens=row["cache_read_tokens"],
|
|
52
|
+
cache_write_tokens=row["cache_write_tokens"],
|
|
53
|
+
cost_usd=row["cost_usd"],
|
|
54
|
+
cost_basis=row["cost_basis"],
|
|
55
|
+
context_used_pct=row["context_used_pct"],
|
|
56
|
+
),
|
|
57
|
+
usage_reported=(bool(row["usage_reported"]) if row["usage_reported"] is not None else None),
|
|
58
|
+
billing_mode=row["billing_mode"],
|
|
59
|
+
billing_source=row["billing_source"],
|
|
60
|
+
started_at=parse_required_timestamp(row["started_at"]),
|
|
61
|
+
ended_at=parse_timestamp(row["ended_at"]),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fetch_run(connection: sqlite3.Connection, run_id: int) -> Run | None:
|
|
66
|
+
"""Read one run by id, or `None` if it doesn't exist."""
|
|
67
|
+
row = connection.execute(f"SELECT {_RUN_COLUMNS} FROM runs WHERE id = ?", (run_id,)).fetchone()
|
|
68
|
+
return _row_to_run(row) if row is not None else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def fetch_runs_for_task(connection: sqlite3.Connection, task_id: str) -> list[Run]:
|
|
72
|
+
"""Read every run recorded for `task_id`, most recently started first."""
|
|
73
|
+
rows = connection.execute(
|
|
74
|
+
f"SELECT {_RUN_COLUMNS} FROM runs WHERE task_id = ? ORDER BY id DESC", (task_id,)
|
|
75
|
+
).fetchall()
|
|
76
|
+
return [_row_to_run(row) for row in rows]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def fetch_taken_over_roles_for_task(
|
|
80
|
+
connection: sqlite3.Connection,
|
|
81
|
+
task_id: str,
|
|
82
|
+
) -> tuple[Role, ...]:
|
|
83
|
+
rows = connection.execute(
|
|
84
|
+
"SELECT role FROM runs WHERE task_id = ? AND status = ? ORDER BY id DESC LIMIT ?",
|
|
85
|
+
(task_id, RunStatus.TAKEN_OVER.value, MAX_TAKEN_OVER_ROLE_COUNT),
|
|
86
|
+
).fetchall()
|
|
87
|
+
return tuple(Role(row["role"]) for row in rows)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def fetch_all_runs(connection: sqlite3.Connection) -> list[Run]:
|
|
91
|
+
"""Read every run, most recently started first."""
|
|
92
|
+
rows = connection.execute(f"SELECT {_RUN_COLUMNS} FROM runs ORDER BY id DESC").fetchall()
|
|
93
|
+
return [_row_to_run(row) for row in rows]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RunStore:
|
|
97
|
+
"""Starts, updates and reads agent session runs."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, connection: sqlite3.Connection, clock: Clock) -> None:
|
|
100
|
+
"""Use `connection` (the engine's writer) and `clock` for every write below."""
|
|
101
|
+
self._connection = connection
|
|
102
|
+
self._clock = clock
|
|
103
|
+
|
|
104
|
+
def start(
|
|
105
|
+
self,
|
|
106
|
+
task_id: str,
|
|
107
|
+
role: Role,
|
|
108
|
+
rung: Rung,
|
|
109
|
+
*,
|
|
110
|
+
billing_declaration: BillingDeclaration = UNKNOWN_BILLING_DECLARATION,
|
|
111
|
+
) -> Run:
|
|
112
|
+
"""Start a new live run for `task_id`'s `role` on `rung`."""
|
|
113
|
+
cursor = self._connection.execute(
|
|
114
|
+
"""
|
|
115
|
+
INSERT INTO runs (
|
|
116
|
+
task_id, role, agent, model, effort, status, usage_reported,
|
|
117
|
+
billing_mode, billing_source, started_at
|
|
118
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)
|
|
119
|
+
""",
|
|
120
|
+
(
|
|
121
|
+
task_id,
|
|
122
|
+
role.value,
|
|
123
|
+
rung.agent,
|
|
124
|
+
rung.model,
|
|
125
|
+
rung.effort,
|
|
126
|
+
RunStatus.LIVE.value,
|
|
127
|
+
billing_declaration.mode,
|
|
128
|
+
billing_declaration.source,
|
|
129
|
+
format_timestamp(self._clock.now()),
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
run_id = require_after_insert(
|
|
133
|
+
cursor.lastrowid, "sqlite did not allocate a rowid for the new run"
|
|
134
|
+
)
|
|
135
|
+
run = fetch_run(self._connection, run_id)
|
|
136
|
+
return require_after_insert(run, f"run {run_id} vanished immediately after insert")
|
|
137
|
+
|
|
138
|
+
def get_live(self, task_id: str, role: Role) -> Run | None:
|
|
139
|
+
"""Return `task_id`'s live run for `role`, if any."""
|
|
140
|
+
return self._get_with_status(task_id, role, RunStatus.LIVE)
|
|
141
|
+
|
|
142
|
+
def get_taken_over(self, task_id: str, role: Role) -> Run | None:
|
|
143
|
+
"""Return `task_id`'s run for `role` that the Operator has taken over, if any."""
|
|
144
|
+
return self._get_with_status(task_id, role, RunStatus.TAKEN_OVER)
|
|
145
|
+
|
|
146
|
+
def mark_taken_over(self, run_id: int) -> None:
|
|
147
|
+
"""Mark `run_id` taken over: its session runs interactively in the Operator's pane."""
|
|
148
|
+
self._set_status_keeping_open(run_id, RunStatus.TAKEN_OVER)
|
|
149
|
+
|
|
150
|
+
def mark_live(self, run_id: int) -> None:
|
|
151
|
+
"""Mark a taken-over `run_id` live again: the engine drives its session."""
|
|
152
|
+
self._set_status_keeping_open(run_id, RunStatus.LIVE)
|
|
153
|
+
|
|
154
|
+
def has_taken_over(self, task_id: str) -> bool:
|
|
155
|
+
"""Report whether the Operator has taken over any role of `task_id`."""
|
|
156
|
+
row = self._connection.execute(
|
|
157
|
+
"SELECT 1 FROM runs WHERE task_id = ? AND status = ? LIMIT 1",
|
|
158
|
+
(task_id, RunStatus.TAKEN_OVER.value),
|
|
159
|
+
).fetchone()
|
|
160
|
+
return row is not None
|
|
161
|
+
|
|
162
|
+
def _get_with_status(self, task_id: str, role: Role, status: RunStatus) -> Run | None:
|
|
163
|
+
row = self._connection.execute(
|
|
164
|
+
f"SELECT {_RUN_COLUMNS} FROM runs WHERE task_id = ? AND role = ? AND status = ?",
|
|
165
|
+
(task_id, role.value, status.value),
|
|
166
|
+
).fetchone()
|
|
167
|
+
return _row_to_run(row) if row is not None else None
|
|
168
|
+
|
|
169
|
+
def _set_status_keeping_open(self, run_id: int, status: RunStatus) -> None:
|
|
170
|
+
self._connection.execute("UPDATE runs SET status = ? WHERE id = ?", (status.value, run_id))
|
|
171
|
+
|
|
172
|
+
def for_task(self, task_id: str) -> list[Run]:
|
|
173
|
+
"""Return every run recorded for `task_id`."""
|
|
174
|
+
return fetch_runs_for_task(self._connection, task_id)
|
|
175
|
+
|
|
176
|
+
def set_session(self, run_id: int, session_id: str | None, resume_command: str | None) -> None:
|
|
177
|
+
"""Record `run_id`'s launched session id and resume command.
|
|
178
|
+
|
|
179
|
+
`session_id` is `None` when `SessionStarted` says the session can't be resumed,
|
|
180
|
+
and `resume_command` is `None` when the agent's `resume_command()` returns none.
|
|
181
|
+
"""
|
|
182
|
+
self._connection.execute(
|
|
183
|
+
"UPDATE runs SET session_id = ?, resume_command = ? WHERE id = ?",
|
|
184
|
+
(session_id, resume_command, run_id),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def set_rung(self, run_id: int, rung: Rung) -> None:
|
|
188
|
+
"""Record a same-agent model or effort change on an existing session."""
|
|
189
|
+
self._connection.execute(
|
|
190
|
+
"UPDATE runs SET model = ?, effort = ? WHERE id = ?",
|
|
191
|
+
(rung.model, rung.effort, run_id),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def set_last_message(self, run_id: int, message: str) -> None:
|
|
195
|
+
"""Persist `run_id`'s latest known message from the agent."""
|
|
196
|
+
self._connection.execute("UPDATE runs SET last_message = ? WHERE id = ?", (message, run_id))
|
|
197
|
+
|
|
198
|
+
def record_usage(self, run_id: int, usage: Usage) -> None:
|
|
199
|
+
"""Overwrite `run_id`'s usage with `usage`'s latest cumulative snapshot."""
|
|
200
|
+
self._connection.execute(
|
|
201
|
+
"""
|
|
202
|
+
UPDATE runs SET input_tokens = ?, output_tokens = ?, cache_read_tokens = ?,
|
|
203
|
+
cache_write_tokens = ?, cost_usd = ?, cost_basis = ?, context_used_pct = ?,
|
|
204
|
+
usage_reported = 1
|
|
205
|
+
WHERE id = ?
|
|
206
|
+
""",
|
|
207
|
+
(
|
|
208
|
+
usage.input_tokens,
|
|
209
|
+
usage.output_tokens,
|
|
210
|
+
usage.cache_read_tokens,
|
|
211
|
+
usage.cache_write_tokens,
|
|
212
|
+
usage.cost_usd,
|
|
213
|
+
usage.cost_basis,
|
|
214
|
+
usage.context_used_pct,
|
|
215
|
+
run_id,
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def close(self, run_id: int) -> None:
|
|
220
|
+
"""Mark `run_id` closed: the task ended, the session is not resumable."""
|
|
221
|
+
self._set_status(run_id, RunStatus.CLOSED)
|
|
222
|
+
|
|
223
|
+
def _set_status(self, run_id: int, status: RunStatus) -> None:
|
|
224
|
+
self._connection.execute(
|
|
225
|
+
"UPDATE runs SET status = ?, ended_at = ? WHERE id = ?",
|
|
226
|
+
(status.value, format_timestamp(self._clock.now()), run_id),
|
|
227
|
+
)
|