Skip to content

Commit 7dc26c0

Browse files
llama-atmosphere-agent: move model-facing texts into resource files
The system prompt and the run_command description were Java string concatenations. They are now plain text next to the classes, in src/main/resources/net/ladenthin/llama/atmosphere/: - system-prompt.txt ({workspace}, {shell_section}) - system-prompt-shell.txt ({shell}; the section with --allow-shell) - system-prompt-no-shell.txt (the section without it) - run-command-tool.txt ({shell}; the tool description) each with a .license sidecar so REUSE stays green without an SPDX comment reaching the model. LocalAgent.prompt(name) loads them (UTF-8, trimmed; a missing file throws). New test promptResourcesLoadAndEveryPlaceholderIsFilled fails on a missing file or a placeholder left unfilled; the tool-description test checks the same. README and CLAUDE.md point at the files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
1 parent 8070817 commit 7dc26c0

13 files changed

Lines changed: 103 additions & 21 deletions

File tree

‎CLAUDE.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2205,7 +2205,12 @@ a `jvm.config` takes no comments, so REUSE can only read its metadata from that
22052205
`REUSE Compliance Check` job fails on `main` — which is how it was found, the PR run having been cancelled.
22062206
Spotless (palantir) is configured in its own pom; the model-free CI job runs `spotless:check`.
22072207

