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
shikiru/engine/events.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Shared event construction for engine lifecycle boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shikiru.agents.contracts import COST_BASIS_UNKNOWN
|
|
8
|
+
from shikiru.domain.events import EventKind
|
|
9
|
+
from shikiru.domain.ladder import pick_rung
|
|
10
|
+
from shikiru.domain.recovery import Failed, ProtocolError, Stalled
|
|
11
|
+
from shikiru.domain.reports import (
|
|
12
|
+
DECISION_ANSWER,
|
|
13
|
+
STATUS_COMPLETED,
|
|
14
|
+
ConsultAnswer,
|
|
15
|
+
FeedbackAnalysis,
|
|
16
|
+
ImplementationReport,
|
|
17
|
+
PlanReport,
|
|
18
|
+
TriageReport,
|
|
19
|
+
Verdict,
|
|
20
|
+
)
|
|
21
|
+
from shikiru.domain.steps import ACTING_ROLE
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from shikiru.agents.contracts import Rung, Usage
|
|
25
|
+
from shikiru.config.settings import Settings
|
|
26
|
+
from shikiru.domain.recovery import TurnResult
|
|
27
|
+
from shikiru.domain.reports import ReportKind
|
|
28
|
+
from shikiru.domain.steps import Outcome
|
|
29
|
+
from shikiru.domain.tasks import Role, Step, Task
|
|
30
|
+
from shikiru.engine.turn_execution import TurnExecution
|
|
31
|
+
from shikiru.store.events import EventLog
|
|
32
|
+
|
|
33
|
+
TURN_FIELD = "turn"
|
|
34
|
+
ROLE_FIELD = "role"
|
|
35
|
+
ITERATION_FIELD = "iteration"
|
|
36
|
+
REPORT_KIND_FIELD = "report_kind"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def emit_step_started(events: EventLog, task: Task, settings: Settings) -> None:
|
|
40
|
+
"""Record the selected role and rung when a task step starts."""
|
|
41
|
+
if task.step is None:
|
|
42
|
+
return
|
|
43
|
+
role = ACTING_ROLE[task.step]
|
|
44
|
+
rung = pick_rung(task, role, settings)
|
|
45
|
+
events.append(
|
|
46
|
+
EventKind.STEP_STARTED,
|
|
47
|
+
task_id=task.id,
|
|
48
|
+
message=f"{task.id} {task.step.value} started",
|
|
49
|
+
data={
|
|
50
|
+
"step": task.step.value,
|
|
51
|
+
"role": role.value,
|
|
52
|
+
"iteration": task.iteration,
|
|
53
|
+
"agent": rung.agent,
|
|
54
|
+
"model": rung.model,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def emit_turn_started(
|
|
60
|
+
events: EventLog,
|
|
61
|
+
task: Task,
|
|
62
|
+
step: Step,
|
|
63
|
+
role: Role,
|
|
64
|
+
rung: Rung,
|
|
65
|
+
*,
|
|
66
|
+
kind: ReportKind,
|
|
67
|
+
run_id: int | None = None,
|
|
68
|
+
turn: str | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Record that `role` started its `step` turn on `rung`, owing a report of `kind`."""
|
|
71
|
+
events.append(
|
|
72
|
+
EventKind.TURN_STARTED,
|
|
73
|
+
task_id=task.id,
|
|
74
|
+
message=f"{task.id} {role.value} turn started",
|
|
75
|
+
data={
|
|
76
|
+
"step": step.value,
|
|
77
|
+
"role": role.value,
|
|
78
|
+
"iteration": task.iteration,
|
|
79
|
+
"agent": rung.agent,
|
|
80
|
+
"model": rung.model,
|
|
81
|
+
"effort": rung.effort,
|
|
82
|
+
REPORT_KIND_FIELD: kind.value,
|
|
83
|
+
**_turn_data(turn),
|
|
84
|
+
**({"run_id": run_id} if run_id is not None else {}),
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def emit_turn_stopped(
|
|
90
|
+
events: EventLog,
|
|
91
|
+
task: Task,
|
|
92
|
+
step: Step,
|
|
93
|
+
role: Role,
|
|
94
|
+
*,
|
|
95
|
+
rung: Rung,
|
|
96
|
+
run_id: int,
|
|
97
|
+
turn: str | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Record that `role`'s `step` turn ended without a result because the task was stopped."""
|
|
100
|
+
events.append(
|
|
101
|
+
EventKind.TURN_STOPPED,
|
|
102
|
+
task_id=task.id,
|
|
103
|
+
message=f"{task.id} {role.value} turn stopped",
|
|
104
|
+
data={
|
|
105
|
+
"step": step.value,
|
|
106
|
+
"role": role.value,
|
|
107
|
+
"iteration": task.iteration,
|
|
108
|
+
"agent": rung.agent,
|
|
109
|
+
"model": rung.model,
|
|
110
|
+
"effort": rung.effort,
|
|
111
|
+
"run_id": run_id,
|
|
112
|
+
**_turn_data(turn),
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def emit_nudge(
|
|
118
|
+
events: EventLog, task: Task, step: Step, role: Role, *, turn: str | None = None
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Record that the engine nudged `role`'s stalled `step` turn."""
|
|
121
|
+
events.append(
|
|
122
|
+
EventKind.NUDGE,
|
|
123
|
+
task_id=task.id,
|
|
124
|
+
message=f"{task.id} {role.value} nudge",
|
|
125
|
+
data={"step": step.value, "role": role.value, **_turn_data(turn)},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def emit_turn_failed(
|
|
130
|
+
events: EventLog,
|
|
131
|
+
task: Task,
|
|
132
|
+
step: Step,
|
|
133
|
+
result: TurnResult,
|
|
134
|
+
*,
|
|
135
|
+
rung: Rung,
|
|
136
|
+
run_id: int,
|
|
137
|
+
turn: str | None = None,
|
|
138
|
+
role: Role | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Record a turn that ended without a valid report."""
|
|
141
|
+
if isinstance(result, Failed):
|
|
142
|
+
message = result.message
|
|
143
|
+
data: dict[str, object] = {"kind": result.kind.value, "resets_at": result.resets_at}
|
|
144
|
+
elif isinstance(result, Stalled):
|
|
145
|
+
message = "agent stalled"
|
|
146
|
+
data = {}
|
|
147
|
+
elif isinstance(result, ProtocolError):
|
|
148
|
+
message = "agent protocol error"
|
|
149
|
+
data = {}
|
|
150
|
+
else:
|
|
151
|
+
message = type(result).__name__
|
|
152
|
+
data = {}
|
|
153
|
+
data.update(
|
|
154
|
+
{
|
|
155
|
+
"step": step.value,
|
|
156
|
+
"result": type(result).__name__,
|
|
157
|
+
**_provenance_data(role, task.iteration),
|
|
158
|
+
"agent": rung.agent,
|
|
159
|
+
"model": rung.model,
|
|
160
|
+
"effort": rung.effort,
|
|
161
|
+
"run_id": run_id,
|
|
162
|
+
**_turn_data(turn),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
events.append(EventKind.TURN_FAILED, task_id=task.id, message=message, data=data)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def emit_turn_completed(
|
|
169
|
+
events: EventLog, task: Task, outcome: Outcome, execution: TurnExecution
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Record a completed turn and, for a verdict, the verdict itself."""
|
|
172
|
+
summary = _turn_summary(outcome)
|
|
173
|
+
events.append(
|
|
174
|
+
EventKind.TURN_COMPLETED,
|
|
175
|
+
task_id=task.id,
|
|
176
|
+
message=summary,
|
|
177
|
+
data={
|
|
178
|
+
"step": task.step.value if task.step is not None else None,
|
|
179
|
+
"iteration": task.iteration,
|
|
180
|
+
"role": execution.role.value,
|
|
181
|
+
"agent": execution.rung.agent,
|
|
182
|
+
"model": execution.rung.model,
|
|
183
|
+
"effort": execution.rung.effort,
|
|
184
|
+
"run_id": execution.run_id,
|
|
185
|
+
REPORT_KIND_FIELD: execution.kind.value,
|
|
186
|
+
"outcome": _turn_outcome(outcome),
|
|
187
|
+
"summary": summary,
|
|
188
|
+
"question": _turn_question(outcome),
|
|
189
|
+
"usage_delta": _usage_delta(execution.usage_before, execution.usage_after),
|
|
190
|
+
**_turn_data(execution.turn_dir.name),
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
if isinstance(outcome, Verdict):
|
|
194
|
+
emit_verdict(
|
|
195
|
+
events,
|
|
196
|
+
task,
|
|
197
|
+
outcome,
|
|
198
|
+
summary,
|
|
199
|
+
turn=execution.turn_dir.name,
|
|
200
|
+
role=execution.role,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def emit_verdict(
|
|
205
|
+
events: EventLog,
|
|
206
|
+
task: Task,
|
|
207
|
+
verdict: Verdict,
|
|
208
|
+
summary: str,
|
|
209
|
+
*,
|
|
210
|
+
turn: str | None = None,
|
|
211
|
+
role: Role | None = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Record the Leader's (or the engine's) verdict on a task."""
|
|
214
|
+
events.append(
|
|
215
|
+
EventKind.VERDICT,
|
|
216
|
+
task_id=task.id,
|
|
217
|
+
message=summary,
|
|
218
|
+
data={
|
|
219
|
+
"decision": verdict.decision,
|
|
220
|
+
"summary": summary,
|
|
221
|
+
"issues": [issue.model_dump() for issue in verdict.issues],
|
|
222
|
+
"residual_risks": list(verdict.residual_risks),
|
|
223
|
+
**_provenance_data(role, task.iteration),
|
|
224
|
+
**_turn_data(turn),
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _turn_data(turn: str | None) -> dict[str, str]:
|
|
230
|
+
return {TURN_FIELD: turn} if turn is not None else {}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _provenance_data(role: Role | None, iteration: int) -> dict[str, str | int]:
|
|
234
|
+
data: dict[str, str | int] = {ITERATION_FIELD: iteration}
|
|
235
|
+
if role is not None:
|
|
236
|
+
data[ROLE_FIELD] = role.value
|
|
237
|
+
return data
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _turn_summary(outcome: Outcome) -> str:
|
|
241
|
+
if isinstance(outcome, TriageReport):
|
|
242
|
+
return outcome.title
|
|
243
|
+
if isinstance(outcome, ConsultAnswer):
|
|
244
|
+
return outcome.answer if outcome.decision == DECISION_ANSWER else "escalate"
|
|
245
|
+
return outcome.summary
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _turn_question(outcome: Outcome) -> str:
|
|
249
|
+
return outcome.question if isinstance(outcome, ImplementationReport) else ""
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _turn_outcome(outcome: Outcome) -> str:
|
|
253
|
+
if isinstance(outcome, TriageReport):
|
|
254
|
+
return outcome.mode.value
|
|
255
|
+
if isinstance(outcome, PlanReport | FeedbackAnalysis):
|
|
256
|
+
return STATUS_COMPLETED
|
|
257
|
+
if isinstance(outcome, ConsultAnswer):
|
|
258
|
+
return outcome.decision
|
|
259
|
+
if isinstance(outcome, ImplementationReport):
|
|
260
|
+
return outcome.status
|
|
261
|
+
return outcome.decision
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _usage_delta(before: Usage, after: Usage) -> dict[str, int | float | str | None]:
|
|
265
|
+
cost_delta, cost_basis = _comparable_cost_delta(before, after)
|
|
266
|
+
return {
|
|
267
|
+
"input_tokens": after.input_tokens - before.input_tokens,
|
|
268
|
+
"output_tokens": after.output_tokens - before.output_tokens,
|
|
269
|
+
"cache_read_tokens": after.cache_read_tokens - before.cache_read_tokens,
|
|
270
|
+
"cache_write_tokens": after.cache_write_tokens - before.cache_write_tokens,
|
|
271
|
+
"cost_usd": cost_delta,
|
|
272
|
+
"cost_basis": cost_basis,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _comparable_cost_delta(before: Usage, after: Usage) -> tuple[float | None, str]:
|
|
277
|
+
if (
|
|
278
|
+
before.cost_usd is None
|
|
279
|
+
or after.cost_usd is None
|
|
280
|
+
or before.cost_basis != after.cost_basis
|
|
281
|
+
or after.cost_basis == COST_BASIS_UNKNOWN
|
|
282
|
+
or after.cost_usd < before.cost_usd
|
|
283
|
+
):
|
|
284
|
+
return None, COST_BASIS_UNKNOWN
|
|
285
|
+
return after.cost_usd - before.cost_usd, after.cost_basis
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Gathers the evidence of a task escalation: reports, workspace facts and the turn tail."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from shikiru.domain.steps import ACTING_ROLE
|
|
10
|
+
from shikiru.errors import ShikiruError
|
|
11
|
+
from shikiru.paths import run_dir
|
|
12
|
+
from shikiru.view_log import read_tail_lines, view_log_path
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from shikiru.domain.tasks import Role, Step, Task
|
|
18
|
+
from shikiru.engine.history import ReportHistory
|
|
19
|
+
from shikiru.engine.task_facts import TaskFacts
|
|
20
|
+
|
|
21
|
+
_LOGGER = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
ESCALATION_PANE_TAIL_LINES = 60
|
|
24
|
+
"""Lines of pane scrollback captured as escalation evidence."""
|
|
25
|
+
|
|
26
|
+
FACTS_UNAVAILABLE = "(facts unavailable: {error})"
|
|
27
|
+
HISTORY_UNAVAILABLE = "(report history unavailable, so no reports are shown: {error})"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Evidence:
|
|
32
|
+
"""The recorded reports, rendered facts and recent turn signals of one task."""
|
|
33
|
+
|
|
34
|
+
reports: list[dict[str, str]]
|
|
35
|
+
facts: str
|
|
36
|
+
view_tail: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EvidenceCollector:
|
|
40
|
+
"""Collects the evidence that still exists for a task from its files and git."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, *, history: ReportHistory, facts: TaskFacts, project_root: Path) -> None:
|
|
43
|
+
"""Read reports through `history`, facts through `facts` and turn tails under the root."""
|
|
44
|
+
self._history = history
|
|
45
|
+
self._facts = facts
|
|
46
|
+
self._project_root = project_root
|
|
47
|
+
|
|
48
|
+
async def collect(self, task: Task, step: Step | None, role: Role | None = None) -> Evidence:
|
|
49
|
+
"""Gather `task`'s evidence; the turn tail is that of `role` or of `step`'s acting role."""
|
|
50
|
+
history_note = ""
|
|
51
|
+
try:
|
|
52
|
+
history = self._history.load(task.id)
|
|
53
|
+
except (OSError, ValueError) as error:
|
|
54
|
+
_LOGGER.exception("could not load report history for escalation of %s", task.id)
|
|
55
|
+
history = []
|
|
56
|
+
history_note = HISTORY_UNAVAILABLE.format(error=error)
|
|
57
|
+
facts = await self._workspace_facts(task)
|
|
58
|
+
return Evidence(
|
|
59
|
+
reports=self._history.context(history),
|
|
60
|
+
facts="\n\n".join(part for part in (history_note, facts) if part),
|
|
61
|
+
view_tail=self._view_tail(task, step, role),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def _workspace_facts(self, task: Task) -> str:
|
|
65
|
+
if task.repo is None:
|
|
66
|
+
return ""
|
|
67
|
+
try:
|
|
68
|
+
return await self._facts.text(task)
|
|
69
|
+
except ShikiruError as error:
|
|
70
|
+
return FACTS_UNAVAILABLE.format(error=error)
|
|
71
|
+
|
|
72
|
+
def _view_tail(self, task: Task, step: Step | None, role_override: Role | None) -> str:
|
|
73
|
+
role = role_override
|
|
74
|
+
if role is None and step is not None:
|
|
75
|
+
role = ACTING_ROLE[step]
|
|
76
|
+
if role is None:
|
|
77
|
+
return ""
|
|
78
|
+
path = view_log_path(run_dir(self._project_root, task.id), role.value)
|
|
79
|
+
if not path.is_file():
|
|
80
|
+
return ""
|
|
81
|
+
return "\n".join(read_tail_lines(path, ESCALATION_PANE_TAIL_LINES))
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Records engine faults and their recovery as `engine_fault` and `engine_recovered` events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import traceback
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from shikiru.domain.events import EventKind
|
|
12
|
+
from shikiru.domain.faults import (
|
|
13
|
+
COMMAND_KEY,
|
|
14
|
+
DETAIL_KEY,
|
|
15
|
+
ERROR_KEY,
|
|
16
|
+
EXIT_CODE_KEY,
|
|
17
|
+
RECOVERABLE_KEY,
|
|
18
|
+
REPEAT_KEY,
|
|
19
|
+
SCOPE_KEY,
|
|
20
|
+
STDERR_KEY,
|
|
21
|
+
TRACEBACK_KEY,
|
|
22
|
+
)
|
|
23
|
+
from shikiru.errors import CommandFailed, CommandTimeoutError, InvariantError, ShikiruError
|
|
24
|
+
from shikiru.store.database import OperationalDatabaseError
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import AsyncGenerator
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from logging import Logger
|
|
30
|
+
|
|
31
|
+
from shikiru.clock import Clock
|
|
32
|
+
from shikiru.config.settings import FaultsConfig
|
|
33
|
+
from shikiru.store.events import EventLog
|
|
34
|
+
|
|
35
|
+
RECOVERABLE_FAULTS = (ShikiruError, OSError, OperationalDatabaseError)
|
|
36
|
+
"""Environmental faults (a failed tmux, git, file or database call), retried while they last."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_recoverable(error: Exception) -> bool:
|
|
40
|
+
"""Return whether `error` is environmental; an `InvariantError` is a logic bug, never one."""
|
|
41
|
+
return isinstance(error, RECOVERABLE_FAULTS) and not isinstance(error, InvariantError)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _evidence(error: Exception) -> dict[str, Any]:
|
|
45
|
+
evidence: dict[str, Any] = {
|
|
46
|
+
DETAIL_KEY: str(error),
|
|
47
|
+
TRACEBACK_KEY: "".join(traceback.format_exception(error)),
|
|
48
|
+
}
|
|
49
|
+
if isinstance(error, CommandFailed | CommandTimeoutError):
|
|
50
|
+
evidence[COMMAND_KEY] = list(error.argv)
|
|
51
|
+
if isinstance(error, CommandFailed):
|
|
52
|
+
evidence[EXIT_CODE_KEY] = error.returncode
|
|
53
|
+
evidence[STDERR_KEY] = error.stderr
|
|
54
|
+
return evidence
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class _Streak:
|
|
59
|
+
recorded_at: datetime | None = None
|
|
60
|
+
occurrences: int = 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FaultReporter:
|
|
64
|
+
"""Logs every fault and records one `engine_fault` event per scope, task and error class.
|
|
65
|
+
|
|
66
|
+
A repeat of the same fault is counted and recorded again only after
|
|
67
|
+
`repeat_notify_seconds`, so a persistent or flapping fault stays visible without flooding
|
|
68
|
+
the log or the Operator, and a new error class is recorded at once. Each record carries the
|
|
69
|
+
full error, its traceback and, for a failed command, the command, exit code and stderr.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self, *, events: EventLog, clock: Clock, settings: FaultsConfig, logger: Logger
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Record in `events`, rate-limited with `clock`."""
|
|
76
|
+
self._events = events
|
|
77
|
+
self._clock = clock
|
|
78
|
+
self._settings = settings
|
|
79
|
+
self._logger = logger
|
|
80
|
+
self._streaks: dict[tuple[str, str | None, str], _Streak] = {}
|
|
81
|
+
self._open: set[tuple[str, str | None]] = set()
|
|
82
|
+
|
|
83
|
+
def report(self, error: Exception, *, scope: str, task_id: str | None = None) -> bool:
|
|
84
|
+
"""Record `error` raised in `scope` unless just recorded, and classify it.
|
|
85
|
+
|
|
86
|
+
Returns whether it is a recoverable fault.
|
|
87
|
+
"""
|
|
88
|
+
recoverable = isinstance(error, RECOVERABLE_FAULTS) and not isinstance(
|
|
89
|
+
error, InvariantError
|
|
90
|
+
)
|
|
91
|
+
kind = type(error).__name__
|
|
92
|
+
now = self._clock.now()
|
|
93
|
+
key = (scope, task_id, kind)
|
|
94
|
+
streak = self._streaks.setdefault(key, _Streak())
|
|
95
|
+
streak.occurrences += 1
|
|
96
|
+
interval = timedelta(seconds=self._settings.repeat_notify_seconds)
|
|
97
|
+
if streak.recorded_at is None or now - streak.recorded_at >= interval:
|
|
98
|
+
try:
|
|
99
|
+
self._events.append(
|
|
100
|
+
EventKind.ENGINE_FAULT,
|
|
101
|
+
task_id=task_id,
|
|
102
|
+
message=f"{scope} failed: {kind}: {error}",
|
|
103
|
+
data={
|
|
104
|
+
SCOPE_KEY: scope,
|
|
105
|
+
ERROR_KEY: kind,
|
|
106
|
+
RECOVERABLE_KEY: recoverable,
|
|
107
|
+
REPEAT_KEY: streak.occurrences,
|
|
108
|
+
**_evidence(error),
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
except OperationalDatabaseError:
|
|
112
|
+
self._logger.exception("could not record the fault of %s", scope)
|
|
113
|
+
return recoverable
|
|
114
|
+
streak.recorded_at = now
|
|
115
|
+
streak.occurrences = 0
|
|
116
|
+
self._open.add((scope, task_id))
|
|
117
|
+
return recoverable
|
|
118
|
+
|
|
119
|
+
def resolve(self, scope: str, *, task_id: str | None = None) -> None:
|
|
120
|
+
"""Record an `engine_recovered` event when `scope` failed, was recorded, and now works."""
|
|
121
|
+
if (scope, task_id) not in self._open:
|
|
122
|
+
return
|
|
123
|
+
try:
|
|
124
|
+
self._events.append(
|
|
125
|
+
EventKind.ENGINE_RECOVERED,
|
|
126
|
+
task_id=task_id,
|
|
127
|
+
message=f"{scope} recovered",
|
|
128
|
+
data={SCOPE_KEY: scope},
|
|
129
|
+
)
|
|
130
|
+
except OperationalDatabaseError:
|
|
131
|
+
self._logger.exception("could not record the recovery of %s", scope)
|
|
132
|
+
return
|
|
133
|
+
self._open.remove((scope, task_id))
|
|
134
|
+
|
|
135
|
+
@asynccontextmanager
|
|
136
|
+
async def isolate(self, scope: str, *, task_id: str | None = None) -> AsyncGenerator[None]:
|
|
137
|
+
"""Absorb any fault of the block: log it with its traceback and record it for `task_id`."""
|
|
138
|
+
try:
|
|
139
|
+
yield
|
|
140
|
+
except Exception as error:
|
|
141
|
+
self._logger.exception("%s failed", scope)
|
|
142
|
+
self.report(error, scope=scope, task_id=task_id)
|
|
143
|
+
else:
|
|
144
|
+
self.resolve(scope, task_id=task_id)
|