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/titles.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""`TaskTitles`: pane and window titles, and window state decorations.
|
|
2
|
+
|
|
3
|
+
Refreshed at every task state boundary (dispatch, step, iteration, status, hold, rung and usage
|
|
4
|
+
change). The window's `@shk_state_color`/`@shk_state_glyph` mirror the task's own state — its
|
|
5
|
+
acting role and step, or a hold/escalation override — distinct from `engine.decorations`, which
|
|
6
|
+
mirrors a *session's* live activity on its own pane.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from shikiru.agents.contracts import COST_BASIS_API_EQUIVALENT, COST_BASIS_BILLED, Rung, Usage
|
|
15
|
+
from shikiru.domain.ladder import pick_rung
|
|
16
|
+
from shikiru.domain.levels import usage_level
|
|
17
|
+
from shikiru.domain.runs import NO_USAGE, RunStatus, context_title, cost_title
|
|
18
|
+
from shikiru.domain.steps import ACTING_ROLE
|
|
19
|
+
from shikiru.domain.tasks import Hold, Role, Status, Step
|
|
20
|
+
from shikiru.paths import run_dir
|
|
21
|
+
from shikiru.ui.glyphs import (
|
|
22
|
+
GLYPH_CONSULT,
|
|
23
|
+
GLYPH_ENGINE,
|
|
24
|
+
GLYPH_ESCALATED,
|
|
25
|
+
GLYPH_OPERATOR,
|
|
26
|
+
GLYPH_PAUSED,
|
|
27
|
+
GLYPH_QUOTA,
|
|
28
|
+
QUOTA_MARKER,
|
|
29
|
+
ROLE_GLYPHS,
|
|
30
|
+
STAGE_IMPLEMENT,
|
|
31
|
+
STAGE_MERGE,
|
|
32
|
+
STAGE_PLAN,
|
|
33
|
+
STAGE_TRIAGE,
|
|
34
|
+
STAGE_VERIFY,
|
|
35
|
+
)
|
|
36
|
+
from shikiru.ui.tokens import (
|
|
37
|
+
ALERT_COLOR,
|
|
38
|
+
AMBER,
|
|
39
|
+
ENGINE_COLOR,
|
|
40
|
+
HOLD_COLOR,
|
|
41
|
+
OPERATOR_COLOR,
|
|
42
|
+
PRIMARY_TEXT_COLOR,
|
|
43
|
+
ROLE_COLORS,
|
|
44
|
+
SAGE,
|
|
45
|
+
SECONDARY_TEXT_COLOR,
|
|
46
|
+
USAGE_LEVEL_COLORS,
|
|
47
|
+
)
|
|
48
|
+
from shikiru.view_log import (
|
|
49
|
+
clear_quota_status,
|
|
50
|
+
quota_status_path,
|
|
51
|
+
write_quota_status,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
from shikiru.config.settings import Settings
|
|
58
|
+
from shikiru.domain.runs import Run
|
|
59
|
+
from shikiru.domain.tasks import Task
|
|
60
|
+
from shikiru.engine.quota_waits import QuotaWaits, TaskQuotaWait
|
|
61
|
+
from shikiru.store.runs import RunStore
|
|
62
|
+
from shikiru.store.tasks import TaskStore
|
|
63
|
+
from shikiru.tmux.decorator import PaneDecorator
|
|
64
|
+
|
|
65
|
+
_STEP_GLYPHS: dict[Step, str] = {
|
|
66
|
+
Step.TRIAGE: STAGE_TRIAGE,
|
|
67
|
+
Step.PLAN: STAGE_PLAN,
|
|
68
|
+
Step.IMPLEMENT: STAGE_IMPLEMENT,
|
|
69
|
+
Step.CONSULT: GLYPH_CONSULT,
|
|
70
|
+
Step.VERIFY: STAGE_VERIFY,
|
|
71
|
+
Step.INTEGRATE: STAGE_MERGE,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_TITLED_ROLES = (Role.LEADER, Role.IMPLEMENTER)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _quota_shown(task: Task) -> bool:
|
|
78
|
+
return (
|
|
79
|
+
Hold.PAUSED not in task.holds
|
|
80
|
+
and Hold.INTERVENTION not in task.holds
|
|
81
|
+
and task.status is not Status.ESCALATED
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _window_state(task: Task, quota: TaskQuotaWait | None) -> tuple[str, str]:
|
|
86
|
+
if Hold.PAUSED in task.holds:
|
|
87
|
+
return HOLD_COLOR, GLYPH_PAUSED
|
|
88
|
+
if Hold.INTERVENTION in task.holds:
|
|
89
|
+
return OPERATOR_COLOR, GLYPH_OPERATOR
|
|
90
|
+
if task.status is Status.ESCALATED:
|
|
91
|
+
return ALERT_COLOR, GLYPH_ESCALATED
|
|
92
|
+
if quota is not None:
|
|
93
|
+
return AMBER, QUOTA_MARKER
|
|
94
|
+
if task.step is None:
|
|
95
|
+
return ENGINE_COLOR, GLYPH_ENGINE
|
|
96
|
+
return ROLE_COLORS[ACTING_ROLE[task.step]], _STEP_GLYPHS[task.step]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _pane_state(
|
|
100
|
+
task: Task, role: Role, *, taken_over: bool, quota_blocked: bool
|
|
101
|
+
) -> tuple[str, str]:
|
|
102
|
+
if taken_over:
|
|
103
|
+
return OPERATOR_COLOR, GLYPH_OPERATOR
|
|
104
|
+
if Hold.PAUSED in task.holds:
|
|
105
|
+
return HOLD_COLOR, GLYPH_PAUSED
|
|
106
|
+
if quota_blocked:
|
|
107
|
+
return AMBER, GLYPH_QUOTA
|
|
108
|
+
return ROLE_COLORS[role], ROLE_GLYPHS[role]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _flag_parts(task: Task, *, taken_over: bool) -> list[tuple[str, str, str]]:
|
|
112
|
+
flags: list[tuple[str, str, str]] = []
|
|
113
|
+
if Hold.PAUSED in task.holds:
|
|
114
|
+
flags.append((GLYPH_PAUSED, "paused", HOLD_COLOR))
|
|
115
|
+
if taken_over or Hold.INTERVENTION in task.holds:
|
|
116
|
+
flags.append((GLYPH_OPERATOR, "operator", OPERATOR_COLOR))
|
|
117
|
+
if task.status is Status.ESCALATED:
|
|
118
|
+
flags.append((GLYPH_ESCALATED, "escalated", ALERT_COLOR))
|
|
119
|
+
return flags
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _flags(task: Task, *, taken_over: bool = False, include_glyphs: bool = True) -> str:
|
|
123
|
+
flags = _flag_parts(task, taken_over=taken_over)
|
|
124
|
+
if include_glyphs:
|
|
125
|
+
return " ".join(f"{glyph} {word}" for glyph, word, _ in flags)
|
|
126
|
+
return " ".join(word for _, word, _ in flags)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _flag_segments(task: Task, *, taken_over: bool) -> tuple[tuple[str, str], ...]:
|
|
130
|
+
parts = _flag_parts(task, taken_over=taken_over)
|
|
131
|
+
segments: list[tuple[str, str]] = []
|
|
132
|
+
for index, (_, word, color) in enumerate(parts):
|
|
133
|
+
if index:
|
|
134
|
+
segments.append((" ", PRIMARY_TEXT_COLOR))
|
|
135
|
+
segments.append((word, color))
|
|
136
|
+
return tuple(segments)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _rung_label(rung: Rung) -> str:
|
|
140
|
+
effort = f"/{rung.effort}" if rung.effort is not None else ""
|
|
141
|
+
return f"{rung.agent}:{rung.model}{effort}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TaskTitles:
|
|
145
|
+
"""Sets `task`'s window/pane titles and window state from `dashboard.titles`.
|
|
146
|
+
|
|
147
|
+
A role's pane taken over by the Operator is titled and coloured as the Operator's;
|
|
148
|
+
otherwise it shows the pause hold or the role's own colour and glyph.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
decorator: PaneDecorator,
|
|
154
|
+
runs: RunStore,
|
|
155
|
+
tasks: TaskStore,
|
|
156
|
+
settings: Settings,
|
|
157
|
+
*,
|
|
158
|
+
waits: QuotaWaits,
|
|
159
|
+
project_root: Path,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Decorate via `decorator` using current task rows, live run usage and quota `waits`.
|
|
162
|
+
|
|
163
|
+
Each role pane's quota status is published under `project_root`'s run directories for
|
|
164
|
+
the pane's view process.
|
|
165
|
+
"""
|
|
166
|
+
self._decorator = decorator
|
|
167
|
+
self._runs = runs
|
|
168
|
+
self._tasks = tasks
|
|
169
|
+
self._settings = settings
|
|
170
|
+
self._waits = waits
|
|
171
|
+
self._project_root = project_root
|
|
172
|
+
self._refresh_lock = asyncio.Lock()
|
|
173
|
+
self._published: dict[str, TaskQuotaWait | None] = {}
|
|
174
|
+
|
|
175
|
+
async def sync_quota(self, task: Task) -> None:
|
|
176
|
+
"""Refresh `task`'s decorations when its quota wait differs from the one last published."""
|
|
177
|
+
current = self._tasks.require(task.id)
|
|
178
|
+
if (
|
|
179
|
+
current.id not in self._published
|
|
180
|
+
or self._waits.resolve(current) != self._published[current.id]
|
|
181
|
+
):
|
|
182
|
+
await self.refresh(current)
|
|
183
|
+
|
|
184
|
+
async def refresh(self, task: Task) -> None:
|
|
185
|
+
"""Set titles from the latest task row, serializing title publications."""
|
|
186
|
+
async with self._refresh_lock:
|
|
187
|
+
current = self._tasks.require(task.id)
|
|
188
|
+
quota = self._waits.resolve(current)
|
|
189
|
+
self._published[current.id] = quota
|
|
190
|
+
if current.window_id is not None:
|
|
191
|
+
await self._refresh_window(current, current.window_id, quota)
|
|
192
|
+
for role in _TITLED_ROLES:
|
|
193
|
+
pane_id = current.panes.get(role)
|
|
194
|
+
if pane_id is not None:
|
|
195
|
+
await self._refresh_pane(current, role, pane_id, quota)
|
|
196
|
+
|
|
197
|
+
async def _refresh_window(
|
|
198
|
+
self, task: Task, window_target: str, quota: TaskQuotaWait | None
|
|
199
|
+
) -> None:
|
|
200
|
+
color, glyph = _window_state(task, quota)
|
|
201
|
+
fields = {"task_id": task.id, "title": task.title}
|
|
202
|
+
await self._decorator.refresh_window(
|
|
203
|
+
window_target,
|
|
204
|
+
self._settings.dashboard.titles.window,
|
|
205
|
+
fields,
|
|
206
|
+
color=color,
|
|
207
|
+
glyph=glyph,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def _refresh_pane(
|
|
211
|
+
self, task: Task, role: Role, pane_id: str, quota: TaskQuotaWait | None
|
|
212
|
+
) -> None:
|
|
213
|
+
configured_rung = pick_rung(task, role, self._settings)
|
|
214
|
+
run = self._active_run(task.id, role)
|
|
215
|
+
taken_over = run is not None and run.status is RunStatus.TAKEN_OVER
|
|
216
|
+
effective_rung = (
|
|
217
|
+
Rung(agent=run.agent, model=run.model, effort=run.effort)
|
|
218
|
+
if run is not None
|
|
219
|
+
else configured_rung
|
|
220
|
+
)
|
|
221
|
+
pending_rung = (
|
|
222
|
+
f" · next {_rung_label(configured_rung)}" if effective_rung != configured_rung else ""
|
|
223
|
+
)
|
|
224
|
+
usage = run.usage if run is not None else NO_USAGE
|
|
225
|
+
usage_reported = run.usage_reported if run is not None else None
|
|
226
|
+
template = getattr(self._settings.dashboard.titles, role.value)
|
|
227
|
+
fields = {
|
|
228
|
+
"task_id": task.id,
|
|
229
|
+
"title": task.title,
|
|
230
|
+
"project": self._settings.project.name,
|
|
231
|
+
"role": role.value,
|
|
232
|
+
"role_label": role.value.capitalize(),
|
|
233
|
+
"glyph": GLYPH_OPERATOR if taken_over else ROLE_GLYPHS[role],
|
|
234
|
+
"agent": effective_rung.agent,
|
|
235
|
+
"model": effective_rung.model,
|
|
236
|
+
"effort": effective_rung.effort if effective_rung.effort is not None else "",
|
|
237
|
+
"pending_rung": pending_rung,
|
|
238
|
+
"iteration": task.iteration,
|
|
239
|
+
"step": task.step.value if task.step is not None else "",
|
|
240
|
+
"status": task.status.value,
|
|
241
|
+
"context": context_title(usage),
|
|
242
|
+
"cost": cost_title(usage),
|
|
243
|
+
"flags": _flags(task),
|
|
244
|
+
"flags_text": _flags(task, taken_over=taken_over, include_glyphs=False),
|
|
245
|
+
}
|
|
246
|
+
cost_color = SECONDARY_TEXT_COLOR
|
|
247
|
+
if usage.cost_usd is not None and usage_reported is not False:
|
|
248
|
+
if usage.cost_basis == COST_BASIS_BILLED:
|
|
249
|
+
cost_color = SAGE
|
|
250
|
+
elif usage.cost_basis == COST_BASIS_API_EQUIVALENT:
|
|
251
|
+
cost_color = AMBER
|
|
252
|
+
await self._decorator.set_pane_title_styling(
|
|
253
|
+
pane_id,
|
|
254
|
+
template,
|
|
255
|
+
fields,
|
|
256
|
+
field_colors={
|
|
257
|
+
"role": ROLE_COLORS[role],
|
|
258
|
+
"role_label": ROLE_COLORS[role],
|
|
259
|
+
"agent": PRIMARY_TEXT_COLOR,
|
|
260
|
+
"model": PRIMARY_TEXT_COLOR,
|
|
261
|
+
"effort": SECONDARY_TEXT_COLOR,
|
|
262
|
+
"pending_rung": SECONDARY_TEXT_COLOR,
|
|
263
|
+
"iteration": SECONDARY_TEXT_COLOR,
|
|
264
|
+
"context": self._context_color(usage, usage_reported),
|
|
265
|
+
"cost": cost_color,
|
|
266
|
+
},
|
|
267
|
+
field_segments={"flags_text": _flag_segments(task, taken_over=taken_over)},
|
|
268
|
+
)
|
|
269
|
+
role_wait = (
|
|
270
|
+
quota.roles.get(role)
|
|
271
|
+
if quota is not None and not taken_over and _quota_shown(task)
|
|
272
|
+
else None
|
|
273
|
+
)
|
|
274
|
+
color, glyph = _pane_state(
|
|
275
|
+
task, role, taken_over=taken_over, quota_blocked=role_wait is not None
|
|
276
|
+
)
|
|
277
|
+
await self._decorator.set_pane_state(pane_id, color=color, glyph=glyph)
|
|
278
|
+
status_path = quota_status_path(run_dir(self._project_root, task.id), role.value)
|
|
279
|
+
if role_wait is None:
|
|
280
|
+
clear_quota_status(status_path)
|
|
281
|
+
else:
|
|
282
|
+
write_quota_status(status_path, role_wait.action, role_wait.at)
|
|
283
|
+
|
|
284
|
+
def _context_color(self, usage: Usage, usage_reported: bool | None) -> str:
|
|
285
|
+
if usage.context_used_pct is None or usage_reported is False:
|
|
286
|
+
return SECONDARY_TEXT_COLOR
|
|
287
|
+
titles = self._settings.dashboard.titles
|
|
288
|
+
level = usage_level(
|
|
289
|
+
usage.context_used_pct,
|
|
290
|
+
warn_pct=titles.context_warn_pct,
|
|
291
|
+
alert_pct=titles.context_alert_pct,
|
|
292
|
+
)
|
|
293
|
+
return USAGE_LEVEL_COLORS[level]
|
|
294
|
+
|
|
295
|
+
def _active_run(self, task_id: str, role: Role) -> Run | None:
|
|
296
|
+
live = self._runs.get_live(task_id, role)
|
|
297
|
+
return live if live is not None else self._runs.get_taken_over(task_id, role)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Persists domain transitions of a task and projects them into events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shikiru.domain.events import EventKind
|
|
8
|
+
from shikiru.domain.reports import PlanReport, Verdict
|
|
9
|
+
from shikiru.domain.tasks import Status, TaskOrigin
|
|
10
|
+
from shikiru.engine.events import emit_step_started
|
|
11
|
+
from shikiru.errors import InvariantError
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from shikiru.config.settings import Settings
|
|
15
|
+
from shikiru.domain.steps import Outcome, Transition
|
|
16
|
+
from shikiru.domain.tasks import Task
|
|
17
|
+
from shikiru.engine.titles import TaskTitles
|
|
18
|
+
from shikiru.store.events import EventLog
|
|
19
|
+
from shikiru.store.tasks import TaskStore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TransitionWriter:
|
|
23
|
+
"""The single place a `Transition` becomes stored task state, events and a refreshed title."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self, *, tasks: TaskStore, events: EventLog, titles: TaskTitles, settings: Settings
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Write transitions to `tasks`, record their events and refresh titles."""
|
|
29
|
+
self._tasks = tasks
|
|
30
|
+
self._events = events
|
|
31
|
+
self._titles = titles
|
|
32
|
+
self._settings = settings
|
|
33
|
+
|
|
34
|
+
async def apply(self, task: Task, transition: Transition, outcome: Outcome | None) -> Task:
|
|
35
|
+
"""Apply a task transition and refresh its visible title."""
|
|
36
|
+
updated = self.persist(task, transition, outcome)
|
|
37
|
+
await self._titles.refresh(updated)
|
|
38
|
+
return updated
|
|
39
|
+
|
|
40
|
+
def persist(self, task: Task, transition: Transition, outcome: Outcome | None) -> Task:
|
|
41
|
+
"""Write a task transition, emitting its step event, and return the stored task."""
|
|
42
|
+
if transition.enqueue_new_tasks:
|
|
43
|
+
if outcome is None:
|
|
44
|
+
message = f"{task.id}'s transition enqueues new tasks but has no outcome"
|
|
45
|
+
raise InvariantError(message)
|
|
46
|
+
self._apply_new_tasks(task, transition, outcome)
|
|
47
|
+
else:
|
|
48
|
+
self._tasks.apply_transition(task.id, transition)
|
|
49
|
+
if transition.status in (Status.DONE, Status.CANCELLED):
|
|
50
|
+
self._tasks.set_slot(task.id, None)
|
|
51
|
+
if transition.status is Status.ESCALATED:
|
|
52
|
+
self._tasks.set_wait_reason(task.id, None)
|
|
53
|
+
updated = self._tasks.require(task.id)
|
|
54
|
+
if (
|
|
55
|
+
task.step != updated.step
|
|
56
|
+
and updated.step is not None
|
|
57
|
+
and updated.status in (Status.RUNNING, Status.INTEGRATING)
|
|
58
|
+
):
|
|
59
|
+
emit_step_started(self._events, updated, self._settings)
|
|
60
|
+
return updated
|
|
61
|
+
|
|
62
|
+
def _apply_new_tasks(self, task: Task, transition: Transition, outcome: Outcome) -> None:
|
|
63
|
+
if not isinstance(outcome, PlanReport | Verdict):
|
|
64
|
+
message = f"{task.id}'s transition enqueues new tasks from a {type(outcome).__name__}"
|
|
65
|
+
raise InvariantError(message)
|
|
66
|
+
children = self._tasks.apply_scope_change(
|
|
67
|
+
task.id, transition, outcome.new_tasks, origin=TaskOrigin.LEADER
|
|
68
|
+
)
|
|
69
|
+
if not outcome.new_tasks and not outcome.scope_changes:
|
|
70
|
+
return
|
|
71
|
+
self._events.append(
|
|
72
|
+
EventKind.SCOPE_CHANGE,
|
|
73
|
+
task_id=task.id,
|
|
74
|
+
message=f"{task.id} scope changed: {len(children)} new task(s)",
|
|
75
|
+
data={
|
|
76
|
+
"new_task_ids": [child.id for child in children],
|
|
77
|
+
"scope_changes": list(outcome.scope_changes),
|
|
78
|
+
},
|
|
79
|
+
)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Prompt-context preparation for a task's next turn."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from shikiru.domain.reports import FeedbackAction, ReportKind
|
|
10
|
+
from shikiru.domain.tasks import Step
|
|
11
|
+
from shikiru.engine.task_facts import require_repo
|
|
12
|
+
from shikiru.engine.turns import TurnRequest
|
|
13
|
+
from shikiru.errors import InvariantError
|
|
14
|
+
from shikiru.paths import task_root
|
|
15
|
+
from shikiru.prompts.renderer import PromptName
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from shikiru.config.settings import Settings
|
|
21
|
+
from shikiru.domain.feedback import FeedbackInput
|
|
22
|
+
from shikiru.domain.tasks import Task
|
|
23
|
+
from shikiru.engine.history import ReportHistory
|
|
24
|
+
from shikiru.engine.task_facts import TaskFacts
|
|
25
|
+
from shikiru.store.escalations import EscalationStore
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class NextTurn:
|
|
30
|
+
"""How recovery wants the next turn started: as a handoff or as a continuation."""
|
|
31
|
+
|
|
32
|
+
handoff: bool = False
|
|
33
|
+
continue_turn: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ORDINARY_TURN = NextTurn()
|
|
37
|
+
|
|
38
|
+
_JSON_INDENT = 2
|
|
39
|
+
|
|
40
|
+
FEEDBACK_TEMPLATES = frozenset(
|
|
41
|
+
{PromptName.FEEDBACK_ANALYSIS, PromptName.FEEDBACK_REWORK, PromptName.FEEDBACK_VERIFY}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ContextKey(StrEnum):
|
|
46
|
+
"""The variables the prompt templates read from a turn's context."""
|
|
47
|
+
|
|
48
|
+
REQUEST = "request"
|
|
49
|
+
TASK_ID = "task_id"
|
|
50
|
+
WORKSPACE = "workspace"
|
|
51
|
+
TRACKERS = "trackers"
|
|
52
|
+
REPORTS = "reports"
|
|
53
|
+
QUESTION = "question"
|
|
54
|
+
OPERATOR_DECISION = "operator_decision"
|
|
55
|
+
FACTS = "facts"
|
|
56
|
+
BASE = "base"
|
|
57
|
+
FINAL_ROUND = "final_round"
|
|
58
|
+
CONFLICT = "conflict"
|
|
59
|
+
MODE = "mode"
|
|
60
|
+
FEEDBACK_SNAPSHOT = "feedback_snapshot"
|
|
61
|
+
FEEDBACK_ANALYSIS = "feedback_analysis"
|
|
62
|
+
FEEDBACK_WORK = "feedback_work"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def template_for_step(step: Step, iteration: int) -> tuple[PromptName, ReportKind]:
|
|
66
|
+
"""Return the prompt template and report kind of `step`'s ordinary turn."""
|
|
67
|
+
if step is Step.TRIAGE:
|
|
68
|
+
return PromptName.TRIAGE, ReportKind.TRIAGE
|
|
69
|
+
if step is Step.PLAN:
|
|
70
|
+
return PromptName.PLAN, ReportKind.PLAN
|
|
71
|
+
if step is Step.IMPLEMENT:
|
|
72
|
+
template = PromptName.IMPLEMENT if iteration <= 1 else PromptName.REWORK
|
|
73
|
+
return template, ReportKind.IMPLEMENTATION
|
|
74
|
+
if step is Step.CONSULT:
|
|
75
|
+
return PromptName.CONSULT, ReportKind.CONSULT
|
|
76
|
+
if step is Step.VERIFY:
|
|
77
|
+
return PromptName.VERIFY, ReportKind.VERDICT
|
|
78
|
+
if step is Step.INTEGRATE:
|
|
79
|
+
return PromptName.CONFLICTS, ReportKind.IMPLEMENTATION
|
|
80
|
+
message = f"No turn template exists for step {step!r}"
|
|
81
|
+
raise InvariantError(message)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TurnContextPreparer:
|
|
85
|
+
"""Builds each turn's `TurnRequest`."""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
history: ReportHistory,
|
|
91
|
+
escalation_store: EscalationStore,
|
|
92
|
+
facts: TaskFacts,
|
|
93
|
+
settings: Settings,
|
|
94
|
+
project_root: Path,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Prepare contexts from report history, operator answers and workspace facts."""
|
|
97
|
+
self._history = history
|
|
98
|
+
self._escalation_store = escalation_store
|
|
99
|
+
self._facts = facts
|
|
100
|
+
self._settings = settings
|
|
101
|
+
self._project_root = project_root
|
|
102
|
+
|
|
103
|
+
async def prepare(
|
|
104
|
+
self,
|
|
105
|
+
task: Task,
|
|
106
|
+
step: Step,
|
|
107
|
+
*,
|
|
108
|
+
session_handoff: bool,
|
|
109
|
+
next_turn: NextTurn = ORDINARY_TURN,
|
|
110
|
+
template_override: PromptName | None = None,
|
|
111
|
+
kind_override: ReportKind | None = None,
|
|
112
|
+
conflict_context: str | None = None,
|
|
113
|
+
feedback: FeedbackInput | None = None,
|
|
114
|
+
) -> TurnRequest:
|
|
115
|
+
"""Build the request of `task`'s next `step` turn."""
|
|
116
|
+
history = self._history.load(task.id)
|
|
117
|
+
continue_turn = next_turn.continue_turn
|
|
118
|
+
if continue_turn:
|
|
119
|
+
template_override = PromptName.CONTINUE
|
|
120
|
+
template, kind = template_for_step(step, task.iteration)
|
|
121
|
+
if template_override is not None:
|
|
122
|
+
template = template_override
|
|
123
|
+
if kind_override is not None:
|
|
124
|
+
kind = kind_override
|
|
125
|
+
context: dict[str, object] = {
|
|
126
|
+
ContextKey.REQUEST: task.request,
|
|
127
|
+
ContextKey.TASK_ID: task.id,
|
|
128
|
+
ContextKey.WORKSPACE: str(task_root(self._project_root, task.id)),
|
|
129
|
+
ContextKey.TRACKERS: list(self._settings.project.trackers),
|
|
130
|
+
ContextKey.REPORTS: self._history.context(history),
|
|
131
|
+
}
|
|
132
|
+
if template is PromptName.CONSULT:
|
|
133
|
+
context[ContextKey.QUESTION] = self._history.latest_question(history)
|
|
134
|
+
context[ContextKey.OPERATOR_DECISION] = (
|
|
135
|
+
self._escalation_store.latest_answer(task.id)
|
|
136
|
+
if task.counters.consults == 0
|
|
137
|
+
else None
|
|
138
|
+
)
|
|
139
|
+
fresh = session_handoff and not continue_turn
|
|
140
|
+
handoff = (fresh and bool(history)) or next_turn.handoff
|
|
141
|
+
needs_facts = (
|
|
142
|
+
template in (PromptName.VERIFY, PromptName.CONFLICTS, *FEEDBACK_TEMPLATES) or handoff
|
|
143
|
+
)
|
|
144
|
+
facts_text = await self._facts.text(task) if needs_facts else ""
|
|
145
|
+
if handoff and template not in FEEDBACK_TEMPLATES:
|
|
146
|
+
template = PromptName.HANDOFF
|
|
147
|
+
if needs_facts:
|
|
148
|
+
context[ContextKey.FACTS] = facts_text
|
|
149
|
+
if template in FEEDBACK_TEMPLATES:
|
|
150
|
+
context.update(self._feedback_context(task, template, feedback))
|
|
151
|
+
if template is PromptName.VERIFY:
|
|
152
|
+
context[ContextKey.BASE] = require_repo(task).base_ref
|
|
153
|
+
context[ContextKey.FINAL_ROUND] = (
|
|
154
|
+
task.iteration >= self._settings.orchestration.max_iterations
|
|
155
|
+
)
|
|
156
|
+
if template is PromptName.CONFLICTS:
|
|
157
|
+
context[ContextKey.CONFLICT] = conflict_context or ""
|
|
158
|
+
return TurnRequest(
|
|
159
|
+
step=step,
|
|
160
|
+
iteration=task.iteration,
|
|
161
|
+
kind=kind,
|
|
162
|
+
template=template,
|
|
163
|
+
context=context,
|
|
164
|
+
facts=facts_text,
|
|
165
|
+
feedback=feedback,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _feedback_context(
|
|
170
|
+
task: Task, template: PromptName, feedback: FeedbackInput | None
|
|
171
|
+
) -> dict[str, object]:
|
|
172
|
+
if feedback is None:
|
|
173
|
+
message = f"{task.id} {template.value} turn has no feedback to work from"
|
|
174
|
+
raise InvariantError(message)
|
|
175
|
+
context: dict[str, object] = {
|
|
176
|
+
ContextKey.MODE: task.mode.value if task.mode is not None else ""
|
|
177
|
+
}
|
|
178
|
+
if template is PromptName.FEEDBACK_ANALYSIS:
|
|
179
|
+
context[ContextKey.FEEDBACK_SNAPSHOT] = feedback.snapshot.model_dump_json(
|
|
180
|
+
indent=_JSON_INDENT
|
|
181
|
+
)
|
|
182
|
+
return context
|
|
183
|
+
analysis = feedback.analysis
|
|
184
|
+
if analysis is None:
|
|
185
|
+
message = f"{task.id} {template.value} turn has no analysis to work from"
|
|
186
|
+
raise InvariantError(message)
|
|
187
|
+
if template is PromptName.FEEDBACK_REWORK:
|
|
188
|
+
addressed = {
|
|
189
|
+
decision.source_id
|
|
190
|
+
for decision in analysis.decisions
|
|
191
|
+
if decision.action is FeedbackAction.ADDRESS
|
|
192
|
+
}
|
|
193
|
+
context[ContextKey.FEEDBACK_SNAPSHOT] = feedback.snapshot.only(
|
|
194
|
+
addressed
|
|
195
|
+
).model_dump_json(indent=_JSON_INDENT)
|
|
196
|
+
context[ContextKey.FEEDBACK_WORK] = [
|
|
197
|
+
work.model_dump_json(indent=_JSON_INDENT) for work in analysis.work
|
|
198
|
+
]
|
|
199
|
+
return context
|
|
200
|
+
context[ContextKey.BASE] = require_repo(task).base_ref
|
|
201
|
+
context[ContextKey.FINAL_ROUND] = feedback.final_round
|
|
202
|
+
context[ContextKey.FEEDBACK_SNAPSHOT] = feedback.snapshot.model_dump_json(
|
|
203
|
+
indent=_JSON_INDENT
|
|
204
|
+
)
|
|
205
|
+
context[ContextKey.FEEDBACK_ANALYSIS] = analysis.model_dump_json(indent=_JSON_INDENT)
|
|
206
|
+
return context
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""The outcome of one executed agent turn, as the flow and its collaborators pass it around."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from shikiru.agents.contracts import Rung, Usage
|
|
12
|
+
from shikiru.domain.recovery import TurnResult
|
|
13
|
+
from shikiru.domain.reports import ReportKind
|
|
14
|
+
from shikiru.domain.tasks import Role
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TurnExecution:
|
|
19
|
+
"""A turn's result with the role, rung, directory and usage it ran with."""
|
|
20
|
+
|
|
21
|
+
result: TurnResult
|
|
22
|
+
task_id: str
|
|
23
|
+
role: Role
|
|
24
|
+
turn_dir: Path
|
|
25
|
+
rung: Rung
|
|
26
|
+
run_id: int
|
|
27
|
+
usage_before: Usage
|
|
28
|
+
usage_after: Usage
|
|
29
|
+
kind: ReportKind
|
|
30
|
+
quota_wait_exhausted: bool = False
|