de65991807
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>
149 lines
7.4 KiB
Bash
Executable File
149 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# run-el-test.sh — build and RUN one engine test (tests/*.el), printing its assertions.
|
|
#
|
|
# WHY THIS EXISTS (2026-08-06): the engine's tests/*.el files were never runnable from the
|
|
# tree. `elc` is a COMPILER — it emits C to stdout and exits; it does not execute anything.
|
|
# So "the tests" could only ever be read, not run, and a signature change could silently
|
|
# break them (exactly what happened when bridge_save gained its `wire` argument). This
|
|
# script closes that: emit the test to C, link it against the engine modules, execute it.
|
|
#
|
|
# HOW IT WORKS
|
|
# 1. elc <test>.el -> C on stdout (the test file's `main` + prototypes)
|
|
# 2. elb (once, cached) -> per-module C for the whole engine into a scratch dir
|
|
# 3. cc test.c + all modules EXCEPT soul.c (soul.c owns the real `main`) + the runtime
|
|
# 4. run it
|
|
#
|
|
# The test C references only the engine functions it actually calls, so there are no
|
|
# duplicate-symbol collisions with the module objects.
|
|
#
|
|
# RUNTIME: the REPO-PINNED vendor/el-runtime (NOT ~/el-sdk/el_runtime.c — that June build
|
|
# is missing builtins August code calls: engram_wm_count, engram_wm_top_json,
|
|
# http_delete_json, http_serve_async; linking against it fails with "symbol(s) not found").
|
|
#
|
|
# USAGE
|
|
# tests/run-el-test.sh tests/test_bridge_serialization.el # one test
|
|
# tests/run-el-test.sh --all # every tests/test_*.el
|
|
# REBUILD=1 tests/run-el-test.sh ... # force module regeneration
|
|
#
|
|
# Tests that need a live API key / running soul (see each file's header) will report their
|
|
# own skips or failures — this runner does not fake them.
|
|
set -uo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT" || exit 2
|
|
|
|
ELC="${ELC:-$HOME/el-sdk/elc}"
|
|
ELB="${ELB:-$HOME/Development/el-sdk/bin/elb}"
|
|
RUNTIME_DIR="${RUNTIME_DIR:-$REPO_ROOT/vendor/el-runtime/v1.0.0-20260501}"
|
|
SCRATCH="${SCRATCH:-/tmp/el-test-$(basename "$REPO_ROOT")}"
|
|
MODDIR="$SCRATCH/modules"
|
|
OPENSSL_INC="${OPENSSL_INC:-/opt/homebrew/opt/openssl@3/include}"
|
|
OPENSSL_LIB="${OPENSSL_LIB:-/opt/homebrew/opt/openssl@3/lib}"
|
|
|
|
for req in "$ELC" "$ELB" "$RUNTIME_DIR/el_runtime.c"; do
|
|
[ -e "$req" ] || { echo "run-el-test: missing required input: $req" >&2; exit 2; }
|
|
done
|
|
|
|
mkdir -p "$MODDIR" || exit 2
|
|
|
|
# ── Step 1: engine modules (cached — regeneration is the slow part) ────────────
|
|
if [ "${REBUILD:-0}" = "1" ] || [ ! -f "$MODDIR/chat.c" ] || [ "chat.el" -nt "$MODDIR/chat.c" ]; then
|
|
echo "run-el-test: generating engine modules into $MODDIR (this takes ~1-2 min)..."
|
|
# elb's own final link step fails by design here (it wants to produce a binary named
|
|
# `neuron` and we only need the per-module .c files it emits first). Ignore its rc.
|
|
"$ELB" --elc="$ELC" --runtime="$RUNTIME_DIR" --out="$MODDIR/" >"$SCRATCH/elb.log" 2>&1
|
|
if [ ! -f "$MODDIR/chat.c" ]; then
|
|
echo "run-el-test: FATAL — elb produced no chat.c; see $SCRATCH/elb.log" >&2
|
|
tail -5 "$SCRATCH/elb.log" >&2
|
|
exit 2
|
|
fi
|
|
# elb rewrites *.elh in the source tree as a side effect (cosmetic banner churn plus a
|
|
# stray soul..elh). Say so; the caller decides whether to `git restore` them.
|
|
echo "run-el-test: NOTE — elb regenerated *.elh in the source tree (cosmetic churn is expected; a stray soul..elh may appear)."
|
|
fi
|
|
|
|
# soul.c is needed for its engine functions (layered_cycle et al.) but it also owns the
|
|
# daemon's real `main`, which would collide with the test's own. Compile it ONCE to an
|
|
# object with `main` renamed away, and link that instead of the .c.
|
|
SOUL_OBJ="$SCRATCH/soul-nomain.o"
|
|
if [ "${REBUILD:-0}" = "1" ] || [ ! -f "$SOUL_OBJ" ] || [ "$MODDIR/soul.c" -nt "$SOUL_OBJ" ]; then
|
|
cc -std=c11 -O1 -DHAVE_CURL -Dmain=el_soul_daemon_main_unused \
|
|
-I "$RUNTIME_DIR" -I "$MODDIR" -I "$OPENSSL_INC" \
|
|
-include dist/elp-c-decls.h -Wno-error=implicit-function-declaration \
|
|
-c "$MODDIR/soul.c" -o "$SOUL_OBJ" 2>"$SCRATCH/soul-nomain.err" \
|
|
|| { echo "run-el-test: FATAL — could not compile soul.c without main" >&2
|
|
grep -E 'error:' "$SCRATCH/soul-nomain.err" | head -5 >&2; exit 2; }
|
|
fi
|
|
|
|
# Every module except soul.c (linked as the renamed object above) and the stray soul.elh.c.
|
|
MODS=("$SOUL_OBJ")
|
|
for f in "$MODDIR"/*.c; do
|
|
case "$(basename "$f")" in
|
|
soul.c|soul.elh.c) continue ;;
|
|
esac
|
|
MODS+=("$f")
|
|
done
|
|
[ "${#MODS[@]}" -gt 1 ] || { echo "run-el-test: no module objects found" >&2; exit 2; }
|
|
|
|
run_one() {
|
|
local test_el="$1"
|
|
local name; name="$(basename "$test_el" .el)"
|
|
local cfile="$SCRATCH/$name.c"
|
|
local bin="$SCRATCH/$name"
|
|
|
|
printf '\n══ %s ══\n' "$name"
|
|
|
|
if ! "$ELC" "$test_el" >"$cfile" 2>"$SCRATCH/$name.elc.err"; then
|
|
echo "COMPILE FAILED (elc):"; tail -10 "$SCRATCH/$name.elc.err"; return 1
|
|
fi
|
|
[ -s "$cfile" ] || { echo "COMPILE FAILED (elc produced empty C)"; return 1; }
|
|
|
|
if ! cc -std=c11 -O1 -DHAVE_CURL -rdynamic \
|
|
-I "$RUNTIME_DIR" -I "$MODDIR" -I "$OPENSSL_INC" -L "$OPENSSL_LIB" \
|
|
-include dist/elp-c-decls.h -Wno-error=implicit-function-declaration \
|
|
-o "$bin" "$cfile" "${MODS[@]}" "$RUNTIME_DIR/el_runtime.c" \
|
|
-lssl -lcrypto -lcurl -lpthread -lm 2>"$SCRATCH/$name.link.err"; then
|
|
echo "LINK FAILED:"; grep -E '"_|error:' "$SCRATCH/$name.link.err" | head -10; return 1
|
|
fi
|
|
|
|
# THE RUNNER OWNS THE VERDICT — the test files cannot be trusted to report it.
|
|
#
|
|
# Every tests/*.el assert helper does `let pass_count = pass_count + 1` INSIDE an if
|
|
# BLOCK. El's scope rule (the same one chat.el documents at every while-body mutation:
|
|
# "mutations inside if *blocks* don't escape scope") means those counters never
|
|
# increment, so all 9 counted test files print "N passed, M failed" as "0 passed, 0
|
|
# failed" — forever, whatever actually happened. A summary that can never report a
|
|
# failure is worth exactly as much as an assertion that can never fail. Logged as a
|
|
# bug for the real in-file fix; until then the verdict is computed HERE, from the
|
|
# assert helpers' own per-line output, which IS reliable.
|
|
local out="$SCRATCH/$name.out"
|
|
"$bin" 2>&1 | tee "$out"; local rc=${PIPESTATUS[0]}
|
|
|
|
# NOTE: `grep -c` prints 0 AND exits 1 when there are no matches, so a `|| echo 0`
|
|
# fallback appends a SECOND zero and every later integer test breaks on "0\n0".
|
|
# (Caught by running this script — which is the whole argument for running things.)
|
|
local n_pass n_fail
|
|
n_pass=$(grep -c '^ PASS: ' "$out" 2>/dev/null); n_pass=${n_pass:-0}
|
|
n_fail=$(grep -c '^ FAIL: ' "$out" 2>/dev/null); n_fail=${n_fail:-0}
|
|
echo "── $name: $n_pass passed, $n_fail failed (counted by the runner, not by the file's dead counters)"
|
|
if [ "$n_fail" -gt 0 ]; then
|
|
echo " failing assertions:"; grep '^ FAIL: ' "$out" | sed 's/^/ /'
|
|
return 1
|
|
fi
|
|
if [ "$n_pass" -eq 0 ]; then
|
|
echo " WARNING: no assertions ran — treating as FAILURE (a test that asserts nothing is not a passing test)"
|
|
return 1
|
|
fi
|
|
[ $rc -eq 0 ] || { echo " (test binary exited rc=$rc)"; return 1; }
|
|
return 0
|
|
}
|
|
|
|
rc_all=0
|
|
if [ "${1:-}" = "--all" ]; then
|
|
for t in tests/test_*.el; do run_one "$t" || rc_all=1; done
|
|
else
|
|
[ $# -ge 1 ] || { echo "usage: tests/run-el-test.sh <tests/test_x.el> | --all" >&2; exit 2; }
|
|
for t in "$@"; do run_one "$t" || rc_all=1; done
|
|
fi
|
|
exit $rc_all
|