turnloop 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.
- turnloop/__init__.py +3 -0
- turnloop/__main__.py +4 -0
- turnloop/agent/__init__.py +0 -0
- turnloop/agent/factory.py +175 -0
- turnloop/agent/headless.py +150 -0
- turnloop/agent/loop.py +396 -0
- turnloop/agent/subagent.py +182 -0
- turnloop/agent/system_prompt.py +263 -0
- turnloop/cli.py +202 -0
- turnloop/commands/__init__.py +0 -0
- turnloop/commands/dispatch.py +428 -0
- turnloop/commands/loader.py +140 -0
- turnloop/config.py +425 -0
- turnloop/context/__init__.py +0 -0
- turnloop/context/budget.py +74 -0
- turnloop/context/compaction.py +441 -0
- turnloop/context/memory.py +120 -0
- turnloop/core/__init__.py +0 -0
- turnloop/core/events.py +145 -0
- turnloop/core/ids.py +29 -0
- turnloop/core/messages.py +217 -0
- turnloop/core/tokens.py +104 -0
- turnloop/diagnostics.py +233 -0
- turnloop/errors.py +64 -0
- turnloop/experiments/__init__.py +0 -0
- turnloop/experiments/configs/bakeoff.yaml +50 -0
- turnloop/experiments/configs/ceiling-check.yaml +31 -0
- turnloop/experiments/configs/context-glm.yaml +59 -0
- turnloop/experiments/configs/context.yaml +65 -0
- turnloop/experiments/configs/glm-selfhosted.yaml +36 -0
- turnloop/experiments/configs/groq-free.yaml +28 -0
- turnloop/experiments/configs/hard-calibration.yaml +26 -0
- turnloop/experiments/configs/hard-glm.yaml +41 -0
- turnloop/experiments/configs/loop.yaml +34 -0
- turnloop/experiments/configs/nvidia-smoke.yaml +22 -0
- turnloop/experiments/configs/reliability.yaml +44 -0
- turnloop/experiments/configs/rewrite-calibration.yaml +24 -0
- turnloop/experiments/configs/smoke.yaml +22 -0
- turnloop/experiments/graders.py +209 -0
- turnloop/experiments/report.py +371 -0
- turnloop/experiments/runner.py +495 -0
- turnloop/experiments/suite/default.yaml +185 -0
- turnloop/experiments/suite/fixtures/bulky/module_1.py +670 -0
- turnloop/experiments/suite/fixtures/bulky/module_2.py +670 -0
- turnloop/experiments/suite/fixtures/bulky/module_3.py +670 -0
- turnloop/experiments/suite/fixtures/bulky/module_4.py +670 -0
- turnloop/experiments/suite/fixtures/bulky/module_5.py +670 -0
- turnloop/experiments/suite/fixtures/bulky/module_6.py +672 -0
- turnloop/experiments/suite/fixtures/bulky/module_7.py +670 -0
- turnloop/experiments/suite/fixtures/bulky/module_8.py +670 -0
- turnloop/experiments/suite/fixtures/circular_import/models.py +11 -0
- turnloop/experiments/suite/fixtures/circular_import/services.py +15 -0
- turnloop/experiments/suite/fixtures/circular_import/test_cancel.py +12 -0
- turnloop/experiments/suite/fixtures/cli_flag/app.py +16 -0
- turnloop/experiments/suite/fixtures/cross_file_contract/decode.py +7 -0
- turnloop/experiments/suite/fixtures/cross_file_contract/encode.py +10 -0
- turnloop/experiments/suite/fixtures/cross_file_contract/test_roundtrip.py +11 -0
- turnloop/experiments/suite/fixtures/cross_file_contract/test_wire_format.py +7 -0
- turnloop/experiments/suite/fixtures/empty/.keep +1 -0
- turnloop/experiments/suite/fixtures/failing_test/calc.py +14 -0
- turnloop/experiments/suite/fixtures/failing_test/test_calc.py +17 -0
- turnloop/experiments/suite/fixtures/generate_bulky.py +80 -0
- turnloop/experiments/suite/fixtures/grep_edit/ingest.py +5 -0
- turnloop/experiments/suite/fixtures/grep_edit/load.py +5 -0
- turnloop/experiments/suite/fixtures/grep_edit/transform.py +5 -0
- turnloop/experiments/suite/fixtures/misleading_traceback/checkout.py +21 -0
- turnloop/experiments/suite/fixtures/misleading_traceback/invoice.py +8 -0
- turnloop/experiments/suite/fixtures/misleading_traceback/pricing.py +11 -0
- turnloop/experiments/suite/fixtures/misleading_traceback/test_checkout.py +12 -0
- turnloop/experiments/suite/fixtures/misleading_traceback/test_invoice.py +6 -0
- turnloop/experiments/suite/fixtures/needle/inventory.py +18 -0
- turnloop/experiments/suite/fixtures/needle/pricing.py +17 -0
- turnloop/experiments/suite/fixtures/needle/shipping.py +13 -0
- turnloop/experiments/suite/fixtures/regression_trap/test_validators.py +14 -0
- turnloop/experiments/suite/fixtures/regression_trap/validators.py +23 -0
- turnloop/experiments/suite/fixtures/rename/billing.py +7 -0
- turnloop/experiments/suite/fixtures/rename/report.py +5 -0
- turnloop/experiments/suite/fixtures/rename/test_billing.py +11 -0
- turnloop/experiments/suite/fixtures/spec/parser.py +19 -0
- turnloop/experiments/suite/fixtures/spec/test_parser.py +28 -0
- turnloop/experiments/suite/fixtures/subtle_spec_edge/leaderboard.py +12 -0
- turnloop/experiments/suite/fixtures/subtle_spec_edge/test_leaderboard.py +28 -0
- turnloop/experiments/suite/fixtures/util/util.py +9 -0
- turnloop/hooks/__init__.py +0 -0
- turnloop/hooks/runner.py +221 -0
- turnloop/mcp/__init__.py +0 -0
- turnloop/mcp/adapter.py +109 -0
- turnloop/mcp/client.py +299 -0
- turnloop/permissions/__init__.py +0 -0
- turnloop/permissions/engine.py +211 -0
- turnloop/permissions/rules.py +325 -0
- turnloop/providers/__init__.py +0 -0
- turnloop/providers/anthropic.py +318 -0
- turnloop/providers/base.py +329 -0
- turnloop/providers/gemini.py +217 -0
- turnloop/providers/mock.py +226 -0
- turnloop/providers/openai_compat.py +357 -0
- turnloop/providers/pricing.py +148 -0
- turnloop/providers/registry.py +75 -0
- turnloop/providers/sse.py +114 -0
- turnloop/sessions/__init__.py +0 -0
- turnloop/sessions/models.py +139 -0
- turnloop/sessions/store.py +271 -0
- turnloop/tools/__init__.py +0 -0
- turnloop/tools/ask.py +133 -0
- turnloop/tools/base.py +286 -0
- turnloop/tools/bash.py +423 -0
- turnloop/tools/builtin.py +63 -0
- turnloop/tools/edit.py +238 -0
- turnloop/tools/glob.py +235 -0
- turnloop/tools/grep.py +314 -0
- turnloop/tools/read.py +151 -0
- turnloop/tools/runner.py +284 -0
- turnloop/tools/shell.py +222 -0
- turnloop/tools/skill.py +89 -0
- turnloop/tools/task.py +161 -0
- turnloop/tools/textio.py +87 -0
- turnloop/tools/todo.py +140 -0
- turnloop/tools/webfetch.py +235 -0
- turnloop/tools/write.py +97 -0
- turnloop/tui/__init__.py +0 -0
- turnloop/tui/app.py +330 -0
- turnloop/tui/app.tcss +84 -0
- turnloop/tui/bridge.py +142 -0
- turnloop/tui/widgets/__init__.py +0 -0
- turnloop/tui/widgets/permission.py +131 -0
- turnloop/tui/widgets/status.py +110 -0
- turnloop/tui/widgets/transcript.py +146 -0
- turnloop-0.1.0.dist-info/METADATA +916 -0
- turnloop-0.1.0.dist-info/RECORD +132 -0
- turnloop-0.1.0.dist-info/WHEEL +4 -0
- turnloop-0.1.0.dist-info/entry_points.txt +3 -0
turnloop/diagnostics.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""`turnloop doctor`.
|
|
2
|
+
|
|
3
|
+
Not a nicety on Windows. Two of this harness's failure modes are invisible from a
|
|
4
|
+
traceback — the WSL `bash` stub, and a self-hosted endpoint that is merely cold
|
|
5
|
+
rather than broken — and both are things a user will otherwise spend an hour on.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from turnloop.config import Settings
|
|
15
|
+
from turnloop.core.tokens import rough_tokens, tool_spec_tokens
|
|
16
|
+
|
|
17
|
+
OK = "ok"
|
|
18
|
+
WARN = "warn"
|
|
19
|
+
FAIL = "fail"
|
|
20
|
+
|
|
21
|
+
_MARKS = {OK: "+", WARN: "!", FAIL: "x"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_doctor(settings: Settings, cwd: Path) -> int:
|
|
25
|
+
rows: list[tuple[str, str, str]] = []
|
|
26
|
+
|
|
27
|
+
rows.append((OK, "python", f"{sys.version.split()[0]} on {sys.platform}"))
|
|
28
|
+
rows.append((OK, "project root", str(settings.project_root)))
|
|
29
|
+
rows.append((OK, "config layers", ", ".join(settings.sources)))
|
|
30
|
+
|
|
31
|
+
rows += _shell_rows()
|
|
32
|
+
rows += _tooling_rows()
|
|
33
|
+
rows += _provider_rows(settings)
|
|
34
|
+
rows += _context_rows(settings, cwd)
|
|
35
|
+
rows += _permission_rows(settings)
|
|
36
|
+
rows += _extras_rows(settings)
|
|
37
|
+
|
|
38
|
+
width = max(len(label) for _, label, _ in rows) + 2
|
|
39
|
+
for status, label, detail in rows:
|
|
40
|
+
print(f" {_MARKS[status]} {label.ljust(width)} {detail}")
|
|
41
|
+
|
|
42
|
+
failures = sum(1 for status, _, _ in rows if status == FAIL)
|
|
43
|
+
warnings = sum(1 for status, _, _ in rows if status == WARN)
|
|
44
|
+
print()
|
|
45
|
+
if failures:
|
|
46
|
+
print(f"{failures} problem(s), {warnings} warning(s).")
|
|
47
|
+
return 1
|
|
48
|
+
print(f"No problems. {warnings} warning(s).")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _shell_rows() -> list[tuple[str, str, str]]:
|
|
53
|
+
from turnloop.errors import ShellNotFound
|
|
54
|
+
from turnloop.tools.shell import IS_WINDOWS, _is_wsl_stub, resolve_shell
|
|
55
|
+
|
|
56
|
+
rows: list[tuple[str, str, str]] = []
|
|
57
|
+
try:
|
|
58
|
+
spec = resolve_shell()
|
|
59
|
+
rows.append((OK, "shell", f"{spec.kind} at {spec.exe}"))
|
|
60
|
+
if not spec.posix:
|
|
61
|
+
rows.append(
|
|
62
|
+
(
|
|
63
|
+
WARN,
|
|
64
|
+
"shell dialect",
|
|
65
|
+
"PowerShell. Models write POSIX shell by default, so expect "
|
|
66
|
+
"some Bash calls to fail. Installing Git for Windows fixes this.",
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
rows.append((OK, "shell dialect", "POSIX"))
|
|
71
|
+
except ShellNotFound as exc:
|
|
72
|
+
rows.append((FAIL, "shell", str(exc)))
|
|
73
|
+
|
|
74
|
+
if IS_WINDOWS:
|
|
75
|
+
on_path = shutil.which("bash")
|
|
76
|
+
if on_path and _is_wsl_stub(Path(on_path)):
|
|
77
|
+
rows.append(
|
|
78
|
+
(
|
|
79
|
+
WARN,
|
|
80
|
+
"bash on PATH",
|
|
81
|
+
f"{on_path} is the WSL launcher, not a real bash. It is ignored "
|
|
82
|
+
"deliberately — with no distribution installed it exits 255 with "
|
|
83
|
+
"UTF-16LE error text.",
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
return rows
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _tooling_rows() -> list[tuple[str, str, str]]:
|
|
90
|
+
rows = []
|
|
91
|
+
for name, purpose in (
|
|
92
|
+
("git", "ignore-aware Glob, repository context"),
|
|
93
|
+
("rg", "fast Grep (falls back to pure Python)"),
|
|
94
|
+
):
|
|
95
|
+
found = shutil.which(name)
|
|
96
|
+
rows.append(
|
|
97
|
+
(OK if found else WARN, name, found or f"not found — {purpose} degrades")
|
|
98
|
+
)
|
|
99
|
+
return rows
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _provider_rows(settings: Settings) -> list[tuple[str, str, str]]:
|
|
103
|
+
import os
|
|
104
|
+
|
|
105
|
+
rows: list[tuple[str, str, str]] = []
|
|
106
|
+
active = settings.provider
|
|
107
|
+
for name in sorted(settings.providers):
|
|
108
|
+
cfg = settings.providers[name]
|
|
109
|
+
marker = " (active)" if name == active else ""
|
|
110
|
+
detail = f"{cfg.kind} {cfg.model}"
|
|
111
|
+
|
|
112
|
+
if cfg.api_key_env:
|
|
113
|
+
if os.environ.get(cfg.api_key_env):
|
|
114
|
+
detail += f", {cfg.api_key_env} set"
|
|
115
|
+
status = OK
|
|
116
|
+
else:
|
|
117
|
+
detail += f", {cfg.api_key_env} NOT set"
|
|
118
|
+
status = FAIL if name == active else WARN
|
|
119
|
+
else:
|
|
120
|
+
status = OK
|
|
121
|
+
if cfg.kind == "openai_compat" and not cfg.api_key:
|
|
122
|
+
detail += ", unauthenticated"
|
|
123
|
+
|
|
124
|
+
if cfg.caps.cost_per_hour:
|
|
125
|
+
detail += f", ${cfg.caps.cost_per_hour:.2f}/hr wall clock"
|
|
126
|
+
if cfg.health_url:
|
|
127
|
+
detail += ", health-gated"
|
|
128
|
+
if cfg.timeout_s is None and cfg.health_url:
|
|
129
|
+
detail += ", unbounded read timeout"
|
|
130
|
+
|
|
131
|
+
rows.append((status, f"provider {name}{marker}", detail))
|
|
132
|
+
return rows
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _context_rows(settings: Settings, cwd: Path) -> list[tuple[str, str, str]]:
|
|
136
|
+
from turnloop.agent.system_prompt import build_system
|
|
137
|
+
from turnloop.providers.base import Capabilities
|
|
138
|
+
from turnloop.tools.builtin import build_registry
|
|
139
|
+
|
|
140
|
+
cfg = settings.provider_config()
|
|
141
|
+
caps: Capabilities = cfg.caps
|
|
142
|
+
registry = build_registry(settings, cwd)
|
|
143
|
+
verbosity = settings.verbosity_for()
|
|
144
|
+
system = build_system(settings, cwd, registry, caps, settings.permission_mode)
|
|
145
|
+
|
|
146
|
+
system_tokens = rough_tokens("\n\n".join(system))
|
|
147
|
+
tools_tokens = tool_spec_tokens(registry.specs(verbosity))
|
|
148
|
+
from turnloop.context.budget import output_reserve
|
|
149
|
+
|
|
150
|
+
reserve = output_reserve(settings.compaction.reserve_output_tokens, caps.max_output)
|
|
151
|
+
available = caps.max_context - reserve - system_tokens - tools_tokens
|
|
152
|
+
|
|
153
|
+
rows = [
|
|
154
|
+
(OK, "tools", f"{len(registry)} registered ({', '.join(registry.names())})"),
|
|
155
|
+
(
|
|
156
|
+
OK,
|
|
157
|
+
"context budget",
|
|
158
|
+
f"{caps.max_context:,} window − {reserve:,} output − {system_tokens:,} system "
|
|
159
|
+
f"− {tools_tokens:,} tools = {available:,} for history",
|
|
160
|
+
),
|
|
161
|
+
(OK, "tool verbosity", verbosity),
|
|
162
|
+
]
|
|
163
|
+
if available < 20_000:
|
|
164
|
+
rows.append(
|
|
165
|
+
(
|
|
166
|
+
WARN,
|
|
167
|
+
"context headroom",
|
|
168
|
+
f"only {available:,} tokens for conversation. Consider "
|
|
169
|
+
"tool_verbosity=terse for this provider.",
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return rows
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _permission_rows(settings: Settings) -> list[tuple[str, str, str]]:
|
|
176
|
+
from turnloop.errors import ConfigError
|
|
177
|
+
from turnloop.permissions.rules import parse_rules
|
|
178
|
+
|
|
179
|
+
rows: list[tuple[str, str, str]] = []
|
|
180
|
+
rows.append((OK, "permission mode", settings.permission_mode))
|
|
181
|
+
for label, raws in (
|
|
182
|
+
("allow", settings.permissions.allow),
|
|
183
|
+
("deny", settings.permissions.deny),
|
|
184
|
+
("ask", settings.permissions.ask),
|
|
185
|
+
):
|
|
186
|
+
try:
|
|
187
|
+
parsed = parse_rules(raws)
|
|
188
|
+
rows.append((OK, f"rules ({label})", f"{len(parsed)} valid"))
|
|
189
|
+
except ConfigError as exc:
|
|
190
|
+
rows.append((FAIL, f"rules ({label})", str(exc)))
|
|
191
|
+
|
|
192
|
+
if settings.permission_mode == "bypass":
|
|
193
|
+
rows.append(
|
|
194
|
+
(
|
|
195
|
+
WARN,
|
|
196
|
+
"bypass mode",
|
|
197
|
+
"every call is allowed except explicit deny rules. Deny rules still apply.",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
return rows
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _extras_rows(settings: Settings) -> list[tuple[str, str, str]]:
|
|
204
|
+
from turnloop.commands.loader import load_commands, load_skills
|
|
205
|
+
from turnloop.context.memory import discover_memory
|
|
206
|
+
|
|
207
|
+
rows: list[tuple[str, str, str]] = []
|
|
208
|
+
commands = load_commands(settings.project_root)
|
|
209
|
+
skills = load_skills(settings.project_root)
|
|
210
|
+
memory = discover_memory(Path.cwd(), settings.project_root)
|
|
211
|
+
|
|
212
|
+
rows.append((OK, "slash commands", f"{len(commands)} user-defined" if commands else "none"))
|
|
213
|
+
rows.append((OK, "skills", ", ".join(sorted(skills)) if skills else "none"))
|
|
214
|
+
if memory:
|
|
215
|
+
total = sum(m.tokens for m in memory)
|
|
216
|
+
status = WARN if total > 4_000 else OK
|
|
217
|
+
rows.append(
|
|
218
|
+
(
|
|
219
|
+
status,
|
|
220
|
+
"memory files",
|
|
221
|
+
f"{len(memory)} file(s), ~{total:,} tokens"
|
|
222
|
+
+ (" (over the 4,000 budget; the excess is dropped)" if total > 4_000 else ""),
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
rows.append((OK, "memory files", "none (create TURNLOOP.md to add project instructions)"))
|
|
227
|
+
|
|
228
|
+
enabled = [n for n, c in settings.mcp_servers.items() if c.enabled]
|
|
229
|
+
rows.append((OK, "mcp servers", ", ".join(enabled) if enabled else "none configured"))
|
|
230
|
+
|
|
231
|
+
hook_events = [event for event, matchers in settings.hooks.items() if matchers]
|
|
232
|
+
rows.append((OK, "hooks", ", ".join(hook_events) if hook_events else "none configured"))
|
|
233
|
+
return rows
|
turnloop/errors.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Exception taxonomy.
|
|
2
|
+
|
|
3
|
+
The rule that shapes this file: anything raised below the tool layer must be
|
|
4
|
+
classifiable into retry / cold-boot / fatal without inspecting strings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TurnloopError(Exception):
|
|
11
|
+
"""Base for every error this package raises deliberately."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigError(TurnloopError):
|
|
15
|
+
"""Malformed settings file, unknown provider name, bad permission rule."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProviderError(TurnloopError):
|
|
19
|
+
"""Base for provider/transport failures."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, *, status: int | None = None, body: str | None = None,
|
|
22
|
+
retry_after: float | None = None):
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.status = status
|
|
25
|
+
self.body = body
|
|
26
|
+
# Seconds the server asked us to wait. A per-minute token quota needs the
|
|
27
|
+
# server's number, not our exponential guess: backoff of 2/4/8s against a
|
|
28
|
+
# 60-second window just spends the retry budget without waiting it out.
|
|
29
|
+
self.retry_after = retry_after
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FatalProviderError(ProviderError):
|
|
33
|
+
"""4xx that will never succeed on retry: bad schema, bad auth, wrong model."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RetryableProviderError(ProviderError):
|
|
37
|
+
"""429/5xx or a mid-stream disconnect. Exponential backoff applies."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ColdBootTimeout(ProviderError):
|
|
41
|
+
"""A self-hosted endpoint never became healthy inside its boot budget."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StreamTruncated(RetryableProviderError):
|
|
45
|
+
"""The stream ended without a terminal event. Partial output is discarded."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ContextOverflow(TurnloopError):
|
|
49
|
+
"""History still exceeds the window after every compaction tier ran."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ToolError(TurnloopError):
|
|
53
|
+
"""A tool failed in a way the model should see and can act on.
|
|
54
|
+
|
|
55
|
+
Never propagates past ToolRunner.dispatch — it becomes an error tool_result.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PermissionDenied(ToolError):
|
|
60
|
+
"""A deny rule, or plan mode, blocked this call."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ShellNotFound(TurnloopError):
|
|
64
|
+
"""No usable shell on this machine (see tools/shell.py for why that happens)."""
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Provider bake-off: the same tasks across every backend.
|
|
2
|
+
#
|
|
3
|
+
# Cost warning. The `glm` arm runs on a self-hosted 4xH200 endpoint at $18.16/hour
|
|
4
|
+
# with a ~29 minute cold boot. The runner groups execution by provider precisely so
|
|
5
|
+
# this arm boots once and then runs everything, instead of paying a cold boot per
|
|
6
|
+
# task. Even so: `modal app stop glm-5-2-serve` afterwards.
|
|
7
|
+
#
|
|
8
|
+
# Ordering note: `glm` is listed last, but the runner sorts mock first and otherwise
|
|
9
|
+
# alphabetically, so provider grouping — not arm order — determines execution.
|
|
10
|
+
|
|
11
|
+
experiment: provider-bakeoff
|
|
12
|
+
suite: default
|
|
13
|
+
repeats: 3
|
|
14
|
+
|
|
15
|
+
tasks:
|
|
16
|
+
- fix_failing_test
|
|
17
|
+
- implement_from_spec
|
|
18
|
+
- multi_file_rename
|
|
19
|
+
- add_cli_flag
|
|
20
|
+
- grep_then_edit
|
|
21
|
+
- structural_function
|
|
22
|
+
- find_the_bug
|
|
23
|
+
- refuse_outside_root
|
|
24
|
+
|
|
25
|
+
arms:
|
|
26
|
+
- name: sonnet
|
|
27
|
+
provider: anthropic
|
|
28
|
+
model: claude-sonnet-4-5
|
|
29
|
+
|
|
30
|
+
- name: haiku
|
|
31
|
+
provider: anthropic
|
|
32
|
+
model: claude-haiku-4-5
|
|
33
|
+
|
|
34
|
+
- name: gpt-4.1
|
|
35
|
+
provider: openai
|
|
36
|
+
model: gpt-4.1
|
|
37
|
+
|
|
38
|
+
- name: gemini-flash
|
|
39
|
+
provider: gemini
|
|
40
|
+
model: gemini-2.5-flash
|
|
41
|
+
|
|
42
|
+
- name: gpt-oss-120b
|
|
43
|
+
provider: groq
|
|
44
|
+
model: openai/gpt-oss-120b
|
|
45
|
+
|
|
46
|
+
# Self-hosted GLM-5.2, 744B MoE at W4A16 on 4xH200. 65k context, so it also
|
|
47
|
+
# exercises the compaction path that the hosted arms mostly never reach.
|
|
48
|
+
- name: glm-5.2-selfhosted
|
|
49
|
+
provider: glm
|
|
50
|
+
tool_verbosity: terse
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Does a weaker model break the ceiling?
|
|
2
|
+
#
|
|
3
|
+
# GLM-5.2 scored 100% on all four of these tasks across 84 runs, which means no
|
|
4
|
+
# context-engineering ablation run against it can discriminate — every arm ties at
|
|
5
|
+
# the top. An ablation needs a model that sometimes fails.
|
|
6
|
+
#
|
|
7
|
+
# This is the cheapest possible test of whether gpt-4o-mini sits in the useful band.
|
|
8
|
+
# One repeat, four tasks, one arm. What we want is a pass rate somewhere around
|
|
9
|
+
# 40-70%: high enough that the agent is genuinely working, low enough that removing
|
|
10
|
+
# the system prompt or the tool descriptions has room to show up.
|
|
11
|
+
#
|
|
12
|
+
# 0% would be as useless as 100% — a floor is just a ceiling upside down.
|
|
13
|
+
#
|
|
14
|
+
# Note `forces_compaction` will NOT trigger compaction here. The bulky fixture is
|
|
15
|
+
# sized against a 65,536-token window (tier 3 at ~43k); gpt-4o-mini's 128k window
|
|
16
|
+
# puts the trigger near 90k, which the fixture's top-3-files-in-full total of ~50k
|
|
17
|
+
# does not reach. That task measures only correctness on this provider.
|
|
18
|
+
|
|
19
|
+
experiment: ceiling-check
|
|
20
|
+
suite: default
|
|
21
|
+
repeats: 1
|
|
22
|
+
|
|
23
|
+
tasks:
|
|
24
|
+
- fix_failing_test
|
|
25
|
+
- implement_from_spec
|
|
26
|
+
- grep_then_edit
|
|
27
|
+
- forces_compaction
|
|
28
|
+
|
|
29
|
+
arms:
|
|
30
|
+
- name: gpt-4o-mini
|
|
31
|
+
provider: blaxel
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# context.yaml, retargeted at the self-hosted GLM-5.2 on Modal.
|
|
2
|
+
#
|
|
3
|
+
# Same seven arms and four tasks as context.yaml; only the provider differs. This
|
|
4
|
+
# exists as a separate file rather than an edit because the two endpoints are not
|
|
5
|
+
# interchangeable: NIM gives a 200k window for free but rate-limits hard, while this
|
|
6
|
+
# one gives 65,536 tokens on a GPU billed at $18.16/hour of wall clock.
|
|
7
|
+
#
|
|
8
|
+
# 65k is the window the compaction ladder was designed against, so `forces_compaction`
|
|
9
|
+
# is a real test here rather than a formality -- on a 200k window it barely triggers.
|
|
10
|
+
#
|
|
11
|
+
# COST: 84 runs at roughly 30s each is about 45 minutes of GPU, so on the order of
|
|
12
|
+
# $13, on top of whatever the cold boot already cost. Only run this in an already-warm
|
|
13
|
+
# window, and run `modal app stop glm-5-2-serve` the moment it finishes -- the
|
|
14
|
+
# container otherwise idles for another 10 minutes before scaling down.
|
|
15
|
+
|
|
16
|
+
experiment: context-engineering-glm
|
|
17
|
+
suite: default
|
|
18
|
+
repeats: 3
|
|
19
|
+
|
|
20
|
+
tasks:
|
|
21
|
+
- fix_failing_test
|
|
22
|
+
- implement_from_spec
|
|
23
|
+
- grep_then_edit
|
|
24
|
+
- forces_compaction
|
|
25
|
+
|
|
26
|
+
arms:
|
|
27
|
+
- name: baseline
|
|
28
|
+
provider: glm
|
|
29
|
+
tool_verbosity: normal
|
|
30
|
+
compaction: summarize
|
|
31
|
+
system_variant: default
|
|
32
|
+
|
|
33
|
+
- name: terse-tools
|
|
34
|
+
provider: glm
|
|
35
|
+
tool_verbosity: terse
|
|
36
|
+
compaction: summarize
|
|
37
|
+
system_variant: default
|
|
38
|
+
|
|
39
|
+
- name: verbose-tools
|
|
40
|
+
provider: glm
|
|
41
|
+
tool_verbosity: verbose
|
|
42
|
+
compaction: summarize
|
|
43
|
+
system_variant: default
|
|
44
|
+
|
|
45
|
+
- name: micro-only-compaction
|
|
46
|
+
provider: glm
|
|
47
|
+
compaction: micro_only
|
|
48
|
+
|
|
49
|
+
- name: no-compaction
|
|
50
|
+
provider: glm
|
|
51
|
+
compaction: none
|
|
52
|
+
|
|
53
|
+
- name: minimal-prompt
|
|
54
|
+
provider: glm
|
|
55
|
+
system_variant: minimal
|
|
56
|
+
|
|
57
|
+
- name: careful-prompt
|
|
58
|
+
provider: glm
|
|
59
|
+
system_variant: careful
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Context-engineering ablations, all on one model so the model is not the variable.
|
|
2
|
+
#
|
|
3
|
+
# The model is GLM-5.2 via NVIDIA NIM: a 200k window, which is what makes these
|
|
4
|
+
# ablations runnable at all. `forces_compaction` and the multi-file tasks cannot
|
|
5
|
+
# be expressed inside the 7k the Groq free tier allows.
|
|
6
|
+
#
|
|
7
|
+
# Three questions, and the reason each is worth asking:
|
|
8
|
+
#
|
|
9
|
+
# 1. Tool-description verbosity. Verbose descriptions cost ~1,100 tokens more than
|
|
10
|
+
# terse ones with the current 10-tool set. On a 65k window that is 2% of
|
|
11
|
+
# everything. If pass rate is unchanged, terse is free money; if it drops, the
|
|
12
|
+
# descriptions are load-bearing and that is worth knowing too.
|
|
13
|
+
#
|
|
14
|
+
# 2. Compaction strategy. `summarize` spends a model call; `micro_only` spends
|
|
15
|
+
# nothing but forgets more; `none` is the control that shows what compaction is
|
|
16
|
+
# actually buying.
|
|
17
|
+
#
|
|
18
|
+
# 3. System prompt variant. `minimal` strips the behavioral guidance down to two
|
|
19
|
+
# sentences. This measures how much of a coding agent's competence comes from
|
|
20
|
+
# its harness rather than its weights — the single most interesting number here.
|
|
21
|
+
|
|
22
|
+
experiment: context-engineering
|
|
23
|
+
suite: default
|
|
24
|
+
repeats: 3
|
|
25
|
+
|
|
26
|
+
tasks:
|
|
27
|
+
- fix_failing_test
|
|
28
|
+
- implement_from_spec
|
|
29
|
+
- grep_then_edit
|
|
30
|
+
- forces_compaction
|
|
31
|
+
|
|
32
|
+
arms:
|
|
33
|
+
- name: baseline
|
|
34
|
+
provider: nvidia
|
|
35
|
+
tool_verbosity: normal
|
|
36
|
+
compaction: summarize
|
|
37
|
+
system_variant: default
|
|
38
|
+
|
|
39
|
+
- name: terse-tools
|
|
40
|
+
provider: nvidia
|
|
41
|
+
tool_verbosity: terse
|
|
42
|
+
compaction: summarize
|
|
43
|
+
system_variant: default
|
|
44
|
+
|
|
45
|
+
- name: verbose-tools
|
|
46
|
+
provider: nvidia
|
|
47
|
+
tool_verbosity: verbose
|
|
48
|
+
compaction: summarize
|
|
49
|
+
system_variant: default
|
|
50
|
+
|
|
51
|
+
- name: micro-only-compaction
|
|
52
|
+
provider: nvidia
|
|
53
|
+
compaction: micro_only
|
|
54
|
+
|
|
55
|
+
- name: no-compaction
|
|
56
|
+
provider: nvidia
|
|
57
|
+
compaction: none
|
|
58
|
+
|
|
59
|
+
- name: minimal-prompt
|
|
60
|
+
provider: nvidia
|
|
61
|
+
system_variant: minimal
|
|
62
|
+
|
|
63
|
+
- name: careful-prompt
|
|
64
|
+
provider: nvidia
|
|
65
|
+
system_variant: careful
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Same model weights, two serving stacks.
|
|
2
|
+
#
|
|
3
|
+
# GLM-5.2 self-hosted on vLLM (Modal, `--tool-call-parser glm47`) against the same
|
|
4
|
+
# GLM-5.2 hosted on NVIDIA NIM. The model is held constant, so any difference in
|
|
5
|
+
# tool-calling reliability is attributable to the serving stack rather than to the
|
|
6
|
+
# weights -- which is the only way to measure a parser.
|
|
7
|
+
#
|
|
8
|
+
# The two stacks demonstrably differ on the wire: NIM emits each tool call whole in
|
|
9
|
+
# a single delta (id + name + complete arguments JSON), while vLLM's glm47 parser
|
|
10
|
+
# streams the arguments as fragments to be accumulated by index. Whether that costs
|
|
11
|
+
# reliability is the question this run answers.
|
|
12
|
+
#
|
|
13
|
+
# COST: the self-hosted arm bills $18.16/hour of wall clock from cold boot, and the
|
|
14
|
+
# container idles down after 10 minutes. Keep this config small and run it only in
|
|
15
|
+
# a window where the endpoint is already warm. Nine runs is roughly six minutes of
|
|
16
|
+
# GPU. Run `modal app stop glm-5-2-serve` when finished.
|
|
17
|
+
#
|
|
18
|
+
# Arms are executed grouped by provider, so the GPU arm runs its tasks back to back
|
|
19
|
+
# inside one warm window instead of cold-booting per task.
|
|
20
|
+
|
|
21
|
+
experiment: glm-selfhosted-vs-nim
|
|
22
|
+
suite: default
|
|
23
|
+
repeats: 3
|
|
24
|
+
|
|
25
|
+
tasks:
|
|
26
|
+
- fix_failing_test
|
|
27
|
+
- structural_function
|
|
28
|
+
- find_the_bug
|
|
29
|
+
|
|
30
|
+
arms:
|
|
31
|
+
- name: glm-5.2-nim
|
|
32
|
+
provider: nvidia
|
|
33
|
+
|
|
34
|
+
- name: glm-5.2-selfhosted
|
|
35
|
+
provider: glm
|
|
36
|
+
tool_verbosity: terse
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Groq free tier, three small tasks, single repeat.
|
|
2
|
+
#
|
|
3
|
+
# Sized for the constraint rather than the model: the on-demand tier allows 8,000
|
|
4
|
+
# tokens per minute *including* requested completion tokens, which leaves ~2,400 for
|
|
5
|
+
# conversation history. That rules out the multi-file tasks — they cannot fit — and
|
|
6
|
+
# it means a multi-turn task spends real wall-clock waiting for the quota window.
|
|
7
|
+
#
|
|
8
|
+
# The mock arm is the free control: same tasks, no network, so a difference between
|
|
9
|
+
# the two rows is the model rather than the harness.
|
|
10
|
+
|
|
11
|
+
experiment: groq-free
|
|
12
|
+
suite: default
|
|
13
|
+
repeats: 1
|
|
14
|
+
|
|
15
|
+
tasks:
|
|
16
|
+
- fix_failing_test
|
|
17
|
+
- structural_function
|
|
18
|
+
- find_the_bug
|
|
19
|
+
|
|
20
|
+
arms:
|
|
21
|
+
- name: mock-control
|
|
22
|
+
provider: mock
|
|
23
|
+
mock_mode: echo
|
|
24
|
+
|
|
25
|
+
- name: gpt-oss-120b
|
|
26
|
+
provider: groq
|
|
27
|
+
model: openai/gpt-oss-120b
|
|
28
|
+
tool_verbosity: terse
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Difficulty calibration for the five ceiling-breaker tasks.
|
|
2
|
+
#
|
|
3
|
+
# Each of these has a plausible wrong solution baked in (see default.yaml's
|
|
4
|
+
# "ceiling-breakers" section), but "has a decoy" is a design intent, not a
|
|
5
|
+
# measurement. This config exists to check the intent held: run each task a
|
|
6
|
+
# few times against a real model and see where the pass rate lands.
|
|
7
|
+
#
|
|
8
|
+
# Target band is 40-70% per task. Below that and the task is just hard, not
|
|
9
|
+
# discriminating; above it and the decoy isn't tempting enough to matter.
|
|
10
|
+
# One arm, three repeats, cheap enough to iterate on the fixtures themselves
|
|
11
|
+
# before trusting them in a bigger sweep.
|
|
12
|
+
|
|
13
|
+
experiment: hard-calibration
|
|
14
|
+
suite: default
|
|
15
|
+
repeats: 3
|
|
16
|
+
|
|
17
|
+
tasks:
|
|
18
|
+
- regression_trap
|
|
19
|
+
- cross_file_contract
|
|
20
|
+
- subtle_spec_edge
|
|
21
|
+
- circular_import
|
|
22
|
+
- misleading_traceback
|
|
23
|
+
|
|
24
|
+
arms:
|
|
25
|
+
- name: gpt-4o-mini
|
|
26
|
+
provider: blaxel
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# The five ceiling-breaker tasks against the real target model.
|
|
2
|
+
#
|
|
3
|
+
# Calibration happens on Blaxel (free, ~5s/request); this config exists to confirm
|
|
4
|
+
# the difficulty transfers to GLM-5.2, which is the model the project actually
|
|
5
|
+
# targets. A task tuned to trip gpt-4o-mini is not automatically hard for a
|
|
6
|
+
# stronger model, and a suite calibrated against the wrong model measures nothing.
|
|
7
|
+
#
|
|
8
|
+
# Two arms so the comparison is in one table: the self-hosted GPU and the same five
|
|
9
|
+
# tasks on Blaxel. Provider grouping means the GPU arm runs its 15 back to back
|
|
10
|
+
# inside a single warm window rather than cold-booting per task.
|
|
11
|
+
#
|
|
12
|
+
# COST: 15 GPU runs at roughly a minute each is ~15 minutes, about $4.50, plus the
|
|
13
|
+
# cold boot (~29 min / ~$9) if the container is not already up. Only run this in an
|
|
14
|
+
# already-warm window, and run `modal app stop glm-5-2-serve -y` the moment it
|
|
15
|
+
# finishes — the container idles a further 10 minutes before scaling down on its own.
|
|
16
|
+
|
|
17
|
+
experiment: hard-glm
|
|
18
|
+
suite: default
|
|
19
|
+
repeats: 3
|
|
20
|
+
|
|
21
|
+
# Three of the five, not all five. Calibration on gpt-4o-mini put
|
|
22
|
+
# `misleading_traceback` and `subtle_spec_edge` at 3/3 — a task the *weaker* model
|
|
23
|
+
# solves every time cannot discriminate on the stronger one, so paying GPU minutes
|
|
24
|
+
# to watch GLM-5.2 also score 100% buys nothing.
|
|
25
|
+
#
|
|
26
|
+
# The three kept are the ones whose answer is genuinely unknown: `regression_trap`
|
|
27
|
+
# landed at 1/3 (already in band), and the two that floored at 0/3 are the real
|
|
28
|
+
# question — difficulty is model-relative, so a task a weak model never solves may
|
|
29
|
+
# sit squarely in the useful band for a stronger one.
|
|
30
|
+
tasks:
|
|
31
|
+
- regression_trap
|
|
32
|
+
- cross_file_contract
|
|
33
|
+
- circular_import
|
|
34
|
+
|
|
35
|
+
arms:
|
|
36
|
+
- name: gpt-4o-mini
|
|
37
|
+
provider: blaxel
|
|
38
|
+
|
|
39
|
+
- name: glm-5.2-selfhosted
|
|
40
|
+
provider: glm
|
|
41
|
+
tool_verbosity: terse
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Agent-loop variants on identical tasks.
|
|
2
|
+
#
|
|
3
|
+
# All three arms run the same harness and the same model; only the instruction
|
|
4
|
+
# scaffolding differs. That is deliberate — if the loops were separate code paths,
|
|
5
|
+
# a difference between arms could be a difference in implementation quality rather
|
|
6
|
+
# than in strategy.
|
|
7
|
+
#
|
|
8
|
+
# What to look for: fan-out should win on wall-clock for the multi-file tasks and
|
|
9
|
+
# lose on token cost (every subagent re-pays for the system prompt and tool
|
|
10
|
+
# schemas). Plan-then-execute should show more turns and better pass rates on the
|
|
11
|
+
# tasks with several steps.
|
|
12
|
+
|
|
13
|
+
experiment: loop-variants
|
|
14
|
+
suite: default
|
|
15
|
+
repeats: 3
|
|
16
|
+
|
|
17
|
+
tasks:
|
|
18
|
+
- multi_file_rename
|
|
19
|
+
- grep_then_edit
|
|
20
|
+
- find_the_bug
|
|
21
|
+
- fix_failing_test
|
|
22
|
+
|
|
23
|
+
arms:
|
|
24
|
+
- name: single
|
|
25
|
+
provider: nvidia
|
|
26
|
+
loop_variant: single
|
|
27
|
+
|
|
28
|
+
- name: fanout
|
|
29
|
+
provider: nvidia
|
|
30
|
+
loop_variant: fanout
|
|
31
|
+
|
|
32
|
+
- name: plan-then-execute
|
|
33
|
+
provider: nvidia
|
|
34
|
+
loop_variant: plan_then_execute
|