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.
Files changed (169) hide show
  1. coding_agent/__init__.py +20 -0
  2. coding_agent/_lazy.py +17 -0
  3. coding_agent/addons/__init__.py +35 -0
  4. coding_agent/addons/approvals.py +224 -0
  5. coding_agent/addons/compaction/__init__.py +36 -0
  6. coding_agent/addons/compaction/addon.py +88 -0
  7. coding_agent/addons/compaction/compactor.py +138 -0
  8. coding_agent/addons/compaction/prompts.py +20 -0
  9. coding_agent/addons/compaction/transcript.py +118 -0
  10. coding_agent/addons/evaluation/__init__.py +68 -0
  11. coding_agent/addons/evaluation/addon.py +199 -0
  12. coding_agent/addons/evaluation/evaluators.py +245 -0
  13. coding_agent/addons/evaluation/handlers.py +66 -0
  14. coding_agent/addons/evaluation/policy.py +277 -0
  15. coding_agent/addons/evaluation/protocol.py +192 -0
  16. coding_agent/addons/evaluation/state.py +284 -0
  17. coding_agent/addons/langfuse/__init__.py +24 -0
  18. coding_agent/addons/langfuse/addon.py +465 -0
  19. coding_agent/addons/langfuse/install.py +79 -0
  20. coding_agent/addons/langfuse/serialize.py +158 -0
  21. coding_agent/addons/learning/__init__.py +33 -0
  22. coding_agent/addons/learning/addon.py +181 -0
  23. coding_agent/addons/learning/loop.py +247 -0
  24. coding_agent/addons/learning/prompts.py +26 -0
  25. coding_agent/addons/learning/sanitize.py +64 -0
  26. coding_agent/addons/learning/store.py +522 -0
  27. coding_agent/addons/manager/__init__.py +18 -0
  28. coding_agent/addons/manager/bridge.py +74 -0
  29. coding_agent/addons/manager/discovery.py +34 -0
  30. coding_agent/addons/manager/report.py +33 -0
  31. coding_agent/addons/manager/runtime.py +76 -0
  32. coding_agent/addons/manager/tool_changes.py +16 -0
  33. coding_agent/addons/persistence/__init__.py +41 -0
  34. coding_agent/addons/persistence/active.py +132 -0
  35. coding_agent/addons/persistence/artifacts.py +110 -0
  36. coding_agent/addons/persistence/collection.py +166 -0
  37. coding_agent/addons/persistence/compatibility.py +30 -0
  38. coding_agent/addons/persistence/images.py +133 -0
  39. coding_agent/addons/persistence/jsonl.py +1195 -0
  40. coding_agent/addons/persistence/presentation.py +104 -0
  41. coding_agent/addons/plugins/__init__.py +19 -0
  42. coding_agent/addons/plugins/install.py +37 -0
  43. coding_agent/addons/plugins/manager.py +140 -0
  44. coding_agent/addons/plugins/models.py +50 -0
  45. coding_agent/addons/skills/__init__.py +28 -0
  46. coding_agent/addons/skills/addon.py +26 -0
  47. coding_agent/addons/skills/args.py +78 -0
  48. coding_agent/addons/skills/bundled_skills/addon-authoring/SKILL.md +77 -0
  49. coding_agent/addons/skills/bundled_skills/context-budget/SKILL.md +12 -0
  50. coding_agent/addons/skills/bundled_skills/orient-first/SKILL.md +16 -0
  51. coding_agent/addons/skills/bundled_skills/security-boundary/SKILL.md +16 -0
  52. coding_agent/addons/skills/bundled_skills/skill-discipline/SKILL.md +13 -0
  53. coding_agent/addons/skills/bundled_skills/surgical-edit/SKILL.md +11 -0
  54. coding_agent/addons/skills/bundled_skills/systematic-debug/SKILL.md +18 -0
  55. coding_agent/addons/skills/bundled_skills/verify-before-done/SKILL.md +11 -0
  56. coding_agent/addons/skills/frontmatter.py +57 -0
  57. coding_agent/addons/skills/models.py +57 -0
  58. coding_agent/addons/skills/registry.py +109 -0
  59. coding_agent/addons/skills/tool.py +54 -0
  60. coding_agent/addons/voice/__init__.py +5 -0
  61. coding_agent/addons/voice/addon.py +106 -0
  62. coding_agent/addons/voice/credentials.py +42 -0
  63. coding_agent/addons/voice/playback.py +42 -0
  64. coding_agent/addons/voice/speak.py +287 -0
  65. coding_agent/addons/voice/stream.py +651 -0
  66. coding_agent/addons/voice/summary.py +126 -0
  67. coding_agent/agent.py +523 -0
  68. coding_agent/bench/__init__.py +1 -0
  69. coding_agent/bench/cli.py +200 -0
  70. coding_agent/bench/patch.py +78 -0
  71. coding_agent/commands/__init__.py +5 -0
  72. coding_agent/commands/catalog.py +179 -0
  73. coding_agent/commands/jev_argument.py +40 -0
  74. coding_agent/config/__init__.py +51 -0
  75. coding_agent/config/config.py +376 -0
  76. coding_agent/config/credentials.py +153 -0
  77. coding_agent/config/personalities.json +34 -0
  78. coding_agent/config/personalities.py +170 -0
  79. coding_agent/config/prompts.py +67 -0
  80. coding_agent/protocols/__init__.py +5 -0
  81. coding_agent/protocols/acp/README.md +36 -0
  82. coding_agent/protocols/acp/__init__.py +27 -0
  83. coding_agent/protocols/acp/mapping.py +164 -0
  84. coding_agent/protocols/acp/server.py +261 -0
  85. coding_agent/protocols/acp/transport.py +72 -0
  86. coding_agent/protocols/stdio/README.md +36 -0
  87. coding_agent/protocols/stdio/__init__.py +37 -0
  88. coding_agent/protocols/stdio/catalog.py +49 -0
  89. coding_agent/protocols/stdio/commands.py +218 -0
  90. coding_agent/protocols/stdio/server.py +181 -0
  91. coding_agent/protocols/stdio/transport.py +71 -0
  92. coding_agent/py.typed +0 -0
  93. coding_agent/resources/__init__.py +5 -0
  94. coding_agent/resources/usage.py +264 -0
  95. coding_agent/run/__init__.py +1 -0
  96. coding_agent/run/cli.py +99 -0
  97. coding_agent/run/detach.py +37 -0
  98. coding_agent/run/interrupted.py +67 -0
  99. coding_agent/run/log_sink.py +46 -0
  100. coding_agent/tools/__init__.py +73 -0
  101. coding_agent/tools/ask_user.py +61 -0
  102. coding_agent/tools/base.py +136 -0
  103. coding_agent/tools/bash.py +288 -0
  104. coding_agent/tools/bash_jobs.py +116 -0
  105. coding_agent/tools/generate_image.py +123 -0
  106. coding_agent/tools/memory.py +100 -0
  107. coding_agent/tools/patch.py +218 -0
  108. coding_agent/tools/read_file.py +167 -0
  109. coding_agent/tools/search.py +433 -0
  110. coding_agent/tools/web_search.py +151 -0
  111. coding_agent/tools/write_file.py +40 -0
  112. coding_agent/tui/EFFICIENCY_PLAN.md +25 -0
  113. coding_agent/tui/__init__.py +22 -0
  114. coding_agent/tui/__main__.py +122 -0
  115. coding_agent/tui/app.py +456 -0
  116. coding_agent/tui/chrome/__init__.py +33 -0
  117. coding_agent/tui/chrome/footer.py +274 -0
  118. coding_agent/tui/chrome/topbar.py +259 -0
  119. coding_agent/tui/commands/__init__.py +45 -0
  120. coding_agent/tui/commands/catalog.py +35 -0
  121. coding_agent/tui/commands/manager.py +557 -0
  122. coding_agent/tui/composer/__init__.py +18 -0
  123. coding_agent/tui/composer/input.py +219 -0
  124. coding_agent/tui/composer/slash_menu.py +341 -0
  125. coding_agent/tui/composer/surface.py +330 -0
  126. coding_agent/tui/composer/voice.py +267 -0
  127. coding_agent/tui/composer/voice_mode.py +491 -0
  128. coding_agent/tui/runtime/__init__.py +23 -0
  129. coding_agent/tui/runtime/events.py +728 -0
  130. coding_agent/tui/runtime/sink.py +115 -0
  131. coding_agent/tui/runtime/state.py +145 -0
  132. coding_agent/tui/runtime/subagent.py +531 -0
  133. coding_agent/tui/runtime/turn.py +206 -0
  134. coding_agent/tui/screens/__init__.py +54 -0
  135. coding_agent/tui/screens/ask.py +69 -0
  136. coding_agent/tui/screens/context.py +195 -0
  137. coding_agent/tui/screens/dashboard.py +91 -0
  138. coding_agent/tui/screens/diff.py +302 -0
  139. coding_agent/tui/screens/extensions.py +68 -0
  140. coding_agent/tui/screens/file_selector.py +167 -0
  141. coding_agent/tui/screens/history.py +458 -0
  142. coding_agent/tui/screens/langfuse.py +142 -0
  143. coding_agent/tui/screens/learning.py +113 -0
  144. coding_agent/tui/screens/modal.py +131 -0
  145. coding_agent/tui/screens/onboard.py +455 -0
  146. coding_agent/tui/screens/resume.py +163 -0
  147. coding_agent/tui/theme/__init__.py +49 -0
  148. coding_agent/tui/theme/load.py +325 -0
  149. coding_agent/tui/theme/style.py +95 -0
  150. coding_agent/tui/theme/theme.toml +1687 -0
  151. coding_agent/tui/tools/__init__.py +53 -0
  152. coding_agent/tui/tools/activity.py +184 -0
  153. coding_agent/tui/tools/calls.py +560 -0
  154. coding_agent/tui/tools/code_view.py +52 -0
  155. coding_agent/tui/tools/images.py +250 -0
  156. coding_agent/tui/tools/labels.py +135 -0
  157. coding_agent/tui/tools/modal.py +137 -0
  158. coding_agent/tui/tools/partial_args.py +19 -0
  159. coding_agent/tui/tools/snapshots.py +382 -0
  160. coding_agent/tui/transcript/__init__.py +36 -0
  161. coding_agent/tui/transcript/messages.py +408 -0
  162. coding_agent/tui/transcript/process.py +504 -0
  163. coding_agent/tui/transcript/surface.py +345 -0
  164. coding_agent/tui/transcript/thinking_markdown.py +314 -0
  165. symphony_code-0.0.1.dist-info/METADATA +66 -0
  166. symphony_code-0.0.1.dist-info/RECORD +169 -0
  167. symphony_code-0.0.1.dist-info/WHEEL +4 -0
  168. symphony_code-0.0.1.dist-info/entry_points.txt +5 -0
  169. 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
+ ]