symphony-code 0.0.1__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.
- coding_agent/__init__.py +20 -0
- coding_agent/_lazy.py +17 -0
- coding_agent/addons/__init__.py +35 -0
- coding_agent/addons/approvals.py +224 -0
- coding_agent/addons/compaction/__init__.py +36 -0
- coding_agent/addons/compaction/addon.py +88 -0
- coding_agent/addons/compaction/compactor.py +138 -0
- coding_agent/addons/compaction/prompts.py +20 -0
- coding_agent/addons/compaction/transcript.py +118 -0
- coding_agent/addons/evaluation/__init__.py +68 -0
- coding_agent/addons/evaluation/addon.py +199 -0
- coding_agent/addons/evaluation/evaluators.py +245 -0
- coding_agent/addons/evaluation/handlers.py +66 -0
- coding_agent/addons/evaluation/policy.py +277 -0
- coding_agent/addons/evaluation/protocol.py +192 -0
- coding_agent/addons/evaluation/state.py +284 -0
- coding_agent/addons/langfuse/__init__.py +24 -0
- coding_agent/addons/langfuse/addon.py +465 -0
- coding_agent/addons/langfuse/install.py +79 -0
- coding_agent/addons/langfuse/serialize.py +158 -0
- coding_agent/addons/learning/__init__.py +33 -0
- coding_agent/addons/learning/addon.py +181 -0
- coding_agent/addons/learning/loop.py +247 -0
- coding_agent/addons/learning/prompts.py +26 -0
- coding_agent/addons/learning/sanitize.py +64 -0
- coding_agent/addons/learning/store.py +522 -0
- coding_agent/addons/manager/__init__.py +18 -0
- coding_agent/addons/manager/bridge.py +74 -0
- coding_agent/addons/manager/discovery.py +34 -0
- coding_agent/addons/manager/report.py +33 -0
- coding_agent/addons/manager/runtime.py +76 -0
- coding_agent/addons/manager/tool_changes.py +16 -0
- coding_agent/addons/persistence/__init__.py +41 -0
- coding_agent/addons/persistence/active.py +132 -0
- coding_agent/addons/persistence/artifacts.py +110 -0
- coding_agent/addons/persistence/collection.py +166 -0
- coding_agent/addons/persistence/compatibility.py +30 -0
- coding_agent/addons/persistence/images.py +133 -0
- coding_agent/addons/persistence/jsonl.py +1195 -0
- coding_agent/addons/persistence/presentation.py +104 -0
- coding_agent/addons/plugins/__init__.py +19 -0
- coding_agent/addons/plugins/install.py +37 -0
- coding_agent/addons/plugins/manager.py +140 -0
- coding_agent/addons/plugins/models.py +50 -0
- coding_agent/addons/skills/__init__.py +28 -0
- coding_agent/addons/skills/addon.py +26 -0
- coding_agent/addons/skills/args.py +78 -0
- coding_agent/addons/skills/bundled_skills/addon-authoring/SKILL.md +77 -0
- coding_agent/addons/skills/bundled_skills/context-budget/SKILL.md +12 -0
- coding_agent/addons/skills/bundled_skills/orient-first/SKILL.md +16 -0
- coding_agent/addons/skills/bundled_skills/security-boundary/SKILL.md +16 -0
- coding_agent/addons/skills/bundled_skills/skill-discipline/SKILL.md +13 -0
- coding_agent/addons/skills/bundled_skills/surgical-edit/SKILL.md +11 -0
- coding_agent/addons/skills/bundled_skills/systematic-debug/SKILL.md +18 -0
- coding_agent/addons/skills/bundled_skills/verify-before-done/SKILL.md +11 -0
- coding_agent/addons/skills/frontmatter.py +57 -0
- coding_agent/addons/skills/models.py +57 -0
- coding_agent/addons/skills/registry.py +109 -0
- coding_agent/addons/skills/tool.py +54 -0
- coding_agent/addons/voice/__init__.py +5 -0
- coding_agent/addons/voice/addon.py +106 -0
- coding_agent/addons/voice/credentials.py +42 -0
- coding_agent/addons/voice/playback.py +42 -0
- coding_agent/addons/voice/speak.py +287 -0
- coding_agent/addons/voice/stream.py +651 -0
- coding_agent/addons/voice/summary.py +126 -0
- coding_agent/agent.py +523 -0
- coding_agent/bench/__init__.py +1 -0
- coding_agent/bench/cli.py +200 -0
- coding_agent/bench/patch.py +78 -0
- coding_agent/commands/__init__.py +5 -0
- coding_agent/commands/catalog.py +179 -0
- coding_agent/commands/jev_argument.py +40 -0
- coding_agent/config/__init__.py +51 -0
- coding_agent/config/config.py +376 -0
- coding_agent/config/credentials.py +153 -0
- coding_agent/config/personalities.json +34 -0
- coding_agent/config/personalities.py +170 -0
- coding_agent/config/prompts.py +67 -0
- coding_agent/protocols/__init__.py +5 -0
- coding_agent/protocols/acp/README.md +36 -0
- coding_agent/protocols/acp/__init__.py +27 -0
- coding_agent/protocols/acp/mapping.py +164 -0
- coding_agent/protocols/acp/server.py +261 -0
- coding_agent/protocols/acp/transport.py +72 -0
- coding_agent/protocols/stdio/README.md +36 -0
- coding_agent/protocols/stdio/__init__.py +37 -0
- coding_agent/protocols/stdio/catalog.py +49 -0
- coding_agent/protocols/stdio/commands.py +218 -0
- coding_agent/protocols/stdio/server.py +181 -0
- coding_agent/protocols/stdio/transport.py +71 -0
- coding_agent/py.typed +0 -0
- coding_agent/resources/__init__.py +5 -0
- coding_agent/resources/usage.py +264 -0
- coding_agent/run/__init__.py +1 -0
- coding_agent/run/cli.py +99 -0
- coding_agent/run/detach.py +37 -0
- coding_agent/run/interrupted.py +67 -0
- coding_agent/run/log_sink.py +46 -0
- coding_agent/tools/__init__.py +73 -0
- coding_agent/tools/ask_user.py +61 -0
- coding_agent/tools/base.py +136 -0
- coding_agent/tools/bash.py +288 -0
- coding_agent/tools/bash_jobs.py +116 -0
- coding_agent/tools/generate_image.py +123 -0
- coding_agent/tools/memory.py +100 -0
- coding_agent/tools/patch.py +218 -0
- coding_agent/tools/read_file.py +167 -0
- coding_agent/tools/search.py +433 -0
- coding_agent/tools/web_search.py +151 -0
- coding_agent/tools/write_file.py +40 -0
- coding_agent/tui/EFFICIENCY_PLAN.md +25 -0
- coding_agent/tui/__init__.py +22 -0
- coding_agent/tui/__main__.py +122 -0
- coding_agent/tui/app.py +456 -0
- coding_agent/tui/chrome/__init__.py +33 -0
- coding_agent/tui/chrome/footer.py +274 -0
- coding_agent/tui/chrome/topbar.py +259 -0
- coding_agent/tui/commands/__init__.py +45 -0
- coding_agent/tui/commands/catalog.py +35 -0
- coding_agent/tui/commands/manager.py +557 -0
- coding_agent/tui/composer/__init__.py +18 -0
- coding_agent/tui/composer/input.py +219 -0
- coding_agent/tui/composer/slash_menu.py +341 -0
- coding_agent/tui/composer/surface.py +330 -0
- coding_agent/tui/composer/voice.py +267 -0
- coding_agent/tui/composer/voice_mode.py +491 -0
- coding_agent/tui/runtime/__init__.py +23 -0
- coding_agent/tui/runtime/events.py +728 -0
- coding_agent/tui/runtime/sink.py +115 -0
- coding_agent/tui/runtime/state.py +145 -0
- coding_agent/tui/runtime/subagent.py +531 -0
- coding_agent/tui/runtime/turn.py +206 -0
- coding_agent/tui/screens/__init__.py +54 -0
- coding_agent/tui/screens/ask.py +69 -0
- coding_agent/tui/screens/context.py +195 -0
- coding_agent/tui/screens/dashboard.py +91 -0
- coding_agent/tui/screens/diff.py +302 -0
- coding_agent/tui/screens/extensions.py +68 -0
- coding_agent/tui/screens/file_selector.py +167 -0
- coding_agent/tui/screens/history.py +458 -0
- coding_agent/tui/screens/langfuse.py +142 -0
- coding_agent/tui/screens/learning.py +113 -0
- coding_agent/tui/screens/modal.py +131 -0
- coding_agent/tui/screens/onboard.py +455 -0
- coding_agent/tui/screens/resume.py +163 -0
- coding_agent/tui/theme/__init__.py +49 -0
- coding_agent/tui/theme/load.py +325 -0
- coding_agent/tui/theme/style.py +95 -0
- coding_agent/tui/theme/theme.toml +1687 -0
- coding_agent/tui/tools/__init__.py +53 -0
- coding_agent/tui/tools/activity.py +184 -0
- coding_agent/tui/tools/calls.py +560 -0
- coding_agent/tui/tools/code_view.py +52 -0
- coding_agent/tui/tools/images.py +250 -0
- coding_agent/tui/tools/labels.py +135 -0
- coding_agent/tui/tools/modal.py +137 -0
- coding_agent/tui/tools/partial_args.py +19 -0
- coding_agent/tui/tools/snapshots.py +382 -0
- coding_agent/tui/transcript/__init__.py +36 -0
- coding_agent/tui/transcript/messages.py +408 -0
- coding_agent/tui/transcript/process.py +504 -0
- coding_agent/tui/transcript/surface.py +345 -0
- coding_agent/tui/transcript/thinking_markdown.py +314 -0
- symphony_code-0.0.1.dist-info/METADATA +66 -0
- symphony_code-0.0.1.dist-info/RECORD +169 -0
- symphony_code-0.0.1.dist-info/WHEEL +4 -0
- symphony_code-0.0.1.dist-info/entry_points.txt +5 -0
- symphony_code-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Deterministic next-step policy for Jev findings.
|
|
2
|
+
|
|
3
|
+
Jev is a decision layer, not a chat model. TypeSafe, the AI SDK evaluation
|
|
4
|
+
docs, and agent examples such as Foreman all keep this split:
|
|
5
|
+
|
|
6
|
+
typed answers + probabilities -> code-owned policy -> one allowed action
|
|
7
|
+
|
|
8
|
+
This module is that policy. It never calls the evaluator, never rewrites the
|
|
9
|
+
workspace, and never grants extra tools. ``JevAddon`` dispatches named
|
|
10
|
+
handlers from ``last_decision.action``. Honour/follow-up (a second
|
|
11
|
+
``harness.run``) is opt-in and off by default.
|
|
12
|
+
|
|
13
|
+
Fail-open: skipped/error results, missing answers, and low-confidence booleans
|
|
14
|
+
all become ``continue_normally``. Actuation requires an explicit probability
|
|
15
|
+
that clears the threshold; a bare ``True`` / label is a weak signal only.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Literal, Optional
|
|
22
|
+
|
|
23
|
+
from coding_agent.addons.evaluation.protocol import (
|
|
24
|
+
CONTINUE_NORMALLY,
|
|
25
|
+
EvaluationFinding,
|
|
26
|
+
EvaluationResult,
|
|
27
|
+
)
|
|
28
|
+
from coding_agent.addons.evaluation.state import bound_text
|
|
29
|
+
|
|
30
|
+
NUDGE_MARKER = "Jev critic note:"
|
|
31
|
+
NUDGE_TEXT_LIMIT = 280
|
|
32
|
+
JEV_SYSTEM_SEGMENT = (
|
|
33
|
+
"# Jev mode\n"
|
|
34
|
+
f"Treat injected `{NUDGE_MARKER}` as authoritative. "
|
|
35
|
+
"On remaining work, review, or conflicting complete/remaining findings, "
|
|
36
|
+
"continue or fix before claiming the task is done."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Actions the add-on is allowed to honour. Anything else is treated as continue.
|
|
40
|
+
PolicyAction = Literal[
|
|
41
|
+
"continue_normally",
|
|
42
|
+
"replan",
|
|
43
|
+
"gather",
|
|
44
|
+
"ask",
|
|
45
|
+
"retry",
|
|
46
|
+
"review",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Boolean P(true) must clear this before we treat it as a yes. Matches the
|
|
50
|
+
# AI SDK guidance that 0.5 is not a safe auto-action threshold.
|
|
51
|
+
BOOLEAN_ACT_THRESHOLD = 0.8
|
|
52
|
+
|
|
53
|
+
# Cap follow-up runs so a noisy evaluator cannot loop the agent forever.
|
|
54
|
+
MAX_HONOURED_FOLLOW_UPS = 1
|
|
55
|
+
|
|
56
|
+
_START_NEXT_ACTIONS = {"continue", "gather", "replan", "ask"}
|
|
57
|
+
_FINISH_NEXT_ACTIONS = {"finish", "continue", "retry", "review"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class PolicyDecision:
|
|
62
|
+
"""One allowed next step derived from an evaluation result."""
|
|
63
|
+
|
|
64
|
+
action: PolicyAction = CONTINUE_NORMALLY
|
|
65
|
+
reason: str = ""
|
|
66
|
+
finding: Optional[EvaluationFinding] = None
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def should_act(self) -> bool:
|
|
70
|
+
return self.action != CONTINUE_NORMALLY
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def apply_findings_gate(result: EvaluationResult) -> str:
|
|
74
|
+
"""Return the policy action name. Kept as a stable string API."""
|
|
75
|
+
return decide_next_step(result).action
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def decide_next_step(result: EvaluationResult) -> PolicyDecision:
|
|
79
|
+
"""Map Jev findings onto one next step.
|
|
80
|
+
|
|
81
|
+
Precedence is intentional and conservative:
|
|
82
|
+
|
|
83
|
+
1. Evaluator failure / skip -> continue (fail open).
|
|
84
|
+
2. Finish conflict: ``task_complete`` and ``remaining_work`` both true
|
|
85
|
+
-> ``review``. TypeSafe questions are independent; both-true is invalid.
|
|
86
|
+
3. Explicit ``next_action`` choice, when it belongs to the phase.
|
|
87
|
+
4. High-confidence booleans that imply the same action (start phase;
|
|
88
|
+
finish never actuates on ``task_complete`` or ``remaining_work`` alone).
|
|
89
|
+
5. Otherwise continue.
|
|
90
|
+
"""
|
|
91
|
+
if result.status != "ok":
|
|
92
|
+
return PolicyDecision(
|
|
93
|
+
action=CONTINUE_NORMALLY,
|
|
94
|
+
reason=f"evaluator {result.status}; fail open",
|
|
95
|
+
)
|
|
96
|
+
findings = {finding.question: finding for finding in result.findings}
|
|
97
|
+
if result.phase == "start":
|
|
98
|
+
return _decide_start(findings)
|
|
99
|
+
if result.phase == "finish":
|
|
100
|
+
return _decide_finish(findings)
|
|
101
|
+
return PolicyDecision(reason="unknown evaluation phase")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def is_jev_nudge(message: object) -> bool:
|
|
105
|
+
"""True when ``message`` is a previously injected Jev critic note."""
|
|
106
|
+
content = getattr(message, "content", message)
|
|
107
|
+
if isinstance(content, str):
|
|
108
|
+
text = content
|
|
109
|
+
elif isinstance(content, list):
|
|
110
|
+
parts = []
|
|
111
|
+
for part in content:
|
|
112
|
+
if isinstance(part, str):
|
|
113
|
+
parts.append(part)
|
|
114
|
+
elif isinstance(part, dict):
|
|
115
|
+
parts.append(str(part.get("text") or ""))
|
|
116
|
+
else:
|
|
117
|
+
parts.append(str(getattr(part, "text", "") or part))
|
|
118
|
+
text = "".join(parts)
|
|
119
|
+
else:
|
|
120
|
+
text = str(content or "")
|
|
121
|
+
return NUDGE_MARKER in text
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def critic_message(decision: PolicyDecision) -> str:
|
|
125
|
+
"""Instruction injected into the conversation so the chat model can act."""
|
|
126
|
+
rationale = _rationale(decision)
|
|
127
|
+
question = decision.finding.question if decision.finding is not None else ""
|
|
128
|
+
if decision.action == "replan":
|
|
129
|
+
body = (
|
|
130
|
+
f"Revise the plan before further implementation. Reason: {rationale} "
|
|
131
|
+
"Reconsider the approach before making further changes."
|
|
132
|
+
)
|
|
133
|
+
elif decision.action == "gather":
|
|
134
|
+
body = (
|
|
135
|
+
f"Gather more information before acting. Reason: {rationale} "
|
|
136
|
+
"Inspect the workspace with read-only tools before making changes."
|
|
137
|
+
)
|
|
138
|
+
elif decision.action == "ask":
|
|
139
|
+
body = (
|
|
140
|
+
f"Ask the user one concrete question with ask_user. Reason: {rationale}"
|
|
141
|
+
)
|
|
142
|
+
elif decision.action == "retry":
|
|
143
|
+
body = (
|
|
144
|
+
f"Finish remaining work from the current workspace. Reason: {rationale}"
|
|
145
|
+
)
|
|
146
|
+
elif decision.action == "review" or question == "unsupported_claims":
|
|
147
|
+
if question == "unsupported_claims":
|
|
148
|
+
body = f"Verify before claiming the task is done. Reason: {rationale}"
|
|
149
|
+
else:
|
|
150
|
+
body = (
|
|
151
|
+
f"Human review is required. Do not claim the task is done. "
|
|
152
|
+
f"Reason: {rationale}"
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
body = rationale
|
|
156
|
+
return bound_text(f"{NUDGE_MARKER} {body}", NUDGE_TEXT_LIMIT) if body else ""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _decide_start(findings: dict[str, EvaluationFinding]) -> PolicyDecision:
|
|
160
|
+
next_action = _choice(findings.get("next_action"), _START_NEXT_ACTIONS)
|
|
161
|
+
if next_action == "replan" or _boolean_true(findings.get("needs_replan")):
|
|
162
|
+
finding = findings.get("needs_replan") if _boolean_true(findings.get("needs_replan")) else findings.get("next_action")
|
|
163
|
+
return PolicyDecision(action="replan", reason=_rationale_from(finding, "replan"), finding=finding)
|
|
164
|
+
if next_action == "ask":
|
|
165
|
+
finding = findings.get("next_action")
|
|
166
|
+
return PolicyDecision(action="ask", reason=_rationale_from(finding, "ask"), finding=finding)
|
|
167
|
+
if next_action == "gather":
|
|
168
|
+
finding = findings.get("next_action")
|
|
169
|
+
return PolicyDecision(action="gather", reason=_rationale_from(finding, "gather"), finding=finding)
|
|
170
|
+
if next_action in {"continue", None} and _boolean_false(findings.get("plan_sufficient")):
|
|
171
|
+
finding = findings.get("plan_sufficient")
|
|
172
|
+
return PolicyDecision(action="replan", reason=_rationale_from(finding, "insufficient plan"), finding=finding)
|
|
173
|
+
return PolicyDecision(reason="start findings do not require a control action")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _decide_finish(findings: dict[str, EvaluationFinding]) -> PolicyDecision:
|
|
177
|
+
if _boolean_false(findings.get("rule_satisfied")):
|
|
178
|
+
finding = findings["rule_satisfied"]
|
|
179
|
+
return PolicyDecision(action="retry", reason=_rationale_from(finding, "user rule not satisfied"), finding=finding)
|
|
180
|
+
if _weak_true(findings.get("task_complete")) and _weak_true(findings.get("remaining_work")):
|
|
181
|
+
finding = findings.get("remaining_work") or findings.get("task_complete")
|
|
182
|
+
return PolicyDecision(
|
|
183
|
+
action="review",
|
|
184
|
+
reason=_rationale_from(finding, "conflicting finish findings"),
|
|
185
|
+
finding=finding,
|
|
186
|
+
)
|
|
187
|
+
next_action = _choice(findings.get("next_action"), _FINISH_NEXT_ACTIONS)
|
|
188
|
+
if next_action == "review" or _boolean_true(findings.get("unsupported_claims")):
|
|
189
|
+
finding = findings.get("unsupported_claims") if _boolean_true(findings.get("unsupported_claims")) else findings.get("next_action")
|
|
190
|
+
return PolicyDecision(action="review", reason=_rationale_from(finding, "review"), finding=finding)
|
|
191
|
+
if next_action == "retry":
|
|
192
|
+
finding = findings.get("next_action")
|
|
193
|
+
return PolicyDecision(action="retry", reason=_rationale_from(finding, "retry"), finding=finding)
|
|
194
|
+
if next_action == "continue":
|
|
195
|
+
finding = findings.get("next_action")
|
|
196
|
+
return PolicyDecision(action="retry", reason=_rationale_from(finding, "remaining work"), finding=finding)
|
|
197
|
+
if next_action == "finish":
|
|
198
|
+
return PolicyDecision(reason="finish findings allow the run to complete")
|
|
199
|
+
return PolicyDecision(reason="finish findings do not require a control action")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _choice(finding: Optional[EvaluationFinding], allowed: set[str]) -> Optional[str]:
|
|
203
|
+
if finding is None or finding.kind != "choice":
|
|
204
|
+
return None
|
|
205
|
+
value = str(finding.value or finding.label or "").strip().lower()
|
|
206
|
+
if value in allowed:
|
|
207
|
+
return value
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _boolean_true(finding: Optional[EvaluationFinding]) -> bool:
|
|
212
|
+
return _boolean_probability(finding) >= BOOLEAN_ACT_THRESHOLD
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _boolean_false(finding: Optional[EvaluationFinding]) -> bool:
|
|
216
|
+
probability = _boolean_probability(finding)
|
|
217
|
+
return probability >= 0.0 and probability <= (1.0 - BOOLEAN_ACT_THRESHOLD)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _boolean_probability(finding: Optional[EvaluationFinding]) -> float:
|
|
221
|
+
"""Return P(true), or -1 when the finding is missing/unusable.
|
|
222
|
+
|
|
223
|
+
A bare bool or label is not p=1.0. Actuation needs an explicit probability.
|
|
224
|
+
"""
|
|
225
|
+
if finding is None or finding.kind != "boolean":
|
|
226
|
+
return -1.0
|
|
227
|
+
probs = getattr(finding, "probs", None)
|
|
228
|
+
if isinstance(probs, dict) and "true" in probs:
|
|
229
|
+
try:
|
|
230
|
+
return float(probs["true"])
|
|
231
|
+
except (TypeError, ValueError):
|
|
232
|
+
return -1.0
|
|
233
|
+
value = finding.value
|
|
234
|
+
if isinstance(value, bool):
|
|
235
|
+
return -1.0
|
|
236
|
+
if isinstance(value, (int, float)):
|
|
237
|
+
return float(value)
|
|
238
|
+
return -1.0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _weak_true(finding: Optional[EvaluationFinding]) -> bool:
|
|
242
|
+
"""Label/bool without probs still counts as a weak yes for conflict."""
|
|
243
|
+
if finding is None or finding.kind != "boolean":
|
|
244
|
+
return False
|
|
245
|
+
probability = _boolean_probability(finding)
|
|
246
|
+
if probability >= 0.0:
|
|
247
|
+
return probability >= 0.5
|
|
248
|
+
if isinstance(finding.value, bool):
|
|
249
|
+
return finding.value
|
|
250
|
+
label = str(finding.label or "").strip().lower()
|
|
251
|
+
return label in {"true", "yes"}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _rationale(decision: PolicyDecision) -> str:
|
|
255
|
+
return _rationale_from(decision.finding, decision.reason or decision.action)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _rationale_from(finding: Optional[EvaluationFinding], fallback: str) -> str:
|
|
259
|
+
if finding is None:
|
|
260
|
+
return bound_text(fallback, NUDGE_TEXT_LIMIT)
|
|
261
|
+
text = (finding.rationale or finding.label or "").strip()
|
|
262
|
+
return bound_text(text or fallback, NUDGE_TEXT_LIMIT)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
__all__ = [
|
|
266
|
+
"BOOLEAN_ACT_THRESHOLD",
|
|
267
|
+
"JEV_SYSTEM_SEGMENT",
|
|
268
|
+
"MAX_HONOURED_FOLLOW_UPS",
|
|
269
|
+
"NUDGE_MARKER",
|
|
270
|
+
"NUDGE_TEXT_LIMIT",
|
|
271
|
+
"PolicyAction",
|
|
272
|
+
"PolicyDecision",
|
|
273
|
+
"apply_findings_gate",
|
|
274
|
+
"critic_message",
|
|
275
|
+
"decide_next_step",
|
|
276
|
+
"is_jev_nudge",
|
|
277
|
+
]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Evaluator protocol, result types, and v1 question sets.
|
|
2
|
+
|
|
3
|
+
Evaluator methods are named ``on_start`` / ``on_finish`` independently of the
|
|
4
|
+
harness hooks. The add-on evaluates completed runs with ``on_finish`` from
|
|
5
|
+
``Addon.after_run``; ``on_start`` remains available to protocol consumers.
|
|
6
|
+
|
|
7
|
+
Findings are advisory. The add-on may inject one user-role critic note into
|
|
8
|
+
messages and request a bounded revision when configured. It does not read
|
|
9
|
+
workspace plans or change tool availability. Generic callers may still supply
|
|
10
|
+
``plan_text`` when constructing evaluator state.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Literal, Optional, Protocol
|
|
17
|
+
|
|
18
|
+
DecisionKind = Literal["boolean", "choice", "score", "skipped", "error"]
|
|
19
|
+
EvaluationPhase = Literal["start", "finish"]
|
|
20
|
+
EvaluationStatus = Literal["ok", "skipped", "error"]
|
|
21
|
+
|
|
22
|
+
CONTINUE_NORMALLY = "continue_normally"
|
|
23
|
+
|
|
24
|
+
START_QUESTIONS: dict[str, Any] = {
|
|
25
|
+
"plan_sufficient": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"instructions": "Is the current plan sufficient to complete the request?",
|
|
28
|
+
},
|
|
29
|
+
"needs_replan": {
|
|
30
|
+
"type": "boolean",
|
|
31
|
+
"instructions": "Should the agent replan before taking further action?",
|
|
32
|
+
},
|
|
33
|
+
"next_action": {
|
|
34
|
+
"type": "choice",
|
|
35
|
+
"instructions": (
|
|
36
|
+
"What should the agent do next? Advisory only; Symphony will not auto-act."
|
|
37
|
+
),
|
|
38
|
+
"criteria": {
|
|
39
|
+
"continue": "Proceed with the current plan",
|
|
40
|
+
"gather": "Gather more information first",
|
|
41
|
+
"replan": "Revise the plan (advisory only)",
|
|
42
|
+
"ask": "Ask the user a clarifying question",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
FINISH_QUESTIONS: dict[str, Any] = {
|
|
48
|
+
"task_complete": {
|
|
49
|
+
"type": "boolean",
|
|
50
|
+
"instructions": "Did the agent complete the user's request?",
|
|
51
|
+
},
|
|
52
|
+
"remaining_work": {
|
|
53
|
+
"type": "boolean",
|
|
54
|
+
"instructions": "Is there remaining work the agent should still do?",
|
|
55
|
+
},
|
|
56
|
+
"unsupported_claims": {
|
|
57
|
+
"type": "boolean",
|
|
58
|
+
"instructions": (
|
|
59
|
+
"Did the agent make claims unsupported by tool observations or files changed?"
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
"next_action": {
|
|
63
|
+
"type": "choice",
|
|
64
|
+
"instructions": (
|
|
65
|
+
"Recommended next action after this run. Advisory only; choose finish, "
|
|
66
|
+
"continue, retry, or review."
|
|
67
|
+
),
|
|
68
|
+
"criteria": {
|
|
69
|
+
"finish": "The task is done",
|
|
70
|
+
"continue": "Continue working",
|
|
71
|
+
"retry": "Retry the approach",
|
|
72
|
+
"review": "Human review is needed",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class EvaluationDecision:
|
|
80
|
+
"""One answered evaluation question."""
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
kind: DecisionKind
|
|
84
|
+
value: Any = None
|
|
85
|
+
probs: Optional[dict[str, float]] = None
|
|
86
|
+
label: str = ""
|
|
87
|
+
rationale: str = ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class EvaluationFinding:
|
|
92
|
+
"""Structured, non-actuating critic finding."""
|
|
93
|
+
|
|
94
|
+
question: str
|
|
95
|
+
kind: DecisionKind
|
|
96
|
+
value: Any = None
|
|
97
|
+
probs: Optional[dict[str, float]] = None
|
|
98
|
+
label: str = ""
|
|
99
|
+
rationale: str = ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class EvaluationResult:
|
|
104
|
+
"""Outcome of ``on_start`` or ``on_finish``."""
|
|
105
|
+
|
|
106
|
+
phase: EvaluationPhase
|
|
107
|
+
status: EvaluationStatus = "ok"
|
|
108
|
+
decisions: list[EvaluationDecision] = field(default_factory=list)
|
|
109
|
+
findings: list[EvaluationFinding] = field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Evaluator(Protocol):
|
|
113
|
+
"""Critic that inspects a semantic run snapshot. Never the chat model."""
|
|
114
|
+
|
|
115
|
+
async def on_start(self, state: Any) -> EvaluationResult:
|
|
116
|
+
"""Called from ``JevAddon.before_run``."""
|
|
117
|
+
|
|
118
|
+
async def on_finish(self, state: Any) -> EvaluationResult:
|
|
119
|
+
"""Called from ``JevAddon.after_run``."""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def findings_from_decisions(decisions: list[EvaluationDecision]) -> list[EvaluationFinding]:
|
|
123
|
+
return [
|
|
124
|
+
EvaluationFinding(
|
|
125
|
+
question=decision.name,
|
|
126
|
+
kind=decision.kind,
|
|
127
|
+
value=decision.value,
|
|
128
|
+
probs=decision.probs,
|
|
129
|
+
label=decision.label,
|
|
130
|
+
rationale=decision.rationale,
|
|
131
|
+
)
|
|
132
|
+
for decision in decisions
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def skipped_result(phase: EvaluationPhase, rationale: str = "") -> EvaluationResult:
|
|
137
|
+
decision = EvaluationDecision(
|
|
138
|
+
name="evaluator",
|
|
139
|
+
kind="skipped",
|
|
140
|
+
label="skipped",
|
|
141
|
+
rationale=rationale,
|
|
142
|
+
)
|
|
143
|
+
return EvaluationResult(
|
|
144
|
+
phase=phase,
|
|
145
|
+
status="skipped",
|
|
146
|
+
decisions=[decision],
|
|
147
|
+
findings=findings_from_decisions([decision]),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def error_result(phase: EvaluationPhase, rationale: str = "") -> EvaluationResult:
|
|
152
|
+
decision = EvaluationDecision(
|
|
153
|
+
name="evaluator",
|
|
154
|
+
kind="error",
|
|
155
|
+
label="error",
|
|
156
|
+
rationale=rationale,
|
|
157
|
+
)
|
|
158
|
+
return EvaluationResult(
|
|
159
|
+
phase=phase,
|
|
160
|
+
status="error",
|
|
161
|
+
decisions=[decision],
|
|
162
|
+
findings=findings_from_decisions([decision]),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def apply_findings_gate(result: EvaluationResult) -> str:
|
|
167
|
+
"""Return the code-owned next-step action for this result.
|
|
168
|
+
|
|
169
|
+
The mapping lives in ``evaluation.policy`` so thresholds and honour rules
|
|
170
|
+
stay out of the protocol types. Honour/follow-up is off unless configured.
|
|
171
|
+
"""
|
|
172
|
+
from coding_agent.addons.evaluation.policy import apply_findings_gate as decide
|
|
173
|
+
|
|
174
|
+
return decide(result)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
"CONTINUE_NORMALLY",
|
|
179
|
+
"DecisionKind",
|
|
180
|
+
"EvaluationDecision",
|
|
181
|
+
"EvaluationFinding",
|
|
182
|
+
"EvaluationPhase",
|
|
183
|
+
"EvaluationResult",
|
|
184
|
+
"EvaluationStatus",
|
|
185
|
+
"Evaluator",
|
|
186
|
+
"FINISH_QUESTIONS",
|
|
187
|
+
"START_QUESTIONS",
|
|
188
|
+
"apply_findings_gate",
|
|
189
|
+
"error_result",
|
|
190
|
+
"findings_from_decisions",
|
|
191
|
+
"skipped_result",
|
|
192
|
+
]
|