add non-interactive mode and the subagent delegation prompt

Port the two latest changes of the Perl original.

Non-interactive mode: a prompt as an argument runs that one task and exits.
Every word is joined into a single task, so quoting is optional, and an empty
or whitespace-only argument still opens the REPL. The ready banner moves to
stderr so stdout carries the answer, progress and token stats alone, stdout
is line buffered so a piped run arrives as it is written, and the process
exits with the task status: the command's own for !cmd, 1 on failure, 130 on
Ctrl-C. dispatch() returns an int for that.

Subagent delegation: the system prompt now tells the model it can split a
multi-stage task by copying .solo_sess.json into a fresh directory under the
scratch dir and re-launching this binary from there with the same endpoint,
model, tools and prompt, which the copy carries over. The prompt names our
own path, resolved through /proc/self/exe, and notes that a subagent answers
on stdout while its banner goes to stderr, so independent subtasks run in
parallel with &. The prompt buffer is now sized from the expanded paths, and
trailing slashes are stripped from SOLO_ENDPOINT as in the original, so the
command in the prompt is always well formed.

Also print the token stats only on the turn that ends the tool loop, as the
original does: they used to land in the middle of a tool run and chop up
whatever was piped out of a one-shot run.

Tests go to 28, all offline: 15 new ones, including a loopback mock endpoint
that drives a tool call and checks where the stats land.
This commit is contained in:
Luxferre
2026-10-04 17:00:39 +03:00
parent ecc48cd472
commit 141c6cc414
3 changed files with 196 additions and 28 deletions
+41
View File
@@ -187,6 +187,44 @@ A trailing backslash continues a line, which is handy for long tasks:
> build the project, then run the tests, and fix whatever fails \
Or hand it a prompt as an argument and it runs that one task and exits, with
no editor and no prompt loop: see [Non-interactive mode](#non-interactive-mode)
below.
## Non-interactive mode
Hand Solo-C a prompt as an argument and it runs that single task and exits. The
answer arrives on `stdout` together with progress and token stats, the ready
banner on `stderr`, so a task drops straight into pipes, scripts and `cron`:
./solo "list every TODO in this repo and fix the trivial ones" | tee todo-report.md
answer=$(solo "summarize the last commit")
solo '!make test'
All words are joined into one task, so quoting is optional, and an empty or
whitespace-only argument opens the REPL instead. Solo-C exits `0` when the task
is done, `1` when it failed or the endpoint was unreachable, `130` on `Ctrl-C`,
and with the command's own status for a `!cmd` task.
## Subagents
Solo-C splits large work by itself. The system prompt tells it to copy the
current session file `.solo_sess.json` into a fresh directory under
`$SOLO_TMPDIR` and launch another copy of itself from there, on the same
endpoint and with the same model, tools and system prompt:
mkdir -p $SOLO_TMPDIR/sub1 && cp .solo_sess.json $SOLO_TMPDIR/sub1/
cd $SOLO_TMPDIR/sub1
SOLO_ENDPOINT=https://api.kilo.ai/api/openrouter ~/solo 'audit the SQL layer, read-only'
The subagent runs its own tool loop in its own session file and reports its
final answer on `stdout`, while its ready banner goes to `stderr`, so
independent subtasks can run in parallel without filling the caller's context
with their tool output. The prompt names Solo-C's own absolute path, resolved
through `/proc/self/exe`, so the binary is relaunched wherever it was started
from. Put `SOLO_MODEL`, `SOLO_REASON`, `SOLO_TEMP` or `SOLO_SYSPROMPT` in front
of `SOLO_ENDPOINT` to give a subagent a different setup.
## Configuration
Everything is configurable at three levels, from weakest to strongest:
@@ -207,6 +245,9 @@ Everything is configurable at three levels, from weakest to strongest:
| scratch dir | `$TMPDIR/solo`, or `/tmp/solo` | `SOLO_TMPDIR` |
| colors | on when stdout is a TTY | `NO_COLOR=1` |
In non-interactive mode the banner is printed on `stderr`, but the color check
still follows `stdout`, so a piped run stays free of escape codes.
Session commands take precedence and are persisted, so a model you set by hand
survives a restart even if the environment says something else.