Skip to content

Commit 1c23d8e

Browse files
Merge pull request #457 from bernardladenthin/claude/busy-archimedes-lo9uyy
Ship the agent as a core-less release jar, gate publishing on it; fix Windows tool-call test
2 parents 727738b + 0bae468 commit 1c23d8e

9 files changed

Lines changed: 427 additions & 33 deletions

File tree

‎.github/smoke-agent-jar.sh‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env bash
2+
3+
# SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
4+
#
5+
# SPDX-License-Identifier: MIT
6+
7+
# Smoke test for the llama-atmosphere-agent release asset, run exactly the way the README
8+
# tells a user to run it: the agent jar lies next to a core fat jar and is started with
9+
# `java -jar`, so the core is found only through the agent manifest's Class-Path. That
10+
# makes this the check that the two release assets actually fit together (same version in
11+
# the file names, nothing missing on either side), which no test run from the source tree
12+
# can see.
13+
#
14+
# 1. the agent jar carries no core: started alone, loading a model fails with
15+
# NoClassDefFoundError for net.ladenthin.llama.LlamaModel;
16+
# 2. --help exits 0 and prints the usage;
17+
# 3. a one-shot prompt with the model loaded in-process answers "2+2" with a 4;
18+
# 4. a one-shot prompt that needs a reading tool (read_file) reads a marker file from
19+
# --workspace, so the whole tool loop runs through the shipped jars.
20+
#
21+
# Usage: smoke-agent-jar.sh <jar-dir> <model-path>
22+
# <jar-dir> must hold exactly one llama-atmosphere-agent-*-jar-with-dependencies.jar and at
23+
# least one core fat jar its Class-Path names. Output of each run is kept in agent-*.log in
24+
# the working directory (uploaded by the CI job on failure).
25+
set -euo pipefail
26+
27+
JAR_DIR="${1:?usage: smoke-agent-jar.sh <jar-dir> <model-path>}"
28+
MODEL="${2:?usage: smoke-agent-jar.sh <jar-dir> <model-path>}"
29+
TIMEOUT="${AGENT_SMOKE_TIMEOUT:-600}"
30+
# The checks grep plain text; never let a CI runner that forces colour put escapes in it.
31+
export NO_COLOR=1
32+
unset CLICOLOR_FORCE
33+
34+
fail() {
35+
echo "::error::$*" >&2
36+
exit 1
37+
}
38+
39+
[ -f "$MODEL" ] || fail "model not found: $MODEL"
40+
MODEL="$(cd "$(dirname "$MODEL")" && pwd)/$(basename "$MODEL")"
41+
JAR_DIR="$(cd "$JAR_DIR" && pwd)"
42+
43+
mapfile -t AGENTS < <(find "$JAR_DIR" -maxdepth 1 -name 'llama-atmosphere-agent-*-jar-with-dependencies.jar' | sort)
44+
[ "${#AGENTS[@]}" -eq 1 ] || fail "expected exactly 1 agent jar in $JAR_DIR, got ${#AGENTS[@]}: ${AGENTS[*]:-none}"
45+
AGENT="${AGENTS[0]}"
46+
echo "Agent jar: $(basename "$AGENT") ($(du -h "$AGENT" | cut -f1))"
47+
48+
# The manifest names the core jars by file name; at least one of them must be here, or
49+
# `java -jar` would start without a core. unzip wraps manifest lines at 72 bytes with a
50+
# leading space, so the continuation lines are joined first.
51+
CLASS_PATH="$(unzip -p "$AGENT" META-INF/MANIFEST.MF | tr -d '\r' | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n //g' \
52+
| sed -n 's/^Class-Path: //p')"
53+
[ -n "$CLASS_PATH" ] || fail "agent manifest has no Class-Path"
54+
found=""
55+
for entry in $CLASS_PATH; do
56+
if [ -f "$JAR_DIR/$entry" ]; then
57+
found="$entry"
58+
break
59+
fi
60+
done
61+
[ -n "$found" ] || fail "none of the core jars the agent manifest names is in $JAR_DIR: $CLASS_PATH (present: $(ls "$JAR_DIR"))"
62+
echo "Core jar picked up via Class-Path: $found"
63+
64+
# 1. Without a core next to it the agent must not work: that is what makes it small.
65+
ALONE="$(mktemp -d)"
66+
cp "$AGENT" "$ALONE/"
67+
set +e
68+
timeout "$TIMEOUT" java -jar "$ALONE/$(basename "$AGENT")" --model "$MODEL" --plain --prompt hi \
69+
> agent-alone.log 2>&1 < /dev/null
70+
rc=$?
71+
set -e
72+
rm -rf "$ALONE"
73+
[ "$rc" -ne 0 ] || fail "the agent jar ran without a core jar next to it - does it bundle the core?"
74+
grep -q 'NoClassDefFoundError: net/ladenthin/llama/LlamaModel' agent-alone.log \
75+
|| { cat agent-alone.log; fail "agent without core failed, but not for the missing core (see above)"; }
76+
echo "OK: the agent jar carries no core"
77+
78+
# 2. --help
79+
java -jar "$AGENT" --help > agent-help.log 2>&1 < /dev/null || { cat agent-help.log; fail "--help exited non-zero"; }
80+
grep -q 'Usage: LocalAgent' agent-help.log || { cat agent-help.log; fail "--help printed no usage"; }
81+
echo "OK: --help"
82+
83+
run_agent() {
84+
local log="$1"
85+
shift
86+
set +e
87+
timeout "$TIMEOUT" java -jar "$AGENT" --model "$MODEL" --ngl 0 --plain --temperature 0 "$@" \
88+
> "$log" 2>"${log%.log}.err.log" < /dev/null
89+
local status=$?
90+
set -e
91+
if [ "$status" -ne 0 ]; then
92+
echo "===== $log =====" && cat "$log"
93+
echo "===== ${log%.log}.err.log (last 80 lines) =====" && tail -n 80 "${log%.log}.err.log"
94+
fail "agent exited with $status ($log)"
95+
fi
96+
}
97+
98+
# 3. A plain answer through the in-process server.
99+
run_agent agent-answer.log --prompt 'What is 2 + 2? Answer with one short sentence.'
100+
grep -q '4' agent-answer.log || { cat agent-answer.log; fail "the answer does not contain 4"; }
101+
echo "OK: plain answer"
102+
103+
# 4. A tool round: the marker exists only in the file, so it reaches the output only
104+
# through read_file (whose result the console prints) or an answer built from it.
105+
WORKSPACE="$(mktemp -d)"
106+
MARKER="AGENT_SMOKE_$(date +%s)_$RANDOM"
107+
printf '%s\n' "$MARKER" > "$WORKSPACE/marker.txt"
108+
run_agent agent-tool.log --workspace "$WORKSPACE" \
109+
--prompt 'Read the file marker.txt with the read_file tool and tell me its exact content.'
110+
rm -rf "$WORKSPACE"
111+
grep -q 'read_file' agent-tool.log || { cat agent-tool.log; fail "the model did not call read_file"; }
112+
grep -q "$MARKER" agent-tool.log || { cat agent-tool.log; fail "the marker never reached the output"; }
113+
echo "OK: tool round (read_file)"
114+
115+
echo "Agent release asset smoke test passed."

