Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,101 @@ jobs:
# build falls back to the empty-asset stub. npm runs only here, in one controlled
# job — never in the dockcross cross-compilers (which have no node) or per-platform.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# llama-atmosphere-agent: the standalone (non-reactor, unpublished) local coding-agent
# project that wires Atmosphere's built-in OpenAI-compatible agent runtime to this
# project's OpenAiCompatServer. Two jobs, mirroring the langchain4j pair:
# - model-free: unit tests + the wire-contract tests, which drive the REAL
# OpenAiCompatServer over a loopback socket with a scripted backend (no native lib,
# no GGUF) and pin the streamed tool_calls / role=tool / multi-round shape — seconds,
# on every PR.
# - model-backed: the same loop against the cached Qwen2.5-1.5B tool model through the
# downloaded Linux native library (chat, streaming, tool call + result, read/write/read
# loop). Validation-only, not a publish gate: a small model's wording is not a release
# signal, the deterministic contract is the model-free job.
# The project is built with -Dllama.version=<reactor version> against the core that was just
# installed to the local repo, so it always tests the code of this checkout.
# ---------------------------------------------------------------------------

test-java-llama-atmosphere-agent:
name: Build and Test llama-atmosphere-agent (model-free)
needs: startgate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: temurin
- name: Install parent + core net.ladenthin:llama into the local repo (Java only)
run: >
mvn -B --no-transfer-progress -pl llama -am -DskipTests -Denforcer.skip=true
-Dspotless.check.skip=true -Dspotbugs.skip=true
-Dmaven.javadoc.skip=true -Dmaven.source.skip=true -Dgpg.skip=true install
- name: Resolve the reactor version
run: echo "VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version | tail -n1)" >> "$GITHUB_ENV"
- name: Spotless check
run: mvn -B --no-transfer-progress -f llama-atmosphere-agent/pom.xml "-Dllama.version=${VERSION}" spotless:check
- name: Build and test (unit + model-free wire contract against the real OpenAiCompatServer)
run: mvn -B --no-transfer-progress -f llama-atmosphere-agent/pom.xml "-Dllama.version=${VERSION}" verify

