@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.
@@ -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
+ ```
@@ -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?}`; read them with `jq` (or the `read_session_messages(path)` helper if you are working from a source checkout). Persistence is best-effort: a write failure logs a warning, returns `session_path: undefined`, and never fails the run.
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.
@@ -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 with `agent_name` (default `lich`), the version, and the first provider's model, e.g. `lich v0.3.0 — llama3.2 (ollama)`. Quit with `/exit`, `/quit`, `/q`, or Ctrl+C.
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.1the 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.3.0llama3.2 (ollama) <- header: agent_name (default lich), version, model, kind
18
- you › list the files here <- your input, echoed into the transcript
17
+ lich v0.5.1the 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 · [idle] · /path/.lich/sessions/...jsonl
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** — static version/model/provider line.
27
- - **Transcript** — user lines (`you ›`), replies (`lich ›`), tool rows (`⏺ name(args)` with a result line), and meta notices (`· context compressed ...`, `· error: ...`). The view keeps the newest 50 blocks; older lines scroll out of the transcript (session JSONLs still hold everything — see [limitations](#known-limitations)).
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
- | `[idle]` / `[thinking]` / `[tool]` | Current phase: waiting for input, calling the model, or executing a tool. |
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moikapy/lich",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Lich — a TypeScript AI agent harness (library + CLI) inspired by Hermes",
5
5
  "type": "module",
6
6
  "license": "MIT",