co-agent-coder 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. co_agent_coder-0.1.0.dist-info/METADATA +396 -0
  2. co_agent_coder-0.1.0.dist-info/RECORD +51 -0
  3. co_agent_coder-0.1.0.dist-info/WHEEL +5 -0
  4. co_agent_coder-0.1.0.dist-info/entry_points.txt +5 -0
  5. co_agent_coder-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. co_agent_coder-0.1.0.dist-info/top_level.txt +1 -0
  7. coagent/__init__.py +84 -0
  8. coagent/__main__.py +6 -0
  9. coagent/agent/__init__.py +27 -0
  10. coagent/agent/budget.py +220 -0
  11. coagent/agent/loop.py +1102 -0
  12. coagent/agent/prompts.py +228 -0
  13. coagent/agent/state.py +181 -0
  14. coagent/cli.py +351 -0
  15. coagent/config.py +307 -0
  16. coagent/context/__init__.py +26 -0
  17. coagent/context/artifacts.py +431 -0
  18. coagent/context/repo_map.py +590 -0
  19. coagent/context/retrieval.py +375 -0
  20. coagent/context/tokens.py +93 -0
  21. coagent/evals/__init__.py +0 -0
  22. coagent/gitx/__init__.py +52 -0
  23. coagent/gitx/commit.py +91 -0
  24. coagent/gitx/durable.py +147 -0
  25. coagent/gitx/git.py +351 -0
  26. coagent/gitx/lock.py +157 -0
  27. coagent/gitx/log.py +85 -0
  28. coagent/gitx/resume.py +199 -0
  29. coagent/gitx/session.py +233 -0
  30. coagent/llm/__init__.py +83 -0
  31. coagent/llm/base.py +235 -0
  32. coagent/llm/grammar.py +306 -0
  33. coagent/llm/registry.py +147 -0
  34. coagent/llm/transformers_backend.py +326 -0
  35. coagent/py.typed +0 -0
  36. coagent/safety/__init__.py +24 -0
  37. coagent/safety/denylist.py +146 -0
  38. coagent/safety/policy.py +219 -0
  39. coagent/safety/redact.py +146 -0
  40. coagent/templates/config.toml +104 -0
  41. coagent/tools/__init__.py +155 -0
  42. coagent/tools/artifacts.py +307 -0
  43. coagent/tools/base.py +226 -0
  44. coagent/tools/fs.py +507 -0
  45. coagent/tools/notebooks.py +875 -0
  46. coagent/tools/pyt.py +240 -0
  47. coagent/tools/shell.py +558 -0
  48. coagent/ui/__init__.py +50 -0
  49. coagent/ui/gradio_app.py +353 -0
  50. coagent/ui/notebook.py +342 -0
  51. coagent/ui/render.py +422 -0
