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
pull from: feat/soul-openai-tools-v2
merge into: :main
:main
:fix-mcp-wrapper-tool-schemas
:docs-cognitive-architecture
:docs/session-2026-08-13-language-faculty
:docs/geo-operators-el-cutover-reversal-2026-08-13
:docs/engram-cognitive-architecture-2026-08-12
:engram-store-wiring
:el-route-decorators
:fix/bound-session-payload
:feat/cgi-identity-accessors
:fix/restore-soul-identity-producer
:rebase/openai-tools-onto-main
:rebase/gate-state-key-reads-onto-main
:rebase/structural-audit-onto-main
:feat/self-seeded-context
:integrate/semantic-plus-writethrough
:feat/semantic-leg-dropout
:feat/semantic-substrate
:feat/gold-set-heldout
:feat/evidence-weighted-merge
:feat/structural-audit
:feat/claim24-unfloored-semantic
:feat/executive-filter-recall
:feat/bm25-lexical-leg
:feat/semantic-seeded-assoc
:feat/associative-recall-leg
:feat/hybrid-semantic-recall
:chore/retrieval-measurement-contract
:feat/retrieval-eval-harness
:feat/soul-write-through
:feat/recall-through-activation
:chore/regen-soul-amalgam-20260807
:feat/gate-state-key-reads
:fix/129-on-openai-tools
:fix/129-history-amplification
:fix/liveness-engine-91
:feat/soul-openai-tools-v2
:fix/resume-server-tool-replay
:fix/soul-history-provenance-20260805
:feat/soul-plain-chat-generation-20260805
:feat/soul-native-web-search-20260804
:ci/pin-vendored-runtime
:reconcile/soul-union-main
:fix/bound-session-payload-main
:fix/receipts-wrapper-forget
:fix/receipts-agent-tools
:reconcile/neuron-wip-to-main
:hotfix/trackb-threat-to-others
:reconcile/hotfix-to-main-launch
:hotfix/elc-source-typos
:fix/safety-contact-truncation
:fix/genesis-boot-crash
:fix/immutable-on-hotfix
:feat/bounded-persona-floor
:ci/rdynamic-http-handler
:ci/harden-gate-boot
:feat/agent-phase1-soul
:salvage/elh-state-20260704
:feat/soul-model-self-report
:feat/openai-format-providers
:el-runtime-repoint
:feat/plan-mode-endpoint
:fix/operator-identity-home-resolution
:fix/wrapper-backlog-endpoints
:fix/list-typed-slice-offset
:feat/connectors-call-route
:fix/chat-vision-attachments
:fix/prevent-engram-corruption
:fix/emergency-regressions
:fix/session-continuity-hook
:fix/context-dedup-shared-ids
:fix/engram-float-parser
:improve/recall-context-format
:improve/recall-context-dedup
:improve/recall-cross-session-continuity
:improve/recall-emotional-recall
:improve/recall-activation-seed
:improve/recall-recall-completeness
:improve/recall-temporal-precision
:improve/recall-engram-scoring
:improve/recall-recall-reliability
:improve/recall-session-start-recall
:improve/reliability-engram-write
:improve/reliability-state-management
:improve/soul-memory-formation
:improve/safety-crisis-detection
:improve/reliability-route-error-recovery
:improve/reliability-llm-retry
:improve/reliability-session-boundary
:improve/reliability-safety-resilience
:improve/reliability-engram-connection
:improve/soul-routes-api
:improve/reliability-cross-session-affective
:propose/agent-workspace-root-read
:improve/reliability-conv-history
:improve/soul-strip
:improve/soul-chat-pipeline
:docs/conversation-retrieval-design
:propose/no-fake-tools-in-chat-mode
:fix/ci-soul-build-single-file
:fix/canonical-self-bridge
:feat/agent-tool-workspace-scope
:fix/agentic-tools-duplicate-web-search
:green/agentic-fixes
:feat/connectors-soul
:feat/layer-safety
:feat/layer-imprint
:feat/layer-stewardship
:test/layer-composition
:test/layer-safety
:test/layer-stewardship
:test/layer-imprint
:feat/memory-delete-update
:feat/native-web-search
No Reviewers
Labels
Clear labels
BETA-CRITICAL
blocks-public-beta
HELD
ORTHOGONAL
P0
POST-BETA
security
On the beta critical path — must resolve before ship
Must be fixed before any public beta
Blocked pending an external decision (counsel / dual sign-off)
Parallel workstream (testing/hardening) — safe to assign off critical path
Drop-everything severity
Important but not blocking the beta ship
Security vulnerability
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
will.anderson
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: neuron-technologies/neuron#122
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feat/soul-openai-tools-v2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.elsays 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 inSPEC-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_URLand puts the provider key inANTHROPIC_API_KEY+SOUL_API_KEY(installer/macos/neuron-daemons.sh:288-300). The engine's fork read onlyNEURON_LLM_0_*, which nothing sets in a customer build — souse_openaiwas always false and every non-Anthropic user's turn went toapi.anthropic.comcarrying the wrong key.Proven side-by-side, not inferred. Identical env, probe server logging every hit:
15cf7d1b…, matches the manifest pin){"error":"llm unavailable"}{"error":"llm unavailable"}{"reply":"PROBE-OK-OPENAI-DIALECT","model":"llama-3.3-70b-versatile","agentic":true}[llm] using legacy ANTHROPIC_API_KEY fallback)POST /v1/chat/completions,Authorization: Bearer gsk_…Since
A3-ROUND9-CHECKLISTstep 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_bulletsper 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 one2— a box-drawing rule (U+2500 =E2 94 80) sliced in half, from a memory containing table borders.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 withoutinput_schema(server-sideweb_search) are skipped: they cannot execute on this wire.openai_agentic_loop()— forked, not parameterised, soagentic_loop(which carries every round-7/8/9 fix) is provably untouched and gate9 stays a valid regression check for free.parallel_tool_calls:falsesent 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.json_get_raw) — a JSONnullstaysnull; nothing is lost to a decode/re-encode round trip.bridge_save()gains awirescalar;agentic_resumebranches on it and reads it from the blob's scalar head only. An unbounded first-match scan would run intomessages_raw(model-controlled) — that is exactly the round-9 resume defect. Pinned by a test that plants a structural decoy.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:elcis a compiler, it emits C and exits. This builds and runs them. It also computes the verdict itself, because every counted test file'sN passed, M failedsummary is a permanent 0/0 — the counters increment insideifBLOCKS, 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
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)
/v1/chat/completionsto a base URL already ending in/v1→/v1/v1/…404. Runtime/plain-chat territory; not rewired mid-beta. Noteopenai_chat_complete()has zero callers — that lane is served entirely by the runtime chain.run_progressresets 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.shneeds 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".🤖 Generated with Claude Code
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_progressresets 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.
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.shfinally making the engine's tests runnable. The scalar-head read inagentic_resumewith 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:
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.mdhas 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 nowtests/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.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>Rebased onto
main(post-f1f52bc) asrebase/openai-tools-onto-main— 27 commits of drift resolved.This branch changes
.elsources, 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 leftdist/soul.cholding 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.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.