test-java-llama-atmosphere-agent-integration:
name: Integration Test llama-atmosphere-agent (model-backed)
needs: [crosscompile-linux-x86_64, verify-model-cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Download Linux x86_64 native library (reused, not rebuilt)
uses: actions/download-artifact@v8
with:
name: Linux-x86_64-libraries
path: ${{ github.workspace }}/llama/src/main/resources/net/ladenthin/llama/
- name: Restore shared GGUF model cache (populated by download-models; no re-download)
uses: actions/cache/restore@v6
with:
path: models/
key: gguf-models-${{ hashFiles('.github/models.csv') }}
enableCrossOsArchive: true
- uses: actions/setup-java@v6
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
- name: Install parent + core net.ladenthin:llama (bundles the downloaded native library)
run: >
mvn -B --no-transfer-progress -pl llama -am -DskipTests -Denforcer.skip=true
-Dspotless.check.skip=true -Dspotbugs.skip=true
-Dmaven.javadoc.skip=true -Dmaven.source.skip=true -Dgpg.skip=true install
- name: Resolve the reactor version
run: echo "VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version | tail -n1)" >> "$GITHUB_ENV"
- name: Run the Atmosphere tool-loop integration test (cached Qwen2.5-1.5B tool model, CPU)
run: >
mvn -B --no-transfer-progress -f llama-atmosphere-agent/pom.xml "-Dllama.version=${VERSION}" test
-Dtest=AtmosphereToolLoopIntegrationTest -Dsurefire.failIfNoSpecifiedTests=false
-Dnet.ladenthin.llama.tool.model=models/${TOOL_MODEL_NAME}
-Dnet.ladenthin.llama.test.ngl=0
# Model-backed and crossing JNI: same crash diagnostics as the langchain4j integration job.
- name: Print crash logs (on failure)
if: failure()
shell: bash
run: |
shopt -s nullglob
for f in llama-atmosphere-agent/hs_err_pid*.log; do
echo "===== $f (first 200 lines; full file in the uploaded artifact) ====="
sed -n '1,200p' "$f"
done
for f in llama-atmosphere-agent/target/surefire-reports/*.dumpstream llama-atmosphere-agent/target/surefire-reports/*.dump; do
echo "===== $f ====="
cat "$f"
done
- if: failure()
uses: actions/upload-artifact@v7
with:
name: error-log-atmosphere-agent-integration
path: |
${{ github.workspace }}/llama-atmosphere-agent/hs_err_pid*.log
${{ github.workspace }}/core.*
${{ github.workspace }}/llama-atmosphere-agent/*.hprof

build-webui:
name: Build WebUI assets (shared)
needs: startgate
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by

## [Unreleased]

### Added
- **`llama-atmosphere-agent/` — a local, offline JVM coding agent** (Claude Code / OpenCode reduced to
the essentials) that drives [Atmosphere](https://github.com/Atmosphere/atmosphere)'s built-in
OpenAI-compatible agent runtime **headless** (no Spring Boot, no servlet container) against this
project's `OpenAiCompatServer`: streaming, the model→tool→model loop, Atmosphere's workspace-confined
file tools and an opt-in `run_command` tool. Standalone Maven project (not a reactor module, not
published): `mvn compile exec:java -Dexec.args="--base-url http://127.0.0.1:8080/v1 …"` against a
running java-llama.cpp / llama-server, or `--model x.gguf` to host the model in-process. Verified two
ways and wired into CI: model-free wire-contract tests drive the *real* `OpenAiCompatServer` with a
scripted engine (tool-call deltas by index, parallel calls, four consecutive rounds with full history,
401 handling, the one known Atmosphere gap on in-stream errors), and a model-backed job runs the loop
against the Qwen2.5-1.5B tool model. Result: Atmosphere works **unchanged** (verdict A).
- `OpenAiBackend`, `ChunkSink` and `OpenAiCompatServer(OpenAiBackend, OpenAiServerConfig)` are now
**public** — the inference-engine seam behind the OpenAI-compatible server, previously package-private
and used only by the core's own tests, so that sibling projects can drive the real HTTP surface
without a native library or model.

### Changed
- **BREAKING (runtime): the shipped SLF4J binding is now `slf4j-simple`, not `logback-classic`.**
Two independent reasons, and the first is a hard failure rather than a preference:
Expand Down
65 changes: 65 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2003,6 +2003,9 @@ missed again.)
release version now appears in only ~4 spots here, not ~20 — the runtime details live once in the
classifier table.)
- **`llama-langchain4j/README.md`** — its own `<dependency>` snippet.
- **`llama-atmosphere-agent/pom.xml`** — the `llama.version` property default (standalone project,
outside the reactor, so `versions:set` skips it), plus the `-Dllama.version=<version>` snippets
in the root README's "Local coding agent" section and the project's own README.
- **`llama-android/README.md`** and **`llama-kotlin/README.md`** — their Gradle dependency
snippets, plus the `llama-android`/`llama-kotlin` snippets in the root README's
"Importing in Android" section.
Expand Down Expand Up @@ -2132,6 +2135,68 @@ snapshots to the Central snapshots repo (`publishAllPublicationsToCentralSnapsho
releases as a signed Central Portal bundle upload (staging repo → zip → Publisher API).
`llama-kotlin` rides the normal reactor `mvn -P release deploy`.

## Local coding agent with Atmosphere (`llama-atmosphere-agent/`, standalone)

A **copy-and-run terminal coding agent** (Claude Code / OpenCode reduced to the essentials, offline)
that pairs [Atmosphere](https://github.com/Atmosphere/atmosphere)'s built-in OpenAI-compatible
agent runtime with this project's `OpenAiCompatServer`. Like `android-llmservice/` it is a
**standalone Maven project, NOT a reactor module and NOT published** — it is an application, and it
needs Java 21 (Atmosphere's floor) while the core stays Java 8. CI builds it against the core it just
installed (`-Dllama.version=<reactor version>`); a user copies the folder, sets a released
`llama.version`, and runs `mvn compile exec:java -Dexec.args="…"`.

**What Atmosphere is, for this purpose.** `org.atmosphere:atmosphere-ai` (4.0.70) ships
`BuiltInAgentRuntime` + `OpenAiCompatibleClient`: a zero-framework OpenAI client that *always*
streams (`stream:true`), accumulates `delta.tool_calls` by `index`, executes `ToolDefinition`
executors, re-submits the conversation (assistant `tool_calls` message **without** a `content` key,
then one `role:"tool"` message per call with `tool_call_id` + `name`), and loops until
`finish_reason` is not `tool_calls`. It reads `LLM_BASE_URL`/`LLM_MODEL`/`LLM_API_KEY` or takes
`AiConfig.configure(mode, model, apiKey, baseUrl)`; `GET /models` is best-effort; the Responses API
is used only when the base URL contains `api.openai.com`; `tool_choice`/`parallel_tool_calls`/
`response_format` are not sent. It runs headless — `runtime.execute(AgentExecutionContext,
StreamingSession)` — so no Spring Boot, servlet container or `@Agent` scanning is involved; its
built-in `FileSystemTools` resolve the `AgentFileSystem` from `StreamingSession.injectables()`,
which is how the tools are confined to a workspace. The `@Agent`/`@AiTool` annotations and the
Spring Boot starter are a deployment layer on top of the same runtime.

**Verified compatibility (verdict A — works unchanged).** Two test layers, both in the project:

- `AtmosphereWireContractTest` + `LocalAgentTest` — **model-free, every PR, seconds**: the *real*
`OpenAiCompatServer` (routing, bearer auth, `/v1/models`, SSE framing) over a loopback socket with
a `ScriptedBackend` replaying llama.cpp-shaped chunks (role delta, `tool_calls` deltas with
`index`/`id`/`name` and fragmented `arguments`, `finish_reason:"tool_calls"`). Pins: one tool
round; four rounds incl. a parallel pair with interleaved fragments and the whole history kept;
chunk-by-chunk streaming and history replay; `temperature`/`max_tokens` on the wire; 401 on a
wrong key before the backend is reached; and the **one known gap** — an engine failure *after* the
stream started is an SSE `data: {"error":…}` under HTTP 200 (upstream llama-server does the same),
which Atmosphere's parser ignores (it reads only `choices[0]`), so the turn completes with the
text so far instead of erroring. That is a SHOULD for Atmosphere's `OpenAiCompatibleClient`, not
for this project.
- `AtmosphereToolLoopIntegrationTest` — **model-backed, CI only** (`test-java-llama-atmosphere-agent-integration`,
validation-only, not a publish gate): the same loop against the cached Qwen2.5-1.5B tool model
through the downloaded Linux natives — plain chat, streaming (≥ 2 chunks), a tool call whose result
is answered, a read→write→read loop that changes a temp file. Self-skips without the GGUF.

**The one core change this needed:** `OpenAiBackend`, `ChunkSink` and
`OpenAiCompatServer(OpenAiBackend, OpenAiServerConfig)` are now **public** (they were the
package-private test seam). A sibling module cannot otherwise drive the real server without a model;
the alternative — a same-named package in the sibling's test tree — is a split package that breaks
the moment anything runs on the module path.

**Layout.** `AgentOptions` (CLI parsing, pure), `AgentRunner` (the whole Atmosphere wiring, ~40
lines: `AiConfig.configure` → `BuiltInAgentRuntime` → `AgentExecutionContext` + `ToolLoopPolicies`),
`ConsoleSession` (streams to stdout, prints `⚙ tool {args}` / `↳ result`, supplies the
`WorkspaceAgentFileSystem` via `injectables()`), `ShellTool` (opt-in `run_command`, `sh -c` /
`cmd /c` in the workspace, timeout kills the process tree, output tail-truncated), `LocalAgent`
(`--base-url` = external server, `--model` = in-process `LlamaModel` + loopback `OpenAiCompatServer`
with `enableJinja()`, one-shot `--prompt` or a `you>` REPL with `/clear` `/exit`). Spotless (palantir)
is configured in its own pom; the model-free CI job runs `spotless:check`.

**Version bump note.** The pom's `llama.version` property defaults to the current reactor version
(CI always overrides it). `versions:set` does not touch this standalone pom, so bump the default by
hand together with the two README snippets (`README.md` "Local coding agent" + the project's own
README) — the same class as the `llama-langchain4j/README.md` snippet.

## Android app "LLM Service" (`android-llmservice/`)

A shippable, **KISS fully-offline on-device chat app** consuming the `llama-android` AAR +
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,29 @@ See [`llama-langchain4j/README.md`](llama-langchain4j/) for streaming/embedding/
examples and the current mapping limitations (tool calling, JSON mode, and multimodal input are
not yet forwarded).

### Local coding agent with Atmosphere (`llama-atmosphere-agent/`)

A copy-and-run **terminal coding agent on the JVM** — Claude Code / OpenCode reduced to the
essentials, fully offline — built from [Atmosphere](https://github.com/Atmosphere/atmosphere)'s
built-in OpenAI-compatible agent runtime (streaming, tool loop, workspace file tools) driven
**headless** against this project's OpenAI-compatible server. It is a standalone Maven project (not a
reactor module, not published); you copy the folder and run it:

```bash
# against a server you started (java-llama.cpp fat jar with --jinja, or llama-server) ...
mvn -q compile exec:java -Dllama.version=<version> \
-Dexec.args="--base-url http://127.0.0.1:8080/v1 --workspace /path/to/project --allow-shell"
# ... or with the GGUF loaded in-process
mvn -q compile exec:java -Dllama.version=<version> \
-Dexec.args="--model /models/Qwen2.5-7B-Instruct-Q4_K_M.gguf --ngl 99 --workspace /path/to/project"
```

The full streaming tool-calling loop (tools → `delta.tool_calls` → Java tool → `role:"tool"` result →
next turn, over several rounds) is verified on every PR against the real `OpenAiCompatServer` with
no model, and in CI against the Qwen2.5-1.5B tool model. See
[`llama-atmosphere-agent/README.md`](llama-atmosphere-agent/) for the options and the verified
compatibility matrix.

### Model/Inference Configuration

There are two sets of parameters you can configure, `ModelParameters` and `InferenceParameters`. Both provide builder
Expand Down
26 changes: 26 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ so everything below is genuinely still open.

## Open — jllama-specific

### Atmosphere coding agent (`llama-atmosphere-agent/`) — follow-ups

The headless loop is verified (see CLAUDE.md "Local coding agent with Atmosphere"). Still open:

- **First model-backed CI run.** `test-java-llama-atmosphere-agent-integration` was added without a
run on GitHub's runners; the three assertions are about the loop (tool invoked, result answered,
file changed), but a 1.5B model on a CPU runner may still need a prompt or budget tweak. Read its
first run before trusting it as a signal.
- **Tool rounds are not carried across REPL turns** — only `user`/`assistant` text is replayed, so a
second question cannot refer to a tool result of the first. Keep the full Atmosphere
`ChatMessage` list (incl. `tool_calls`/`tool` messages) per turn instead.
- **Approval for destructive tools.** `write_file`/`delete`/`run_command` run unasked. Atmosphere's
`ToolDefinition.requiresApproval` + an `ApprovalStrategy` on the context would give a Claude-Code
style "allow this?" prompt on the console.
- **In-stream engine errors are swallowed by Atmosphere** (pinned in
`AtmosphereWireContractTest.midStreamEngineFailureCompletesSilentlyRatherThanErroring`): an SSE
`data: {"error":…}` after HTTP 200 is ignored by `OpenAiCompatibleClient.processSSELine` (it reads
only `choices[0]`). Worth an upstream PR to Atmosphere; until then the console shows an empty turn.
- **Spring Boot `@Agent` variant** (WebSocket/SSE UI via `atmosphere-ai-spring-boot-starter` and
`LLM_BASE_URL`) is expected to work on the same runtime but is not CI-covered; a smoke that boots
the starter against the scripted `OpenAiCompatServer` would close that.
- **Anthropic Messages surface.** The server also speaks `/v1/messages`; the Anthropic adapter
(`org.atmosphere:atmosphere-anthropic`) was not tested against it.
- **Model recommendation table** for the agent (which local GGUFs actually complete an
edit→build→test loop) — needs a GPU host, not CI.

### LlamaLoader extraction-directory isolation (optional follow-up, low priority)

Left over from the 2026-06-20 code audit (18/18 findings fixed in PRs #258/#260, regression tests in
Expand Down
Loading
Loading