coder-eval 0.8.2__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 (127) hide show
  1. coder_eval/.gitattributes +2 -0
  2. coder_eval/__init__.py +3 -0
  3. coder_eval/agent.py +302 -0
  4. coder_eval/agents/__init__.py +41 -0
  5. coder_eval/agents/_logging.py +89 -0
  6. coder_eval/agents/antigravity_agent.py +811 -0
  7. coder_eval/agents/claude_code_agent.py +1701 -0
  8. coder_eval/agents/codex_agent.py +2055 -0
  9. coder_eval/agents/noop_agent.py +100 -0
  10. coder_eval/agents/registry.py +163 -0
  11. coder_eval/agents/watchdog.py +116 -0
  12. coder_eval/analysis.py +88 -0
  13. coder_eval/cli/__init__.py +87 -0
  14. coder_eval/cli/aggregate_command.py +153 -0
  15. coder_eval/cli/console.py +7 -0
  16. coder_eval/cli/evaluate_command.py +164 -0
  17. coder_eval/cli/plan_command.py +152 -0
  18. coder_eval/cli/report_command.py +121 -0
  19. coder_eval/cli/run_command.py +686 -0
  20. coder_eval/cli/run_helpers.py +137 -0
  21. coder_eval/cli/run_task_internal_command.py +210 -0
  22. coder_eval/cli/utils.py +37 -0
  23. coder_eval/config.py +212 -0
  24. coder_eval/criteria/__init__.py +163 -0
  25. coder_eval/criteria/_classification_aggregate.py +106 -0
  26. coder_eval/criteria/agent_judge.py +425 -0
  27. coder_eval/criteria/base.py +248 -0
  28. coder_eval/criteria/classification_match.py +107 -0
  29. coder_eval/criteria/command_executed.py +181 -0
  30. coder_eval/criteria/commands_efficiency.py +73 -0
  31. coder_eval/criteria/file_check.py +119 -0
  32. coder_eval/criteria/file_contains.py +85 -0
  33. coder_eval/criteria/file_exists.py +45 -0
  34. coder_eval/criteria/file_matches_regex.py +86 -0
  35. coder_eval/criteria/json_check.py +184 -0
  36. coder_eval/criteria/llm_judge.py +260 -0
  37. coder_eval/criteria/reference_comparison.py +130 -0
  38. coder_eval/criteria/run_command.py +220 -0
  39. coder_eval/criteria/skill_triggered.py +111 -0
  40. coder_eval/criteria/uipath_eval.py +223 -0
  41. coder_eval/errors/__init__.py +26 -0
  42. coder_eval/errors/agent.py +26 -0
  43. coder_eval/errors/budget.py +28 -0
  44. coder_eval/errors/categories.py +205 -0
  45. coder_eval/errors/categorization.py +240 -0
  46. coder_eval/errors/executor.py +124 -0
  47. coder_eval/errors/judge.py +12 -0
  48. coder_eval/errors/retry.py +211 -0
  49. coder_eval/errors/timeout.py +63 -0
  50. coder_eval/evaluation/__init__.py +10 -0
  51. coder_eval/evaluation/checker.py +260 -0
  52. coder_eval/evaluation/judge_anthropic.py +55 -0
  53. coder_eval/evaluation/judge_bedrock.py +114 -0
  54. coder_eval/evaluation/judge_context.py +497 -0
  55. coder_eval/evaluation/judge_models.py +53 -0
  56. coder_eval/evaluation/judge_persistence.py +291 -0
  57. coder_eval/evaluation/judge_usage.py +50 -0
  58. coder_eval/evaluation/sub_agent.py +231 -0
  59. coder_eval/evaluation/summaries.py +48 -0
  60. coder_eval/evaluation/verdict_tool.py +213 -0
  61. coder_eval/formatting.py +191 -0
  62. coder_eval/isolation/__init__.py +15 -0
  63. coder_eval/isolation/docker_runner.py +1253 -0
  64. coder_eval/logging_config.py +399 -0
  65. coder_eval/models/__init__.py +337 -0
  66. coder_eval/models/agent_config.py +373 -0
  67. coder_eval/models/container_paths.py +26 -0
  68. coder_eval/models/criteria.py +942 -0
  69. coder_eval/models/enums.py +121 -0
  70. coder_eval/models/experiment.py +314 -0
  71. coder_eval/models/judge.py +90 -0
  72. coder_eval/models/judge_defaults.py +11 -0
  73. coder_eval/models/limits.py +89 -0
  74. coder_eval/models/merge_strategy.py +132 -0
  75. coder_eval/models/mutations.py +95 -0
  76. coder_eval/models/results.py +787 -0
  77. coder_eval/models/routing.py +160 -0
  78. coder_eval/models/sandbox.py +371 -0
  79. coder_eval/models/tasks.py +631 -0
  80. coder_eval/models/telemetry.py +454 -0
  81. coder_eval/models/templates.py +108 -0
  82. coder_eval/orchestration/__init__.py +13 -0
  83. coder_eval/orchestration/batch.py +690 -0
  84. coder_eval/orchestration/config.py +111 -0
  85. coder_eval/orchestration/config_merge.py +431 -0
  86. coder_eval/orchestration/evaluation.py +94 -0
  87. coder_eval/orchestration/experiment.py +837 -0
  88. coder_eval/orchestration/overrides.py +206 -0
  89. coder_eval/orchestration/task_loader.py +437 -0
  90. coder_eval/orchestrator.py +2078 -0
  91. coder_eval/path_utils.py +79 -0
  92. coder_eval/plugins.py +81 -0
  93. coder_eval/pricing.py +169 -0
  94. coder_eval/py.typed +0 -0
  95. coder_eval/reports.py +1066 -0
  96. coder_eval/reports_experiment.py +761 -0
  97. coder_eval/reports_html.py +1659 -0
  98. coder_eval/reports_stats.py +250 -0
  99. coder_eval/resources/__init__.py +137 -0
  100. coder_eval/resources/default_experiment.yaml +85 -0
  101. coder_eval/resources/default_ignore_patterns.yaml +60 -0
  102. coder_eval/resources/tags.yaml +55 -0
  103. coder_eval/sandbox.py +1127 -0
  104. coder_eval/scoring/__init__.py +11 -0
  105. coder_eval/scoring/ast_similarity.py +38 -0
  106. coder_eval/scoring/complexity.py +115 -0
  107. coder_eval/scoring/quality.py +154 -0
  108. coder_eval/scoring/signature_similarity.py +57 -0
  109. coder_eval/scoring/similarity.py +103 -0
  110. coder_eval/scoring/token_similarity.py +44 -0
  111. coder_eval/simulation/__init__.py +27 -0
  112. coder_eval/simulation/termination.py +88 -0
  113. coder_eval/simulation/user_simulator.py +324 -0
  114. coder_eval/streaming/__init__.py +44 -0
  115. coder_eval/streaming/callbacks.py +57 -0
  116. coder_eval/streaming/collector.py +193 -0
  117. coder_eval/streaming/events.py +198 -0
  118. coder_eval/streaming/renderers.py +248 -0
  119. coder_eval/streaming/wire.py +115 -0
  120. coder_eval/telemetry.py +407 -0
  121. coder_eval/utils.py +517 -0
  122. coder_eval-0.8.2.dist-info/METADATA +242 -0
  123. coder_eval-0.8.2.dist-info/RECORD +127 -0
  124. coder_eval-0.8.2.dist-info/WHEEL +4 -0
  125. coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
  126. coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
  127. coder_eval-0.8.2.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,337 @@
