kronk-cli 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -21
- package/package.json +1 -1
- package/src/agent.js +16 -4
- package/src/argv.js +11 -1
- package/src/boot.js +35 -2
- package/src/config.js +21 -3
- package/src/index.js +87 -28
- package/src/mcp.js +4 -1
- package/src/pane.js +276 -0
- package/src/rescue.js +306 -0
- package/src/rescueStore.js +277 -0
- package/src/resolve.js +27 -0
- package/src/setup.js +16 -3
- package/src/subagent.js +186 -30
- package/src/tasklog.js +7 -1
- package/src/ui.js +20 -6
- package/src/update.js +291 -0
- package/src/version.js +35 -0
package/README.md
CHANGED
|
@@ -450,6 +450,88 @@ The profile it writes is the one documented under
|
|
|
450
450
|
[Tip: use an `/AGENT` profile](#tip-use-an-agent-profile) — `context-window`, `nseq-max`,
|
|
451
451
|
`preserve_thinking` and `max_tokens`, and deliberately no sampling parameters.
|
|
452
452
|
|
|
453
|
+
### `kronk-cli rescue`
|
|
454
|
+
|
|
455
|
+
Everywhere else in this document, `kronk-cli` is something *you* talk to. `rescue` is the other
|
|
456
|
+
direction: a job queue so anything that can spawn a process — a script, another CLI, anything
|
|
457
|
+
external that wants to hand off a token-heavy job without sitting on it — can submit a
|
|
458
|
+
self-contained task to the local model, go do something else, and come back for the answer.
|
|
459
|
+
|
|
460
|
+
```console
|
|
461
|
+
$ kronk-cli rescue "trace how the SSE parser handles multi-line data fields" --role explore
|
|
462
|
+
9k2m1x-4d2-a1b2c3
|
|
463
|
+
|
|
464
|
+
$ kronk-cli rescue status 9k2m1x-4d2-a1b2c3
|
|
465
|
+
running (step 4)
|
|
466
|
+
|
|
467
|
+
$ kronk-cli rescue wait 9k2m1x-4d2-a1b2c3
|
|
468
|
+
Multi-line `data:` fields are joined with `\n` in src/sse.js:41-53 before the
|
|
469
|
+
frame is handed to the caller — see accumulateToolCalls's buffer.
|
|
470
|
+
$ echo $?
|
|
471
|
+
0
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
It is the same delegation `kronk-cli` already does internally — one self-contained task, its own
|
|
475
|
+
context, a report that stands alone — with the caller moved outside the process instead of inside
|
|
476
|
+
it. See [Sub-agents](#sub-agents) for the contract a rescue job runs under; it is the identical
|
|
477
|
+
one.
|
|
478
|
+
|
|
479
|
+
| Command | |
|
|
480
|
+
|---|---|
|
|
481
|
+
| `kronk-cli rescue "<prompt>" [--role explore\|code] [--model <id>] [-y\|-a] [--steps <n>] [--json]` | Submit. Runs detached; prints a job id and exits immediately — nothing here blocks |
|
|
482
|
+
| `kronk-cli rescue status <id> [--json]` | A snapshot: `queued`, `running (step n)`, `done`, `stopped`, `interrupted` or `failed` |
|
|
483
|
+
| `kronk-cli rescue wait <id> [--json]` | Block until the job reaches one of those last four states, then print its report |
|
|
484
|
+
| `kronk-cli rescue result <id> [--json]` | Print the report. Refuses (exit `4`) with the current state if the job has not finished yet — use `wait` instead of polling `result` in a loop |
|
|
485
|
+
|
|
486
|
+
**`--role`** is the same split `kronk-cli` gives its own sub-agents: `explore` can only read —
|
|
487
|
+
`read_file`, `list_dir`, `search` — and `code` can also write files and run commands. Default is
|
|
488
|
+
`explore`, the read-only one. **`--model`** is resolved exactly as `-m` resolves it everywhere
|
|
489
|
+
else in this program — exact id, else the closest substring, preferring a served `/AGENT`
|
|
490
|
+
profile — against what Kronk is serving *right now*; a name that matches nothing is refused at
|
|
491
|
+
submit time rather than silently falling back, because a caller that asked for a specific model
|
|
492
|
+
should hear about a bad name immediately, on the process it is still attached to, not from a job
|
|
493
|
+
that failed twenty minutes later. Leaving `--model` out defers the choice to the job itself, the
|
|
494
|
+
same default the session would otherwise use.
|
|
495
|
+
|
|
496
|
+
**Without `-y`/`-a`, a tool that needs approval is refused and reported, never auto-allowed.**
|
|
497
|
+
Nobody is at the prompt a rescue job would otherwise show, so there is no "ask anyway" — a refused
|
|
498
|
+
tool call comes back in the transcript exactly like a human said no, and the job carries on (or
|
|
499
|
+
does not) from there. This is deliberate and does not have a flag to turn it off: delegating a
|
|
500
|
+
job from outside the process must not become a way past the approval prompts that delegating from
|
|
501
|
+
inside it already cannot get past.
|
|
502
|
+
|
|
503
|
+
**One at a time.** Kronk holds one resident copy of the model, so a second `rescue` submitted
|
|
504
|
+
while one is running does not run alongside it — its `status` reports `queued` until the first
|
|
505
|
+
job finishes, exactly as the module comment on delegation in `src/subagent.js` already argues for
|
|
506
|
+
sub-agents. A runner that dies mid-job (killed, the machine restarting) does not wedge the queue
|
|
507
|
+
behind it: the next job to check finds the lock's process gone, marks the abandoned job
|
|
508
|
+
`interrupted`, and takes its turn.
|
|
509
|
+
|
|
510
|
+
**`--json`** on any of the four commands prints one object instead of prose:
|
|
511
|
+
|
|
512
|
+
```json
|
|
513
|
+
{ "id": "9k2m1x-4d2-a1b2c3", "state": "done", "role": "explore",
|
|
514
|
+
"model": "unsloth/Qwen3.6-35B-A3B-UD-Q4_K_M/AGENT", "steps": 3, "ms": 11421,
|
|
515
|
+
"report": "Multi-line `data:` fields are joined with…", "transcript": [ /* the full run */ ] }
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`wait`'s exit code is the one place a script gets the state without parsing anything:
|
|
519
|
+
|
|
520
|
+
| State | Meaning | `wait` exit code |
|
|
521
|
+
|---|---|---|
|
|
522
|
+
| `done` | Finished on its own terms | `0` |
|
|
523
|
+
| `stopped` | Hit its step cap before finishing | `1` |
|
|
524
|
+
| `interrupted` | Killed, or recovered from a runner that died mid-job | `2` |
|
|
525
|
+
| `failed` | The model call itself errored | `3` |
|
|
526
|
+
|
|
527
|
+
**Records outlive the process that submitted them, on purpose.** They are not sub-agent
|
|
528
|
+
transcripts (`src/tasklog.js`) and do not live in the same place or on the same clock — that
|
|
529
|
+
directory is swept on this session's own schedule specifically because nothing outside the
|
|
530
|
+
process is meant to still want it once the session ends, and a rescue job's whole point is that
|
|
531
|
+
something outside the process still does. A rescue record is kept indefinitely until `wait` or
|
|
532
|
+
`result` actually delivers it, and only then aged out, 24 hours later — `KRONK_RESCUE_DIR`
|
|
533
|
+
overrides where they live, same idea as `KRONK_TASK_LOG_DIR`.
|
|
534
|
+
|
|
453
535
|
---
|
|
454
536
|
|
|
455
537
|
## Startup: the model is loaded before you type
|
|
@@ -530,6 +612,70 @@ run (`kronk-cli "prompt"`) prints no banner and prints nothing here either.
|
|
|
530
612
|
|
|
531
613
|
---
|
|
532
614
|
|
|
615
|
+
## Checking for updates
|
|
616
|
+
|
|
617
|
+
`kronk-cli --version` prints what is actually running:
|
|
618
|
+
|
|
619
|
+
```console
|
|
620
|
+
$ kronk-cli --version
|
|
621
|
+
kronk-cli 0.7.0
|
|
622
|
+
kronk 1.32.7
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
The second line is `kronk --version`, read through the same `runKronk` helper `setup` uses,
|
|
626
|
+
capped at a few seconds so a stuck binary cannot hang the command. It is omitted, not guessed
|
|
627
|
+
at, when `kronk` is not on `PATH` or does not answer in time — this has to work with **no server
|
|
628
|
+
running**, which is the ordinary case for asking a program what it is.
|
|
629
|
+
|
|
630
|
+
The startup banner adds a third line when a newer `kronk-cli` exists:
|
|
631
|
+
|
|
632
|
+
```console
|
|
633
|
+
██ kronk-cli · local agent, no network
|
|
634
|
+
model unsloth/Qwen3.6-35B-A3B-UD-Q4_K_M/AGENT
|
|
635
|
+
server http://localhost:11435/v1
|
|
636
|
+
update kronk-cli 0.7.0 → 0.8.1 · run: brew upgrade kronk-cli
|
|
637
|
+
/help for commands · Ctrl-C to interrupt · /exit to quit
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**It asks the package manager, never the network directly.** No `fetch`, no URL literal — the
|
|
641
|
+
version comes from whatever tool installed this copy, detected from the path `src/index.js`
|
|
642
|
+
itself resolved to:
|
|
643
|
+
|
|
644
|
+
| Route | How it's detected | What answers |
|
|
645
|
+
|---|---|---|
|
|
646
|
+
| Homebrew | a `Cellar/kronk-cli/` path | `brew outdated --json` |
|
|
647
|
+
| npm global install | a `node_modules/kronk-cli` path | `npm view kronk-cli version` |
|
|
648
|
+
| git checkout | a `.git` above the file | `git fetch --tags` + the newest tag |
|
|
649
|
+
| `packaging/install.sh` | a `lib/kronk-cli` path | nothing — see below |
|
|
650
|
+
|
|
651
|
+
The hint always matches the route: a Homebrew install is told `brew upgrade kronk-cli`, an npm
|
|
652
|
+
one `npm install -g kronk-cli@latest`, a checkout `git pull`. Suggesting the wrong one would send
|
|
653
|
+
someone to a manager that has never heard of this copy, which is worse than saying nothing.
|
|
654
|
+
|
|
655
|
+
The git checkout route is the one that makes a real network call, through `git fetch --tags`
|
|
656
|
+
rather than any request this program issues itself, and it honours the same off-switch as
|
|
657
|
+
everything else here.
|
|
658
|
+
|
|
659
|
+
**The install script has nothing to ask.** It unpacks a tarball into `$PREFIX/lib/kronk-cli` and,
|
|
660
|
+
unlike the other three routes, there is no registry behind it that knows the latest release. It
|
|
661
|
+
writes a small receipt (`$PREFIX/lib/kronk-cli/.install-receipt.json`, version and source) at
|
|
662
|
+
install time so this route is not silent forever; when that receipt is present the banner shows
|
|
663
|
+
a plain nudge — `update kronk-cli 0.7.0 · re-run the install script` — with no version
|
|
664
|
+
comparison, because there is genuinely nothing to compare against. A copy installed before the
|
|
665
|
+
receipt existed, or one someone assembled by hand, shows no update line at all, which is the
|
|
666
|
+
honest answer for a route with no manager behind it.
|
|
667
|
+
|
|
668
|
+
**Never on the path that slows startup down.** The check result is cached to disk with a
|
|
669
|
+
timestamp and reused for up to a day; a stale or missing cache still renders instantly (from
|
|
670
|
+
whatever was cached last, or nothing) while a fresh check for the *next* session runs
|
|
671
|
+
unawaited in the background. Nothing about this can delay, block, or fail the REPL — a package
|
|
672
|
+
manager that hangs, errors, or prints garbage simply produces no line.
|
|
673
|
+
|
|
674
|
+
`KRONK_UPDATE_CHECK=false` (or `"updateCheck": false` in the config file) turns all of this off:
|
|
675
|
+
no cache read, no subprocess, ever.
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
533
679
|
## Performance
|
|
534
680
|
|
|
535
681
|
The transcripts above are real sessions, kept to show what the UI looks like — not a benchmark
|
|
@@ -598,6 +744,7 @@ same way this run did.
|
|
|
598
744
|
| `--no-subagents` | off | Remove the `task` tool, so the model cannot delegate — see [Sub-agents](#sub-agents) |
|
|
599
745
|
| `--steps <n>` | unlimited | Cap tool calls per task. `0`, `off`, `none`, `inf`, `unlimited` all mean no cap |
|
|
600
746
|
| `-h`, `--help` | — | Print all options and exit |
|
|
747
|
+
| `-v`, `--version` | — | Print the kronk-cli version, and Kronk's if it can be had without a network call, then exit |
|
|
601
748
|
| `--` | — | End option parsing; everything after it is prompt text, dashes and all |
|
|
602
749
|
|
|
603
750
|
Anything not consumed as an option becomes the prompt, except a token that looks like an
|
|
@@ -680,7 +827,7 @@ Two consequences worth knowing:
|
|
|
680
827
|
| `/steps [n\|off]` | show or set the tool-call cap |
|
|
681
828
|
| `/thinking` | show or hide the model's reasoning |
|
|
682
829
|
| `/think` | turn reasoning off entirely — much faster |
|
|
683
|
-
| `/agents` | the sub-agents `task` can delegate to,
|
|
830
|
+
| `/agents` | the sub-agents `task` can delegate to, what each may touch, and the default model |
|
|
684
831
|
| `/tasks [n]` | sub-agent runs this session; with `n`, that one in full |
|
|
685
832
|
| `/mcp` | list attached MCP servers and their tools |
|
|
686
833
|
| `/context` | how much of the context window is used |
|
|
@@ -713,14 +860,16 @@ Two consequences worth knowing:
|
|
|
713
860
|
| `KRONK_DISTILL` | `true` | `false` disables tool-output distillation |
|
|
714
861
|
| `KRONK_DISTILL_AT` | `8000` | Characters of output that trigger distillation |
|
|
715
862
|
| `KRONK_SUBAGENTS` | `true` | `false` removes the `task` tool |
|
|
716
|
-
| `KRONK_SUBAGENT_MODEL` | the main model |
|
|
863
|
+
| `KRONK_SUBAGENT_MODEL` | the main model | Default model for a task that names none itself — substring match, same as `KRONK_MODEL`. A task can pick its own from what Kronk is serving, see [Picking a model per task](#picking-a-model-per-task) |
|
|
717
864
|
| `KRONK_SUBAGENT_STEPS` | `40` | Tool-call cap for one delegated task |
|
|
718
865
|
| `KRONK_TASK_LOG` | `true` | `false` stops writing sub-agent transcripts, and `/tasks` says so |
|
|
719
866
|
| `KRONK_TASK_LOG_DIR` | the temp dir | Where transcripts are written |
|
|
867
|
+
| `KRONK_RESCUE_DIR` | the temp dir | Where `kronk-cli rescue` keeps its job records — see [`kronk-cli rescue`](#kronk-cli-rescue) |
|
|
720
868
|
| `KRONK_WARM` | `true` | `false` skips the boot-time model preload |
|
|
721
869
|
| `KRONK_AUTO_COMPACT` | `true` | `false` disables automatic compaction |
|
|
722
870
|
| `KRONK_COMPACT_AT` | `0.85` | Fraction of the window that triggers compaction |
|
|
723
871
|
| `KRONK_THEME` | `auto` | `dark` or `light` pins the palette; `auto` reads the terminal's background |
|
|
872
|
+
| `KRONK_UPDATE_CHECK` | `true` | `false` never asks the package manager whether a newer kronk-cli exists — no subprocess, ever |
|
|
724
873
|
| `NO_COLOR` | — | Any value disables colour |
|
|
725
874
|
| `FORCE_COLOR` | — | `0` disables colour; any other value keeps it on through a pipe |
|
|
726
875
|
|
|
@@ -908,9 +1057,10 @@ Kronk reports the **effective** window for whichever model id you selected — t
|
|
|
908
1057
|
Every limit is read from the selected model's profile, and re-read when the selection changes:
|
|
909
1058
|
the window, the output cap (`sampling-parameters.max_tokens`), whether the chat template
|
|
910
1059
|
understands `preserve_thinking`, and the model's native maximum. `/model` mid-session moves all
|
|
911
|
-
of them, and a sub-agent running on `KRONK_SUBAGENT_MODEL`
|
|
912
|
-
|
|
913
|
-
|
|
1060
|
+
of them, and a sub-agent — whether it is running on `KRONK_SUBAGENT_MODEL` or on a model a task
|
|
1061
|
+
named for itself, see [Picking a model per task](#picking-a-model-per-task) — compacts against
|
|
1062
|
+
*its own* window rather than the main model's. The one number that is not taken from the profile
|
|
1063
|
+
is a `KRONK_MAX_TOKENS` you set yourself — yours always wins.
|
|
914
1064
|
|
|
915
1065
|
Three places surface it:
|
|
916
1066
|
|
|
@@ -1238,30 +1388,53 @@ The model can hand one job to a **sub-agent**: a fresh agent with its own contex
|
|
|
1238
1388
|
own tool loop, and no memory of your conversation. It runs the task, and the only thing that
|
|
1239
1389
|
ever comes back is its report.
|
|
1240
1390
|
|
|
1391
|
+
While it runs, it draws in a box below the prompt that redraws in place:
|
|
1392
|
+
|
|
1241
1393
|
```console
|
|
1242
1394
|
› which tools in this repo need approval, and where is that decided?
|
|
1243
1395
|
|
|
1244
1396
|
1 ⚙ task explore: list every tool definition in src/, say which are gated and where
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1397
|
+
┌─ explore ──────────────────────────────────────── 6/40 · 48s ─┐
|
|
1398
|
+
│ 4/40 ⚙ read src/tools.js │
|
|
1399
|
+
│ ✓ 587 lines │
|
|
1400
|
+
│ 5/40 ⚙ search /NEEDS_APPROVAL/ in src │
|
|
1401
|
+
│ ✓ 3 lines │
|
|
1402
|
+
│ 6/40 ⚙ read src/subagent.js │
|
|
1403
|
+
└─ esc to collapse · ctrl-c to stop ────────────────────────────┘
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
When it finishes the box is erased, and what is left in the scrollback is the answer and one
|
|
1407
|
+
line saying what the run cost:
|
|
1408
|
+
|
|
1409
|
+
```console
|
|
1410
|
+
1 ⚙ task explore: list every tool definition in src/, say which are gated and where
|
|
1253
1411
|
Six built-in tools. write_file and bash are gated, by NEEDS_APPROVAL at
|
|
1254
1412
|
src/tools.js:261. MCP tools are gated by name, mcpNeedsApproval at :270.
|
|
1255
1413
|
read_file, list_dir, search, set_plan and task are not gated.
|
|
1256
|
-
└ 3 steps · 11.4s · /
|
|
1414
|
+
└ 3 steps · 11.4s · /tasks 1 for the transcript
|
|
1257
1415
|
✓ 3 lines
|
|
1258
1416
|
|
|
1259
1417
|
`write_file` and `bash`, from the `NEEDS_APPROVAL` set in src/tools.js:246…
|
|
1260
1418
|
```
|
|
1261
1419
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1420
|
+
The box is the run's only appearance on screen, and that is deliberate. A nine-step survey
|
|
1421
|
+
printed into the conversation leaves forty lines of someone else's work between the question
|
|
1422
|
+
you asked and the answer you got; scroll back ten minutes later and the conversation is mostly
|
|
1423
|
+
sub-agent. While the run is going a live view is the only sign the thing is alive, and it is
|
|
1424
|
+
where you notice one thrashing — but once it has ended, almost none of it is worth keeping.
|
|
1425
|
+
|
|
1426
|
+
Nothing is lost by erasing it. Everything the box shows is written to the run's transcript as
|
|
1427
|
+
it happens, so the box is a view of a file that outlives it, and `/tasks 1` below is how you
|
|
1428
|
+
read it back. Three keys and one fallback are worth knowing:
|
|
1429
|
+
|
|
1430
|
+
- **`esc` collapses the box immediately** and the run carries on. For when you have seen enough
|
|
1431
|
+
and want your screen back.
|
|
1432
|
+
- **`ctrl-c`** stops the run, as it stops anything else.
|
|
1433
|
+
- **An approval prompt wipes the box first.** A `code` sub-agent's `write_file` and `bash` stop
|
|
1434
|
+
for the same question the main agent's do, and that question gets the terminal to itself.
|
|
1435
|
+
- **On a pipe or a dumb terminal there is no box at all.** `kronk-cli "…" | grep`, a CI job and
|
|
1436
|
+
an `--auto` run nobody is watching each get the plain nested lines instead, in order, with no
|
|
1437
|
+
cursor control in them.
|
|
1265
1438
|
|
|
1266
1439
|
Three files were read. None of them is in your conversation: the 800-odd lines they cost were
|
|
1267
1440
|
spent in a context that no longer exists, and all that is left in your window is the four-line
|
|
@@ -1273,10 +1446,10 @@ already leaving it.
|
|
|
1273
1446
|
|
|
1274
1447
|
### Reading a run back
|
|
1275
1448
|
|
|
1276
|
-
The report on screen is dimmed and capped at fourteen lines
|
|
1277
|
-
the sub-agent read, what it ran, and any line fifteen of its
|
|
1278
|
-
context. Delegating is supposed to cost the *conversation*
|
|
1279
|
-
cost you the ability to check.
|
|
1449
|
+
The report on screen is dimmed and capped at fourteen lines, and the box it ran in is gone. That
|
|
1450
|
+
used to be the end of it: what the sub-agent read, what it ran, and any line fifteen of its
|
|
1451
|
+
report went out of scope with its context. Delegating is supposed to cost the *conversation*
|
|
1452
|
+
nothing — it was never supposed to cost you the ability to check.
|
|
1280
1453
|
|
|
1281
1454
|
So each run is written to a file as it goes, and `/tasks` reads them back.
|
|
1282
1455
|
|
|
@@ -1402,6 +1575,38 @@ the delegation *and* the work.
|
|
|
1402
1575
|
> works — *"use a sub-agent to survey the test suite first"* — and is usually how you will
|
|
1403
1576
|
> drive this.
|
|
1404
1577
|
|
|
1578
|
+
### Picking a model per task
|
|
1579
|
+
|
|
1580
|
+
`KRONK_SUBAGENT_MODEL` is a session-wide setting — every delegated task ran on it, or on the main
|
|
1581
|
+
model, and changing your mind meant leaving the session. But which model suits a task is a
|
|
1582
|
+
property of the *task*, not of the session: a survey that reads thirty files and answers one
|
|
1583
|
+
question wants the small fast model; reproducing a build failure and fixing it wants the big one.
|
|
1584
|
+
|
|
1585
|
+
So the `task` tool takes an optional `model`, and the delegating model picks it from what Kronk is
|
|
1586
|
+
actually serving — every chat model it has, with a bare id dropped when its own `/AGENT` profile
|
|
1587
|
+
is served beside it, offered as a fixed list rather than free
|
|
1588
|
+
text, so it can never ask for a model that does not exist. Leaving it out is unchanged:
|
|
1589
|
+
`KRONK_SUBAGENT_MODEL` if one is set, otherwise the main model.
|
|
1590
|
+
|
|
1591
|
+
```console
|
|
1592
|
+
1 ⚙ task explore: survey every call site of parseLimits · Qwen3.6-4B/AGENT
|
|
1593
|
+
┌─ explore · Qwen3.6-4B/AGENT ──────────────────────────────── 3/40 · 9s ─┐
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
The model actually used is named on the opening line and in the box, because naming one that is
|
|
1597
|
+
not already resident is not free. Kronk holds one large model at a time, so a task that asks for
|
|
1598
|
+
a different one pays a real cold load — the same 10–30s spinner `kronk-cli` itself waits through
|
|
1599
|
+
at startup — and may evict the model the rest of the session is running on. `/tasks` records which
|
|
1600
|
+
model each run was on, so a delegation that took a minute longer than the others is explained by
|
|
1601
|
+
the list, not a mystery.
|
|
1602
|
+
|
|
1603
|
+
A task's own model gets *its* window and output cap, not the session's, read from that model's own
|
|
1604
|
+
limits the first time anything runs on it and kept for the rest of the session — the same
|
|
1605
|
+
argument [Context window](#context-window) already makes for `KRONK_SUBAGENT_MODEL`: a 32k
|
|
1606
|
+
sub-agent must not compact against the main model's 131k window. A model named that Kronk turns
|
|
1607
|
+
out not to be serving any more — unloaded mid-session, say — is not a failed delegation: the task
|
|
1608
|
+
still runs, on the same fallback as no `model` at all, with a note saying so in its report.
|
|
1609
|
+
|
|
1405
1610
|
## Autonomous mode
|
|
1406
1611
|
|
|
1407
1612
|
`--auto`, or `/auto` in the REPL, swaps in a system prompt that tells the model to finish the
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -12,7 +12,7 @@ import { maybeDistill } from './distill.js';
|
|
|
12
12
|
import {
|
|
13
13
|
carryChecklist, clearPlan, openItems, outstandingLines, planLines, pushNudge,
|
|
14
14
|
} from './plan.js';
|
|
15
|
-
import { runTask, taskTools
|
|
15
|
+
import { runTask, taskTools } from './subagent.js';
|
|
16
16
|
|
|
17
17
|
export const SYSTEM = `You are kronk-cli, a terse coding assistant running fully offline on the user's machine.
|
|
18
18
|
|
|
@@ -75,6 +75,9 @@ function endTurn(messages, { out = console.log, plan = true } = {}) {
|
|
|
75
75
|
return messages;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Stands in for `liveLine()` under a pane, which is drawing the same seconds itself. */
|
|
79
|
+
const QUIET_LIVE = { update() {}, done() {} };
|
|
80
|
+
|
|
78
81
|
/**
|
|
79
82
|
* Run one user turn to completion, looping while the model requests tools.
|
|
80
83
|
*
|
|
@@ -89,6 +92,10 @@ function endTurn(messages, { out = console.log, plan = true } = {}) {
|
|
|
89
92
|
export async function runTurn({
|
|
90
93
|
messages, model, signal, approve, grant, mcp, auto = false, maxSteps = config.maxSteps,
|
|
91
94
|
tools: toolset = TOOLS, plan: usePlan = true, out = console.log, stream = true, depth = 0,
|
|
95
|
+
// Print nothing that redraws the current line — the spinner and the live
|
|
96
|
+
// command line. Set by src/subagent.js when a pane is drawing the run, and
|
|
97
|
+
// the pane owns the bottom of the screen for as long as it is up.
|
|
98
|
+
quiet = false,
|
|
92
99
|
// Called with the transcript so far after every round. Only src/subagent.js
|
|
93
100
|
// passes one: a delegated run is the transcript nobody would otherwise keep,
|
|
94
101
|
// and the round that ends it may be a throw or a step cap rather than a
|
|
@@ -125,7 +132,7 @@ export async function runTurn({
|
|
|
125
132
|
: '(stopped: step cap reached)' });
|
|
126
133
|
return endTurn(messages, { out, plan: usePlan });
|
|
127
134
|
}
|
|
128
|
-
let sp = spinner(depth ? 'sub-agent' : 'thinking');
|
|
135
|
+
let sp = quiet ? null : spinner(depth ? 'sub-agent' : 'thinking');
|
|
129
136
|
let text = '';
|
|
130
137
|
let reasoning = '';
|
|
131
138
|
let calls = [];
|
|
@@ -278,7 +285,12 @@ export async function runTurn({
|
|
|
278
285
|
// The same list the model was offered, not just the name it used: with
|
|
279
286
|
// delegation off, or below the top level, a `task` call is a call to a
|
|
280
287
|
// tool that does not exist and falls through to runTool saying so.
|
|
281
|
-
|
|
288
|
+
//
|
|
289
|
+
// By name, not by identity — src/subagent.js rebuilds the task tool's
|
|
290
|
+
// definition object whenever the served-model list changes, so an
|
|
291
|
+
// object captured in `tools` at the top of this turn need not be the
|
|
292
|
+
// same reference `taskTools` would hand back if asked again now.
|
|
293
|
+
const isTask = !isMcp && call.name === 'task' && tools.some((t) => t.function.name === 'task');
|
|
282
294
|
const label = isMcp
|
|
283
295
|
? `${c.magenta('⚙')} ${call.name} ${c.grey(JSON.stringify(args).slice(0, 120))}`
|
|
284
296
|
: `${c.blue(`⚙ ${describe(call.name, args)}`)}`;
|
|
@@ -342,7 +354,7 @@ export async function runTurn({
|
|
|
342
354
|
result = `error: the sub-agent failed — ${e.message}`;
|
|
343
355
|
}
|
|
344
356
|
} else {
|
|
345
|
-
const live = liveLine();
|
|
357
|
+
const live = quiet ? QUIET_LIVE : liveLine();
|
|
346
358
|
try {
|
|
347
359
|
result = isMcp
|
|
348
360
|
? await mcp.call(call.name, args)
|
package/src/argv.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
const SPECS = [
|
|
20
20
|
{ names: ['-h', '--help'], kind: 'flag', key: 'help' },
|
|
21
|
+
{ names: ['-v', '--version'], kind: 'flag', key: 'version' },
|
|
21
22
|
{ names: ['-l', '--models', '--list'], kind: 'flag', key: 'models' },
|
|
22
23
|
{ names: ['--mcp-list'], kind: 'flag', key: 'mcpList' },
|
|
23
24
|
{ names: ['--no-context'], kind: 'flag', key: 'noContext' },
|
|
@@ -32,6 +33,12 @@ const SPECS = [
|
|
|
32
33
|
{ names: ['--steps'], kind: 'value', key: 'steps' },
|
|
33
34
|
{ names: ['--context'], kind: 'value', key: 'context' },
|
|
34
35
|
{ names: ['--mcp'], kind: 'optional', key: 'mcp' },
|
|
36
|
+
{ names: ['--role'], kind: 'value', key: 'role' },
|
|
37
|
+
{ names: ['--json'], kind: 'flag', key: 'json' },
|
|
38
|
+
// Internal: how `kronk-cli rescue` re-invokes itself as the detached
|
|
39
|
+
// worker for one job id (src/rescue.js). Not documented in --help — it is
|
|
40
|
+
// not a thing anyone types, only a thing the CLI passes to itself.
|
|
41
|
+
{ names: ['--rescue-worker'], kind: 'value', key: 'rescueWorker' },
|
|
35
42
|
];
|
|
36
43
|
|
|
37
44
|
const BY_NAME = new Map(SPECS.flatMap((s) => s.names.map((n) => [n, s])));
|
|
@@ -101,7 +108,7 @@ const unknown = (token) => {
|
|
|
101
108
|
export function parseArgv(argv) {
|
|
102
109
|
const out = {
|
|
103
110
|
error: null,
|
|
104
|
-
help: false, models: false, mcpList: false,
|
|
111
|
+
help: false, version: false, models: false, mcpList: false,
|
|
105
112
|
noContext: false, noCompact: false, noWarm: false, noThink: false,
|
|
106
113
|
noSubagents: false,
|
|
107
114
|
auto: false, yes: false, dryRun: false,
|
|
@@ -109,6 +116,9 @@ export function parseArgv(argv) {
|
|
|
109
116
|
context: null,
|
|
110
117
|
steps: null,
|
|
111
118
|
mcp: false, mcpNames: null,
|
|
119
|
+
role: null,
|
|
120
|
+
json: false,
|
|
121
|
+
rescueWorker: null,
|
|
112
122
|
words: [],
|
|
113
123
|
};
|
|
114
124
|
const fail = (message) => ({ ...out, error: message });
|
package/src/boot.js
CHANGED
|
@@ -2,10 +2,43 @@ import { config, DEFAULT_MODEL } from './config.js';
|
|
|
2
2
|
import { listLoaded, warm } from './client.js';
|
|
3
3
|
import { c, spinner } from './ui.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const PROFILE = '/AGENT';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every model Kronk is serving that a delegated task may be offered: the chat
|
|
9
|
+
* models, minus the embedding and reranking ones, and minus a bare id whose
|
|
10
|
+
* own /AGENT profile is also being served — Kronk holds each profile as a
|
|
11
|
+
* separate resident copy of the same weights, so offering both would let a
|
|
12
|
+
* task load a second copy of a model that is already in the pool.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately *not* `pickDefault`'s rule. That one collapses to "the /AGENT
|
|
15
|
+
* profiles, if there are any", which is right when choosing a single default
|
|
16
|
+
* — a tuned profile beats an untuned model — and wrong for building a menu,
|
|
17
|
+
* because it throws away every model that has no profile of its own. The
|
|
18
|
+
* small fast model a survey wants is exactly the one least likely to have had
|
|
19
|
+
* a profile written for it, so applying the default-picking rule here would
|
|
20
|
+
* drop the most useful entry in the list and leave a menu of nothing but
|
|
21
|
+
* large models — on a pool that holds one large model at a time, a menu where
|
|
22
|
+
* every choice costs an eviction.
|
|
23
|
+
*/
|
|
24
|
+
export function chatModels(ids) {
|
|
25
|
+
const chat = ids.filter((id) => !/embedding|rerank/i.test(id));
|
|
26
|
+
const profiled = new Set(chat
|
|
27
|
+
.filter((id) => id.endsWith(PROFILE))
|
|
28
|
+
.map((id) => id.slice(0, -PROFILE.length)));
|
|
29
|
+
return chat.filter((id) => !profiled.has(id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Last resort when neither the flag nor DEFAULT_MODEL is being served.
|
|
34
|
+
*
|
|
35
|
+
* Its own filter, unchanged: one model is being chosen on the user's behalf
|
|
36
|
+
* with nothing to go on, and there a tuned /AGENT profile wins over an
|
|
37
|
+
* untuned model outright rather than merely being listed beside it.
|
|
38
|
+
*/
|
|
6
39
|
export function pickDefault(ids) {
|
|
7
40
|
const chat = ids.filter((id) => !/embedding|rerank/i.test(id));
|
|
8
|
-
const agent = chat.filter((id) => id.endsWith(
|
|
41
|
+
const agent = chat.filter((id) => id.endsWith(PROFILE));
|
|
9
42
|
const pool = agent.length ? agent : chat;
|
|
10
43
|
return pool.sort((a, b) => b.length - a.length)[0] ?? null;
|
|
11
44
|
}
|
package/src/config.js
CHANGED
|
@@ -77,6 +77,12 @@ export const config = {
|
|
|
77
77
|
// one is not: nobody is watching a sub-agent, and its report is worth less
|
|
78
78
|
// than the window it would spend earning it.
|
|
79
79
|
subagents: (process.env.KRONK_SUBAGENTS ?? String(file.subagents ?? 'true')) !== 'false',
|
|
80
|
+
// The default model for a task that names none. A task's own `model`
|
|
81
|
+
// argument, when it has one, wins over this — see the `task` tool in
|
|
82
|
+
// src/subagent.js. Either way, the chosen model's window and output cap
|
|
83
|
+
// are resolved per model, not stored here: src/subagent.js keeps its own
|
|
84
|
+
// cache, because which limits apply now depends on the task, not the
|
|
85
|
+
// session.
|
|
80
86
|
subagentModel: process.env.KRONK_SUBAGENT_MODEL ?? file.subagentModel ?? null,
|
|
81
87
|
subagentSteps: Number(process.env.KRONK_SUBAGENT_STEPS ?? file.subagentSteps ?? 40),
|
|
82
88
|
// Each sub-agent run is written to a file as it goes, so `/tasks` can show
|
|
@@ -86,9 +92,24 @@ export const config = {
|
|
|
86
92
|
taskLog: (process.env.KRONK_TASK_LOG ?? String(file.taskLog ?? 'true')) !== 'false',
|
|
87
93
|
// Same reason as modelConfigPath: so tests never go near the real one.
|
|
88
94
|
taskLogDir: process.env.KRONK_TASK_LOG_DIR ?? file.taskLogDir ?? null,
|
|
95
|
+
// Where `kronk-cli rescue` keeps its job records — src/rescueStore.js.
|
|
96
|
+
// Deliberately not taskLogDir: that directory's lifetime is owned by
|
|
97
|
+
// src/tasklog.js's sweep() and cleanup(), and a rescue record has to
|
|
98
|
+
// survive both, so it lives somewhere neither of them has ever heard of.
|
|
99
|
+
rescueDir: process.env.KRONK_RESCUE_DIR ?? file.rescueDir ?? null,
|
|
89
100
|
// Kronk's per-model runtime settings. `setup` is the only thing that writes
|
|
90
101
|
// it; the override exists so tests never go near the real one.
|
|
91
102
|
modelConfigPath: process.env.KRONK_MODEL_CONFIG ?? file.modelConfigPath ?? MODEL_CONFIG,
|
|
103
|
+
// The startup banner's "a newer kronk-cli exists" line asks the package
|
|
104
|
+
// manager that installed this copy, never the network directly — see
|
|
105
|
+
// src/update.js. Off entirely for anyone who wants a program that runs no
|
|
106
|
+
// subprocess it was not asked to run.
|
|
107
|
+
updateCheck: (process.env.KRONK_UPDATE_CHECK ?? String(file.updateCheck ?? 'true')) !== 'false',
|
|
108
|
+
// Same reason as modelConfigPath and taskLogDir: so tests never touch the
|
|
109
|
+
// real one, and a cold machine's first run does not have to guess where to
|
|
110
|
+
// put it.
|
|
111
|
+
updateCacheFile: process.env.KRONK_UPDATE_CACHE ?? file.updateCacheFile
|
|
112
|
+
?? join(homedir(), '.kronk-cli-update-cache.json'),
|
|
92
113
|
// Paths a previous session granted read-only to the sandbox, via `always` at
|
|
93
114
|
// the credential prompt. Read-only is the whole point: KRONK_SANDBOX_ALLOW
|
|
94
115
|
// grants writes as well, and these are credential stores.
|
|
@@ -101,9 +122,6 @@ export const config = {
|
|
|
101
122
|
projectPrimer: null,
|
|
102
123
|
contextWindow: null, // filled in at boot from the chosen model's profile
|
|
103
124
|
nativeContext: null,
|
|
104
|
-
// The sub-agent model's own limits, when one is configured. A sub-agent on
|
|
105
|
-
// a 32k model must not compact against the main model's 131k window.
|
|
106
|
-
subagentLimits: null,
|
|
107
125
|
templatePreservesThinking: false, // filled in at boot from the model's template
|
|
108
126
|
samplingOverride: null, // filled in at boot: params where the profile overrides the model's own
|
|
109
127
|
rcPath: RC,
|