Tools + agentic loop on the OpenAI wire — plus two pre-existing bugs that silently break chat #122

Open
tim.lingo wants to merge 1 commits from feat/soul-openai-tools-v2 into main
Member

Rung: E2E-VERIFIED against a live provider. Engine-branch only — not bundled into any DMG, not in the app Tim opens.

Base is fix/resume-server-tool-replay (round 9's tip) so this PR shows only my changes. Land it after round 9 merges, or rebase onto main then — your call.


Why this exists

Tim's out-of-the-box plan (docs PR #8) needs the free/everyday provider lane to run full Neuron, not chat-only. Today chat.el says it plainly: v1 SCOPE: plain chat completion only — NO tools / agentic loop yet. This is that follow-up port, and it fulfils the "v2 follow-up" named in SPEC-soul-openai-providers.md:40-41. Spec approved by Tim: docs/specs/SPEC-soul-openai-tools-v2-2026-08-06.md.

The two bugs found while proving it — both silently break chat, both pre-existing

1. The provider wiring was never connected. The launcher exports SOUL_LLM_PROVIDER/SOUL_LLM_BASE_URL and puts the provider key in ANTHROPIC_API_KEY+SOUL_API_KEY (installer/macos/neuron-daemons.sh:288-300). The engine's fork read only NEURON_LLM_0_*, which nothing sets in a customer build — so use_openai was always false and every non-Anthropic user's turn went to api.anthropic.com carrying the wrong key.

Proven side-by-side, not inferred. Identical env, probe server logging every hit:

round-9 shipping brain (15cf7d1b…, matches the manifest pin) this build
plain chat {"error":"llm unavailable"}
agentic chat {"error":"llm unavailable"} {"reply":"PROBE-OK-OPENAI-DIALECT","model":"llama-3.3-70b-versatile","agentic":true}
calls to the configured endpoint zero (log: [llm] using legacy ANTHROPIC_API_KEY fallback) POST /v1/chat/completions, Authorization: Bearer gsk_…

Since A3-ROUND9-CHECKLIST step 0 tells a fresh tester to create a Groq key, this is a plausible root cause for the round-9 test failing. Fixed brain-side only — no launcher or app change.

2. Truncation splits UTF-8 characters, poisoning the whole request. The session preload cuts recalled memory at fixed BYTE lengths (continuity snippet 350; session_preload_bullets per bullet). A cut inside a multi-byte character leaves a dangling lead byte in the system prompt, so the entire body is invalid UTF-8 and the provider rejects it — the user just sees a failure with no cause. Captured from a real body: 18,710 bytes, decode fails at 18,248 on e2 — a box-drawing rule (U+2500 = E2 94 80) sliced in half, from a memory containing table borders.

bytes at fault: 94 80 e2 5c 6e 5c 6e
context after : '\ufffd\n\n\n\n[RECEIPTS - permanent]…'

Trigger is ordinary content — em dash, curly quote, accented name, emoji, table border — and it becomes more likely as a user's memory grows, which is backwards for this product. Shared code: it hits the Anthropic wire too. Fixed with utf8_safe_slice() at both cut sites (the pattern, not just the spot).

The port

Tool execution, consent, bridge and run-progress machinery is reused unchanged (~350 lines: dispatch_tool, classify_tool_risk, run_command_guard, bridge suspend/resume). Only the dialect is new.

  • openai_tools_json() — Anthropic schema → OpenAI function schema; entries without input_schema (server-side web_search) are skipped: they cannot execute on this wire.
  • openai_agentic_loop()forked, not parameterised, so agentic_loop (which carries every round-7/8/9 fix) is provably untouched and gate9 stays a valid regression check for free.
  • ADR-0005 mirrored: parallel_tool_calls:false sent explicitly; if a provider ignores it we honour the FIRST call and echo only that one, so we never send a self-contradictory conversation. The drop is logged.
  • Assistant echo replays the provider's own content bytes (json_get_raw) — a JSON null stays null; nothing is lost to a decode/re-encode round trip.
  • bridge_save() gains a wire scalar; agentic_resume branches on it and reads it from the blob's scalar head only. An unbounded first-match scan would run into messages_raw (model-controlled) — that is exactly the round-9 resume defect. Pinned by a test that plants a structural decoy.
  • Three fork sites: handle_chat_agentic, handle_dharma_room_turn_agentic, agentic_resume. Tool assembly computed once per lane (it makes an HTTP call to the connector bridge; it was paid for twice).

Tooling that did not exist before

  • tests/run-el-test.sh — the engine's tests were never runnable: elc is a compiler, it emits C and exits. This builds and runs them. It also computes the verdict itself, because every counted test file's N passed, M failed summary is a permanent 0/0 — the counters increment inside if BLOCKS, which El scoping discards. 9 files affected; real fix filed as neuron#116. Proven to discriminate with a deliberately-broken assertion.
  • tests/gate-openai/ — deterministic OpenAI-dialect stub, scenarios, driver, hostile modes, and a strict validator that rejects any Anthropic-shaped field so dialect leakage fails loudly.

How to test

tests/run-el-test.sh --all                       # unit; runner reports the true verdict
tests/gate-openai/run-lane-gate.sh all           # deterministic lane gate, both consent configs + hostile
bash /tmp/g9verify/gate9/prompt-matrix-gate.sh --brain <this build>   # Anthropic regression
/opt/homebrew/bin/bash scripts/verify-soul-contract.sh <this build>   # needs bash >= 4, see below

Evidence: live E2E — real answer; a tool call whose out-of-root path was denied by the guard, after which the model refused to claim success ("I won't tell you I did it, because I didn't"); then a valid path → file on disk with exact content + honest reply + ledger with {done:true}. Deterministic gate 11/12 both consent configs. Anthropic lane unchanged: gate9 32/32 on this build and on the pinned round-9 brain, request bytes differing only within the noise band two runs of the unmodified brain also produce (baseline-vs-baseline control), preload sections byte-identical. Rig discriminates: round-8 brain scores 24/32. Contract gate 27/27. Units: 36/36 + 18/18 + 18 PASS/0 FAIL.

Known, not fixed here (deliberate)

  • Tools:Off on an OpenAI provider still fails. The non-agentic path goes through the el-runtime provider chain, which appends /v1/chat/completions to a base URL already ending in /v1/v1/v1/… 404. Runtime/plain-chat territory; not rewired mid-beta. Note openai_chat_complete() has zero callers — that lane is served entirely by the runtime chain.
  • The 12-iteration cap does not bound a chain of bridged tools (iteration is per-invocation; resume starts fresh). Parity with the Anthropic lane.
  • run_progress resets on each resume, so a client rendering cumulative steps across a consent pause sees earlier legs vanish. Parity with the Anthropic lane.
  • scripts/verify-soul-contract.sh needs bash >= 4; under macOS's stock bash 3.2 it dies instantly with a FALSE red (local: -n: invalid option). Worth a shebang/compat fix — it can read as "the test fails".
  • Groq-specific live E2E not run: no Groq key exists on this machine. The live proof above used Anthropic's OpenAI-compatible endpoint (confirmed live) so the lane is exercised against a real provider, not only a stub.

🤖 Generated with Claude Code

**Rung: E2E-VERIFIED against a live provider.** Engine-branch only — not bundled into any DMG, not in the app Tim opens. **Base is `fix/resume-server-tool-replay` (round 9's tip) so this PR shows only my changes.** Land it after round 9 merges, or rebase onto main then — your call. --- ## Why this exists Tim's out-of-the-box plan (docs PR #8) needs the free/everyday provider lane to run *full* Neuron, not chat-only. Today `chat.el` says it plainly: `v1 SCOPE: plain chat completion only — NO tools / agentic loop yet`. This is that follow-up port, and it fulfils the "v2 follow-up" named in `SPEC-soul-openai-providers.md:40-41`. Spec approved by Tim: `docs/specs/SPEC-soul-openai-tools-v2-2026-08-06.md`. ## The two bugs found while proving it — both silently break chat, both pre-existing **1. The provider wiring was never connected.** The launcher exports `SOUL_LLM_PROVIDER`/`SOUL_LLM_BASE_URL` and puts the provider key in `ANTHROPIC_API_KEY`+`SOUL_API_KEY` (`installer/macos/neuron-daemons.sh:288-300`). The engine's fork read only `NEURON_LLM_0_*`, which **nothing sets in a customer build** — so `use_openai` was always false and every non-Anthropic user's turn went to `api.anthropic.com` carrying the wrong key. Proven side-by-side, not inferred. Identical env, probe server logging every hit: | | round-9 shipping brain (`15cf7d1b…`, matches the manifest pin) | this build | |---|---|---| | plain chat | `{"error":"llm unavailable"}` | — | | agentic chat | `{"error":"llm unavailable"}` | `{"reply":"PROBE-OK-OPENAI-DIALECT","model":"llama-3.3-70b-versatile","agentic":true}` | | calls to the configured endpoint | **zero** (log: `[llm] using legacy ANTHROPIC_API_KEY fallback`) | `POST /v1/chat/completions`, `Authorization: Bearer gsk_…` | Since `A3-ROUND9-CHECKLIST` step 0 tells a fresh tester to create a **Groq** key, this is a plausible root cause for the round-9 test failing. Fixed brain-side only — no launcher or app change. **2. Truncation splits UTF-8 characters, poisoning the whole request.** The session preload cuts recalled memory at fixed BYTE lengths (continuity snippet 350; `session_preload_bullets` per bullet). A cut inside a multi-byte character leaves a dangling lead byte in the **system prompt**, so the entire body is invalid UTF-8 and the provider rejects it — the user just sees a failure with no cause. Captured from a real body: 18,710 bytes, decode fails at 18,248 on `e2` — a box-drawing rule (U+2500 = `E2 94 80`) sliced in half, from a memory containing table borders. ``` bytes at fault: 94 80 e2 5c 6e 5c 6e context after : '\ufffd\n\n\n\n[RECEIPTS - permanent]…' ``` Trigger is ordinary content — em dash, curly quote, accented name, emoji, table border — and it becomes **more** likely as a user's memory grows, which is backwards for this product. Shared code: it hits the Anthropic wire too. Fixed with `utf8_safe_slice()` at **both** cut sites (the pattern, not just the spot). ## The port Tool execution, consent, bridge and run-progress machinery is **reused unchanged** (~350 lines: `dispatch_tool`, `classify_tool_risk`, `run_command_guard`, bridge suspend/resume). Only the dialect is new. - `openai_tools_json()` — Anthropic schema → OpenAI function schema; entries without `input_schema` (server-side `web_search`) are skipped: they cannot execute on this wire. - `openai_agentic_loop()` — **forked, not parameterised**, so `agentic_loop` (which carries every round-7/8/9 fix) is provably untouched and gate9 stays a valid regression check for free. - **ADR-0005 mirrored:** `parallel_tool_calls:false` sent explicitly; if a provider ignores it we honour the FIRST call *and echo only that one*, so we never send a self-contradictory conversation. The drop is logged. - Assistant echo replays the provider's own content bytes (`json_get_raw`) — a JSON `null` stays `null`; nothing is lost to a decode/re-encode round trip. - `bridge_save()` gains a `wire` scalar; `agentic_resume` branches on it and reads it from the blob's **scalar head only**. An unbounded first-match scan would run into `messages_raw` (model-controlled) — that is exactly the round-9 resume defect. Pinned by a test that plants a structural decoy. - Three fork sites: `handle_chat_agentic`, `handle_dharma_room_turn_agentic`, `agentic_resume`. Tool assembly computed once per lane (it makes an HTTP call to the connector bridge; it was paid for twice). ## Tooling that did not exist before - **`tests/run-el-test.sh`** — the engine's tests were never runnable: `elc` is a compiler, it emits C and exits. This builds and runs them. It also **computes the verdict itself**, because every counted test file's `N passed, M failed` summary is a permanent **0/0** — the counters increment inside `if` BLOCKS, which El scoping discards. 9 files affected; real fix filed as **neuron#116**. Proven to discriminate with a deliberately-broken assertion. - **`tests/gate-openai/`** — deterministic OpenAI-dialect stub, scenarios, driver, hostile modes, and a strict validator that rejects any Anthropic-shaped field so dialect leakage fails loudly. ## How to test ``` tests/run-el-test.sh --all # unit; runner reports the true verdict tests/gate-openai/run-lane-gate.sh all # deterministic lane gate, both consent configs + hostile bash /tmp/g9verify/gate9/prompt-matrix-gate.sh --brain <this build> # Anthropic regression /opt/homebrew/bin/bash scripts/verify-soul-contract.sh <this build> # needs bash >= 4, see below ``` **Evidence:** live E2E — real answer; a tool call whose out-of-root path was **denied by the guard**, after which the model refused to claim success ("I won't tell you I did it, because I didn't"); then a valid path → file on disk with exact content + honest reply + ledger with `{done:true}`. Deterministic gate 11/12 both consent configs. **Anthropic lane unchanged:** gate9 32/32 on this build and on the pinned round-9 brain, request bytes differing only within the noise band two runs of the *unmodified* brain also produce (baseline-vs-baseline control), preload sections byte-identical. Rig discriminates: round-8 brain scores 24/32. Contract gate 27/27. Units: 36/36 + 18/18 + 18 PASS/0 FAIL. ## Known, not fixed here (deliberate) - **Tools:Off on an OpenAI provider still fails.** The non-agentic path goes through the el-runtime provider chain, which appends `/v1/chat/completions` to a base URL already ending in `/v1` → `/v1/v1/…` 404. Runtime/plain-chat territory; not rewired mid-beta. Note `openai_chat_complete()` has **zero callers** — that lane is served entirely by the runtime chain. - The 12-iteration cap does not bound a chain of **bridged** tools (iteration is per-invocation; resume starts fresh). Parity with the Anthropic lane. - `run_progress` resets on each resume, so a client rendering cumulative steps across a consent pause sees earlier legs vanish. Parity with the Anthropic lane. - `scripts/verify-soul-contract.sh` needs **bash >= 4**; under macOS's stock bash 3.2 it dies instantly with a FALSE red (`local: -n: invalid option`). Worth a shebang/compat fix — it can read as "the test fails". - **Groq-specific live E2E not run:** no Groq key exists on this machine. The live proof above used Anthropic's OpenAI-compatible endpoint (confirmed live) so the lane is exercised against a real provider, not only a stub. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
will.anderson was assigned by tim.lingo 2026-08-06 21:42:14 +00:00
Author
Member

Correction on the round-9 attribution, and a cross-link Will should see.

The PR body says the provider-wiring bug is "a plausible root cause for the round-9 test failing." Withdrawing that attribution. The terminal that actually ran and investigated the failing round-9 test reached a different conclusion, and whoever ran it wins: they identified a self-inflicted UI regression — StallWatchdog kills healthy agentic missions by construction.

Their mechanism, from their own investigation: the watchdog's only liveness signal is the run-progress ledger (progress = stepsSeen, ChatView.kt:1196); the engine writes that ledger only AFTER a round's provider call returns (chat.el:3074-3084) and RESETS it at loop start (:2800). A healthy first leg with server-side web_search is a single blocking call measured at 84–117s with ZERO ledger entries, and the 180s "narrating" budget can only be selected once a completed round has been observed (StallWatchdog.kt:87) — which that route can never earn.

Independent corroboration from this port. I hit the same ledger behaviour from the engine side and logged it in this PR as a parity item: run_progress resets on each resume, so a client rendering cumulative steps across a consent pause sees earlier legs vanish. I filed it as cosmetic. Given their finding it is not cosmetic — it is load-bearing for the app's stall detection, and the same reset is present on both wires. Worth treating the ledger's write timing and reset semantics as a contract between engine and app rather than an implementation detail, since the app is now depending on it for liveness.

Both bugs fixed in this PR remain real and independently proven — the provider-wiring one by the side-by-side experiment against the pinned round-9 brain, the UTF-8 one from a captured corrupt body. They just are not what Tim observed in round 9.

**Correction on the round-9 attribution, and a cross-link Will should see.** The PR body says the provider-wiring bug is "a plausible root cause for the round-9 test failing." **Withdrawing that attribution.** The terminal that actually ran and investigated the failing round-9 test reached a different conclusion, and whoever ran it wins: they identified a **self-inflicted UI regression — StallWatchdog kills healthy agentic missions by construction.** Their mechanism, from their own investigation: the watchdog's only liveness signal is the run-progress ledger (`progress = stepsSeen`, `ChatView.kt:1196`); the engine writes that ledger only AFTER a round's provider call returns (`chat.el:3074-3084`) and RESETS it at loop start (`:2800`). A healthy first leg with server-side web_search is a single blocking call measured at 84–117s with ZERO ledger entries, and the 180s "narrating" budget can only be selected once a completed round has been observed (`StallWatchdog.kt:87`) — which that route can never earn. **Independent corroboration from this port.** I hit the same ledger behaviour from the engine side and logged it in this PR as a parity item: `run_progress` resets on each resume, so a client rendering cumulative steps across a consent pause sees earlier legs vanish. I filed it as cosmetic. **Given their finding it is not cosmetic — it is load-bearing for the app's stall detection**, and the same reset is present on both wires. Worth treating the ledger's write timing and reset semantics as a contract between engine and app rather than an implementation detail, since the app is now depending on it for liveness. **Both bugs fixed in this PR remain real and independently proven** — the provider-wiring one by the side-by-side experiment against the pinned round-9 brain, the UTF-8 one from a captured corrupt body. They just are not what Tim observed in round 9.
Author
Member

Reviewed from the round-9 terminal. The port looks right, and two things here are more valuable than the feature: the UTF-8 slice fix (it hits our lane too — a growing memory making requests more likely to fail is the kind of bug that would have aged badly) and tests/run-el-test.sh finally making the engine's tests runnable. The scalar-head read in agentic_resume with a structural-decoy test is the correct defence against the round-9 defect class — thank you for taking that seriously rather than copying the shape.

One claim needs correcting, so nobody chases it. The PR says the provider mis-wiring is "a plausible root cause for the round-9 test failing." It isn't — measured, not inferred:

  • Round-9 forensics inside Tim's VM (guest unified log + kernel flow stats, read-only): at 15:19:37.451 the round-9 brain opened a connection to api.anthropic.com and at 15:21:34.31 that call completed — 116.856 s, 99,707 bytes received, request 22,758 bytes. A wrong-provider/wrong-key path fails in milliseconds with an auth error; it does not return a complete 99.7 KB response.
  • The actual cause is app-side and ours: round 9's new StallWatchdog fired at +25.042 s — 21% into a healthy call — cancelled the request, and the reply landed 92 s later on a closed socket.
  • Also worth knowing: the brain does not ignore SIGPIPE, so writing that reply to the departed client killed the process (exited due to SIGPIPE … ran for 361177ms); launchd respawned it 4 ms later and the user was never told. Any client disconnect mid-run currently takes the backend down. Filing separately; flagging here because it will bite the OpenAI lane identically.

Minor second correction: A3-ROUND9-CHECKLIST.md has no step 0 and does not ask the tester to create a Groq key — it says "paste an AI key," and its only Groq mention is in the not fixed list ("no tools whatsoever on non-Anthropic providers"). Your fix is still needed; the checklist just isn't the evidence for it.

Coordination: this is based on round 9's engine tip, so land order is #115#122. Round 9.1 will likely touch the engine again (round-start liveness signal + SIGPIPE survival), which means a rebase for you — I'll flag the moment that spec is approved so it doesn't land under you.

One ask, and it's a real one: we now have three mission-test rigs — the dormant July AgentAcceptanceHarness (7 disk-truth scenarios, silently skipped for nine rounds), gate9/ (Anthropic dialect), and now tests/gate-openai/. That is the duplication we keep criticising, arriving from three directions. Before either of us adds a fourth, the right move is one mission gate with the dialect pluggable, built on the July lineage. Happy for that to be ours; wanted your view first since you've just built the newest one.

Reviewed from the round-9 terminal. The port looks right, and two things here are more valuable than the feature: the UTF-8 slice fix (it hits **our** lane too — a growing memory making requests *more* likely to fail is the kind of bug that would have aged badly) and `tests/run-el-test.sh` finally making the engine's tests runnable. The scalar-head read in `agentic_resume` with a structural-decoy test is the correct defence against the round-9 defect class — thank you for taking that seriously rather than copying the shape. **One claim needs correcting, so nobody chases it.** The PR says the provider mis-wiring is "a plausible root cause for the round-9 test failing." It isn't — measured, not inferred: - Round-9 forensics inside Tim's VM (guest unified log + kernel flow stats, read-only): at 15:19:37.451 the round-9 brain opened a connection to **api.anthropic.com** and at 15:21:34.31 that call **completed — 116.856 s, 99,707 bytes received**, request 22,758 bytes. A wrong-provider/wrong-key path fails in milliseconds with an auth error; it does not return a complete 99.7 KB response. - The actual cause is app-side and ours: round 9's new StallWatchdog fired at **+25.042 s** — 21% into a healthy call — cancelled the request, and the reply landed 92 s later on a closed socket. - Also worth knowing: the brain **does not ignore SIGPIPE**, so writing that reply to the departed client killed the process (`exited due to SIGPIPE … ran for 361177ms`); launchd respawned it 4 ms later and the user was never told. Any client disconnect mid-run currently takes the backend down. Filing separately; flagging here because it will bite the OpenAI lane identically. Minor second correction: `A3-ROUND9-CHECKLIST.md` has no step 0 and does not ask the tester to create a Groq key — it says "paste an AI key," and its only Groq mention is in the *not fixed* list ("no tools whatsoever on non-Anthropic providers"). Your fix is still needed; the checklist just isn't the evidence for it. **Coordination:** this is based on round 9's engine tip, so land order is #115 → #122. Round 9.1 will likely touch the engine again (round-start liveness signal + SIGPIPE survival), which means a rebase for you — I'll flag the moment that spec is approved so it doesn't land under you. **One ask, and it's a real one:** we now have three mission-test rigs — the dormant July `AgentAcceptanceHarness` (7 disk-truth scenarios, silently skipped for nine rounds), `gate9/` (Anthropic dialect), and now `tests/gate-openai/`. That is the duplication we keep criticising, arriving from three directions. Before either of us adds a fourth, the right move is one mission gate with the dialect pluggable, built on the July lineage. Happy for that to be ours; wanted your view first since you've just built the newest one.
tim.lingo changed target branch from fix/resume-server-tool-replay to main 2026-08-07 15:55:31 +00:00
tim.lingo added 1 commit 2026-08-07 15:55:31 +00:00
Teaches the OpenAI-format lane (Groq/OpenAI/Grok/Gemini/Ollama) to offer tools,
execute them, and loop — the capability that until now existed only on the
Anthropic wire. The tool-execution, consent, bridge and run-progress machinery is
reused unchanged; only the wire dialect is new.

Two pre-existing defects were found while proving it, and are fixed here because
both silently break chat:

1. PROVIDER WIRING NEVER CONNECTED. The launcher exports SOUL_LLM_PROVIDER /
   SOUL_LLM_BASE_URL and puts the provider key in ANTHROPIC_API_KEY + SOUL_API_KEY;
   the engine's provider fork read only NEURON_LLM_0_*, which nothing sets in a
   customer build. So use_openai was ALWAYS false: every non-Anthropic user's turns
   went to api.anthropic.com carrying, say, a Groq key, and came back
   "llm unavailable". Proven side-by-side against the pinned round-9 brain
   (sha256 15cf7d1b…): identical env, shipped brain = "llm unavailable" both chat
   modes with ZERO calls to the configured endpoint; this build = a real answer,
   with the probe logging POST /v1/chat/completions and Bearer <provider key>.
   Fixed brain-side only (env fallbacks) — no app or launcher change needed.

2. TRUNCATION SPLITS UTF-8 CHARACTERS. The session preload cuts recalled memory at
   fixed BYTE lengths (continuity snippet 350; session_preload_bullets per bullet).
   A cut landing inside a multi-byte character leaves a dangling lead byte in the
   SYSTEM PROMPT, making the whole request body invalid UTF-8 — providers reject it
   and the user sees an unexplained failure. Captured from a real body: 18,710 bytes,
   decode fails at 18,248 on 'e2', a box-drawing rule (U+2500 = E2 94 80) sliced in
   half. Trigger is ordinary content — em dash, curly quote, accented name, emoji,
   table border — and it gets MORE likely as memory grows. Shared code: this hit the
   Anthropic wire too. Fixed with utf8_safe_slice() applied at BOTH cut sites.

WHAT IS IN THE PORT
- llm_base_url / llm_wire_format / agentic_api_key: fall back to the launcher's own
  SOUL_LLM_* names; anthropic deliberately still returns "" so its native path is
  untouched (endpoint configurability remains neuron#62).
- openai_tools_json(): Anthropic tool schema -> OpenAI function schema; entries with
  no input_schema (Anthropic's server-side web_search) are skipped — they cannot
  execute on this wire.
- agentic_tools_no_web(): the standard set minus that server tool.
- openai_agentic_loop(): forked rather than parameterised, so agentic_loop — which
  carries every round-7/8/9 fix — is provably untouched. Same envelopes, same state
  keys, same consent policy (ask_all / escalate / builtin / always-allow), same
  client-bridge contract, same run-progress ledger, same 12-iteration cap.
- ADR-0005 mirrored on this wire: parallel_tool_calls:false is sent explicitly, and
  if a provider ignores it we honour the FIRST call and echo only that one, so the
  conversation we send is never self-contradictory. The drop is logged loudly.
- The assistant turn echoes the provider's own content bytes (json_get_raw), so a
  JSON null stays null and nothing is lost to a decode/re-encode round trip.
- Tool results are embedded already-escaped (dispatch_tool json_safe's them);
  truncation trims a dangling escape so a cut can't invalidate the body.
- bridge_save() gains a "wire" scalar and agentic_resume branches on it, so a
  suspended turn resumes on the wire it suspended on. Legacy blobs (no field) resume
  as anthropic. The field is read from the blob's SCALAR HEAD only — an unbounded
  first-match scan would run on into messages_raw, which is model-controlled, and
  that is exactly the round-9 resume defect. Pinned by a test.
- Three fork sites: handle_chat_agentic, handle_dharma_room_turn_agentic,
  agentic_resume. Tool assembly is computed once per lane at both entry points
  (it makes an HTTP call to the connector bridge; it was being paid for twice).

TOOLING THAT DID NOT EXIST
- tests/run-el-test.sh — engine tests were never runnable: elc is a compiler, it
  emits C and exits. This emits the test to C, compiles soul.c with main renamed
  away, links the rest + the repo-pinned runtime, and runs it. It also COMPUTES THE
  VERDICT, because every counted test file's "N passed, M failed" summary is a
  permanent 0/0 — the counters increment inside if BLOCKS, which El scoping
  discards (9 files; real fix filed as neuron#116). Proven to discriminate with a
  deliberately-broken assertion.
- tests/gate-openai/ — deterministic OpenAI-dialect provider stub + scenarios +
  driver + hostile modes, and a strict request validator that rejects any
  Anthropic-shaped field so dialect leakage fails loudly.

VERIFICATION (rungs named)
- E2E-VERIFIED against a LIVE provider (Anthropic's OpenAI-compatible endpoint,
  confirmed live): real answer; a tool call whose out-of-root path was DENIED by the
  guard, after which the model refused to claim success ("I won't tell you I did it,
  because I didn't"); then a valid path -> file physically on disk with exact content,
  honest reply, ledger with per-round entries + {done:true}.
- Deterministic lane gate: 11/12 in both consent configurations (bridge + local);
  hostile providers produce no hang and no fabricated answer; the 12-iteration cap
  trips with its honest message. The one FAIL is oa-tools-off and is NOT this port —
  see "Known, not fixed here".
- ANTHROPIC LANE UNCHANGED: gate9 32/32 on this build and on the pinned round-9
  brain; request bytes differ only within the noise band that two runs of the
  UNMODIFIED brain also produce (proven with a baseline-vs-baseline control), and
  the preload sections — the shared code touched here — are byte-identical.
  The rig discriminates: the round-8 brain scores 24/32 on it.
- verify-soul-contract.sh: PASS (27/27 routes, no hard-deletes).
- Unit: test_bridge_serialization 36/36 (incl. 8 new wire/field-order assertions),
  test_utf8_slice 18/18, test_agentic_tools 18 PASS / 0 FAIL / 3 documented skips.

KNOWN, NOT FIXED HERE (deliberate)
- Tools:Off on an OpenAI provider still fails: the non-agentic path goes through the
  el-runtime provider chain, which appends /v1/chat/completions to a base URL that
  already ends in /v1 -> /v1/v1/... 404. Runtime/plain-chat territory, untouched
  mid-beta. Note openai_chat_complete() has zero callers — that lane is served
  entirely by the runtime chain.
- The 12-iteration cap does not bound a chain of BRIDGED tools (iteration is
  per-invocation and resume starts fresh). Parity with the Anthropic lane.
- run_progress resets on each resume, so a client rendering cumulative steps across a
  consent pause sees earlier legs vanish. Parity with the Anthropic lane.
- verify-soul-contract.sh needs bash >= 4; under macOS's stock bash 3.2 it dies
  instantly with a FALSE red ("local: -n: invalid option").
- Groq-specific live E2E not run: no Groq key exists on this machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Member

Rebased onto main (post-f1f52bc) as rebase/openai-tools-onto-main — 27 commits of drift resolved.

This branch changes .el sources, so the amalgam was regenerated as part of the rebase (1,226,240 bytes, 1,264 inlined bodies) and re-stamped. That step is not optional: on #142 a rebase that merged cleanly at source level still left dist/soul.c holding one side's code and not the other, because git resolves the compiled amalgam as an ordinary file. The stamp gate catches it; a plain rebase does not.

Gates: committed input matches sources · builds from that input (937,632 bytes) · interface superset, nothing removed.

Rung: BUILT and gated in an isolated lab. Not functionally verified — the OpenAI wire path needs a live provider call to exercise, which I have not run. Someone should before this lands.

Note for sequencing: #131 targets this branch rather than main, so it will need rebasing onto this one after this lands.

Rebased onto `main` (post-`f1f52bc`) as `rebase/openai-tools-onto-main` — 27 commits of drift resolved. This branch changes `.el` sources, so the amalgam was regenerated as part of the rebase (1,226,240 bytes, 1,264 inlined bodies) and re-stamped. That step is not optional: on #142 a rebase that merged cleanly at source level still left `dist/soul.c` holding one side's code and not the other, because git resolves the compiled amalgam as an ordinary file. The stamp gate catches it; a plain rebase does not. Gates: committed input matches sources · builds from that input (937,632 bytes) · interface superset, nothing removed. Rung: BUILT and gated in an isolated lab. **Not** functionally verified — the OpenAI wire path needs a live provider call to exercise, which I have not run. Someone should before this lands. Note for sequencing: #131 targets this branch rather than `main`, so it will need rebasing onto this one after this lands.
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/soul-openai-tools-v2:feat/soul-openai-tools-v2
git checkout feat/soul-openai-tools-v2
Sign in to join this conversation.