‎.github/workflows/publish.yml‎

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -533,17 +533,19 @@ jobs:
533533
# job — never in the dockcross cross-compilers (which have no node) or per-platform.
534534
# ---------------------------------------------------------------------------
535535
# ---------------------------------------------------------------------------
536-
# llama-atmosphere-agent: the standalone (non-reactor, unpublished) local coding-agent
537-
# project that wires Atmosphere's built-in OpenAI-compatible agent runtime to this
538-
# project's OpenAiCompatServer. Two jobs, mirroring the langchain4j pair:
536+
# llama-atmosphere-agent: the standalone (non-reactor, never on Maven Central) local
537+
# coding-agent project that wires Atmosphere's built-in OpenAI-compatible agent runtime to
538+
# this project's OpenAiCompatServer. Three jobs, all publish gates:
539539
# - model-free: unit tests + the wire-contract tests, which drive the REAL
540540
# OpenAiCompatServer over a loopback socket with a scripted backend (no native lib,
541541
# no GGUF) and pin the streamed tool_calls / role=tool / multi-round shape — seconds,
542-
# on every PR.
542+
# on every PR. It also builds the GitHub Release asset (the agent jar WITHOUT the core).
543543
# - model-backed: the same loop against the cached Qwen2.5-1.5B tool model through the
544544
# downloaded Linux native library (chat, streaming, tool call + result, read/write/read
545-
# loop). Validation-only, not a publish gate: a small model's wording is not a release
546-
# signal, the deterministic contract is the model-free job.
545+
# loop). A gate since its assertions stopped pinning wording (content checks are limited
546+
# to facts no instruct model gets wrong and to tool results) and it ran green throughout.
547+
# - smoke-agent-linux (further down, after package-fatjars): the release asset itself,
548+
# started next to the real core fat jar.
547549
# The project is built with -Dllama.version=<reactor version> against the core that was just
548550
# installed to the local repo, so it always tests the code of this checkout.
549551
# ---------------------------------------------------------------------------
@@ -569,6 +571,28 @@ jobs:
569571
run: mvn -B --no-transfer-progress -f llama-atmosphere-agent/pom.xml "-Dllama.version=${VERSION}" spotless:check
570572
- name: Build and test (unit + model-free wire contract against the real OpenAiCompatServer)
571573
run: mvn -B --no-transfer-progress -f llama-atmosphere-agent/pom.xml "-Dllama.version=${VERSION}" verify
574+
# The GitHub Release asset llama-atmosphere-agent-<core version>-jar-with-dependencies.jar:
575+
# the agent plus Atmosphere/JLine, WITHOUT the core (src/assembly/agent-jar.xml), so it is a
576+
# few MB and the natives are not in the release twice. Never deployed to Maven Central.
577+
# smoke-agent-linux launches it next to the real core fat jar; the attach jobs sign it.
578+
- name: Build the agent release jar (without the core)
579+
run: >
580+
mvn -B --no-transfer-progress -f llama-atmosphere-agent/pom.xml "-Dllama.version=${VERSION}"
581+
-P assembly -DskipTests package
582+
- name: Collect the agent release jar + sha256
583+
run: |
584+
mkdir -p agent-jar
585+
cp "llama-atmosphere-agent/target/llama-atmosphere-agent-${VERSION}-jar-with-dependencies.jar" agent-jar/
586+
(cd agent-jar && for f in *.jar; do sha256sum "$f" > "$f.sha256"; done)
587+
ls -la agent-jar
588+
- name: Upload the agent release jar
589+
uses: actions/upload-artifact@v7
590+
with:
591+
name: llama-atmosphere-agent-jar
592+
path: agent-jar/
593+
compression-level: 0 # jars are already deflated
594+
retention-days: 7
595+
if-no-files-found: error
572596

