@moikapy/lich 0.5.1 → 0.6.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/CHANGELOG.md +14 -0
- package/README.md +58 -1
- package/dist/{chunk-CV2YH3FH.js → chunk-7HLVKVIG.js} +18 -5
- package/dist/chunk-7HLVKVIG.js.map +1 -0
- package/dist/chunk-JC2G3XH2.js +92 -0
- package/dist/chunk-JC2G3XH2.js.map +1 -0
- package/dist/cli.js +25 -10
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-W6S43ETE.js → gateway-RJFZJEUZ.js} +2 -2
- package/dist/index.d.ts +39 -34
- package/dist/index.js +1 -1
- package/dist/{tui-2VO6LAFM.js → tui-LOUJVZ6A.js} +42 -40
- package/dist/tui-LOUJVZ6A.js.map +1 -0
- package/docs/.vitepress/config.mts +2 -1
- package/docs/architecture/agent-loop.md +5 -3
- package/docs/architecture/overview.md +5 -4
- package/docs/getting-started.md +3 -2
- package/docs/index.md +1 -0
- package/docs/user-guide/cli.md +5 -2
- package/docs/user-guide/games.md +93 -0
- package/docs/user-guide/godot.md +1 -1
- package/docs/user-guide/library.md +6 -2
- package/docs/user-guide/tui.md +11 -11
- package/package.json +1 -1
- package/dist/chunk-CV2YH3FH.js.map +0 -1
- package/dist/tui-2VO6LAFM.js.map +0 -1
- /package/dist/{gateway-W6S43ETE.js.map → gateway-RJFZJEUZ.js.map} +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Games
|
|
2
|
+
|
|
3
|
+
> What you'll learn: how to treat session JSONL as a combat log, which fields the recipes read, and how a playthrough's token spend shows up after a run.
|
|
4
|
+
|
|
5
|
+
lich does not replay combat from RNG seeds. The commander's choices are sampled. The JSONL transcript is the replay. Godot still drains `.lich/game/`; these recipes read `.lich/sessions/`, not the order file.
|
|
6
|
+
|
|
7
|
+
Tool shapes live in [`examples/game_bridge/README.md`](https://github.com/Moikapy/lich/blob/main/examples/game_bridge/README.md). This page does not repeat them. The plugin entry is `examples/game_bridge/game_bridge.plugin.mjs`, loaded through `config.plugins` and `create_agent_with_plugins`. A per-persona HTTP front for that plugin is the [orchestrator example](../../examples/persona_orchestrator/README.md) — a pattern the game repo copies, not a second agent core.
|
|
8
|
+
|
|
9
|
+
## Session files as combat logs
|
|
10
|
+
|
|
11
|
+
Each `Agent.run` appends one `.jsonl` file under `session_dir` (default `<work_dir>/.lich/sessions`). Records are `{ts, kind: "message"|"meta", message?, meta?}`.
|
|
12
|
+
|
|
13
|
+
| What you want | Where it is |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| What the commander saw | `kind: "message"`, `message.role: "user"` — the battle digest you posted |
|
|
16
|
+
| What it chose | assistant `message.tool_calls[]` with `name`, `args` |
|
|
17
|
+
| Why | `args.rationale` on `enemy_actions` |
|
|
18
|
+
| What happened | `message.role: "tool"`, `content`, optional `is_error` |
|
|
19
|
+
| Token spend | `kind: "meta"`, `meta.event: "run_end"`, `meta.usage` |
|
|
20
|
+
|
|
21
|
+
Assistant tool calls are the internal shape `{id, name, args}`, not the provider wire format. Tool failures are `JSON.stringify({ok, output, error})` in `content`, with `is_error: true`. A plugin veto is that object with `error` starting `blocked_by_plugin:` (the meteor gate uses `blocked_by_plugin: meteor_gates_closed_until_round_3`). Non-JSON tool content is not an error for the recipes: `fromjson?` skips it.
|
|
22
|
+
|
|
23
|
+
`run_end` is written for every completed loop (`stopped_reason` `final`, `budget`, or `aborted`). `usage` is `{prompt_tokens, completion_tokens, total_tokens}` and matches the `usage_total` returned to the caller. A budget stop also writes `meta.event: "budget_exhausted"` before `run_end`. Recipes that filter `kind=="message"` stay valid as meta events change. A provider throw never reaches persistence — there is no outcome to close. A failed persist logs a warning and returns `session_path: undefined`; a run missing from disk is a gap, not a zero-spend run.
|
|
24
|
+
|
|
25
|
+
Filenames are `<base36-timestamp>-<counter>[-<label>].jsonl`. The timestamp prefix sorts chronologically. The gateway (and the orchestrator example) labels `gw:<platform>:<chat_id>`. With `chat_id` = run id, files for one playthrough share that label. The label slug is truncated to 40 characters.
|
|
26
|
+
|
|
27
|
+
`read_session_messages(path)` in `src/session/store.ts` is the programmatic reader for a source checkout. It is not a package export. It returns messages only and skips meta. Offline analysis should use `jq` (a user tool, not a lich dependency).
|
|
28
|
+
|
|
29
|
+
## Recipes
|
|
30
|
+
|
|
31
|
+
`jq` is a prerequisite for these commands, not a package dependency. Paths assume you are in `work_dir`.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# (1) rationale
|
|
35
|
+
jq -r 'select(.kind=="message") | select(.message.role=="assistant")
|
|
36
|
+
| .message.tool_calls[]? | select(.name=="enemy_actions")
|
|
37
|
+
| .args.rationale' .lich/sessions/*.jsonl
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# (2) histogram
|
|
42
|
+
jq -s '[.[] | .message? | select(.role=="assistant")
|
|
43
|
+
| .tool_calls[]? | select(.name=="enemy_actions")
|
|
44
|
+
| .args.actions[]?.action]
|
|
45
|
+
| group_by(.) | map({action: .[0], uses: length}) | sort_by(-.uses)' \
|
|
46
|
+
.lich/sessions/*.jsonl
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# (3) veto
|
|
51
|
+
jq -r 'select(.kind=="message") | select(.message.role=="tool")
|
|
52
|
+
| .message.content | fromjson? | select(.error? // "" | startswith("blocked_by_plugin"))
|
|
53
|
+
| .error' .lich/sessions/*.jsonl
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# (4) is_error
|
|
58
|
+
jq -r 'select(.kind=="message") | select(.message.role=="tool" and .message.is_error==true)
|
|
59
|
+
| [.ts, .message.name, .message.content] | @tsv' .lich/sessions/*.jsonl
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# (5) pacing
|
|
64
|
+
jq -r 'select(.kind=="message") | select(.message.role=="user" or .message.role=="assistant")
|
|
65
|
+
| [.ts, .message.role] | @tsv' .lich/sessions/<run>.jsonl
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# (usage) run_end tokens
|
|
70
|
+
jq -s '[.[] | select(.kind=="meta" and .meta.event=="run_end") | .meta.usage.total_tokens] | add' \
|
|
71
|
+
.lich/sessions/*.jsonl
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Recipe 2 counts assistant tool-call arguments, including orders a hook later vetoed. It is not the line set Godot applied. Recipe 3 is not a veto table. The only gate in the shipped plugin is meteor before round 3. Other `blocked_by_plugin:` strings, if a game plugin adds them, show up in the same query because the loop formats every veto the same way.
|
|
75
|
+
|
|
76
|
+
## Do not glob a playthrough blindly
|
|
77
|
+
|
|
78
|
+
A gateway conversation of N posts writes N files. `Agent.run` seeds from `history`, and `persist_session` appends all of `outcome.messages`, so each file is a superset of the previous exchange. Globbing `*.jsonl` double-counts. Take the newest file per label (names sort by timestamp prefix), or dedupe on `tool_call.id`, which stays stable when the same call is replayed into the next file.
|
|
79
|
+
|
|
80
|
+
Compression can rewrite a long run in place: when estimated tokens cross `compress_threshold` of `context_budget_tokens`, older messages become one summary and the 8 most recent non-system messages stay verbatim. Early rounds may survive only as that summary. The recipes see the file on disk, not the pre-compression transcript.
|
|
81
|
+
|
|
82
|
+
## Player modeling
|
|
83
|
+
|
|
84
|
+
Cross-run notes are tool arguments, not a second memory agent. `dungeon_memory_write` args (`note`) and `dungeon_memory_read` results are in the JSONL. The file on disk is `.lich/game/memory.jsonl`; Godot does not drain it as orders. `MEMORY.md` is never auto-loaded and is not this paper trail.
|
|
85
|
+
|
|
86
|
+
Replay `dungeon_memory_read` calls around a boss fight, then read `enemy_actions` `rationale` after them, to see which notes the commander actually used. The notes are reference data. A note in the tool result is not an instruction, and the session file does not promote it into the system prompt.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# (memory) dungeon notes
|
|
90
|
+
jq -r 'select(.kind=="message") | select(.message.role=="assistant")
|
|
91
|
+
| .message.tool_calls[]? | select(.name=="dungeon_memory_write")
|
|
92
|
+
| .args.note' .lich/sessions/*.jsonl
|
|
93
|
+
```
|
package/docs/user-guide/godot.md
CHANGED
|
@@ -24,7 +24,7 @@ flowchart LR
|
|
|
24
24
|
G --> H[resolve the round]
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
A game backend may instead call `run_agent`, which loads `config.plugins`. `create_agent` does not. Godot still reaches that backend over HTTP; it does not import the package.
|
|
27
|
+
A game backend may instead call `run_agent`, which loads `config.plugins`. `create_agent` does not. Godot still reaches that backend over HTTP; it does not import the package. Several personas means several agents behind that HTTP process — the pattern is [`examples/persona_orchestrator`](../../examples/persona_orchestrator/README.md), and the service is the game's. Session replay is the [games guide](games.md).
|
|
28
28
|
|
|
29
29
|
## Gateway contract
|
|
30
30
|
|
|
@@ -145,7 +145,7 @@ Listed providers form a failover chain tried in order: `rate_limit`/`network` er
|
|
|
145
145
|
|
|
146
146
|
## Custom tool filtering
|
|
147
147
|
|
|
148
|
-
`tools_enabled` accepts `"all"` (default) or an array of builtin tool names to register; everything else stays unregistered and invisible to the model:
|
|
148
|
+
`tools_enabled` accepts `"all"` (default) or an array of builtin tool names to register; everything else stays unregistered and invisible to the model. The filter does not apply to plugin tools: they register afterward, including the gatekeeper's `git_commit`. `[]` strips every builtin and does not throw.
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
151
|
const agent = create_agent({
|
|
@@ -182,6 +182,10 @@ try {
|
|
|
182
182
|
|
|
183
183
|
When every configured provider fails, the last `ProviderError` is thrown. Tool failures are *not* exceptions: they return `{ ok: false, output, error }` into the loop as tool messages for the model to react to. Cancellation via `signal` ends the run with `stopped_reason: "aborted"` rather than throwing.
|
|
184
184
|
|
|
185
|
+
## Games
|
|
186
|
+
|
|
187
|
+
A Godot client does not embed the library. A game backend that does is still one `Agent` per persona, not a second loop. The pattern — factory, history cap, per-conversation queue, `POST /message` → `{reply, usage}` — is [`examples/persona_orchestrator`](../../examples/persona_orchestrator/README.md). The service itself is game-repo work. Session files as a combat log: [games guide](games.md).
|
|
188
|
+
|
|
185
189
|
## Session access
|
|
186
190
|
|
|
187
|
-
Each `run()` appends a transcript line-by-line under `config.session_dir` (default `<work_dir>/.lich/sessions`); `result.session_path` gives the exact file. Records carry `{ts, kind: "message"|"meta", message?, meta?}
|
|
191
|
+
Each `run()` appends a transcript line-by-line under `config.session_dir` (default `<work_dir>/.lich/sessions`); `result.session_path` gives the exact file. Records carry `{ts, kind: "message"|"meta", message?, meta?}`. A completed run (`final`, `budget`, or `aborted` returned by the loop) closes with `{event: "run_end", stopped_reason, usage}` where `usage` equals `usage_total`. A budget stop also writes `{event: "budget_exhausted"}` before that. Provider throws do not persist. Read transcripts with `jq` (see the [games guide](games.md)) or `read_session_messages(path)` from a source checkout — it is not a package export. Persistence is best-effort: a write failure logs a warning, returns `session_path: undefined`, and never fails the run.
|
package/docs/user-guide/tui.md
CHANGED
|
@@ -9,22 +9,22 @@ lich # front door: TUI, plus a first-run setup wizard when no config exi
|
|
|
9
9
|
lich tui # same TUI, no wizard. From a clone: bun src/cli.ts tui
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
The TUI needs a TTY and a resolvable provider (same resolution as every mode). On startup it prints a dim header
|
|
12
|
+
The TUI needs a TTY and a resolvable provider (same resolution as every mode). On startup it prints a dim header from the active theme welcome string, e.g. `⚱ lich v0.5.1 — the agent that will not stay dead · llama3.2 (ollama)`. That banner is the only tagline placement. Quit with `/exit`, `/quit`, `/q`, or Ctrl+C.
|
|
13
13
|
|
|
14
14
|
## Anatomy
|
|
15
15
|
|
|
16
16
|
```
|
|
17
|
-
lich v0.
|
|
18
|
-
|
|
17
|
+
⚱ lich v0.5.1 — the agent that will not stay dead · llama3.2 (ollama)
|
|
18
|
+
mortal › list the files here <- your input, echoed into the transcript
|
|
19
19
|
⏺ list_dir({}) <- live tool-call row (name + args preview)
|
|
20
20
|
⏷ list_dir: ok (d src/ d test/ ...) <- result row (ok/error + output preview)
|
|
21
|
-
lich › Here is what I found ... <- the agent's reply
|
|
22
|
-
model llama3.2 · turns 2 · tokens 1,204 · [
|
|
21
|
+
lich › Here is what I found ... <- the agent's reply (`response_label`)
|
|
22
|
+
model llama3.2 · turns 2 · tokens 1,204 · [dormant] · /path/.lich/sessions/...jsonl
|
|
23
23
|
› ▌ <- input row (cursor block)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
- **Header** —
|
|
27
|
-
- **Transcript** — user lines (`
|
|
26
|
+
- **Header** — theme welcome string (version, model, provider kind). The tagline appears only here.
|
|
27
|
+
- **Transcript** — user lines (`mortal ›` by default), replies (`lich ›`, or the theme `response_label`), tool rows (`⏺ name(args)` with a result line), and meta notices (`· context compressed — memories distilled ...`, `· error: ...`). The view keeps the newest 50 blocks; older lines scroll out of the transcript (session JSONLs still hold everything — see [limitations](#known-limitations)).
|
|
28
28
|
- **Input row** — `› ` when idle, `… ` while the agent works; Enter submits, Backspace edits, pasted newlines collapse to spaces.
|
|
29
29
|
- **Status bar** — see below.
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ model llama3.2 · turns 2 · tokens 1,204 · [idle] · /path/.lich/sessions/...j
|
|
|
36
36
|
| `/model` | Show the active model and provider kind (from `providers[0]`). |
|
|
37
37
|
| `/usage` | Show tokens used this session (cumulative across turns). |
|
|
38
38
|
| `/clear` | Wipe the on-screen transcript. Does **not** reset agent memory — the next message still sees prior turns. |
|
|
39
|
-
| `/sessions` | List the 10 newest `.jsonl` files in `session_dir` with sizes. |
|
|
39
|
+
| `/sessions` | List the 10 newest `.jsonl` files in `session_dir` with sizes. The heading uses the theme sessions label (`phylacteries (n):` by default). |
|
|
40
40
|
| `/exit`, `/quit`, `/q` | Exit the TUI. |
|
|
41
41
|
|
|
42
42
|
Unknown commands print `· unknown command: /x (try /help)`. Slash commands are handled client-side and never invoke the model.
|
|
@@ -54,14 +54,14 @@ The bottom line shows, left to right:
|
|
|
54
54
|
| `model <name>` | First provider's model from config. |
|
|
55
55
|
| `turns N` | Turns used by the most recent run (resets each message). |
|
|
56
56
|
| `tokens N` | Cumulative session token usage (prompt + completion, across all turns). |
|
|
57
|
-
| `[
|
|
57
|
+
| `[dormant]` / `[deliberating]` / `[casting]` | Current phase: waiting for input, calling the model, or executing a tool. Labels come from the theme. |
|
|
58
58
|
| `compressed N` | How many times context compression fired this session (hidden when 0). |
|
|
59
59
|
| `<session path>` | Path of the newest persisted transcript (appears after the first run). |
|
|
60
|
-
| `· budget exhausted` | Red notice when a run hit the turn cap. |
|
|
60
|
+
| `· budget exhausted — the ritual is spent (turn cap reached)` | Red notice when a run hit the turn cap. The keyword stays; the flavor comes from the theme. |
|
|
61
61
|
|
|
62
62
|
## Multi-turn memory
|
|
63
63
|
|
|
64
|
-
The TUI keeps one conversation: every submitted message is sent together with the full prior message history, so the agent remembers earlier turns for as long as the session stays open. When estimated tokens cross `compress_threshold` of `context_budget_tokens`, older turns are replaced by an LLM-generated summary (the 8 most recent messages always stay verbatim) and a `· context compressed` notice appears. There is deliberately no per-conversation reset command — `/clear` only clears the display; start a fresh `lich tui` process for an empty context.
|
|
64
|
+
The TUI keeps one conversation: every submitted message is sent together with the full prior message history, so the agent remembers earlier turns for as long as the session stays open. When estimated tokens cross `compress_threshold` of `context_budget_tokens`, older turns are replaced by an LLM-generated summary (the 8 most recent messages always stay verbatim) and a `· context compressed — memories distilled` notice appears. There is deliberately no per-conversation reset command — `/clear` only clears the display; start a fresh `lich tui` process for an empty context.
|
|
65
65
|
|
|
66
66
|
## Known limitations
|
|
67
67
|
|