coagent/tools/pyt.py ADDED
@@ -0,0 +1,240 @@
1
+ """Parse pytest output into what the model actually needs (DESIGN §7.5).
2
+
3
+ "Only the failing parts are fed back — test names plus the last 40 lines of each
4
+ traceback, capped at ~1500 tokens." A 500-line pytest dump is worse than no
5
+ output: it evicts the context the model needs to fix the failure.
6
+
7
+ The parser is the unit under test here: pure text in, structure out, no process
8
+ and no git. `run_tests` in `tools/shell.py` is a thin shell over it.
9
+
10
+ What pytest actually emits (sections, in order)
11
+ -----------------------------------------------
12
+ ````
13
+ =========================== FAILURES ============================
14
+ ____________________________ test_deep _____________________________
15
+
16
+ def test_deep():
17
+ > helper(3)
18
+
19
+ test_sample.py:5:
20
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
21
+ x = 3
22
+
23
+ def helper(x):
24
+ > raise KeyError("missing")
25
+ E KeyError: 'missing'
26
+
27
+ test_sample.py:2: KeyError
28
+ =========================== short test summary info ===========================
29
+ FAILED test_sample.py::test_deep - KeyError: 'missing'
30
+ 1 failed, 1 passed in 0.08s
31
+ ````
32
+
33
+ Two details worth stating because they are easy to get wrong, and I got them
34
+ wrong first: a failing *top-level test function* has no `file.py:N: in name`
35
+ header at all — the frame footer is `file.py:N: ExceptionType` with an empty
36
+ function name — and the actionable part is the `> ` source pointer plus the
37
+ `E ` error block, not the `def` body around it. Selecting the `E` lines and the
38
+ pointer, rather than a fixed number of trailing lines, is what makes the digest
39
+ survive a traceback that is mostly library frames.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import re
45
+ from dataclasses import dataclass, field
46
+
47
+ __all__ = [
48
+ "ParseResult",
49
+ "is_failure_output",
50
+ "parse_pytest_output",
51
+ "summarise_pytest",
52
+ ]
53
+
54
+ #: e.g. `FAILED tests/test_x.py::test_it - AssertionError: nope`
55
+ _FAILED_LINE = re.compile(r"^(?P<status>FAILED|ERROR)\s+(?P<nodeid>\S+?)(?:\s+-\s+(?P<reason>.*))?$")
56
+ #: Section banner naming one failure: `_____ test_name _____`
57
+ _FAILURE_HEADER = re.compile(r"^_{3,}\s*(?P<name>.+?)\s*_{3,}$")
58
+ #: The `>` source pointer pytest marks the failing line with.
59
+ _POINTER = re.compile(r"^>\s*(?P<code>\S.*)$")
60
+ #: Our own excepted lines: `E KeyError: 'x'`
61
+ _ERROR_LINE = re.compile(r"^E\s{1,4}(?P<text>.*)$")
62
+ #: A frame footer: `test_sample.py:5: ` or `test_sample.py:2: KeyError`
63
+ _FRAME_FOOTER = re.compile(r"^(?P<path>[\w./\\-]+\.py):(?P<line>\d+):\s*(?P<exc>.*)$")
64
+ #: Separator between traceback frames.
65
+ _FRAME_RULE = re.compile(r"^_[\s_]+$")
66
+
67
+
68
+ @dataclass(slots=True)
69
+ class ParseResult:
70
+ """Structured pytest output."""
71
+
72
+ failed_names: list[str] = field(default_factory=list)
73
+ #: One entry per failing test: pointer lines, error lines, and the final
74
+ #: frame footer (`file.py:12: TypeError`).
75
+ traces: list[str] = field(default_factory=list)
76
+ summary_lines: list[str] = field(default_factory=list)
77
+ pytest_version: str = ""
78
+ duration_s: float = 0.0
79
+
80
+ @property
81
+ def failures(self) -> int:
82
+ return len(self.failed_names)
83
+
84
+ @property
85
+ def passed(self) -> int:
86
+ for line in self.summary_lines:
87
+ m = re.search(r"(\d+)\s+passed", line)
88
+ if m:
89
+ return int(m.group(1))
90
+ return 0
91
+
92
+ def render(self, *, cap: int = 1500) -> str:
93
+ """Assemble the model-facing digest within the §7.5 budget.
94
+
95
+ Order matters and is deliberate: the verdict first (it is the answer to
96
+ "did it pass"), then the failing names (the map), then the evidence.
97
+ """
98
+ blocks: list[str] = []
99
+ summary = " ".join(s.strip() for s in self.summary_lines if "short test summary" not in s)
100
+ summary = re.sub(r"[=\s]{3,}", " ", summary).strip()
101
+ if summary:
102
+ blocks.append(summary)
103
+ if self.failed_names:
104
+ blocks.append("failing: " + ", ".join(self.failed_names[:20]))
105
+ return _fit(blocks, self.traces, cap)
106
+
107
+
108
+ def is_failure_output(text: str) -> bool:
109
+ """True when `text` looks like a pytest failure report."""
110
+ return bool(_FAILED_LINE.search(text)) or "short test summary info" in text
111
+
112
+
113
+ def parse_pytest_output(text: str) -> ParseResult:
114
+ """Split pytest output into failed names, per-failure traces, and a summary."""
115
+ lines = text.splitlines()
116
+ result = ParseResult()
117
+ in_failures = False
118
+ current: list[str] | None = None
119
+
120
+ for raw in lines:
121
+ line = raw.rstrip()
122
+ stripped = line.strip()
123
+
124
+ if "short test summary info" in stripped:
125
+ in_failures = False
126
+ _flush(current, result)
127
+ current = None
128
+ result.summary_lines.append(stripped)
129
+ continue
130
+
131
+ if not in_failures and stripped.startswith("===") and "FAILURES" in stripped:
132
+ in_failures = True
133
+ continue
134
+
135
+ if in_failures:
136
+ header = _FAILURE_HEADER.match(stripped)
137
+ if header and "FAILURES" not in stripped:
138
+ _flush(current, result)
139
+ current = [f"--- {header.group('name')}"]
140
+ continue
141
+ if current is not None and not _FRAME_RULE.match(stripped):
142
+ _keep(current, stripped)
143
+ continue
144
+
145
+ failed = _FAILED_LINE.match(stripped)
146
+ if failed:
147
+ result.failed_names.append(failed.group("nodeid"))
148
+ continue
149
+
150
+ if result.summary_lines or _FAILED_LINE.search(stripped):
151
+ result.summary_lines.append(stripped)
152
+
153
+ _flush(current, result)
154
+
155
+ version_m = re.search(r"pytest\s+version\s+([\d.]+)", text)
156
+ if version_m:
157
+ result.pytest_version = version_m.group(1)
158
+ duration_m = re.search(r"in\s+([\d.]+)s", text)
159
+ if duration_m:
160
+ result.duration_s = float(duration_m.group(1))
161
+ return result
162
+
163
+
164
+ def _keep(block: list[str], line: str) -> None:
165
+ """Keep the lines that explain a failure, drop the ones that pad it.
166
+
167
+ Source context (`def`, blank lines, local-variable echoes) is mostly noise
168
+ at this budget; the pointer and the `E` block are the failure.
169
+ """
170
+ if not line:
171
+ return
172
+ if _POINTER.match(line) or _ERROR_LINE.match(line) or _FRAME_FOOTER.match(line):
173
+ block.append(line)
174
+
175
+
176
+ def _flush(block: list[str] | None, result: ParseResult) -> None:
177
+ if not block:
178
+ return
179
+ header, rest = block[0], block[1:]
180
+ if not rest:
181
+ return
182
+ # Keep at most the last 40 lines per failure (§7.5), and never the middle.
183
+ if len(rest) > 40:
184
+ rest = rest[-40:]
185
+ result.traces.append("\n".join([header, *rest]))
186
+
187
+
188
+ def summarise_pytest(text: str, *, cap: int = 1500) -> str:
189
+ """The one-call entry point: the digest, or the original when it is smaller."""
190
+ if not is_failure_output(text):
191
+ return _fit_text(text, cap)
192
+ parsed = parse_pytest_output(text)
193
+ digest = parsed.render(cap=cap)
194
+ return digest if 0 < len(digest) <= len(text) else _fit_text(text, cap)
195
+
196
+
197
+ def _fit(head: list[str], traces: list[str], cap: int) -> str:
198
+ """Fit the invariant header plus as many whole traces as the cap allows.
199
+
200
+ Never truncate a trace mid-line: half a traceback is worse than a whole one
201
+ omitted, because it looks like evidence and is not. Traces are added from the
202
+ *last* backwards, so a tight budget keeps the most recent failure in full.
203
+ """
204
+ header = "\n".join(head)
205
+ if len(header) >= cap:
206
+ return header[:cap]
207
+ room = cap - len(header) - 1
208
+ # Reserve room for the omission note before spending on traces, or the note
209
+ # itself pushes the digest over the cap it is supposed to respect.
210
+ kept: list[str] = []
211
+ for index in range(len(traces), 0, -1):
212
+ trace = traces[index - 1]
213
+ # How many traces would be dropped if we keep this one, including every
214
+ # one already skipped between the end and here.
215
+ omitted_if_kept = index - 1 - len(kept)
216
+ note = (
217
+ f"\n[{omitted_if_kept} more failing trace(s) omitted]" if omitted_if_kept else ""
218
+ )
219
+ cost = len(trace) + 2 + len(note)
220
+ if cost > room:
221
+ break
222
+ kept.append(trace)
223
+ room -= len(trace) + 2
224
+ kept.reverse()
225
+ if not kept and traces:
226
+ # Nothing fit. Saying so is the difference between "no tracebacks" and
227
+ # "tracebacks withheld for budget" — the model must not read the first.
228
+ return header + f"\n[{len(traces)} failing trace(s) omitted for budget]"
229
+ out = header + ("\n" + "\n\n".join(kept) if kept else "")
230
+ omitted = len(traces) - len(kept)
231
+ if omitted:
232
+ out += f"\n[{omitted} more failing trace(s) omitted]"
233
+ return out
234
+
235
+
236
+ def _fit_text(text: str, cap: int) -> str:
237
+ """Fallback for non-pytest output: keep the tail, where the error is."""
238
+ if len(text) <= cap:
239
+ return text
240
+ return "…" + text[-cap:]