Skip to content

Commit 86666f6

Browse files
authored
Merge pull request #608 from morluto/experiment/reduce-codex-tool-discovery-cost
Improve Codex tool discovery efficiency
2 parents 0891065 + 78a5d0c commit 86666f6

8 files changed

Lines changed: 471 additions & 5 deletions

File tree

.agents/skills/jacobian-math/SKILL.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ description: Use Jacobian for specialized exact mathematics, including matrix de
77

88
<!-- Managed by Jacobian's Codex integration. -->
99

10+
In Codex Code Mode, call the nested methods
11+
`tools.mcp__jacobian__math_find(...)` and
12+
`tools.mcp__jacobian__math_run(...)` directly. Do not enumerate, filter, or
13+
print `ALL_TOOLS` merely to locate them; that needlessly adds every matching
14+
tool description to the model context. Return only the typed projection when
15+
available, for example:
16+
17+
```js
18+
const r = await tools.mcp__jacobian__math_find({query: "...", limit: 3});
19+
text(r.structuredContent ?? r);
20+
```
21+
1022
Call `math.run` directly when the requested local outcome exactly matches one of
1123
these stable built-in contracts; replace the example values but preserve the
1224
shown JSON types:
@@ -37,5 +49,9 @@ searches, and missing witnesses as non-conclusions.
3749
3850
When independent checking is requested, calculations or programs authored by
3951
the same model are not independent checker evidence. Use an installed `VERIFY`
40-
capability when available. Claim `VERIFIED` only when the result has assurance
41-
level `VERIFIED` and a local verification record.
52+
capability when available. An artifact URI or checker-result summary is not a
53+
task-local verification-record file: never reconstruct or paraphrase such a
54+
record from the returned fields. Claim `VERIFIED` only when the result has
55+
assurance level `VERIFIED`, the exact record bytes are available, and any
56+
required task authorization and bindings are preserved. Otherwise use a lower
57+
assurance permitted by the task.

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ endif
2929
# in pyproject.toml: direct pytest invocations must not silently inherit a
3030
# signal-based deadline that cannot interrupt a native solver. Process and
3131
# provider lanes run risky work in killable children and set their own deadline.
32-
.PHONY: help help-all uv-version-check setup setup-agent container-image eval-image eval-image-pull eval-image-bind hooks fix lint complexity-check lint-full security-audit typecheck test-architecture architecture ci-plan test-plan test-changed check-changed test-unit test-component test-domain test-composition test-storage test-process test-mcp test-provider test-lean test-e2e test-affected test-all-ci test-compatibility test-stress test-ordering duplicate-code npm-test todo-check coverage build check precommit check-static harbor-plan harbor-prepare-task harbor-validate-task harbor-sync harbor-contracts harbor-execution-check harbor-adapter-checks harbor-validation-tests harbor-host-validation harbor-validate harbor-check harbor-check-task benchmark-inventory benchmark-snapshot benchmark-snapshot-validate benchmark-publish harbor-oracle harbor-oracle-task harbor-oracle-run harbor-oracle-all harbor-adapter-check heldout-validate heldout-render heldout-smoke agent-eval agent-eval-validate agent-eval-compare codex-visibility provider-eval clean docs-command-check docs-linkcheck deploy-check
32+
.PHONY: help help-all uv-version-check setup setup-agent container-image eval-image eval-image-pull eval-image-bind hooks fix lint complexity-check lint-full security-audit typecheck test-architecture architecture ci-plan test-plan test-changed check-changed test-unit test-component test-domain test-composition test-storage test-process test-mcp test-provider test-lean test-e2e test-affected test-all-ci test-compatibility test-stress test-ordering duplicate-code npm-test todo-check coverage build check precommit check-static harbor-plan harbor-prepare-task harbor-validate-task harbor-sync harbor-contracts harbor-execution-check harbor-adapter-checks harbor-validation-tests harbor-host-validation harbor-validate harbor-check harbor-check-task benchmark-inventory benchmark-snapshot benchmark-snapshot-validate benchmark-publish harbor-oracle harbor-oracle-task harbor-oracle-run harbor-oracle-all harbor-adapter-check heldout-validate heldout-render heldout-smoke agent-eval agent-eval-validate agent-eval-compare codex-visibility codex-tool-context provider-eval clean docs-command-check docs-linkcheck deploy-check
3333

