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/helper/helper.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Calls configured Helper headless agents in order and validates every answer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
11
|
+
|
|
12
|
+
from shikiru.agents.contracts import FailureKind, Rung
|
|
13
|
+
from shikiru.domain.fencing import fence
|
|
14
|
+
from shikiru.domain.reports import ReportKind, model_for, schema_for
|
|
15
|
+
from shikiru.errors import (
|
|
16
|
+
CommandFailed,
|
|
17
|
+
CommandTimeoutError,
|
|
18
|
+
ConfigError,
|
|
19
|
+
HeadlessOutputError,
|
|
20
|
+
)
|
|
21
|
+
from shikiru.helper.models import Unrecoverable
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Mapping, Sequence
|
|
25
|
+
from datetime import timedelta
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from shikiru.agents.contracts import Agent, HeadlessAgent
|
|
29
|
+
from shikiru.clock import Clock
|
|
30
|
+
from shikiru.domain.reports import Report
|
|
31
|
+
|
|
32
|
+
_REBUILD_PROMPT = (
|
|
33
|
+
"The agent's turn ended without a valid report. Reconstruct the report for this {kind} "
|
|
34
|
+
"turn from its last message and the facts below. If you cannot determine the report with "
|
|
35
|
+
'confidence, respond instead with \'{{"unrecoverable": "<reason>"}}.\n\n'
|
|
36
|
+
"## Last message\n\n{last_message}\n\n## Facts\n\n{facts}\n"
|
|
37
|
+
)
|
|
38
|
+
_CLASSIFY_PROMPT = (
|
|
39
|
+
"Classify this unrecognised agent failure into QUOTA, UNAVAILABLE, AUTH or OTHER. "
|
|
40
|
+
"Give confidence from 0 to 1 and a reset time only when known. If the text is ambiguous, "
|
|
41
|
+
"return UNKNOWN with low confidence.\n\n## Failure\n\n{failure}\n"
|
|
42
|
+
)
|
|
43
|
+
_EXPECTED_CALL_ERRORS = (
|
|
44
|
+
CommandFailed,
|
|
45
|
+
CommandTimeoutError,
|
|
46
|
+
HeadlessOutputError,
|
|
47
|
+
OSError,
|
|
48
|
+
TimeoutError,
|
|
49
|
+
)
|
|
50
|
+
_LOGGER = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class HelperEntry:
|
|
55
|
+
"""One configured Helper rung and the agent selected for it."""
|
|
56
|
+
|
|
57
|
+
rung: Rung
|
|
58
|
+
agent: Agent
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class _HeadlessEntry:
|
|
63
|
+
rung: Rung
|
|
64
|
+
headless: HeadlessAgent
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FailureClassification(BaseModel):
|
|
68
|
+
"""The Helper's structured classification of an unrecognised agent failure."""
|
|
69
|
+
|
|
70
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
71
|
+
|
|
72
|
+
kind: FailureKind
|
|
73
|
+
confidence: float = Field(ge=0, le=1)
|
|
74
|
+
resets_at: datetime | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Helper:
|
|
78
|
+
"""Call configured headless helpers in order and validate their reports and classifications."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, entries: Sequence[HelperEntry], clock: Clock) -> None:
|
|
81
|
+
if not entries:
|
|
82
|
+
message = "roles.helper.ladder must contain at least one rung"
|
|
83
|
+
raise ConfigError(message)
|
|
84
|
+
self._entries = tuple(self._headless_entry(entry) for entry in entries)
|
|
85
|
+
self._clock = clock
|
|
86
|
+
|
|
87
|
+
async def rebuild_report(
|
|
88
|
+
self,
|
|
89
|
+
kind: ReportKind,
|
|
90
|
+
last_message: str,
|
|
91
|
+
facts: str,
|
|
92
|
+
*,
|
|
93
|
+
cwd: Path,
|
|
94
|
+
timeout: timedelta,
|
|
95
|
+
) -> Report | Unrecoverable:
|
|
96
|
+
"""Reconstruct a missing report from the turn's final message and facts."""
|
|
97
|
+
schema = {"oneOf": [schema_for(kind), Unrecoverable.model_json_schema()]}
|
|
98
|
+
prompt = _REBUILD_PROMPT.format(
|
|
99
|
+
kind=kind.value,
|
|
100
|
+
last_message=fence(last_message, "text"),
|
|
101
|
+
facts=fence(facts, "text"),
|
|
102
|
+
)
|
|
103
|
+
return await self._ask(prompt, schema, model_for(kind), cwd, timeout)
|
|
104
|
+
|
|
105
|
+
async def classify_failure(
|
|
106
|
+
self,
|
|
107
|
+
failure: str,
|
|
108
|
+
*,
|
|
109
|
+
cwd: Path,
|
|
110
|
+
timeout: timedelta,
|
|
111
|
+
) -> FailureClassification | Unrecoverable:
|
|
112
|
+
"""Classify only the plugin's unrecognised failure text."""
|
|
113
|
+
schema = {
|
|
114
|
+
"oneOf": [
|
|
115
|
+
FailureClassification.model_json_schema(),
|
|
116
|
+
Unrecoverable.model_json_schema(),
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
prompt = _CLASSIFY_PROMPT.format(failure=fence(failure, "text"))
|
|
120
|
+
return await self._ask(prompt, schema, FailureClassification, cwd, timeout)
|
|
121
|
+
|
|
122
|
+
def _headless_entry(self, entry: HelperEntry) -> _HeadlessEntry:
|
|
123
|
+
headless = entry.agent.headless()
|
|
124
|
+
if headless is None:
|
|
125
|
+
message = f"agent {entry.rung.agent!r} has no headless support required by roles.helper"
|
|
126
|
+
raise ConfigError(message)
|
|
127
|
+
return _HeadlessEntry(entry.rung, headless)
|
|
128
|
+
|
|
129
|
+
async def _ask[ResultT: BaseModel](
|
|
130
|
+
self,
|
|
131
|
+
prompt: str,
|
|
132
|
+
schema: Mapping[str, object],
|
|
133
|
+
model: type[ResultT],
|
|
134
|
+
cwd: Path,
|
|
135
|
+
timeout: timedelta,
|
|
136
|
+
) -> ResultT | Unrecoverable:
|
|
137
|
+
started_at = self._clock.now()
|
|
138
|
+
failures: list[str] = []
|
|
139
|
+
for entry in self._entries:
|
|
140
|
+
label = f"{entry.rung.agent}:{entry.rung.model}/{entry.rung.effort}"
|
|
141
|
+
remaining = timeout - (self._clock.now() - started_at)
|
|
142
|
+
if remaining.total_seconds() <= 0:
|
|
143
|
+
failures.append(f"{label}: Helper timeout exhausted before call")
|
|
144
|
+
break
|
|
145
|
+
try:
|
|
146
|
+
raw = await entry.headless.ask(
|
|
147
|
+
prompt,
|
|
148
|
+
schema,
|
|
149
|
+
entry.rung.model,
|
|
150
|
+
entry.rung.effort,
|
|
151
|
+
cwd,
|
|
152
|
+
remaining,
|
|
153
|
+
)
|
|
154
|
+
except _EXPECTED_CALL_ERRORS as error:
|
|
155
|
+
failure = f"{label}: {type(error).__name__}: {error}"
|
|
156
|
+
failures.append(failure)
|
|
157
|
+
_LOGGER.warning("Helper rung failed: %s", failure, exc_info=True)
|
|
158
|
+
continue
|
|
159
|
+
result = self._validate(raw, model)
|
|
160
|
+
if not isinstance(result, Unrecoverable):
|
|
161
|
+
return result
|
|
162
|
+
failure = f"{label}: {result.unrecoverable}"
|
|
163
|
+
failures.append(failure)
|
|
164
|
+
_LOGGER.warning("Helper rung returned unusable output: %s", failure)
|
|
165
|
+
return Unrecoverable(unrecoverable=f"Helper ladder exhausted: {'; '.join(failures)}")
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _validate[ResultT: BaseModel](
|
|
169
|
+
raw: Mapping[str, object], model: type[ResultT]
|
|
170
|
+
) -> ResultT | Unrecoverable:
|
|
171
|
+
try:
|
|
172
|
+
return model.model_validate(raw)
|
|
173
|
+
except ValidationError as model_error:
|
|
174
|
+
try:
|
|
175
|
+
return Unrecoverable.model_validate(raw)
|
|
176
|
+
except ValidationError as unrecoverable_error:
|
|
177
|
+
reason = (
|
|
178
|
+
f"invalid helper output for {model.__name__}: {raw!r}; "
|
|
179
|
+
f"{model.__name__} errors: {model_error}; "
|
|
180
|
+
f"unrecoverable errors: {unrecoverable_error}"
|
|
181
|
+
)
|
|
182
|
+
return Unrecoverable(unrecoverable=reason)
|
shikiru/helper/models.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Helper output model for the explicit inability to answer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from shikiru.domain.tasks import DomainModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Unrecoverable(DomainModel):
|
|
9
|
+
"""The Helper's explicit inability to answer, for either call."""
|
|
10
|
+
|
|
11
|
+
unrecoverable: str
|
shikiru/mcp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastMCP operator server."""
|
shikiru/mcp/server.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""The stdio FastMCP adapter over the operator services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
from pydantic import BaseModel, ConfigDict
|
|
9
|
+
|
|
10
|
+
from shikiru.agents.contracts import parse_rung
|
|
11
|
+
from shikiru.config.settings import ComplexityName
|
|
12
|
+
from shikiru.domain.commands import (
|
|
13
|
+
AcceptResolution,
|
|
14
|
+
AnswerResolution,
|
|
15
|
+
CancelResolution,
|
|
16
|
+
Resolution,
|
|
17
|
+
ResolutionKind,
|
|
18
|
+
RetryResolution,
|
|
19
|
+
)
|
|
20
|
+
from shikiru.domain.events import Event, EventKind
|
|
21
|
+
from shikiru.domain.tasks import Mode, Role, Status
|
|
22
|
+
from shikiru.operator.commands import Ack
|
|
23
|
+
from shikiru.operator.events import MAX_WAIT_SECONDS
|
|
24
|
+
from shikiru.operator.queries import (
|
|
25
|
+
EscalationView,
|
|
26
|
+
PeekView,
|
|
27
|
+
StatusView,
|
|
28
|
+
TaskDetail,
|
|
29
|
+
TaskSummary,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from shikiru.operator.services import OperatorServices
|
|
34
|
+
|
|
35
|
+
SERVER_NAME = "shikiru"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ResolutionInput(BaseModel):
|
|
39
|
+
"""Typed MCP input for an escalation resolution."""
|
|
40
|
+
|
|
41
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
42
|
+
|
|
43
|
+
kind: ResolutionKind
|
|
44
|
+
text: str | None = None
|
|
45
|
+
rung: str | None = None
|
|
46
|
+
|
|
47
|
+
def to_domain(self) -> Resolution:
|
|
48
|
+
"""Convert this transport model to a typed domain resolution."""
|
|
49
|
+
if self.kind is ResolutionKind.ANSWER:
|
|
50
|
+
if self.text is None:
|
|
51
|
+
message = "answer resolution requires text"
|
|
52
|
+
raise ValueError(message)
|
|
53
|
+
return AnswerResolution(text=self.text)
|
|
54
|
+
if self.kind is ResolutionKind.RETRY:
|
|
55
|
+
return RetryResolution(rung=parse_rung(self.rung) if self.rung is not None else None)
|
|
56
|
+
if self.kind is ResolutionKind.ACCEPT:
|
|
57
|
+
return AcceptResolution()
|
|
58
|
+
return CancelResolution()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_server(services: OperatorServices) -> FastMCP:
|
|
62
|
+
"""Build a typed FastMCP server over injected operator services."""
|
|
63
|
+
server = FastMCP(SERVER_NAME)
|
|
64
|
+
|
|
65
|
+
_register_query_tools(server, services)
|
|
66
|
+
_register_command_tools(server, services)
|
|
67
|
+
_register_session_tools(server, services)
|
|
68
|
+
_register_event_tools(server, services)
|
|
69
|
+
return server
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _register_query_tools(server: FastMCP, services: OperatorServices) -> None:
|
|
73
|
+
@server.tool()
|
|
74
|
+
def status(task_id: str | None = None) -> StatusView:
|
|
75
|
+
"""Read engine, queue, usage and escalation status, optionally with one task's details."""
|
|
76
|
+
return services.queries.status(task_id)
|
|
77
|
+
|
|
78
|
+
@server.tool()
|
|
79
|
+
def list_tasks(statuses: list[Status] | None = None) -> list[TaskSummary]:
|
|
80
|
+
"""List task summaries, optionally filtered by task status."""
|
|
81
|
+
return services.queries.list_tasks(statuses=statuses)
|
|
82
|
+
|
|
83
|
+
@server.tool()
|
|
84
|
+
def get_task(task_id: str) -> TaskDetail | None:
|
|
85
|
+
"""Read one task's details, history, reports and sessions, or return null if absent."""
|
|
86
|
+
return services.queries.get_task(task_id)
|
|
87
|
+
|
|
88
|
+
@server.tool()
|
|
89
|
+
def list_escalations() -> list[EscalationView]:
|
|
90
|
+
"""List all currently open escalations with their packets and structured decisions."""
|
|
91
|
+
return services.queries.list_escalations()
|
|
92
|
+
|
|
93
|
+
@server.tool()
|
|
94
|
+
def get_escalation(escalation_id: str) -> EscalationView | None:
|
|
95
|
+
"""Read one open escalation with its packet and structured decisions, or null if absent."""
|
|
96
|
+
return services.queries.get_escalation(escalation_id)
|
|
97
|
+
|
|
98
|
+
@server.tool()
|
|
99
|
+
def peek_session(task_id: str, role: Role | None = None) -> PeekView | None:
|
|
100
|
+
"""Read one task role's, or every role's, last message, usage and recent view lines."""
|
|
101
|
+
return services.queries.peek_session(task_id, role)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _register_command_tools(server: FastMCP, services: OperatorServices) -> None:
|
|
105
|
+
@server.tool()
|
|
106
|
+
async def submit_task(
|
|
107
|
+
*,
|
|
108
|
+
request: str,
|
|
109
|
+
title: str | None = None,
|
|
110
|
+
mode: Mode | None = None,
|
|
111
|
+
complexity: ComplexityName | None = None,
|
|
112
|
+
priority: int = 0,
|
|
113
|
+
depends_on: list[str] | None = None,
|
|
114
|
+
leader: str | None = None,
|
|
115
|
+
implementer: str | None = None,
|
|
116
|
+
) -> Ack:
|
|
117
|
+
"""Queue a task with optional title, mode, complexity, priority, dependencies and rungs."""
|
|
118
|
+
return await services.commands.submit_task(
|
|
119
|
+
request,
|
|
120
|
+
title=title,
|
|
121
|
+
mode=mode,
|
|
122
|
+
complexity=complexity,
|
|
123
|
+
priority=priority,
|
|
124
|
+
depends_on=depends_on or (),
|
|
125
|
+
leader=parse_rung(leader) if leader is not None else None,
|
|
126
|
+
implementer=parse_rung(implementer) if implementer is not None else None,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@server.tool()
|
|
130
|
+
async def prioritize_task(task_id: str, priority: int) -> Ack:
|
|
131
|
+
"""Set a queued task's dispatch priority."""
|
|
132
|
+
return await services.commands.prioritize_task(task_id, priority)
|
|
133
|
+
|
|
134
|
+
@server.tool()
|
|
135
|
+
async def pause_task(task_id: str, *, now: bool = False) -> Ack:
|
|
136
|
+
"""Pause a task, optionally requesting an immediate pause."""
|
|
137
|
+
return await services.commands.pause_task(task_id, now=now)
|
|
138
|
+
|
|
139
|
+
@server.tool()
|
|
140
|
+
async def resume_task(task_id: str) -> Ack:
|
|
141
|
+
"""Resume a paused task."""
|
|
142
|
+
return await services.commands.resume_task(task_id)
|
|
143
|
+
|
|
144
|
+
@server.tool()
|
|
145
|
+
async def cancel_task(task_id: str) -> Ack:
|
|
146
|
+
"""Cancel a task."""
|
|
147
|
+
return await services.commands.cancel_task(task_id)
|
|
148
|
+
|
|
149
|
+
@server.tool()
|
|
150
|
+
async def retry_task(task_id: str) -> Ack:
|
|
151
|
+
"""Retry a failed or escalated task from its current step."""
|
|
152
|
+
return await services.commands.retry_task(task_id)
|
|
153
|
+
|
|
154
|
+
@server.tool()
|
|
155
|
+
async def set_rung(task_id: str, role: Role, rung: str) -> Ack:
|
|
156
|
+
"""Set the task role's agent, model and optional effort rung."""
|
|
157
|
+
return await services.commands.set_rung(task_id, role, parse_rung(rung))
|
|
158
|
+
|
|
159
|
+
@server.tool()
|
|
160
|
+
async def resolve_escalation(escalation_id: str, resolution: ResolutionInput) -> Ack:
|
|
161
|
+
"""Resolve an open escalation with an answer, retry, accept or cancel decision."""
|
|
162
|
+
return await services.commands.resolve_escalation(escalation_id, resolution.to_domain())
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _register_session_tools(server: FastMCP, services: OperatorServices) -> None:
|
|
166
|
+
@server.tool()
|
|
167
|
+
async def send_message(task_id: str, role: Role, text: str) -> Ack:
|
|
168
|
+
"""Queue a message the role reads at the start of its next turn."""
|
|
169
|
+
return await services.commands.send_message(task_id, role, text)
|
|
170
|
+
|
|
171
|
+
@server.tool()
|
|
172
|
+
async def take_over(task_id: str, role: Role) -> Ack:
|
|
173
|
+
"""Stop the role's turn and run its session interactively in its pane."""
|
|
174
|
+
return await services.commands.take_over(task_id, role)
|
|
175
|
+
|
|
176
|
+
@server.tool()
|
|
177
|
+
async def hand_back(task_id: str, role: Role) -> Ack:
|
|
178
|
+
"""Return a taken-over role's session to the engine and resume the task."""
|
|
179
|
+
return await services.commands.hand_back(task_id, role)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _register_event_tools(server: FastMCP, services: OperatorServices) -> None:
|
|
183
|
+
@server.tool()
|
|
184
|
+
async def wait_for_events(
|
|
185
|
+
since_id: int,
|
|
186
|
+
kinds: list[EventKind] | None = None,
|
|
187
|
+
task_id: str | None = None,
|
|
188
|
+
timeout_s: float = MAX_WAIT_SECONDS,
|
|
189
|
+
) -> list[Event]:
|
|
190
|
+
"""Wait up to 300 seconds for events after since_id with optional kind and task filters."""
|
|
191
|
+
return await services.events.wait(
|
|
192
|
+
since_id,
|
|
193
|
+
kinds=kinds,
|
|
194
|
+
task=task_id,
|
|
195
|
+
timeout=timeout_s,
|
|
196
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Operator services shared by the CLI, MCP server and dashboard palette."""
|
|
2
|
+
|
|
3
|
+
from shikiru.operator.commands import Ack, AckStatus, OperatorCommands
|
|
4
|
+
from shikiru.operator.doctor import Doctor, DoctorCheck, DoctorReport
|
|
5
|
+
from shikiru.operator.events import EventFeed
|
|
6
|
+
from shikiru.operator.queries import (
|
|
7
|
+
EscalationView,
|
|
8
|
+
OperatorQueries,
|
|
9
|
+
OperatorQueryError,
|
|
10
|
+
PeekView,
|
|
11
|
+
StatusView,
|
|
12
|
+
TaskDetail,
|
|
13
|
+
TaskSummary,
|
|
14
|
+
)
|
|
15
|
+
from shikiru.operator.services import OperatorServices
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Ack",
|
|
19
|
+
"AckStatus",
|
|
20
|
+
"Doctor",
|
|
21
|
+
"DoctorCheck",
|
|
22
|
+
"DoctorReport",
|
|
23
|
+
"EscalationView",
|
|
24
|
+
"EventFeed",
|
|
25
|
+
"OperatorCommands",
|
|
26
|
+
"OperatorQueries",
|
|
27
|
+
"OperatorQueryError",
|
|
28
|
+
"OperatorServices",
|
|
29
|
+
"PeekView",
|
|
30
|
+
"StatusView",
|
|
31
|
+
"TaskDetail",
|
|
32
|
+
"TaskSummary",
|
|
33
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared admission policy for Shikiru-managed tmux client transitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shikiru.errors import UsageError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from shikiru.tmux.client import Tmux
|
|
11
|
+
|
|
12
|
+
_BUSY_SESSION_MESSAGE = (
|
|
13
|
+
"a terminal is already attached to Shikiru session {session!r}; detach it first, then retry"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def admit_attachment(tmux: Tmux, session: str, current_pane: str | None) -> bool:
|
|
18
|
+
"""Return true for a same-client no-op or refuse when another client owns the session."""
|
|
19
|
+
clients = await tmux.session_client_panes(session)
|
|
20
|
+
if not clients:
|
|
21
|
+
return False
|
|
22
|
+
if len(clients) == 1 and current_pane is not None and clients[0] == current_pane:
|
|
23
|
+
return True
|
|
24
|
+
raise UsageError(_BUSY_SESSION_MESSAGE.format(session=session))
|