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,230 @@
|
|
|
1
|
+
"""Runs a step's turns and their recoveries until one completes or the task stops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from shikiru.domain.recovery import Completed
|
|
9
|
+
from shikiru.domain.steps import ACTING_ROLE, Transition
|
|
10
|
+
from shikiru.engine.events import emit_nudge, emit_turn_started, emit_turn_stopped
|
|
11
|
+
from shikiru.engine.turn_context import ORDINARY_TURN, NextTurn
|
|
12
|
+
from shikiru.engine.turn_execution import TurnExecution
|
|
13
|
+
from shikiru.engine.turns import TurnHeld
|
|
14
|
+
from shikiru.errors import InvariantError
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from shikiru.domain.feedback import FeedbackInput
|
|
20
|
+
from shikiru.domain.recovery import TurnCounters
|
|
21
|
+
from shikiru.domain.reports import Report, ReportKind
|
|
22
|
+
from shikiru.domain.tasks import Role, Step, Task
|
|
23
|
+
from shikiru.engine.escalation_filing import EscalationFiler
|
|
24
|
+
from shikiru.engine.recovery import TurnRecovery
|
|
25
|
+
from shikiru.engine.sessions import SessionManager
|
|
26
|
+
from shikiru.engine.turn_context import TurnContextPreparer
|
|
27
|
+
from shikiru.engine.turns import TurnRunner
|
|
28
|
+
from shikiru.prompts.renderer import PromptName
|
|
29
|
+
from shikiru.store.events import EventLog
|
|
30
|
+
from shikiru.store.runs import RunStore
|
|
31
|
+
from shikiru.store.tasks import TaskStore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class TurnOverrides:
|
|
36
|
+
"""What an integration turn changes about a step's ordinary turn."""
|
|
37
|
+
|
|
38
|
+
role: Role | None = None
|
|
39
|
+
template: PromptName | None = None
|
|
40
|
+
kind: ReportKind | None = None
|
|
41
|
+
conflict_context: str | None = None
|
|
42
|
+
feedback: FeedbackInput | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
NO_OVERRIDES = TurnOverrides()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class TurnsCompleted:
|
|
50
|
+
"""The loop ended with a turn that produced `report`."""
|
|
51
|
+
|
|
52
|
+
task: Task
|
|
53
|
+
counters: TurnCounters
|
|
54
|
+
execution: TurnExecution
|
|
55
|
+
report: Report
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class TurnsStopped:
|
|
60
|
+
"""The loop ended because the task was held or escalated."""
|
|
61
|
+
|
|
62
|
+
task: Task
|
|
63
|
+
counters: TurnCounters
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
TurnLoopResult = TurnsCompleted | TurnsStopped
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _acting_role(step: Step, role_override: Role | None) -> Role:
|
|
70
|
+
return role_override if role_override is not None else ACTING_ROLE[step]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TurnLoop:
|
|
74
|
+
"""Runs a step's turn, recovering failed turns until one completes or the task stops."""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
tasks: TaskStore,
|
|
80
|
+
events: EventLog,
|
|
81
|
+
sessions: SessionManager,
|
|
82
|
+
turns: TurnRunner,
|
|
83
|
+
contexts: TurnContextPreparer,
|
|
84
|
+
recovery: TurnRecovery,
|
|
85
|
+
escalations: EscalationFiler,
|
|
86
|
+
runs: RunStore,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Run turns through `turns`, recover through `recovery`, escalate through `escalations`."""
|
|
89
|
+
self._tasks = tasks
|
|
90
|
+
self._events = events
|
|
91
|
+
self._sessions = sessions
|
|
92
|
+
self._turns = turns
|
|
93
|
+
self._contexts = contexts
|
|
94
|
+
self._recovery = recovery
|
|
95
|
+
self._escalations = escalations
|
|
96
|
+
self._runs = runs
|
|
97
|
+
self._resumed: set[str] = set()
|
|
98
|
+
|
|
99
|
+
def expect_resume(self, task_id: str) -> None:
|
|
100
|
+
"""Let `task_id`'s next turn consume a report its interrupted turn already submitted."""
|
|
101
|
+
self._resumed.add(task_id)
|
|
102
|
+
|
|
103
|
+
def forget_resume(self, task_id: str) -> None:
|
|
104
|
+
"""Drop the pending resume of `task_id`."""
|
|
105
|
+
self._resumed.discard(task_id)
|
|
106
|
+
|
|
107
|
+
async def run(
|
|
108
|
+
self,
|
|
109
|
+
task: Task,
|
|
110
|
+
step: Step,
|
|
111
|
+
counters: TurnCounters,
|
|
112
|
+
overrides: TurnOverrides = NO_OVERRIDES,
|
|
113
|
+
) -> TurnLoopResult:
|
|
114
|
+
"""Run `step`'s turn and its recoveries until a report, a hold or an escalation."""
|
|
115
|
+
current = self._tasks.require(task.id)
|
|
116
|
+
if current.held:
|
|
117
|
+
return TurnsStopped(current, counters)
|
|
118
|
+
current_step = step
|
|
119
|
+
execution = await self._execute(current, current_step, overrides, ORDINARY_TURN)
|
|
120
|
+
while execution is not None:
|
|
121
|
+
result = execution.result
|
|
122
|
+
if isinstance(result, Completed):
|
|
123
|
+
return TurnsCompleted(current, counters, execution, result.report)
|
|
124
|
+
role = _acting_role(current_step, overrides.role)
|
|
125
|
+
recovery = await self._recovery.recover(
|
|
126
|
+
current, current_step, role, execution, counters
|
|
127
|
+
)
|
|
128
|
+
current, counters = recovery.task, recovery.counters
|
|
129
|
+
if recovery.escalation is not None:
|
|
130
|
+
transition = Transition.escalate(current, recovery.escalation, step=current_step)
|
|
131
|
+
escalated = await self._escalations.escalate(
|
|
132
|
+
current, transition, None, result=result, role=role
|
|
133
|
+
)
|
|
134
|
+
return TurnsStopped(escalated, counters)
|
|
135
|
+
if recovery.execution is not None:
|
|
136
|
+
execution = recovery.execution
|
|
137
|
+
continue
|
|
138
|
+
if current.step is None:
|
|
139
|
+
message = f"{current.id} recovery left the task with no step"
|
|
140
|
+
raise InvariantError(message)
|
|
141
|
+
current_step = current.step
|
|
142
|
+
current = self._tasks.require(current.id)
|
|
143
|
+
if current.held:
|
|
144
|
+
return TurnsStopped(current, counters)
|
|
145
|
+
execution = await self._execute(current, current_step, overrides, recovery.next_turn)
|
|
146
|
+
return TurnsStopped(self._tasks.require(current.id), counters)
|
|
147
|
+
|
|
148
|
+
async def _execute(
|
|
149
|
+
self, task: Task, step: Step, overrides: TurnOverrides, next_turn: NextTurn
|
|
150
|
+
) -> TurnExecution | None:
|
|
151
|
+
role = _acting_role(step, overrides.role)
|
|
152
|
+
selection = self._sessions.session_for(task, role)
|
|
153
|
+
turn_request = await self._contexts.prepare(
|
|
154
|
+
task,
|
|
155
|
+
step,
|
|
156
|
+
session_handoff=selection.handoff,
|
|
157
|
+
next_turn=next_turn,
|
|
158
|
+
template_override=overrides.template,
|
|
159
|
+
kind_override=overrides.kind,
|
|
160
|
+
conflict_context=overrides.conflict_context,
|
|
161
|
+
feedback=overrides.feedback,
|
|
162
|
+
)
|
|
163
|
+
usage_before = selection.run.usage
|
|
164
|
+
resumed = task.id in self._resumed
|
|
165
|
+
self._resumed.discard(task.id)
|
|
166
|
+
completed = self._turns.completed_turn(task.id, role, turn_request) if resumed else None
|
|
167
|
+
if completed is not None:
|
|
168
|
+
result, turn_dir = completed
|
|
169
|
+
return TurnExecution(
|
|
170
|
+
result=result,
|
|
171
|
+
task_id=task.id,
|
|
172
|
+
role=role,
|
|
173
|
+
turn_dir=turn_dir,
|
|
174
|
+
rung=selection.rung,
|
|
175
|
+
run_id=selection.run.id,
|
|
176
|
+
usage_before=usage_before,
|
|
177
|
+
usage_after=usage_before,
|
|
178
|
+
kind=turn_request.kind,
|
|
179
|
+
)
|
|
180
|
+
turn_name: str | None = None
|
|
181
|
+
|
|
182
|
+
def on_start(turn_dir: Path) -> None:
|
|
183
|
+
nonlocal turn_name
|
|
184
|
+
turn_name = turn_dir.name
|
|
185
|
+
emit_turn_started(
|
|
186
|
+
self._events,
|
|
187
|
+
task,
|
|
188
|
+
step,
|
|
189
|
+
role,
|
|
190
|
+
selection.rung,
|
|
191
|
+
kind=turn_request.kind,
|
|
192
|
+
run_id=selection.run.id,
|
|
193
|
+
turn=turn_name,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def on_nudge() -> None:
|
|
197
|
+
emit_nudge(self._events, task, step, role, turn=turn_name)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
result, turn_dir, selection = await self._turns.run(
|
|
201
|
+
task, role, selection, turn_request, on_start=on_start, on_nudge=on_nudge
|
|
202
|
+
)
|
|
203
|
+
except BaseException as stop:
|
|
204
|
+
emit_turn_stopped(
|
|
205
|
+
self._events,
|
|
206
|
+
task,
|
|
207
|
+
step,
|
|
208
|
+
role,
|
|
209
|
+
rung=selection.rung,
|
|
210
|
+
run_id=selection.run.id,
|
|
211
|
+
turn=turn_name,
|
|
212
|
+
)
|
|
213
|
+
if isinstance(stop, TurnHeld):
|
|
214
|
+
return None
|
|
215
|
+
raise
|
|
216
|
+
live_run = self._runs.get_live(task.id, role)
|
|
217
|
+
if live_run is None:
|
|
218
|
+
message = f"{task.id} {role.value} run disappeared after its turn"
|
|
219
|
+
raise InvariantError(message)
|
|
220
|
+
return TurnExecution(
|
|
221
|
+
result=result,
|
|
222
|
+
task_id=task.id,
|
|
223
|
+
role=role,
|
|
224
|
+
turn_dir=turn_dir,
|
|
225
|
+
rung=selection.rung,
|
|
226
|
+
run_id=selection.run.id,
|
|
227
|
+
usage_before=usage_before,
|
|
228
|
+
usage_after=live_run.usage,
|
|
229
|
+
kind=turn_request.kind,
|
|
230
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""The `turn.pid` record of each running turn, and ending the ones a dead engine left."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import signal
|
|
7
|
+
from dataclasses import asdict
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from shikiru.agents.contracts import role_dir
|
|
12
|
+
from shikiru.paths import RUNS_DIR_NAME, project_state_dir
|
|
13
|
+
from shikiru.private import make_private_dir, write_private_text
|
|
14
|
+
from shikiru.process import TURN_STOP_GRACE, ProcessIdentity
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from shikiru.clock import Clock
|
|
20
|
+
from shikiru.domain.tasks import Role
|
|
21
|
+
from shikiru.process import SubprocessRunner
|
|
22
|
+
|
|
23
|
+
TURN_PID_NAME = "turn.pid"
|
|
24
|
+
EXIT_POLL_SECONDS = 0.1
|
|
25
|
+
_STAGED_SUFFIX = ".staged"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def turn_pid_path(run_dir: Path, role: Role) -> Path:
|
|
29
|
+
"""Return the file recording the process that runs `role`'s current turn."""
|
|
30
|
+
return role_dir(run_dir, role.value) / TURN_PID_NAME
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def read_turn_pid(path: Path) -> ProcessIdentity:
|
|
34
|
+
"""Read the process identity recorded in a `turn.pid` file."""
|
|
35
|
+
return ProcessIdentity(**json.loads(path.read_text()))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TurnProcesses:
|
|
39
|
+
"""Record the process running each role's turn and stop orphans after an engine restart."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, *, runner: SubprocessRunner, clock: Clock, project_root: Path) -> None:
|
|
42
|
+
"""Identify and signal processes through `runner`, waiting on `clock`."""
|
|
43
|
+
self._runner = runner
|
|
44
|
+
self._clock = clock
|
|
45
|
+
self._project_root = project_root
|
|
46
|
+
|
|
47
|
+
def record(self, run_dir: Path, role: Role, pid: int) -> None:
|
|
48
|
+
"""Write `role`'s `turn.pid` with the identity of `pid`, or of its group once it left."""
|
|
49
|
+
path = turn_pid_path(run_dir, role)
|
|
50
|
+
identity = self._runner.identify(pid)
|
|
51
|
+
if identity is None and self._runner.group_is_running(pid):
|
|
52
|
+
identity = ProcessIdentity(pid, None)
|
|
53
|
+
if identity is None:
|
|
54
|
+
path.unlink(missing_ok=True)
|
|
55
|
+
return
|
|
56
|
+
make_private_dir(path.parent)
|
|
57
|
+
staged = path.with_suffix(_STAGED_SUFFIX)
|
|
58
|
+
write_private_text(staged, json.dumps(asdict(identity)))
|
|
59
|
+
staged.replace(path)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def clear(run_dir: Path, role: Role) -> None:
|
|
63
|
+
"""Remove `role`'s `turn.pid` once its turn process has ended."""
|
|
64
|
+
turn_pid_path(run_dir, role).unlink(missing_ok=True)
|
|
65
|
+
|
|
66
|
+
async def end_orphans(self) -> None:
|
|
67
|
+
"""Stop every recorded turn process that still runs, then drop its `turn.pid`."""
|
|
68
|
+
runs_root = project_state_dir(self._project_root) / RUNS_DIR_NAME
|
|
69
|
+
for path in sorted(runs_root.glob(f"*/*/{TURN_PID_NAME}")):
|
|
70
|
+
await self._stop(read_turn_pid(path))
|
|
71
|
+
path.unlink()
|
|
72
|
+
|
|
73
|
+
async def _stop(self, identity: ProcessIdentity) -> None:
|
|
74
|
+
if not self._runner.signal_group(identity, signal.SIGTERM):
|
|
75
|
+
return
|
|
76
|
+
deadline = self._clock.now() + timedelta(seconds=TURN_STOP_GRACE)
|
|
77
|
+
while self._runner.group_is_running(identity.pid) and self._clock.now() < deadline:
|
|
78
|
+
await self._clock.sleep(EXIT_POLL_SECONDS)
|
|
79
|
+
self._runner.signal_group(identity, signal.SIGKILL)
|