3434
help: ## Show available developer commands.
3535
@awk -v public="$(PUBLIC_COMMANDS)" 'BEGIN {FS = ":.*## "; n = split(public, names, " "); for (i = 1; i <= n; i++) wanted[names[i]] = 1; printf "Jacobian common developer commands:\n\n"} /^[a-zA-Z_-]+:.*## / && ($$1 in wanted) {printf " %-18s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
@@ -547,6 +547,7 @@ agent-eval-compare: ## Compare normalized observations (CONTROL=..., TREATMENT=.
547547
VISIBILITY_CASES ?= benchmarks/config/codex-visibility-v2.json
548548
VISIBILITY_REPETITIONS ?= 1
549549
VISIBILITY_REASONING_EFFORT ?= high
550+
VISIBILITY_TOOL_MODE ?= direct
550551

551552
codex-visibility: ## Measure Codex adoption of Jacobian (VISIBILITY_EXECUTE=1, VISIBILITY_MCP_URL=..., VISIBILITY_MODEL=..., VISIBILITY_OUTPUT=...).
552553
@set -e; \
@@ -561,10 +562,16 @@ codex-visibility: ## Measure Codex adoption of Jacobian (VISIBILITY_EXECUTE=1, V
561562
$(UV_RUN) python -m benchmarks.tooling.codex_visibility \
562563
--execute --cases "$(VISIBILITY_CASES)" --mcp-url "$(VISIBILITY_MCP_URL)" \
563564
--model "$(VISIBILITY_MODEL)" --reasoning-effort "$(VISIBILITY_REASONING_EFFORT)" \
565+
--tool-mode "$(VISIBILITY_TOOL_MODE)" \
564566
--repetitions "$(VISIBILITY_REPETITIONS)" --output "$(VISIBILITY_OUTPUT)" \
565567
$(foreach case,$(VISIBILITY_CASES_SELECTED),--case "$(case)") \
566568
$(if $(VISIBILITY_SKILL),--skill "$(VISIBILITY_SKILL)",)
567569

