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.
Files changed (132) hide show
  1. turnloop/__init__.py +3 -0
  2. turnloop/__main__.py +4 -0
  3. turnloop/agent/__init__.py +0 -0
  4. turnloop/agent/factory.py +175 -0
  5. turnloop/agent/headless.py +150 -0
  6. turnloop/agent/loop.py +396 -0
  7. turnloop/agent/subagent.py +182 -0
  8. turnloop/agent/system_prompt.py +263 -0
  9. turnloop/cli.py +202 -0
  10. turnloop/commands/__init__.py +0 -0
  11. turnloop/commands/dispatch.py +428 -0
  12. turnloop/commands/loader.py +140 -0
  13. turnloop/config.py +425 -0
  14. turnloop/context/__init__.py +0 -0
  15. turnloop/context/budget.py +74 -0
  16. turnloop/context/compaction.py +441 -0
  17. turnloop/context/memory.py +120 -0
  18. turnloop/core/__init__.py +0 -0
  19. turnloop/core/events.py +145 -0
  20. turnloop/core/ids.py +29 -0
  21. turnloop/core/messages.py +217 -0
  22. turnloop/core/tokens.py +104 -0
  23. turnloop/diagnostics.py +233 -0
  24. turnloop/errors.py +64 -0
  25. turnloop/experiments/__init__.py +0 -0
  26. turnloop/experiments/configs/bakeoff.yaml +50 -0
  27. turnloop/experiments/configs/ceiling-check.yaml +31 -0
  28. turnloop/experiments/configs/context-glm.yaml +59 -0
  29. turnloop/experiments/configs/context.yaml +65 -0
  30. turnloop/experiments/configs/glm-selfhosted.yaml +36 -0
  31. turnloop/experiments/configs/groq-free.yaml +28 -0
  32. turnloop/experiments/configs/hard-calibration.yaml +26 -0
  33. turnloop/experiments/configs/hard-glm.yaml +41 -0
  34. turnloop/experiments/configs/loop.yaml +34 -0
  35. turnloop/experiments/configs/nvidia-smoke.yaml +22 -0
  36. turnloop/experiments/configs/reliability.yaml +44 -0
  37. turnloop/experiments/configs/rewrite-calibration.yaml +24 -0
  38. turnloop/experiments/configs/smoke.yaml +22 -0
  39. turnloop/experiments/graders.py +209 -0
  40. turnloop/experiments/report.py +371 -0
  41. turnloop/experiments/runner.py +495 -0
  42. turnloop/experiments/suite/default.yaml +185 -0
  43. turnloop/experiments/suite/fixtures/bulky/module_1.py +670 -0
  44. turnloop/experiments/suite/fixtures/bulky/module_2.py +670 -0
  45. turnloop/experiments/suite/fixtures/bulky/module_3.py +670 -0
  46. turnloop/experiments/suite/fixtures/bulky/module_4.py +670 -0
  47. turnloop/experiments/suite/fixtures/bulky/module_5.py +670 -0
  48. turnloop/experiments/suite/fixtures/bulky/module_6.py +672 -0
  49. turnloop/experiments/suite/fixtures/bulky/module_7.py +670 -0
  50. turnloop/experiments/suite/fixtures/bulky/module_8.py +670 -0
  51. turnloop/experiments/suite/fixtures/circular_import/models.py +11 -0
  52. turnloop/experiments/suite/fixtures/circular_import/services.py +15 -0
  53. turnloop/experiments/suite/fixtures/circular_import/test_cancel.py +12 -0
  54. turnloop/experiments/suite/fixtures/cli_flag/app.py +16 -0
  55. turnloop/experiments/suite/fixtures/cross_file_contract/decode.py +7 -0
  56. turnloop/experiments/suite/fixtures/cross_file_contract/encode.py +10 -0
  57. turnloop/experiments/suite/fixtures/cross_file_contract/test_roundtrip.py +11 -0
  58. turnloop/experiments/suite/fixtures/cross_file_contract/test_wire_format.py +7 -0
  59. turnloop/experiments/suite/fixtures/empty/.keep +1 -0
  60. turnloop/experiments/suite/fixtures/failing_test/calc.py +14 -0
  61. turnloop/experiments/suite/fixtures/failing_test/test_calc.py +17 -0
  62. turnloop/experiments/suite/fixtures/generate_bulky.py +80 -0
  63. turnloop/experiments/suite/fixtures/grep_edit/ingest.py +5 -0
  64. turnloop/experiments/suite/fixtures/grep_edit/load.py +5 -0
  65. turnloop/experiments/suite/fixtures/grep_edit/transform.py +5 -0
  66. turnloop/experiments/suite/fixtures/misleading_traceback/checkout.py +21 -0
  67. turnloop/experiments/suite/fixtures/misleading_traceback/invoice.py +8 -0
  68. turnloop/experiments/suite/fixtures/misleading_traceback/pricing.py +11 -0
  69. turnloop/experiments/suite/fixtures/misleading_traceback/test_checkout.py +12 -0
  70. turnloop/experiments/suite/fixtures/misleading_traceback/test_invoice.py +6 -0
  71. turnloop/experiments/suite/fixtures/needle/inventory.py +18 -0
  72. turnloop/experiments/suite/fixtures/needle/pricing.py +17 -0
  73. turnloop/experiments/suite/fixtures/needle/shipping.py +13 -0
  74. turnloop/experiments/suite/fixtures/regression_trap/test_validators.py +14 -0
  75. turnloop/experiments/suite/fixtures/regression_trap/validators.py +23 -0
  76. turnloop/experiments/suite/fixtures/rename/billing.py +7 -0
  77. turnloop/experiments/suite/fixtures/rename/report.py +5 -0
  78. turnloop/experiments/suite/fixtures/rename/test_billing.py +11 -0
  79. turnloop/experiments/suite/fixtures/spec/parser.py +19 -0
  80. turnloop/experiments/suite/fixtures/spec/test_parser.py +28 -0
  81. turnloop/experiments/suite/fixtures/subtle_spec_edge/leaderboard.py +12 -0
  82. turnloop/experiments/suite/fixtures/subtle_spec_edge/test_leaderboard.py +28 -0
  83. turnloop/experiments/suite/fixtures/util/util.py +9 -0
  84. turnloop/hooks/__init__.py +0 -0
  85. turnloop/hooks/runner.py +221 -0
  86. turnloop/mcp/__init__.py +0 -0
  87. turnloop/mcp/adapter.py +109 -0
  88. turnloop/mcp/client.py +299 -0
  89. turnloop/permissions/__init__.py +0 -0
  90. turnloop/permissions/engine.py +211 -0
  91. turnloop/permissions/rules.py +325 -0
  92. turnloop/providers/__init__.py +0 -0
  93. turnloop/providers/anthropic.py +318 -0
  94. turnloop/providers/base.py +329 -0
  95. turnloop/providers/gemini.py +217 -0
  96. turnloop/providers/mock.py +226 -0
  97. turnloop/providers/openai_compat.py +357 -0
  98. turnloop/providers/pricing.py +148 -0
  99. turnloop/providers/registry.py +75 -0
  100. turnloop/providers/sse.py +114 -0
  101. turnloop/sessions/__init__.py +0 -0
  102. turnloop/sessions/models.py +139 -0
  103. turnloop/sessions/store.py +271 -0
  104. turnloop/tools/__init__.py +0 -0
  105. turnloop/tools/ask.py +133 -0
  106. turnloop/tools/base.py +286 -0
  107. turnloop/tools/bash.py +423 -0
  108. turnloop/tools/builtin.py +63 -0
  109. turnloop/tools/edit.py +238 -0
  110. turnloop/tools/glob.py +235 -0
  111. turnloop/tools/grep.py +314 -0
  112. turnloop/tools/read.py +151 -0
  113. turnloop/tools/runner.py +284 -0
  114. turnloop/tools/shell.py +222 -0
  115. turnloop/tools/skill.py +89 -0
  116. turnloop/tools/task.py +161 -0
  117. turnloop/tools/textio.py +87 -0
  118. turnloop/tools/todo.py +140 -0
  119. turnloop/tools/webfetch.py +235 -0
  120. turnloop/tools/write.py +97 -0
  121. turnloop/tui/__init__.py +0 -0
  122. turnloop/tui/app.py +330 -0
  123. turnloop/tui/app.tcss +84 -0
  124. turnloop/tui/bridge.py +142 -0
  125. turnloop/tui/widgets/__init__.py +0 -0
  126. turnloop/tui/widgets/permission.py +131 -0
  127. turnloop/tui/widgets/status.py +110 -0
  128. turnloop/tui/widgets/transcript.py +146 -0
  129. turnloop-0.1.0.dist-info/METADATA +916 -0
  130. turnloop-0.1.0.dist-info/RECORD +132 -0
  131. turnloop-0.1.0.dist-info/WHEEL +4 -0
  132. turnloop-0.1.0.dist-info/entry_points.txt +3 -0
@@ -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