573597
test-java-llama-atmosphere-agent-integration:
574598
name: Integration Test llama-atmosphere-agent (model-backed)
@@ -3665,6 +3689,50 @@ jobs:
36653689
server-err.log
36663690
if-no-files-found: warn
36673691

3692+
# The agent release asset, launched the way the README tells a user to: `java -jar` on the agent
3693+
# jar lying next to the real all-backends Linux fat jar, so the core is found only through the
3694+
# agent manifest's Class-Path. Proves the two assets fit together (matching version in the file
3695+
# names, nothing missing on either side), that the agent jar carries no core, and that a
3696+
# one-shot answer and a read_file tool round work through the shipped jars.
3697+
smoke-agent-linux:
3698+
name: Smoke test the agent release jar (Linux)
3699+
needs: [test-java-llama-atmosphere-agent, package-fatjars, verify-model-cache]
3700+
runs-on: ubuntu-latest
3701+
steps:
3702+
- uses: actions/checkout@v7
3703+
- uses: actions/download-artifact@v8
3704+
with:
3705+
name: llama-atmosphere-agent-jar
3706+
path: agent-assets/
3707+
- uses: actions/download-artifact@v8
3708+
with:
3709+
name: llama-fatjar-smoke-linux
3710+
path: agent-assets/
3711+
- name: Restore shared GGUF model cache (populated by download-models; no re-download)
3712+
uses: actions/cache/restore@v6
3713+
with:
3714+
path: models/
3715+
key: gguf-models-${{ hashFiles('.github/models.csv') }}
3716+
enableCrossOsArchive: true
3717+
- name: Validate model files
3718+
run: .github/validate-models.sh
3719+
- uses: actions/setup-java@v6
3720+
with:
3721+
distribution: 'temurin'
3722+
java-version: ${{ env.JAVA_VERSION }}
3723+
# The agent is Java 21 (Atmosphere's floor), unlike the Java 8 core: its ceiling is 65.
3724+
- name: Verify Java 21 bytecode (no class newer than major 65)
3725+
run: .github/verify-bytecode-version.sh --max-major 65 agent-assets/llama-atmosphere-agent-*-jar-with-dependencies.jar
3726+
- name: Run the agent release-jar smoke test
3727+
run: .github/smoke-agent-jar.sh agent-assets "models/${TOOL_MODEL_NAME}"
3728+
- name: Upload agent logs
3729+
if: failure()
3730+
uses: actions/upload-artifact@v7
3731+
with:
3732+
name: agent-smoke-linux-logs
3733+
path: agent-*.log
3734+
if-no-files-found: warn
3735+
36683736
smoke-fatjar-windows:
36693737
name: Smoke test all-backends fat jar (Windows)
36703738
needs: [package-fatjars, verify-model-cache]
@@ -3880,7 +3948,7 @@ jobs:
38803948