570+
codex-tool-context: ## Measure ALL_TOOLS projection cost in Codex ATIF traces (TRAJECTORIES="...").
571+
@test -n "$(TRAJECTORIES)" || { echo "TRAJECTORIES is required" >&2; exit 2; }
572+
$(UV_RUN) python -m benchmarks.tooling.codex_tool_context $(TRAJECTORIES) \
573+
$(if $(LABEL),--label "$(LABEL)",) $(if $(OUTPUT),--output "$(OUTPUT)",)
574+
568575
provider-eval: ## Run pinned provider feasibility jobs (PROVIDER=cddlib|cgal|gudhi|lean-repl|nauty|regina).
569576
@test -n "$(PROVIDER)" || { echo "PROVIDER is required" >&2; exit 2; }
570577
@case "$(PROVIDER)" in cddlib|cgal|gudhi|lean-repl|nauty|regina) ;; *) echo "unknown provider: $(PROVIDER)" >&2; exit 2;; esac
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""Measure model-visible tool-catalog cost in Codex ATIF trajectories."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import hashlib
7+
import json
8+
import statistics
9+
from pathlib import Path
10+
from typing import Any
11+
12+
from benchmarks.tooling.errors import HarborSuiteError
13+
14+
_JACOBIAN_FIND = "tools.mcp__jacobian__math_find("
15+
_JACOBIAN_RUN = "tools.mcp__jacobian__math_run("
16+
17+
18+
def _read_trajectory(path: Path) -> dict[str, Any]:
19+
try:
20+
value = json.loads(path.read_text(encoding="utf-8"))
21+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
22+
raise HarborSuiteError(f"unable to read ATIF trajectory {path}: {exc}") from exc
23+
if not isinstance(value, dict) or not isinstance(value.get("steps"), list):
24+
raise HarborSuiteError(
25+
f"invalid ATIF trajectory {path}: steps must be an array"
26+
)
27+
return value
28+
29+
30+
def _visible_bytes(value: object) -> int:
31+
if isinstance(value, str):
32+
encoded = value.encode("utf-8")
33+
else:
34+
encoded = json.dumps(
35+
value, ensure_ascii=False, separators=(",", ":"), sort_keys=True
36+
).encode("utf-8")
37+
return len(encoded)
38+
39+
40+
def _integer(value: object) -> int | None:
41+
return value if isinstance(value, int) and not isinstance(value, bool) else None
42+
43+
44+
def _number(value: object) -> int | float | None:
45+
return (
46+
value
47+
if isinstance(value, (int, float)) and not isinstance(value, bool)
48+
else None
49+
)
50+
51+
52+
def _visible_results(step: dict[str, Any]) -> dict[str, int]:
53+
observation = step.get("observation")
54+
results = observation.get("results", []) if isinstance(observation, dict) else []
55+
visible_by_call: dict[str, int] = {}
56+
if not isinstance(results, list):
57+
return visible_by_call
58+
for result in results:
59+
if not isinstance(result, dict):
60+
continue
61+
source_call_id = result.get("source_call_id")
62+
if isinstance(source_call_id, str) and "content" in result:
63+
visible_by_call[source_call_id] = _visible_bytes(result["content"])
64+
return visible_by_call
65+
66+
67+
def _analyze_step(step: object) -> tuple[int, int, int, int, int, int]:
68+
if not isinstance(step, dict):
69+
return (0, 0, 0, 0, 0, 0)
70+
visible_by_call = _visible_results(step)
71+
tool_calls = step.get("tool_calls", [])
72+
if not isinstance(tool_calls, list):
73+
return (0, 0, 0, 0, 0, 0)
74+
scan_count = scan_bytes = unbound_scan_count = tool_output_bytes = 0
75+
direct_find_references = direct_run_references = 0
76+
for call in tool_calls:
77+
if not isinstance(call, dict):
78+
continue
79+
call_id = call.get("tool_call_id")
80+
visible = visible_by_call.get(call_id, 0) if isinstance(call_id, str) else 0
81+
tool_output_bytes += visible
82+
arguments = call.get("arguments")
83+
source = arguments.get("input") if isinstance(arguments, dict) else None
84+
if call.get("function_name") != "exec" or not isinstance(source, str):
85+
continue
86+
direct_find_references += source.count(_JACOBIAN_FIND)
87+
direct_run_references += source.count(_JACOBIAN_RUN)
88+
if "ALL_TOOLS" in source:
89+
scan_count += 1
90+
scan_bytes += visible
91+
if not isinstance(call_id, str) or call_id not in visible_by_call:
92+
unbound_scan_count += 1
93+
return (
94+
scan_count,
95+
scan_bytes,
96+
unbound_scan_count,
97+
tool_output_bytes,
98+
direct_find_references,
99+
direct_run_references,
100+
)
101+
102+
103+
def analyze_trajectory(path: Path) -> dict[str, Any]:
104+
"""Extract directory projection and token-cost facts from one ATIF trace."""
105+
106+
trajectory = _read_trajectory(path)
107+
scan_count = 0
108+
scan_bytes = 0
109+
unbound_scan_count = 0
110+
tool_output_bytes = 0
111+
direct_find_references = 0
112+
direct_run_references = 0
113+
114+
for step in trajectory["steps"]:
115+
step_counts = _analyze_step(step)
116+
scan_count += step_counts[0]
117+
scan_bytes += step_counts[1]
118+
unbound_scan_count += step_counts[2]
119+
tool_output_bytes += step_counts[3]
120+
direct_find_references += step_counts[4]
121+
direct_run_references += step_counts[5]
122+
123+
metrics = trajectory.get("final_metrics")
124+
metrics = metrics if isinstance(metrics, dict) else {}
125+
prompt_tokens = _integer(metrics.get("total_prompt_tokens"))
126+
cached_tokens = _integer(metrics.get("total_cached_tokens"))
127+
uncached_tokens = (
128+
max(0, prompt_tokens - cached_tokens)
129+
if prompt_tokens is not None and cached_tokens is not None
130+
else None
131+
)
132+
return {
133+
"trajectory": str(path),
134+
"trajectory_sha256": "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest(),
135+
"agent": trajectory.get("agent"),
136+
"all_tools_scan_count": scan_count,
137+
"all_tools_model_visible_bytes": scan_bytes,
138+
"all_tools_unbound_observation_count": unbound_scan_count,
139+
"tool_model_visible_bytes": tool_output_bytes,
140+
"direct_jacobian_find_references": direct_find_references,
141+
"direct_jacobian_run_references": direct_run_references,
142+
"prompt_tokens": prompt_tokens,
143+
"cached_prompt_tokens": cached_tokens,
144+
"uncached_prompt_tokens": uncached_tokens,
145+
"completion_tokens": _integer(metrics.get("total_completion_tokens")),
146+
"cost_usd": _number(metrics.get("total_cost_usd")),
147+
}
148+
149+
150+
def _median(trials: list[dict[str, Any]], field: str) -> int | float | None:
151+
values = [
152+
trial[field] for trial in trials if isinstance(trial.get(field), (int, float))
153+
]
154+
return statistics.median(values) if values else None
155+
156+
157+
def build_report(paths: list[Path], *, label: str) -> dict[str, Any]:
158+
"""Build one digest-bound observation report for a set of trajectories."""
159+
160+
if not paths:
161+
raise HarborSuiteError("at least one ATIF trajectory is required")
162+
trials = [analyze_trajectory(path) for path in paths]
163+
return {
164+
"schema_version": "1",
165+
"label": label,
166+
"trial_count": len(trials),
167+
"summary": {
168+
"all_tools_scan_trials": sum(
169+
int(trial["all_tools_scan_count"] > 0) for trial in trials
170+
),
171+
"all_tools_scan_count": sum(
172+
int(trial["all_tools_scan_count"]) for trial in trials
173+
),
174+
"all_tools_model_visible_bytes": sum(
175+
int(trial["all_tools_model_visible_bytes"]) for trial in trials
176+
),
177+
"all_tools_unbound_observation_count": sum(
178+
int(trial["all_tools_unbound_observation_count"]) for trial in trials
179+
),
180+
"median_prompt_tokens": _median(trials, "prompt_tokens"),
181+
"median_cached_prompt_tokens": _median(trials, "cached_prompt_tokens"),
182+
"median_uncached_prompt_tokens": _median(trials, "uncached_prompt_tokens"),
183+
"median_completion_tokens": _median(trials, "completion_tokens"),
184+
"median_cost_usd": _median(trials, "cost_usd"),
185+
},
186+
"trials": trials,
187+
}
188+
189+
190+
def _parser() -> argparse.ArgumentParser:
191+
parser = argparse.ArgumentParser(description=__doc__)
192+
parser.add_argument("trajectories", nargs="+", type=Path)
193+
parser.add_argument("--label", default="observation")
194+
parser.add_argument("--output", type=Path)
195+
return parser
196+
197+
198+
def main() -> int:
199+
args = _parser().parse_args()
200+
try:
201+
report = build_report(args.trajectories, label=args.label)
202+
except HarborSuiteError as exc:
203+
raise SystemExit(str(exc)) from exc
204+
rendered = json.dumps(report, indent=2, sort_keys=True) + "\n"
205+
if args.output is None:
206+
print(rendered, end="")
207+
else:
208+
args.output.parent.mkdir(parents=True, exist_ok=True)
209+
args.output.write_text(rendered, encoding="utf-8")
210+
return 0
211+
212+
213+
if __name__ == "__main__":
214+
raise SystemExit(main())
215+
216+
217+
__all__ = ["analyze_trajectory", "build_report"]

