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,132 @@
1
+ """Per-field merge-strategy marker + reader.
2
+
3
+ A single declarative mechanism for how a config field merges across the 5
4
+ resolution layers (default -> experiment-defaults -> task -> variant -> CLI).
5
+ The strategy is stored on the Pydantic ``FieldInfo`` via ``json_schema_extra``
6
+ and read back by :func:`merge_strategy_of`.
7
+
8
+ A field WITHOUT an explicit ``MergeField`` annotation falls back to a
9
+ type-aware default (:func:`_default_strategy_for`): a nested ``BaseModel`` or a
10
+ free-form ``dict`` field merges ``deep``; a ``list`` or scalar field merges
11
+ ``replace``. ``MergeField(strategy=...)`` is only for a *deliberate* override of
12
+ that default — most commonly a ``list`` that should ``append`` rather than
13
+ ``replace`` (the one genuinely ambiguous case, enforced by lint rule CE014).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import types
19
+ from typing import Annotated, Any, Literal, Union, get_args, get_origin
20
+
21
+ from pydantic import BaseModel, Field
22
+ from pydantic.fields import FieldInfo
23
+
24
+
25
+ MERGE_STRATEGY_KEY = "x_merge_strategy"
26
+ """``json_schema_extra`` key under which a field's explicit merge strategy lives."""
27
+
28
+ APPEND_ORDER_KEY = "x_merge_append_order"
29
+ """``json_schema_extra`` key for an ``append`` field's contribution order."""
30
+
31
+ MergeStrategy = Literal["deep", "append", "replace"]
32
+ """``deep`` = recurse (per-field for nested models, recursive-dict for free-form
33
+ dicts); ``append`` = concatenate lists in layer order; ``replace`` = last layer wins."""
34
+
35
+ AppendOrder = Literal["forward", "reverse"]
36
+ """For ``append`` fields: ``forward`` (default) concatenates in layer order
37
+ (lower precedence first); ``reverse`` puts each higher layer's items FIRST (used
38
+ by ``post_run``, where experiment-defaults cleanup must run after the task's)."""
39
+
40
+
41
+ def MergeField(*, strategy: MergeStrategy, append_order: AppendOrder = "forward", **kwargs: Any) -> Any: # noqa: N802 — Field-style factory
42
+ """A ``Field(...)`` that also records an explicit merge ``strategy``.
43
+
44
+ ``strategy`` is REQUIRED — ``MergeField`` exists only to override the
45
+ type-aware default. A field that wants the default uses a plain ``Field``.
46
+ ``append_order`` only applies to ``strategy="append"`` (``"reverse"`` flips
47
+ the contribution order). Any caller-supplied ``json_schema_extra`` is merged,
48
+ not clobbered.
49
+ """
50
+ extra = dict(kwargs.pop("json_schema_extra", {}) or {})
51
+ extra[MERGE_STRATEGY_KEY] = strategy
52
+ if append_order != "forward":
53
+ extra[APPEND_ORDER_KEY] = append_order
54
+ return Field(json_schema_extra=extra, **kwargs)
55
+
56
+
57
+ def append_order_of(field_info: FieldInfo) -> AppendOrder:
58
+ """Return an ``append`` field's contribution order (default ``forward``)."""
59
+ extra = field_info.json_schema_extra
60
+ if isinstance(extra, dict) and APPEND_ORDER_KEY in extra:
61
+ order = extra[APPEND_ORDER_KEY]
62
+ # The value is written only by MergeField (its `append_order` param is
63
+ # typed AppendOrder), but guard a hand-written
64
+ # `Field(json_schema_extra={...})` with a bogus value so it fails loud
65
+ # here instead of silently mis-ordering an append.
66
+ if order not in get_args(AppendOrder):
67
+ raise ValueError(f"invalid append order {order!r}; expected one of {get_args(AppendOrder)}")
68
+ return order # type: ignore[return-value] # validated against AppendOrder above
69
+ return "forward"
70
+
71
+
72
+ def merge_strategy_of(field_info: FieldInfo) -> MergeStrategy:
73
+ """Return the field's explicit strategy if annotated, else the type-aware default."""
74
+ extra = field_info.json_schema_extra
75
+ if isinstance(extra, dict) and MERGE_STRATEGY_KEY in extra:
76
+ strategy = extra[MERGE_STRATEGY_KEY]
77
+ # The value is written only by MergeField (its `strategy` param is typed
78
+ # MergeStrategy) and CE014 enforces that every list field declares one,
79
+ # but guard a hand-written `Field(json_schema_extra={...})` with a bogus
80
+ # value so it fails loud here instead of silently falling through to
81
+ # "replace" in the merge engine.
82
+ if strategy not in get_args(MergeStrategy):
83
+ raise ValueError(f"invalid merge strategy {strategy!r}; expected one of {get_args(MergeStrategy)}")
84
+ return strategy # type: ignore[return-value] # validated against MergeStrategy above
85
+ return _default_strategy_for(field_info.annotation)
86
+
87
+
88
+ def _default_strategy_for(annotation: Any) -> MergeStrategy:
89
+ """Type-aware default: nested ``BaseModel`` / free-form ``dict`` -> ``deep``;
90
+ ``list`` / scalar -> ``replace``. Unwraps ``Optional`` / ``Annotated`` / union.
91
+ """
92
+ models, is_free_form_dict = classify_annotation(annotation)
93
+ return "deep" if (models or is_free_form_dict) else "replace"
94
+
95
+
96
+ def classify_annotation(annotation: Any) -> tuple[list[type[BaseModel]], bool]:
97
+ """Return ``(nested_model_types, is_free_form_dict)`` for a field annotation.
98
+
99
+ Unwraps ``Optional`` / ``Annotated`` / unions and collects directly-nested
100
+ ``BaseModel`` subclasses (so a resolver can descend into them). A ``dict[...]``
101
+ origin sets the free-form flag (descent stops there; any key is accepted).
102
+ ``list[...]`` and scalars contribute neither — they are leaves.
103
+ """
104
+ if annotation is None:
105
+ return [], False
106
+
107
+ if hasattr(annotation, "__value__"): # PEP 695 ``type X = ...`` alias
108
+ annotation = annotation.__value__
109
+ if get_origin(annotation) is Annotated:
110
+ annotation = get_args(annotation)[0]
111
+
112
+ origin = get_origin(annotation)
113
+ args = get_args(annotation) if origin in (Union, types.UnionType) else (annotation,)
114
+
115
+ models: list[type[BaseModel]] = []
116
+ is_free_form_dict = False
117
+ for arg in args:
118
+ if hasattr(arg, "__value__"):
119
+ arg = arg.__value__
120
+ if get_origin(arg) is Annotated:
121
+ arg = get_args(arg)[0]
122
+ arg_origin = get_origin(arg)
123
+ if arg_origin is dict or arg is dict: # parameterized ``dict[...]`` or the bare ``dict``
124
+ is_free_form_dict = True
125
+ elif arg_origin in (Union, types.UnionType):
126
+ sub_models, sub_dict = classify_annotation(arg)
127
+ models.extend(sub_models)
128
+ is_free_form_dict = is_free_form_dict or sub_dict
129
+ elif arg_origin is None and isinstance(arg, type) and issubclass(arg, BaseModel):
130
+ models.append(arg)
131
+ # list / other origins -> leaf, ignored
132
+ return models, is_free_form_dict
@@ -0,0 +1,95 @@
1
+ """Prompt mutation models and application function.
2
+
3
+ Prompt mutations are ordered transforms applied to a task's base initial_prompt
4
+ at variant resolution time. They enable A/B testing of prompt phrasing without
5
+ duplicating task definitions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Annotated, Literal
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+
16
+ class PromptPrefix(BaseModel):
17
+ """Prepend text to the prompt."""
18
+
19
+ model_config = ConfigDict(extra="forbid")
20
+
21
+ type: Literal["prefix"] = "prefix"
22
+ content: str = Field(description="Text to prepend before the base prompt")
23
+ separator: str = Field(default="\n\n", description="Separator between prefix and base prompt")
24
+
25
+
26
+ class PromptSuffix(BaseModel):
27
+ """Append text to the prompt."""
28
+
29
+ model_config = ConfigDict(extra="forbid")
30
+
31
+ type: Literal["suffix"] = "suffix"
32
+ content: str = Field(description="Text to append after the base prompt")
33
+ separator: str = Field(default="\n\n", description="Separator between base prompt and suffix")
34
+
35
+
36
+ class PromptReplace(BaseModel):
37
+ """Find and replace text in the prompt."""
38
+
39
+ model_config = ConfigDict(extra="forbid")
40
+
41
+ type: Literal["replace"] = "replace"
42
+ pattern: str = Field(description="Text or regex pattern to find")
43
+ replacement: str = Field(description="Replacement text")
44
+ regex: bool = Field(default=False, description="Whether pattern is a regular expression")
45
+
46
+
47
+ class PromptTemplate(BaseModel):
48
+ """Substitute template variables in the prompt using {variable_name} syntax."""
49
+
50
+ model_config = ConfigDict(extra="forbid")
51
+
52
+ type: Literal["template"] = "template"
53
+ variables: dict[str, str] = Field(description="Mapping of variable names to values")
54
+
55
+
56
+ PromptMutation = Annotated[
57
+ PromptPrefix | PromptSuffix | PromptReplace | PromptTemplate,
58
+ Field(discriminator="type"),
59
+ ]
60
+
61
+
62
+ def apply_prompt_mutations(
63
+ base_prompt: str,
64
+ mutations: list[PromptMutation],
65
+ ) -> str:
66
+ """Apply an ordered list of mutations to a base prompt string.
67
+
68
+ Mutations are applied sequentially — each operates on the result of the previous.
69
+
70
+ Args:
71
+ base_prompt: The original prompt text.
72
+ mutations: Ordered list of mutation operations.
73
+
74
+ Returns:
75
+ The transformed prompt string.
76
+
77
+ Raises:
78
+ re.error: If a regex replace has an invalid pattern.
79
+ """
80
+ result = base_prompt
81
+ for m in mutations:
82
+ match m:
83
+ case PromptPrefix():
84
+ result = m.content + m.separator + result
85
+ case PromptSuffix():
86
+ result = result + m.separator + m.content
87
+ case PromptReplace():
88
+ if m.regex:
89
+ result = re.sub(m.pattern, m.replacement, result)
90
+ else:
91
+ result = result.replace(m.pattern, m.replacement)
92
+ case PromptTemplate():
93
+ for key, val in m.variables.items():
94
+ result = result.replace(f"{{{key}}}", val)
95
+ return result