38813949
publish-snapshot:
38823950
name: Publish Snapshot to Central
3883-
needs: [check-snapshot, crosscompile-linux-x86_64-cuda, crosscompile-android-aarch64-opencl, package-android-aar, test-android-emulator, code-style, test-java-llama-langchain4j, test-java-llama-kotlin, package-fatjars, smoke-fatjar-linux, smoke-fatjar-windows, smoke-fatjar-linux-aarch64, smoke-fatjar-windows-arm64, smoke-fatjar-macos]
3951+
needs: [check-snapshot, crosscompile-linux-x86_64-cuda, crosscompile-android-aarch64-opencl, package-android-aar, test-android-emulator, code-style, test-java-llama-langchain4j, test-java-llama-kotlin, test-java-llama-atmosphere-agent, test-java-llama-atmosphere-agent-integration, package-fatjars, smoke-fatjar-linux, smoke-fatjar-windows, smoke-fatjar-linux-aarch64, smoke-fatjar-windows-arm64, smoke-fatjar-macos, smoke-agent-linux]
38843952
if: needs.check-snapshot.result == 'success' && inputs.publish_to_central
38853953
runs-on: ubuntu-latest
38863954
environment: maven-central
@@ -4059,11 +4127,11 @@ jobs:
40594127

40604128
github-snapshot:
40614129
name: Update Snapshot Pre-release on GitHub
4062-
needs: [publish-snapshot, package-fatjars]
4130+
needs: [publish-snapshot, package-fatjars, test-java-llama-atmosphere-agent]
40634131
# Also runs when publish-snapshot FAILED (not when skipped/cancelled): a Central
40644132
# publish-poll timeout reds that job after the artifacts were already uploaded —
40654133
# the GitHub pre-release assets must not be lost in that case.
4066-
if: ${{ !cancelled() && (needs.publish-snapshot.result == 'success' || needs.publish-snapshot.result == 'failure') && needs.package-fatjars.result == 'success' }}
4134+
if: ${{ !cancelled() && (needs.publish-snapshot.result == 'success' || needs.publish-snapshot.result == 'failure') && needs.package-fatjars.result == 'success' && needs.test-java-llama-atmosphere-agent.result == 'success' }}
40674135
runs-on: ubuntu-latest
40684136
# maven-central so the GPG_PRIVATE_KEY / GPG_PASSPHRASE secret is delivered (it is
40694137
# scoped to this environment) for signing the fat jars below. This environment has
@@ -4086,6 +4154,12 @@ jobs:
40864154
with:
40874155
name: llama-fatjars
40884156
path: snapshot-assets/
4157+
# The agent jar (+ sha256) — built without the core, run next to one of the fat jars above.
4158+
# Same directory, so sign-fatjars.sh signs it and the upload glob attaches it.
4159+
- uses: actions/download-artifact@v8
4160+
with:
4161+
name: llama-atmosphere-agent-jar
4162+
path: snapshot-assets/
40894163
# GPG-sign the fat jars so each carries a detached .asc signature alongside its
40904164
# .sha256 checksum — signature parity with the thin jars (which maven-gpg signs at
40914165
# deploy) and with the BAF / srcmorph sibling fat jars. The .sha256 files (integrity)
@@ -4147,7 +4221,7 @@ jobs:
41474221
publish-release:
41484222
name: Publish Release to Central
41494223
if: needs.check-tag.result == 'success' && inputs.publish_to_central
4150-
needs: [check-tag, crosscompile-linux-x86_64-cuda, crosscompile-android-aarch64-opencl, package-android-aar, test-android-emulator, code-style, test-java-llama-langchain4j, test-java-llama-kotlin, package-fatjars, smoke-fatjar-linux, smoke-fatjar-windows, smoke-fatjar-linux-aarch64, smoke-fatjar-windows-arm64, smoke-fatjar-macos]
4224+
needs: [check-tag, crosscompile-linux-x86_64-cuda, crosscompile-android-aarch64-opencl, package-android-aar, test-android-emulator, code-style, test-java-llama-langchain4j, test-java-llama-kotlin, test-java-llama-atmosphere-agent, test-java-llama-atmosphere-agent-integration, package-fatjars, smoke-fatjar-linux, smoke-fatjar-windows, smoke-fatjar-linux-aarch64, smoke-fatjar-windows-arm64, smoke-fatjar-macos, smoke-agent-linux]
41514225
runs-on: ubuntu-latest
41524226
environment: maven-central
41534227
permissions:
@@ -4326,11 +4400,11 @@ jobs:
43264400