benchmarks/tooling/codex_visibility.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ class AdoptionExpectation(StrEnum):
6464
ABSTAIN = "ABSTAIN"
6565

6666

67+
class ToolMode(StrEnum):
68+
"""How Codex receives and dispatches tools during one visibility run."""
69+
70+
DIRECT = "direct"
71+
UNIFIED_EXEC = "unified_exec"
72+
73+
6774
class VisibilityCase(BaseModel):
6875
"""One agent-visible prompt plus hidden trajectory expectations."""
6976

@@ -323,6 +330,7 @@ def _codex_arguments(
323330
reasoning_effort: str,
324331
mcp_url: str,
325332
prompt: str,
333+
tool_mode: ToolMode,
326334
) -> tuple[str, ...]:
327335
arguments = [
328336
"-a",
@@ -343,6 +351,8 @@ def _codex_arguments(
343351
"-c",
344352
f"mcp_servers.jacobian.url={json.dumps(mcp_url)}",
345353
]
354+
if tool_mode is ToolMode.UNIFIED_EXEC:
355+
arguments.extend(("--enable", "unified_exec"))
346356
if os.environ.get("JACOBIAN_MCP_BEARER_TOKEN"):
347357
arguments.extend(
348358
(
@@ -376,6 +386,7 @@ def _run_case(
376386
reasoning_effort: str,
377387
mcp_url: str,
378388
timeout_seconds: float,
389+
tool_mode: ToolMode,
379390
) -> dict[str, Any]:
380391
stem = f"{case.case_id}-r{repetition:02d}"
381392
transcript_path = output / f"{stem}.jsonl"
@@ -389,6 +400,7 @@ def _run_case(
389400
reasoning_effort=reasoning_effort,
390401
mcp_url=mcp_url,
391402
prompt=case.prompt,
403+
tool_mode=tool_mode,
392404
),
393405
cwd=workspace,
394406
timeout_seconds=timeout_seconds,
@@ -441,6 +453,13 @@ def _parser() -> argparse.ArgumentParser:
441453
parser.add_argument("--mcp-url", required=True)
442454
parser.add_argument("--model", required=True)
443455
parser.add_argument("--reasoning-effort", default="high")
456+
parser.add_argument(
457+
"--tool-mode",
458+
type=ToolMode,
459+
choices=tuple(ToolMode),
460+
default=ToolMode.DIRECT,
461+
help="Codex tool dispatch mode; unified_exec matches Harbor Code Mode.",
462+
)
444463
parser.add_argument("--repetitions", type=int, default=1)
445464
parser.add_argument(
446465
"--case",
@@ -518,6 +537,7 @@ def main() -> None:
518537
reasoning_effort=args.reasoning_effort,
519538
mcp_url=args.mcp_url,
520539
timeout_seconds=args.timeout_seconds,
540+
tool_mode=args.tool_mode,
521541
)
522542
for case in selected_cases
523543
for repetition in range(1, args.repetitions + 1)
@@ -549,6 +569,7 @@ def main() -> None:
549569
"codex_version": codex_version,
550570
"model": args.model,
551571
"reasoning_effort": args.reasoning_effort,
572+
"tool_mode": args.tool_mode,
552573
"repetitions": args.repetitions,
553574
"repository_revision": git_head_sha(_ROOT),
554575
},

docs/how-to/run-codex-visibility-evaluation.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ make codex-visibility VISIBILITY_EXECUTE=1 \
2424
VISIBILITY_OUTPUT=benchmarks/results/visibility-skill
2525
```
2626

27+
Set `VISIBILITY_TOOL_MODE=unified_exec` to reproduce Codex Code Mode's nested
28+
tool dispatch, as used by the Harbor adapter. This matters for cost testing:
29+
Code Mode exposes nested methods through `tools`, and a model can otherwise
30+
print matching entries from `ALL_TOOLS` into its own context before calling
31+
them.
32+
2733
The runner refuses to overwrite an existing output directory. Each output binds
2834
the prompt-suite digest, Git revision, Codex version, model, reasoning effort,
2935
skill digest, evaluator and telemetry-parser digests, MCP server metadata, tool
@@ -46,6 +52,23 @@ cumulative Codex input-token count may grow much faster than MCP payload bytes
4652
because each later model turn includes earlier tool results; compare both
4753
dimensions rather than treating adoption alone as a win.
4854

55+
For Harbor ATIF trajectories, measure that projection directly rather than
56+
inferring it from token totals:
57+
58+
```sh
59+
make codex-tool-context \
60+
TRAJECTORIES="benchmarks/results/<job>/<trial>/agent/trajectory.json" \
61+
LABEL=skill-treatment \
62+
OUTPUT=benchmarks/results/tool-context-treatment.json
63+
```
64+
65+
The report counts `exec` source that references `ALL_TOOLS`, binds the matching
66+
observation bytes by tool-call ID, and reports cached and uncached prompt-token
67+
medians separately. Missing call-ID bindings are reported rather than silently
68+
treated as measured zero-byte projections. It is a client-behavior diagnostic,
69+
not a correctness score or proof that a prompt change caused the observed
70+
difference.
71+
4972
For an A/B claim, hold the suite digest, Codex version, model, reasoning effort,
5073
MCP catalog, budgets, and repetition count fixed. Change only the visibility
5174
condition, use multiple repetitions, and report each cue level and expectation

0 commit comments

Comments
 (0)