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
24 changes: 22 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2140,7 +2140,7 @@ releases as a signed Central Portal bundle upload (staging repo → zip → Publ

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

A **copy-and-run terminal coding agent** (Claude Code / OpenCode reduced to the essentials, offline)
A **copy-and-run general-purpose terminal 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
Expand Down Expand Up @@ -2192,7 +2192,7 @@ the moment anything runs on the module path.
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`
`cmd /c` starting in the workspace, timeout kills the process tree, output tail-truncated), `LocalAgent`
(`--base-url` = external server, `--model` = in-process `LlamaModel` + loopback `OpenAiCompatServer`
with `enableJinja()` and `setLogVerbosity(2)` by default — llama.cpp logs to **stderr**, the console the
streamed answer shares, so the per-request `slot …` INFO lines would interleave with it; `--log-verbosity <n>`
Expand All @@ -2205,6 +2205,26 @@ a `jvm.config` takes no comments, so REUSE can only read its metadata from that
`REUSE Compliance Check` job fails on `main` — which is how it was found, the PR run having been cancelled.
Spotless (palantir) is configured in its own pom; the model-free CI job runs `spotless:check`.

**The default system prompt is general-purpose on purpose — do not narrow it back.** Every model-facing
text is a resource, not a Java literal: `src/main/resources/net/ladenthin/llama/atmosphere/` holds
`system-prompt.txt`, `system-prompt-shell.txt`, `system-prompt-no-shell.txt` and `run-command-tool.txt`
(each with a `.license` sidecar for REUSE), loaded by `LocalAgent.prompt(name)` with `{placeholder}`
substitution; `AgentOptionsTest.promptResourcesLoadAndEveryPlaceholderIsFilled` fails on a missing file or
an unfilled placeholder. `LocalAgent.systemPrompt`
and the `ShellTool` description describe `run_command` as running *any* command line through the named
shell (`ShellTool.shellName()`), not limited to the workspace, and tell the model to run a command rather
than explain one. The earlier wording ("careful *coding agent*", `run_command` "to build, test or inspect
the project" / "build, test, grep or list files") made Qwen3-4B refuse "list the docker images" — "my
tools are only for files" — with the tool registered and `docker` on `PATH`; a fresh single-turn run
refused too, so it was the prompt, not the chat history. Without `--allow-shell` the prompt says commands
are unavailable and names the flag, so the model does not invent its own limitation. Pinned by
`AgentOptionsTest.defaultSystemPromptIsGeneralPurposeAndAllowsAnyCommandWithTheShell` and
`shellToolDescriptionDoesNotNarrowItToTheProject`. `ShellToolTest` runs on every platform: each test
picks its command line with `ShellTool.isWindows()` — the same detection `ShellTool.run` uses to choose
`cmd.exe /c` over `sh -c` — so `ls`/`dir /b`, `sleep 30`/`ping -n 30 127.0.0.1 >nul`, and the truncation
test counts the platform's line separator. It used plain POSIX commands before and failed 4 of 5 on
Windows; never skip it per OS, give a new test both command forms instead.

**Version bump note.** The pom's `llama.version` property is the **release** version, not the
reactor's `-SNAPSHOT` (CI always overrides it, so a not-yet-published default never breaks CI).
`versions:set` does not touch this standalone pom, so at release time bump it by hand together with
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1021,8 +1021,9 @@ 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
A copy-and-run **general-purpose terminal agent on the JVM** — Claude Code / OpenCode reduced to the
essentials, fully offline; it edits files and, with `--allow-shell`, runs any command on your machine
(`docker`, `git`, build tools) — 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. With java-llama.cpp already running
Expand All @@ -1043,6 +1044,10 @@ mvn -q compile exec:java \
# or without a separate server: load the GGUF in-process
mvn -q compile exec:java \
-Dexec.args="--model /models/Qwen2.5-7B-Instruct-Q4_K_M.gguf --ngl 99 --workspace /path/to/project"

# everything at once: shell access plus your own system prompt (replaces the built-in one)
mvn -q compile exec:java \
-Dexec.args="--model /models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --ngl 99 --ctx-size 16384 --workspace /path/to/project --allow-shell --system 'You are a local assistant on this machine with full shell access. run_command executes any command line, including docker, git and build tools. When asked about the system, run a command instead of explaining it. Answer in the language of the user.'"
```

The full streaming tool-calling loop (tools → `delta.tool_calls` → Java tool → `role:"tool"` result →
Expand Down
51 changes: 44 additions & 7 deletions llama-atmosphere-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
SPDX-License-Identifier: MIT
-->

# llama-atmosphere-agent — a local JVM coding agent on java-llama.cpp
# llama-atmosphere-agent — a local, general-purpose JVM agent on java-llama.cpp

A minimal, copy-and-run **terminal coding agent** (think Claude Code / OpenCode, reduced to the
essentials) that runs entirely on the JVM and entirely offline:
A minimal, copy-and-run **general-purpose terminal agent** (think Claude Code / OpenCode, reduced to
the essentials): it reads and edits files, and with `--allow-shell` it runs any command on your
machine — `docker`, `git`, build tools, system information. It runs entirely on the JVM and entirely
offline:

- **Model:** any GGUF served by java-llama.cpp's OpenAI-compatible HTTP surface — either a server
you start yourself, or the GGUF loaded **in this process**.
Expand All @@ -16,7 +18,8 @@ essentials) that runs entirely on the JVM and entirely offline:
workspace-confined file tools (`ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`,
`delete`, `rename`). Driven **headless** — no Spring Boot, no servlet container, no `@Agent`
scanning — through `BuiltInAgentRuntime`.
- **Shell:** an opt-in `run_command` tool (`--allow-shell`) so the model can build and test.
- **Shell:** an opt-in `run_command` tool (`--allow-shell`) that runs any command line through the
system shell (`cmd.exe` on Windows, `sh` elsewhere).

This folder is a **standalone Maven project**, deliberately *not* a reactor module and *not*
published: CI builds and tests it against the core of the same checkout; you copy the folder and
Expand Down Expand Up @@ -66,6 +69,18 @@ mvn -q compile exec:java \
-Dexec.args="--model /models/Qwen2.5-7B-Instruct-Q4_K_M.gguf --ngl 99 --workspace /path/to/project"
```

**Everything at once — shell access and your own system prompt:**

```bash
mvn -q compile exec:java \
-Dexec.args="--model /models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --ngl 99 --ctx-size 16384 --workspace /path/to/project --allow-shell --system 'You are a local assistant on this machine with full shell access. run_command executes any command line, including docker, git and build tools. When asked about the system, run a command instead of explaining it. Read a file before you edit it. Answer in the language of the user.'"
```

Then ask, for example, *"which docker images are available?"* or *"build the project and fix the
first compiler error"*. On Windows PowerShell, quote the whole argument instead:
`"-Dexec.args=--model C:\models\… --allow-shell --system '…'"`. Inside `--system '…'` avoid the
apostrophe (write *the user* rather than *user's*): the value is already single-quoted.

GPU natives: pick the core classifier, e.g. `-Dllama.classifier=cuda13-linux-x86-64` or
`vulkan-windows-x86-64` (the vendor runtime must be installed — see the root README's classifier
table). Without it the default CPU jar (incl. macOS Metal) is used. In mode A the classifier is
Expand All @@ -79,8 +94,8 @@ irrelevant: inference stays in the running server, the agent's JVM loads no mode
| `--model <file.gguf>` | load this GGUF in-process instead | — |
| `--ngl <n>` / `--ctx-size <n>` | GPU layers / context size for `--model` | `0` / `8192` |
| `--log-verbosity <n>` / `--verbose` | llama.cpp log threshold for `--model` (1 errors, 2 warnings, 3 info, 4 trace, 5 debug) / log everything | `2` / off |
| `--workspace <dir>` | directory the file tools (and `run_command`) are confined to | cwd |
| `--allow-shell` | register `run_command` | off |
| `--workspace <dir>` | directory the file tools are confined to, and where `run_command` starts | cwd |
| `--allow-shell` | register `run_command`: any command line, starting in the workspace | off |
| `--system <text>` | replace the default system prompt | built-in |
| `--prompt <text>`, `-p` | one turn, then exit | interactive |
| `--temperature <t>` / `--max-tokens <n>` | sampling / per-call budget | `0.2` / `2048` |
Expand All @@ -103,9 +118,31 @@ code page it saw at startup, so umlauts and emoji in the answer would turn into
project's `.mvn/jvm.config` pins `-Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8` for the `mvn`
JVM so both sides agree.

### The system prompt

Without `--system` the agent uses a built-in **general-purpose** prompt: it names the file tools and
the workspace they work on, and — only with `--allow-shell` — states that `run_command` runs *any*
command line on this machine (the shell is named, so the model writes the right syntax) and that the
model should run a command rather than explain one. Without `--allow-shell` it tells the model it
cannot run commands and to suggest the flag, so the model does not invent a limitation of its own.

The wording is plain text, not Java: [`src/main/resources/net/ladenthin/llama/atmosphere/`](src/main/resources/net/ladenthin/llama/atmosphere/)
holds `system-prompt.txt` (placeholders `{workspace}` and `{shell_section}`), `system-prompt-shell.txt` /
`system-prompt-no-shell.txt` (the `{shell_section}` with and without `--allow-shell`; `{shell}` is the
shell's name) and `run-command-tool.txt` (the `run_command` description the model reads). Edit them
there to change the default for everyone; `--system` overrides it per run.

This wording matters more than it looks: an earlier default called the agent a *coding agent* and
described `run_command` as a way to *"build, test or inspect the project"*, and Qwen3-4B then refused
*"list the docker images"* ("my tools are only for files") although the tool was registered and the
command worked. `--system <text>` replaces the default **completely** — include whatever the model
still needs to know (the workspace, the shell, your language) in your own text.

Pick a **tool-capable instruct model** (Qwen2.5/Qwen3-Instruct, Llama-3.x-Instruct, Mistral,
Hermes, …). Quality of the loop is the model's: a 1.5B model calls one tool and reads its result, a
7B–32B model does multi-step edit/build/test work.
7B–32B model does multi-step edit/build/test work. Qwen3-4B-Instruct-2507 is a good fast default (fits
an 8 GB GPU with a 16k context); Qwen2.5-Coder-7B, in contrast, wrote the call as a JSON code block
into its answer instead of calling the tool.

## What is verified, and where

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public static String usage() {
"",
"Agent:",
" --workspace <dir> directory the file tools are confined to (default: cwd)",
" --allow-shell add the run_command tool (runs shell commands in the workspace)",
" --allow-shell add the run_command tool (runs any command line, starting in the workspace)",
" --system <text> replace the default system prompt",
" --prompt <text>, -p run one turn and exit (default: interactive; /exit to quit)",
" --temperature <t> sampling temperature (default " + DEFAULT_TEMPERATURE + ")",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
package net.ladenthin.llama.atmosphere;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
Expand All @@ -23,7 +26,7 @@
import org.jspecify.annotations.Nullable;

/**
* A local, terminal coding agent in the spirit of Claude Code / OpenCode, built from two parts that
* A local, general-purpose terminal agent in the spirit of Claude Code / OpenCode, built from two parts that
* already exist: <b>Atmosphere</b>'s built-in OpenAI-compatible agent runtime (streaming, tool loop,
* workspace file tools) and <b>java-llama.cpp</b>'s OpenAI-compatible server.
*
Expand All @@ -45,6 +48,15 @@ public final class LocalAgent {
/** Wall-clock bound on one user turn, including every tool round. */
private static final Duration TURN_TIMEOUT = Duration.ofMinutes(30);

/** The default system prompt; placeholders {@code {workspace}} and {@code {shell_section}}. */
static final String SYSTEM_PROMPT = "system-prompt.txt";

/** The {@code {shell_section}} with {@code --allow-shell}; placeholder {@code {shell}}. */
static final String SHELL_PROMPT = "system-prompt-shell.txt";

/** The {@code {shell_section}} without {@code --allow-shell}. */
static final String NO_SHELL_PROMPT = "system-prompt-no-shell.txt";

private static final Duration SHELL_TIMEOUT = Duration.ofSeconds(120);
private static final int SHELL_MAX_OUTPUT_CHARS = 20_000;

Expand Down Expand Up @@ -212,20 +224,47 @@ static ModelParameters modelParameters(AgentOptions options) {
/**
* The default system prompt, or the {@code --system} override.
*
* <p>The default describes a general-purpose agent on this machine, not a coding agent confined to a
* project: a small model reads a narrow role or tool description as a prohibition and then refuses
* requests such as "list the docker images" even though {@code run_command} could do it. With
* {@code --allow-shell} the prompt therefore states that any command line is allowed and that the
* model should run a command rather than explain one; without it, the prompt says so honestly
* instead of letting the model invent a limitation. The text itself is in the resources
* {@value #SYSTEM_PROMPT}, {@value #SHELL_PROMPT} and {@value #NO_SHELL_PROMPT} (see {@link #prompt}).
*
* @param options the options
* @return the system prompt
*/
static String systemPrompt(AgentOptions options) {
if (options.getSystemPrompt() != null) {
return options.getSystemPrompt();
}
String shell = options.isAllowShell()
? " Use run_command to build, test or inspect the project with shell commands."
: "";
return "You are a careful coding agent working in the directory " + options.getWorkspace() + "."
+ " Use the tools to inspect and change files: ls, read_file, write_file, edit_file, glob,"
+ " grep, delete, rename. Paths are relative to that directory." + shell
+ " Work step by step: read a file before you edit it, verify the result after a change,"
+ " and finish with a short summary of what you did.";
String shellSection = options.isAllowShell()
? prompt(SHELL_PROMPT).replace("{shell}", ShellTool.shellName())
: prompt(NO_SHELL_PROMPT);
return prompt(SYSTEM_PROMPT)
.replace("{workspace}", options.getWorkspace().toString())
.replace("{shell_section}", shellSection);
}

/**
* A prompt text from the resources next to this class, trimmed.
*
* <p>The wording lives in {@code src/main/resources/net/ladenthin/llama/atmosphere/*.txt} so it can
* be read and edited as text; {@code {placeholders}} are filled in by {@link #systemPrompt}.
*
* @param name the file name, e.g. {@value #SYSTEM_PROMPT}
* @return the file content without leading or trailing whitespace
* @throws IllegalStateException when the resource is missing from the jar
*/
static String prompt(String name) {
try (InputStream in = LocalAgent.class.getResourceAsStream(name)) {
if (in == null) {
throw new IllegalStateException("Prompt resource missing: " + name);
}
return new String(in.readAllBytes(), StandardCharsets.UTF_8).strip();
} catch (IOException e) {
throw new UncheckedIOException("Cannot read prompt resource " + name, e);
}
}
}
Loading
Loading