43274401
github-release-signed:
43284402
name: Attach Signed Binaries to GitHub Release
4329-
needs: [publish-release, package-fatjars]
4403+
needs: [publish-release, package-fatjars, test-java-llama-atmosphere-agent]
43304404
# Also runs when publish-release FAILED (not when skipped/cancelled): a Central
43314405
# publish-poll timeout reds that job after the artifacts were already uploaded —
43324406
# the GitHub release assets must not be lost in that case.
4333-
if: ${{ !cancelled() && (needs.publish-release.result == 'success' || needs.publish-release.result == 'failure') && needs.package-fatjars.result == 'success' }}
4407+
if: ${{ !cancelled() && (needs.publish-release.result == 'success' || needs.publish-release.result == 'failure') && needs.package-fatjars.result == 'success' && needs.test-java-llama-atmosphere-agent.result == 'success' }}
43344408
runs-on: ubuntu-latest
43354409
# maven-central so the GPG_PRIVATE_KEY / GPG_PASSPHRASE secret is delivered (it is
43364410
# scoped to this environment) for signing the fat jars below. This environment has
@@ -4353,6 +4427,12 @@ jobs:
43534427
with:
43544428
name: llama-fatjars
43554429
path: release-assets/
4430+
# The agent jar (+ sha256) — built without the core, run next to one of the fat jars above.
4431+
# Same directory, so sign-fatjars.sh signs it and the upload glob attaches it.
4432+
- uses: actions/download-artifact@v8
4433+
with:
4434+
name: llama-atmosphere-agent-jar
4435+
path: release-assets/
43564436
# GPG-sign the fat jars so each carries a detached .asc signature alongside its
43574437
# .sha256 checksum — signature parity with the thin jars (which maven-gpg signs at
43584438
# deploy) and with the BAF / srcmorph sibling fat jars. The .sha256 files (integrity)

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
1010
## [Unreleased]
1111

1212
### Fixed
13+
- **`ToolCallingIntegrationTest#requiredToolCallIsParsedFromStreamingResponse` failed on both Windows
14+
x86-64 jobs after the b11211 bump** (the Ubuntu run and the blocking twin stayed green). The streamed
15+
request generated its full 512 tokens without a tool call. The prompt ("Write an example") never asked
16+
for the tool, so the grammar-constrained answer was a fragile ~90-token call even when it worked, and a
17+
numerically different CPU path on those runners (most likely upstream's new tiled k-quant matmul,
18+
which picks its microkernel by ISA) tipped greedy decoding over. The test pins how a tool call is
19+
parsed and streamed, not whether a 1.5B model infers one, so the user message now asks for the call
20+
outright; both assertions carry the streamed content, `finish_reason` and chunk count, so a future
21+
failure says what the model did instead of `but: was ""`.
1322
- **`LlamaModel.setLogger` was silently overridden by every model load, and never saw the server's own
1423
log lines.** llama.cpp's `common_init()` — run on each load — re-points `llama_log_set()` at its own
1524
default callback, so a logger set *before* `new LlamaModel(…)` (the natural order) stopped receiving
@@ -30,6 +39,14 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
3039
documented as the no-ops they are (`common_init()` forces both on), `setLogFile` as additive.
3140

3241
### Added
42+
- **The agent is a release asset: `llama-atmosphere-agent-<version>-jar-with-dependencies.jar`**, with
43+
`.sha256` and a GPG `.asc`, on every GitHub release and the rolling `snapshot` pre-release — never on
44+
Maven Central. It carries **no core** (~7 MB instead of hundreds, natives not in the release twice):
45+
put it next to a core fat jar of the same version and `java -jar` finds the core through its manifest
46+
`Class-Path`, or name both with `java -cp`. A new CI job, `smoke-agent-linux`, launches exactly that
47+
pair (bytecode ≤ Java 21, the jar alone must fail for the missing core, `--help`, a one-shot answer and
48+
a `read_file` round on the cached tool model), and it, the model-free agent job and the model-backed
49+
agent integration test now gate both publish jobs.
3350
- **`llama-atmosphere-agent`: `--log-verbosity <n>` (default `2`) and `--verbose`** for the in-process
3451
`--model` mode. llama.cpp's per-request INFO lines go to stderr, the console the streamed answer is
3552
printed to, and interleaved with it; the agent now loads the model with warnings-and-errors only. A

0 commit comments

Comments
 (0)