1
+ """Unified exports for all coder_eval data models.
2
+
3
+ All models can be imported from coder_eval.models regardless of
4
+ which submodule they're defined in.
5
+ """
6
+
7
+ # Agent config
8
+ from coder_eval.models.agent_config import (
9
+ AgentConfig,
10
+ AntigravityAgentConfig,
11
+ BaseAgentConfig,
12
+ ClaudeCodeAgentConfig,
13
+ CodexAgentConfig,
14
+ LocalPluginConfig,
15
+ NoneAgentConfig,
16
+ ResolvedAgentConfig,
17
+ parse_agent_config,
18
+ )
19
+
20
+ # Container paths (leaf constants; re-exported so consumers obey CE001)
21
+ from coder_eval.models.container_paths import (
22
+ CONTAINER_INPUT_DIR,
23
+ CONTAINER_OUTPUT_DIR,
24
+ CONTAINER_TASK_DIR,
25
+ CONTAINER_WORK_DIR,
26
+ RESERVED_CONTAINER_DIRS,
27
+ )
28
+
29
+ # Enums
30
+ # Criteria
31
+ from coder_eval.models.criteria import (
32
+ AgentJudgeCriterion,
33
+ BaseSuccessCriterion,
34
+ ClassificationMatchCriterion,
35
+ CommandExecutedCriterion,
36
+ CommandsEfficiencyCriterion,
37
+ FileCheckCriterion,
38
+ FileContainsCriterion,
39
+ FileExistsCriterion,
40
+ FileMatchesRegexCriterion,
41
+ JMESPathAssertion,
42
+ JsonCheckCriterion,
43
+ LLMJudgeCriterion,
44
+ ReferenceComparisonCriterion,
45
+ RegexPattern,
46
+ RunCommandCriterion,
47
+ SkillTriggeredCriterion,
48
+ SuccessCriterion,
49
+ UiPathEvalCriterion,
50
+ )
51
+ from coder_eval.models.enums import (
52
+ AgentKind,
53
+ AgentState,
54
+ ApiBackend,
55
+ FinalStatus,
56
+ PermissionMode,
57
+ PreservationMode,
58
+ )
59
+
60
+ # Experiment
61
+ from coder_eval.models.experiment import (
62
+ ExperimentDefaults,
63
+ ExperimentDefinition,
64
+ ExperimentResult,
65
+ ExperimentVariant,
66
+ ResolvedTask,
67
+ TaskExperimentSummary,
68
+ TaskResult,
69
+ VariantAggregate,
70
+ VariantResult,
71
+ )
72
+
73
+ # Judge
74
+ from coder_eval.models.judge import JudgeVerdict
75
+
76
+ # Judge defaults
77
+ from coder_eval.models.judge_defaults import DEFAULT_JUDGE_MODEL
78
+
79
+ # Limits
80
+ from coder_eval.models.limits import RunLimits
81
+
82
+ # Merge strategy
83
+ from coder_eval.models.merge_strategy import (
84
+ APPEND_ORDER_KEY,
85
+ MERGE_STRATEGY_KEY,
86
+ AppendOrder,
87
+ MergeField,
88
+ MergeStrategy,
89
+ append_order_of,
90
+ classify_annotation,
91
+ merge_strategy_of,
92
+ )
93
+
94
+ # Mutations
95
+ from coder_eval.models.mutations import (
96
+ PromptMutation,
97
+ PromptPrefix,
98
+ PromptReplace,
99
+ PromptSuffix,
100
+ PromptTemplate,
101
+ apply_prompt_mutations,
102
+ )
103
+
104
+ # Results
105
+ from coder_eval.models.results import (
106
+ ClassificationCriterionResult,
107
+ ClassLabelStats,
108
+ ConfigLineageEntry,
109
+ ConfusionEntry,
110
+ CriterionAggregate,
111
+ CriterionResult,
112
+ CriterionResultUnion,
113
+ CriterionStats,
114
+ EvaluationResult,
115
+ FailedRowSummary,
116
+ JudgeCriterionResult,
117
+ JudgeTranscript,
118
+ JudgeTranscriptToolCall,
119
+ PostRunResult,
120
+ ResultSummary,
121
+ RunSummary,
122
+ SimulationTelemetry,
123
+ SkippedTask,
124
+ SuiteRollup,
125
+ TaskConfigRecord,
126
+ ThresholdCheck,
127
+ TurnRecord,
128
+ )
129
+
130
+ # Routing
131
+ from coder_eval.models.routing import (
132
+ ROUTE_NAMES,
133
+ ApiRoute,
134
+ BedrockRoute,
135
+ DirectRoute,
136
+ JudgeTransport,
137
+ resolve_route,
138
+ to_bedrock_inference_profile,
139
+ )
140
+
141
+ # Sandbox
142
+ from coder_eval.models.sandbox import (
143
+ DockerBuildConfig,
144
+ DockerDriverConfig,
145
+ NodeEnvConfig,
146
+ PythonEnvConfig,
147
+ ResourceLimits,
148
+ SandboxConfig,
149
+ validate_template_sources_list,
150
+ )
151
+
152
+ # Tasks
153
+ from coder_eval.models.tasks import (
154
+ DEFAULT_SIMULATION_STOP_TOKEN,
155
+ CriteriaCheckTiming,
156
+ Dataset,
157
+ PostRunCommand,
158
+ PreRunCommand,
159
+ ReferenceSource,
160
+ SimulationConfig,
161
+ TaskDefinition,
162
+ )
163
+
164
+ # Telemetry
165
+ from coder_eval.models.telemetry import (
166
+ AssistantMessage,
167
+ CommandStatistics,
168
+ CommandTelemetry,
169
+ ContentBlock,
170
+ ReconciliationMessage,
171
+ SlowestCommandInfo,
172
+ TokenUsage,
173
+ TranscriptMessage,
174
+ UserMessage,
175
+ )
176
+
177
+ # Templates
178
+ from coder_eval.models.templates import (
179
+ BaseTemplateSource,
180
+ RepoSource,
181
+ StarterFile,
182
+ StarterFilesSource,
183
+ TemplateDirSource,
184
+ TemplateSource,
185
+ )
186
+
187
+
188
+ __all__ = [ # noqa: RUF022 - Keep grouped by category for readability
189
+ # Agent config
190
+ "AgentConfig",
191
+ "AntigravityAgentConfig",
192
+ "BaseAgentConfig",
193
+ "ClaudeCodeAgentConfig",
194
+ "CodexAgentConfig",
195
+ "LocalPluginConfig",
196
+ "NoneAgentConfig",
197
+ "ResolvedAgentConfig",
198
+ "parse_agent_config",
199
+ # Enums
200
+ "AgentKind",
201
+ "AgentState",
202
+ "ApiBackend",
203
+ "FinalStatus",
204
+ "PermissionMode",
205
+ "PreservationMode",
206
+ # Criteria
207
+ "BaseSuccessCriterion",
208
+ "ClassificationMatchCriterion",
209
+ "FileExistsCriterion",
210
+ "FileContainsCriterion",
211
+ "FileCheckCriterion",
212
+ "JMESPathAssertion",
213
+ "JsonCheckCriterion",
214
+ "RegexPattern",
215
+ "RunCommandCriterion",
216
+ "FileMatchesRegexCriterion",
217
+ "ReferenceComparisonCriterion",
218
+ "CommandExecutedCriterion",
219
+ "CommandsEfficiencyCriterion",
220
+ "UiPathEvalCriterion",
221
+ "LLMJudgeCriterion",
222
+ "AgentJudgeCriterion",
223
+ "SkillTriggeredCriterion",
224
+ "SuccessCriterion",
225
+ # Routing
226
+ "ROUTE_NAMES",
227
+ "ApiRoute",
228
+ "DirectRoute",
229
+ "BedrockRoute",
230
+ "JudgeTransport",
231
+ "resolve_route",
232
+ "to_bedrock_inference_profile",
233
+ # Templates
234
+ "BaseTemplateSource",
235
+ "RepoSource",
236
+ "TemplateDirSource",
237
+ "StarterFilesSource",
238
+ "StarterFile",
239
+ "TemplateSource",
240
+ # Sandbox
241
+ "DockerBuildConfig",
242
+ "CONTAINER_INPUT_DIR",
243
+ "CONTAINER_OUTPUT_DIR",
244
+ "CONTAINER_TASK_DIR",
245
+ "CONTAINER_WORK_DIR",
246
+ "RESERVED_CONTAINER_DIRS",
247
+ "DockerDriverConfig",
248
+ "NodeEnvConfig",
249
+ "PythonEnvConfig",
250
+ "SandboxConfig",
251
+ "ResourceLimits",
252
+ "validate_template_sources_list",
253
+ # Telemetry
254
+ "AssistantMessage",
255
+ "CommandTelemetry",
256
+ "CommandStatistics",
257
+ "ContentBlock",
258
+ "ReconciliationMessage",
259
+ "SlowestCommandInfo",
260
+ "TokenUsage",
261
+ "TranscriptMessage",
262
+ "UserMessage",
263
+ # Results
264
+ "ClassificationCriterionResult",
265
+ "ClassLabelStats",
266
+ "ConfigLineageEntry",
267
+ "ConfusionEntry",
268
+ "CriterionAggregate",
269
+ "CriterionResult",
270
+ "CriterionResultUnion",
271
+ "CriterionStats",
272
+ "FailedRowSummary",
273
+ "ThresholdCheck",
274
+ "JudgeCriterionResult",
275
+ "JudgeTranscript",
276
+ "JudgeTranscriptToolCall",
277
+ "PostRunResult",
278
+ "ResultSummary",
279
+ "TurnRecord",
280
+ "EvaluationResult",
281
+ "SimulationTelemetry",
282
+ "SuiteRollup",
283
+ "TaskConfigRecord",
284
+ "RunSummary",
285
+ "SkippedTask",
286
+ # Judge defaults
287
+ "DEFAULT_JUDGE_MODEL",
288
+ # Judge
289
+ "JudgeVerdict",
290
+ # Limits
291
+ "RunLimits",
292
+ # Merge strategy
293
+ "MERGE_STRATEGY_KEY",
294
+ "APPEND_ORDER_KEY",
295
+ "AppendOrder",
296
+ "MergeField",
297
+ "MergeStrategy",
298
+ "append_order_of",
299
+ "classify_annotation",
300
+ "merge_strategy_of",
301
+ # Tasks
302
+ "TaskDefinition",
303
+ "DEFAULT_SIMULATION_STOP_TOKEN",
304
+ "CriteriaCheckTiming",
305
+ "Dataset",
306
+ "PostRunCommand",
307
+ "PreRunCommand",
308
+ "ReferenceSource",
309
+ "SimulationConfig",
310
+ # Mutations
311
+ "PromptPrefix",
312
+ "PromptSuffix",
313
+ "PromptReplace",
314
+ "PromptTemplate",
315
+ "PromptMutation",
316
+ "apply_prompt_mutations",
317
+ # Experiment
318
+ "ExperimentDefaults",
319
+ "ExperimentDefinition",
320
+ "ExperimentResult",
321
+ "ExperimentVariant",
322
+ "ResolvedTask",
323
+ "TaskExperimentSummary",
324
+ "TaskResult",
325
+ "VariantAggregate",
326
+ "VariantResult",
327
+ ]
328
+
329
+ # Type aliases (forward compatible)
330
+ # Typed as the discriminated union so callers that iterate the result list
331
+ # and use ``isinstance(cr, JudgeCriterionResult)`` get the precise variant
332
+ # membership. The runtime objects are concrete subclasses regardless; the
333
+ # alias change is purely a type-checking precision fix that mirrors the
334
+ # ``EvaluationResult.success_criteria_results`` field type.
335
+ type CriteriaResults = list[CriterionResultUnion]
336
+ type SuccessCriteria = list[SuccessCriterion]
337
+ type TurnRecords = list[TurnRecord]
@@ -0,0 +1,373 @@
1
+ """Agent configuration model and SDK pass-through validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from typing import Annotated, Any, ClassVar, Literal, Self, TypedDict
7
+
8
+ from claude_agent_sdk import ClaudeAgentOptions
9
+ from pydantic import (
10
+ AliasChoices,
11
+ BaseModel,
12
+ BeforeValidator,
13
+ ConfigDict,
14
+ Field,
15
+ SerializeAsAny,
16
+ field_validator,
17
+ model_validator,
18
+ )
19
+
20
+ from coder_eval.models.enums import AgentKind, PermissionMode
21
+ from coder_eval.models.merge_strategy import MergeField
22
+
23
+
24
+ type SettingSource = Literal["user", "project", "local"]
25
+ """Vendor-neutral mirror of claude_agent_sdk.SettingSource (no SDK dependency)."""
26
+
27
+
28
+ class LocalPluginConfig(TypedDict):
29
+ """Vendor-neutral local plugin/skills source: a directory the agent scans for skills.
30
+
31
+ Mirrors the runtime shape of claude_agent_sdk.SdkPluginConfig but carries no SDK
32
+ dependency, so the agnostic BaseAgentConfig can declare ``plugins`` without leaking a
33
+ Claude-Code type onto Codex / NoOp configs. Entries remain plain dicts at runtime
34
+ (TypedDict), so all consumers — Codex skill discovery, docker_runner auto-mount,
35
+ utils.process_plugins, and the Claude SDK pass-through — are unchanged.
36
+ """
37
+
38
+ type: Literal["local"]
39
+ path: str
40
+
41
+
42
+ _VALID_SDK_OPTION_FIELDS: frozenset[str] = frozenset(f.name for f in dataclasses.fields(ClaudeAgentOptions))
43
+ # Keys that `coder_eval` already owns at the AgentConfig level OR that are
44
+ # transport / lifecycle / security-critical. Setting them via the
45
+ # sdk_options pass-through would either silently shadow a typed field, let
46
+ # the user inject pre-LLM lifecycle hooks (hooks / mcp_servers /
47
+ # permission_prompt_tool_name / can_use_tool / agents), or bypass
48
+ # framework-managed runtime state (cwd / env / resume / max_turns / ...).
49
+ # Most relevantly: AgentJudgeCriterion forces setting_sources=[] for
50
+ # security; allowing `hooks` through sdk_options would re-open that hole.
51
+ # NOTE on the allow/deny model: validation works as ALLOW iff
52
+ # (key in _VALID_SDK_OPTION_FIELDS) AND (key not in _FRAMEWORK_OWNED_SDK_FIELDS)
53
+ # i.e. the user-visible set is ``_VALID - _FRAMEWORK_OWNED``. The denylist is
54
+ # explicit so the curated rationale (transport / lifecycle / security / typed-
55
+ # mirror) stays close to the code. To keep this from being fail-open as the
56
+ # SDK grows: ``tests/test_sdk_option_classification.py`` asserts EVERY field
57
+ # on ``ClaudeAgentOptions`` is classified — either typed-mirrored or in
58
+ # ``_FRAMEWORK_OWNED_SDK_FIELDS``. A new SDK release adding an unclassified
59
+ # field will fail that test loudly rather than silently being passed through.
60
+ _FRAMEWORK_OWNED_SDK_FIELDS: frozenset[str] = frozenset(
61
+ {
62
+ # mirrored as typed AgentConfig fields:
63
+ "model",
64
+ "permission_mode",
65
+ "allowed_tools",
66
+ "disallowed_tools",
67
+ "plugins",
68
+ "system_prompt",
69
+ "system_prompt_file",
70
+ "settings",
71
+ # transport / runtime — set by the agent, not the user:
72
+ "cwd",
73
+ "env",
74
+ "stderr",
75
+ "debug_stderr",
76
+ "resume",
77
+ "max_turns",
78
+ "session_id",
79
+ "session_store",
80
+ "session_store_flush",
81
+ # session lifecycle — coder_eval owns this via `resume` and the
82
+ # orchestrator's "advance session_id only on clean turns" logic.
83
+ # Letting YAML override would silently bypass that.
84
+ "continue_conversation",
85
+ "fork_session",
86
+ # budgeting — overlaps with RunLimits.max_usd / RunLimits.max_total_tokens
87
+ # which the orchestrator enforces with explicit FinalStatus codes
88
+ # (TOKEN_BUDGET_EXCEEDED / COST_BUDGET_EXCEEDED). Two independent
89
+ # budget guards would disagree on counts; route everything through
90
+ # RunLimits.
91
+ "max_budget_usd",
92
+ "task_budget",
93
+ # security-critical: arbitrary code injection or settings-bypass
94
+ # surfaces. Keep out of the YAML-visible knob.
95
+ "hooks",
96
+ "mcp_servers",
97
+ "cli_path",
98
+ "extra_args",
99
+ "agents",
100
+ "can_use_tool",
101
+ "permission_prompt_tool_name",
102
+ "tools",
103
+ "sandbox",
104
+ "skills",
105
+ "add_dirs",
106
+ "setting_sources", # framework-controlled to prevent hook injection
107
+ # telemetry: required by ClaudeCodeAgent to recover per-emission
108
+ # output_tokens via message_delta stream events (works around
109
+ # anthropics/claude-code#22686 where the assistant event's
110
+ # output_tokens is a partial streaming snapshot). Letting YAML
111
+ # turn it off would silently drop per-message output accounting.
112
+ "include_partial_messages",
113
+ }
114
+ )
115
+ # Precomputed user-visible allowlist (= valid SDK fields minus framework-owned).
116
+ # Pre-sorted once at module load so error paths don't recompute it.
117
+ _USER_VISIBLE_SDK_FIELDS: tuple[str, ...] = tuple(sorted(_VALID_SDK_OPTION_FIELDS - _FRAMEWORK_OWNED_SDK_FIELDS))
118
+
119
+
120
+ class BaseAgentConfig(BaseModel):
121
+ """Base configuration for all agent types."""
122
+
123
+ model_config = ConfigDict(validate_assignment=True, populate_by_name=True, extra="forbid")
124
+
125
+ # Cross-field merge exclusion: setting either prompt field at any layer clears
126
+ # the sibling (the generic resolver honors this uniformly). ClassVar -> not a
127
+ # model field; Pydantic does not validate/assign it.
128
+ _merge_exclusive_groups: ClassVar[tuple[tuple[str, ...], ...]] = (("system_prompt", "system_prompt_file"),)
129
+
130
+ type: str | None = Field(
131
+ default=None,
132
+ description=(
133
+ "The type of agent to use (claude-code, codex, or any plugin-registered kind). "
134
+ "May be omitted on the task and supplied via experiment defaults or --type. "
135
+ "Validated against the agent registry by parse_agent_config / task resolution."
136
+ ),
137
+ )
138
+ model: str | None = Field(default=None, description="Specific model to use (if applicable)")
139
+ permission_mode: PermissionMode = Field(
140
+ default=PermissionMode.ACCEPT_EDITS, description="Permission mode for agent actions"
141
+ )
142
+ allowed_tools: list[str] | None = MergeField(
143
+ strategy="replace", default=None, description="List of allowed tools (e.g., ['Read', 'Write', 'Bash'])"
144
+ )
145
+ disallowed_tools: list[str] | None = MergeField(
146
+ strategy="replace", default=None, description="List of disallowed tools (e.g., ['TodoWrite'])"
147
+ )
148
+ plugins: list[LocalPluginConfig] | None = MergeField(
149
+ strategy="replace", default=None, description="List of plugins (local skills/plugin sources)"
150
+ )
151
+ system_prompt: str | None = Field(
152
+ default=None,
153
+ description=(
154
+ "Custom system prompt. Replaces the default system prompt. "
155
+ "Supports inline text or multi-line YAML strings. "
156
+ "Mutually exclusive with system_prompt_file."
157
+ ),
158
+ )
159
+ system_prompt_file: str | None = Field(
160
+ default=None,
161
+ description=(
162
+ "Path to a file containing the system prompt (relative to task YAML). "
163
+ "The file contents are loaded at task resolution time and set as system_prompt. "
164
+ "Mutually exclusive with system_prompt."
165
+ ),
166
+ )
167
+
168
+ # Customizable ignore patterns for file tracking
169
+ ignore_patterns: list[str] = MergeField(
170
+ strategy="replace",
171
+ default_factory=list,
172
+ description=(
173
+ "Pattern overrides applied when copying the workspace into a judge "
174
+ "sub-agent sandbox. Plain entries add to the defaults; entries "
175
+ "prefixed with '!' remove a default (gitignore-style negation)."
176
+ ),
177
+ validation_alias=AliasChoices("ignore_patterns", "additional_ignore_patterns"),
178
+ )
179
+
180
+ @field_validator("ignore_patterns")
181
+ @classmethod
182
+ def _validate_ignore_patterns(cls, values: list[str]) -> list[str]:
183
+ from coder_eval.resources import normalize_ignore_pattern_entry
184
+
185
+ return [normalize_ignore_pattern_entry(v) for v in values]
186
+
187
+ @model_validator(mode="after")
188
+ def check_prompt_exclusivity(self) -> Self:
189
+ """Ensure system_prompt and system_prompt_file are mutually exclusive."""
190
+ if self.system_prompt is not None and self.system_prompt_file is not None:
191
+ raise ValueError("Only one of 'system_prompt' or 'system_prompt_file' can be provided, not both")
192
+ return self
193
+
194
+
195
+ class ClaudeCodeAgentConfig(BaseAgentConfig):
196
+ """Claude Code agent configuration."""
197
+
198
+ type: Literal[AgentKind.CLAUDE_CODE] # type: ignore[assignment]
199
+
200
+ claude_settings: str | dict[str, Any] | None = MergeField(
201
+ strategy="deep",
202
+ default=None,
203
+ description=(
204
+ "Claude Code settings passed via --settings. Accepts a JSON-serializable dict "
205
+ "(inlined) or a file path string. Use permissions.deny to block tool access to "
206
+ 'specific paths: {"permissions": {"deny": ["Read(/some/path/**)"]}}. '
207
+ "Merged deeply across config layers when both sides are dicts; a str/None value replaces."
208
+ ),
209
+ )
210
+ sdk_options: dict[str, Any] = Field(
211
+ default_factory=dict,
212
+ description=(
213
+ "Pass-through dict of Claude Code SDK ClaudeAgentOptions fields that coder_eval "
214
+ "does not own directly (e.g. 'effort'). Keys must be valid ClaudeAgentOptions "
215
+ "fields and must NOT be framework-managed (model/allowed_tools/permission_mode/...). "
216
+ "Validated at YAML load; values are forwarded verbatim to the SDK."
217
+ ),
218
+ )
219
+ setting_sources: list[SettingSource] | None = MergeField(
220
+ strategy="replace",
221
+ default=None,
222
+ description=(
223
+ "Claude Code setting sources to load (e.g., ['project', 'user']). "
224
+ "Set to [] for maximum isolation (no host settings or hooks) — used by judge agents and simulators. "
225
+ "Defaults to None, which at runtime becomes ['project'] so .mcp.json is discovered. "
226
+ "Users may override this value for custom setting loading behavior."
227
+ ),
228
+ )
229
+
230
+ @field_validator("sdk_options")
231
+ @classmethod
232
+ def _validate_sdk_options_keys(cls, v: dict[str, Any]) -> dict[str, Any]:
233
+ if not v:
234
+ return v
235
+ for key in v:
236
+ if key not in _VALID_SDK_OPTION_FIELDS:
237
+ raise ValueError(
238
+ f"sdk_options key {key!r} is not a ClaudeAgentOptions field "
239
+ + f"(valid keys: {list(_USER_VISIBLE_SDK_FIELDS)})"
240
+ )
241
+ if key in _FRAMEWORK_OWNED_SDK_FIELDS:
242
+ raise ValueError(
243
+ f"sdk_options key {key!r} is framework-managed; set it as a top-level AgentConfig field instead"
244
+ )
245
+ return v
246
+
247
+
248
+ class CodexAgentConfig(BaseAgentConfig):
249
+ """Codex agent configuration."""
250
+
251
+ type: Literal[AgentKind.CODEX] # type: ignore[assignment]
252
+
253
+
254
+ # Gemini "thinking level" (reasoning effort) for the Antigravity backend. Mirrors
255
+ # google.antigravity.types.ThinkingLevel as a plain Literal so this config module
256
+ # imports without the optional `google-antigravity` SDK installed (the SDK is an
257
+ # opt-in extra; base installs must still load every config class).
258
+ type ThinkingLevel = Literal["minimal", "low", "medium", "high"]
259
+
260
+
261
+ class AntigravityAgentConfig(BaseAgentConfig):
262
+ """Antigravity agent configuration (Google's Gemini coding agent harness).
263
+
264
+ Runs via the ``google-antigravity`` SDK's local harness, authenticated with
265
+ ``GEMINI_API_KEY``. ``model`` defaults (when unset on the task / ``--model`` /
266
+ ``ANTIGRAVITY_MODEL``) to the recommended Gemini 3.1 Pro coding model
267
+ (``gemini-3.1-pro-preview``).
268
+ """
269
+
270
+ type: Literal[AgentKind.ANTIGRAVITY] # type: ignore[assignment]
271
+
272
+ thinking_level: ThinkingLevel = Field(
273
+ default="medium",
274
+ description=(
275
+ "Gemini reasoning effort (minimal/low/medium/high). 'medium' is Google's recommended "
276
+ "daily-driver default — the API otherwise defaults to the more expensive 'high'."
277
+ ),
278
+ )
279
+
280
+
281
+ class NoneAgentConfig(BaseAgentConfig):
282
+ """No-op ("agentless") agent configuration.
283
+
284
+ Selected with ``agent: {type: none}``. Binds to ``NoOpAgent`` (Null Object
285
+ pattern): coder-eval sets up the sandbox, runs ``pre_run``, and checks the
286
+ success_criteria directly — the agent's ``start``/``communicate``/``stop``
287
+ are no-ops and no model API call is made. Use for system / canary checks
288
+ (e.g. Orchestrator or Integration Service connectivity) that reuse the eval
289
+ infrastructure (sandbox, reports, evalboard, ADX) without involving an agent.
290
+
291
+ The task must declare no ``initial_prompt`` / ``initial_prompt_file`` and no
292
+ ``simulation`` (no agent reads them), and every criterion must be
293
+ agent-independent (no ``requires_agent`` criteria) — enforced by
294
+ ``TaskDefinition.check_none_agent``. The inherited ``model`` / prompt /
295
+ tool fields are accepted but ignored.
296
+ """
297
+
298
+ type: Literal[AgentKind.NONE] # type: ignore[assignment]
299
+
300
+
301
+ # Discriminated union type for type hints, validation, and YAML serialization
302
+ # Only includes the concrete subclasses (not BaseAgentConfig) since the discriminator
303
+ # must be a Literal type. BaseAgentConfig is returned by parse_agent_config when type=None.
304
+ type AgentConfig = Annotated[
305
+ ClaudeCodeAgentConfig | CodexAgentConfig | AntigravityAgentConfig | NoneAgentConfig,
306
+ Field(discriminator="type"),
307
+ ]
308
+
309
+
310
+ def parse_agent_config(**kwargs: Any) -> BaseAgentConfig:
311
+ """Factory function for agent configuration with registry-driven dispatch.
312
+
313
+ Routes to the config class the agent registry binds to the ``type`` kind —
314
+ ``ClaudeCodeAgentConfig`` / ``CodexAgentConfig`` / ``NoneAgentConfig`` for the
315
+ built-ins, or any plugin-registered config subclass. If ``type`` is None (or
316
+ omitted) returns a bare ``BaseAgentConfig`` so type resolution can happen
317
+ later at the experiment / CLI layer.
318
+
319
+ This replaces the import-time ``TypeAdapter(AgentConfig)`` discriminated-union
320
+ dispatch (which could only ever see the built-in kinds) with a per-kind
321
+ lookup resolved *after* plugin load — the BYOA seam.
322
+
323
+ Args:
324
+ **kwargs: Configuration fields including the ``type`` kind.
325
+
326
+ Returns:
327
+ The registered config subclass for ``type``, or ``BaseAgentConfig`` when
328
+ ``type`` is None.
329
+
330
+ Raises:
331
+ ValueError: If ``type`` is a kind no agent is registered for.
332
+ ValidationError: If configuration values are invalid for the config class.
333
+
334
+ Example:
335
+ >>> cfg = parse_agent_config(type="claude-code", model="claude-opus-4-7")
336
+ >>> isinstance(cfg, ClaudeCodeAgentConfig)
337
+ True
338
+ """
339
+ from coder_eval.agents.registry import AgentRegistry
340
+ from coder_eval.plugins import ensure_plugins_loaded
341
+
342
+ agent_type = kwargs.get("type")
343
+
344
+ if agent_type is None:
345
+ # No type specified - return BaseAgentConfig with type=None
346
+ # This allows type resolution to happen at the experiment/CLI layer
347
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k != "type"}
348
+ return BaseAgentConfig(**filtered_kwargs)
349
+
350
+ ensure_plugins_loaded()
351
+ registration = AgentRegistry.get(agent_type)
352
+ if registration is None:
353
+ raise AgentRegistry.unregistered_kind_error(agent_type)
354
+ return registration.config_class.model_validate(kwargs)
355
+
356
+
357
+ def _coerce_agent_config(value: Any) -> Any:
358
+ """Coerce a raw agent-config dict to its registered subclass via the registry.
359
+
360
+ Already-built config instances (and ``None``) pass through untouched.
361
+ """
362
+ if isinstance(value, dict):
363
+ return parse_agent_config(**value)
364
+ return value
365
+
366
+
367
+ # The canonical annotation for any field that holds a resolved agent config.
368
+ # BeforeValidator routes a dict through registry dispatch (so plugin kinds resolve
369
+ # to their subclass); SerializeAsAny keeps subclass-only fields on model_dump()
370
+ # instead of the base schema silently dropping them. Used by every persisted
371
+ # agent-config field (TaskDefinition.agent, EvaluationResult.agent_config) so the
372
+ # round-trip guarantee is uniform across the built-in union and plugin kinds.
373
+ type ResolvedAgentConfig = Annotated[SerializeAsAny[BaseAgentConfig], BeforeValidator(_coerce_agent_config)]