2208-
**The default system prompt is general-purpose on purpose — do not narrow it back.** `LocalAgent.systemPrompt`
2208+
**The default system prompt is general-purpose on purpose — do not narrow it back.** Every model-facing
2209+
text is a resource, not a Java literal: `src/main/resources/net/ladenthin/llama/atmosphere/` holds
2210+
`system-prompt.txt`, `system-prompt-shell.txt`, `system-prompt-no-shell.txt` and `run-command-tool.txt`
2211+
(each with a `.license` sidecar for REUSE), loaded by `LocalAgent.prompt(name)` with `{placeholder}`
2212+
substitution; `AgentOptionsTest.promptResourcesLoadAndEveryPlaceholderIsFilled` fails on a missing file or
2213+
an unfilled placeholder. `LocalAgent.systemPrompt`
22092214
and the `ShellTool` description describe `run_command` as running *any* command line through the named
22102215
shell (`ShellTool.shellName()`), not limited to the workspace, and tell the model to run a command rather
22112216
than explain one. The earlier wording ("careful *coding agent*", `run_command` "to build, test or inspect

‎llama-atmosphere-agent/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ command line on this machine (the shell is named, so the model writes the right
126126
model should run a command rather than explain one. Without `--allow-shell` it tells the model it
127127
cannot run commands and to suggest the flag, so the model does not invent a limitation of its own.
128128

129+
The wording is plain text, not Java: [`src/main/resources/net/ladenthin/llama/atmosphere/`](src/main/resources/net/ladenthin/llama/atmosphere/)
130+
holds `system-prompt.txt` (placeholders `{workspace}` and `{shell_section}`), `system-prompt-shell.txt` /
131+
`system-prompt-no-shell.txt` (the `{shell_section}` with and without `--allow-shell`; `{shell}` is the
132+
shell's name) and `run-command-tool.txt` (the `run_command` description the model reads). Edit them
133+
there to change the default for everyone; `--system` overrides it per run.
134+
129135
This wording matters more than it looks: an earlier default called the agent a *coding agent* and
130136
described `run_command` as a way to *"build, test or inspect the project"*, and Qwen3-4B then refused
131137
*"list the docker images"* ("my tools are only for files") although the tool was registered and the

‎llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/LocalAgent.java‎

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
package net.ladenthin.llama.atmosphere;
66

77
import java.io.BufferedReader;
8+
import java.io.IOException;
9+
import java.io.InputStream;
810
import java.io.InputStreamReader;
911
import java.io.PrintStream;
12+
import java.io.UncheckedIOException;
1013
import java.nio.charset.StandardCharsets;
1114
import java.time.Duration;
1215
import java.util.ArrayList;
@@ -45,6 +48,15 @@ public final class LocalAgent {
4548
/** Wall-clock bound on one user turn, including every tool round. */
4649
private static final Duration TURN_TIMEOUT = Duration.ofMinutes(30);
4750

51+
/** The default system prompt; placeholders {@code {workspace}} and {@code {shell_section}}. */
52+
static final String SYSTEM_PROMPT = "system-prompt.txt";
53+
54+
/** The {@code {shell_section}} with {@code --allow-shell}; placeholder {@code {shell}}. */
55+
static final String SHELL_PROMPT = "system-prompt-shell.txt";
56+
57+
/** The {@code {shell_section}} without {@code --allow-shell}. */
58+
static final String NO_SHELL_PROMPT = "system-prompt-no-shell.txt";
59+
4860
private static final Duration SHELL_TIMEOUT = Duration.ofSeconds(120);
4961
private static final int SHELL_MAX_OUTPUT_CHARS = 20_000;
5062

@@ -217,7 +229,8 @@ static ModelParameters modelParameters(AgentOptions options) {
217229
* requests such as "list the docker images" even though {@code run_command} could do it. With
218230
* {@code --allow-shell} the prompt therefore states that any command line is allowed and that the
219231
* model should run a command rather than explain one; without it, the prompt says so honestly
220-
* instead of letting the model invent a limitation.
232+
* instead of letting the model invent a limitation. The text itself is in the resources
233+
* {@value #SYSTEM_PROMPT}, {@value #SHELL_PROMPT} and {@value #NO_SHELL_PROMPT} (see {@link #prompt}).
221234
*
222235
* @param options the options
223236
* @return the system prompt
@@ -226,19 +239,32 @@ static String systemPrompt(AgentOptions options) {
226239
if (options.getSystemPrompt() != null) {
227240
return options.getSystemPrompt();
228241
}
229-
String files = " The file tools ls, read_file, write_file, edit_file, glob, grep, delete and rename work"
230-
+ " on the directory " + options.getWorkspace() + "; their paths are relative to it.";
231-
String shell = options.isAllowShell()
232-
? " You have full shell access: run_command executes any command line through "
233-
+ ShellTool.shellName() + " on this machine, starting in that directory but not"
234-
+ " limited to it, e.g. docker, git, package managers, build tools or system"
235-
+ " information. When the user asks about this machine or wants something done,"
236-
+ " run the command instead of explaining how to do it."
237-
: " You cannot run shell commands in this session; if a request needs one, say so and"
238-
+ " suggest restarting the agent with --allow-shell.";
239-
return "You are a helpful general-purpose assistant running locally on the user's computer, with"
240-
+ " tools to act on it." + files + shell
241-
+ " Work step by step: read a file before you edit it, check the result after a change,"
242-
+ " and finish with a short summary. Answer in the user's language.";
242+
String shellSection = options.isAllowShell()
243+
? prompt(SHELL_PROMPT).replace("{shell}", ShellTool.shellName())
244+
: prompt(NO_SHELL_PROMPT);
245+
return prompt(SYSTEM_PROMPT)
246+
.replace("{workspace}", options.getWorkspace().toString())
247+
.replace("{shell_section}", shellSection);
248+
}
249+
250+
/**
251+
* A prompt text from the resources next to this class, trimmed.
252+
*
253+
* <p>The wording lives in {@code src/main/resources/net/ladenthin/llama/atmosphere/*.txt} so it can
254+
* be read and edited as text; {@code {placeholders}} are filled in by {@link #systemPrompt}.
255+
*
256+
* @param name the file name, e.g. {@value #SYSTEM_PROMPT}
257+
* @return the file content without leading or trailing whitespace
258+
* @throws IllegalStateException when the resource is missing from the jar
259+
*/
260+
static String prompt(String name) {
261+
try (InputStream in = LocalAgent.class.getResourceAsStream(name)) {
262+
if (in == null) {
263+
throw new IllegalStateException("Prompt resource missing: " + name);
264+
}
265+
return new String(in.readAllBytes(), StandardCharsets.UTF_8).strip();
266+
} catch (IOException e) {
267+
throw new UncheckedIOException("Cannot read prompt resource " + name, e);
268+
}
243269
}
244270
}

‎llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ShellTool.java‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public final class ShellTool {
2424
/** Tool name as offered to the model. */
2525
public static final String TOOL_NAME = "run_command";
2626

27+
/** The tool description the model reads (a resource next to this class); placeholder {@code {shell}}. */
28+
static final String DESCRIPTION_RESOURCE = "run-command-tool.txt";
29+
2730
private static final String PARAM_COMMAND = "command";
2831
private static final String PARAM_TIMEOUT = "timeout_seconds";
2932

@@ -39,11 +42,7 @@ private ShellTool() {}
3942
*/
4043
public static ToolDefinition definition(Path workspace, Duration defaultTimeout, int maxOutputChars) {
4144
return ToolDefinition.builder(
42-
TOOL_NAME,
43-
"Run any command line on this machine through " + shellName() + " and return its exit"
44-
+ " code and output (stdout and stderr merged). It starts in the workspace"
45-
+ " directory but is not limited to it: use it for every task a terminal can do,"
46-
+ " e.g. docker, git, package managers, build tools or system information.")
45+
TOOL_NAME, LocalAgent.prompt(DESCRIPTION_RESOURCE).replace("{shell}", shellName()))
4746
.parameter(PARAM_COMMAND, "The command line to run through " + shellName(), "string", true)
4847
.parameter(PARAM_TIMEOUT, "Seconds to wait before the command is killed", "integer", false)
4948
.executor(args -> {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Run any command line on this machine through {shell} and return its exit code and output (stdout and stderr merged). It starts in the workspace directory but is not limited to it: use it for every task a terminal can do, e.g. docker, git, package managers, build tools or system information.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
3+
SPDX-License-Identifier: MIT
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
You cannot run shell commands in this session; if a request needs one, say so and suggest restarting the agent with --allow-shell.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
3+
SPDX-License-Identifier: MIT
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
You have full shell access: run_command executes any command line through {shell} on this machine, starting in that directory but not limited to it, e.g. docker, git, package managers, build tools or system information. When the user asks about this machine or wants something done, run the command instead of explaining how to do it.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
3+
SPDX-License-Identifier: MIT

0 commit comments

Comments
 (0)