@moikapy/lich 0.3.1 → 0.4.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 +18 -0
- package/README.md +12 -2
- package/dist/{chunk-P52U5M3L.js → chunk-MLFJW4JU.js} +399 -70
- package/dist/chunk-MLFJW4JU.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/{gateway-CWPVIU3W.js → gateway-XTYDYT67.js} +2 -2
- package/dist/index.d.ts +25 -11
- package/dist/index.js +1 -1
- package/dist/{tui-K3EPRXTV.js → tui-VYBJSGRV.js} +2 -2
- package/docs/architecture/overview.md +8 -6
- package/docs/architecture/plugins.md +57 -4
- package/docs/architecture/tools.md +16 -4
- package/docs/index.md +3 -3
- package/docs/user-guide/cli.md +13 -0
- package/docs/user-guide/library.md +3 -1
- package/docs/user-guide/plugins.md +56 -3
- package/package.json +1 -1
- package/dist/chunk-P52U5M3L.js.map +0 -1
- /package/dist/{gateway-CWPVIU3W.js.map → gateway-XTYDYT67.js.map} +0 -0
- /package/dist/{tui-K3EPRXTV.js.map → tui-VYBJSGRV.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
create_agent,
|
|
7
7
|
parse_agent_config,
|
|
8
8
|
safe_json_parse
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-MLFJW4JU.js";
|
|
10
10
|
|
|
11
11
|
// src/cli.ts
|
|
12
12
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -346,11 +346,11 @@ async function run_chat(config) {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
async function run_tui_entry(config) {
|
|
349
|
-
const { run_tui } = await import("./tui-
|
|
349
|
+
const { run_tui } = await import("./tui-VYBJSGRV.js");
|
|
350
350
|
return run_tui(config);
|
|
351
351
|
}
|
|
352
352
|
async function run_gateway_entry(config, platforms) {
|
|
353
|
-
const { run_gateway } = await import("./gateway-
|
|
353
|
+
const { run_gateway } = await import("./gateway-XTYDYT67.js");
|
|
354
354
|
return run_gateway(config, platforms.length === 0 ? ["webhook"] : platforms);
|
|
355
355
|
}
|
|
356
356
|
function is_main_module() {
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
create_agent,
|
|
3
3
|
logger,
|
|
4
4
|
sleep
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-MLFJW4JU.js";
|
|
6
6
|
|
|
7
7
|
// src/gateway/types.ts
|
|
8
8
|
var ERROR_SNIPPET_CHARS = 300;
|
|
@@ -749,4 +749,4 @@ function install_signal_handlers(bus, adapters) {
|
|
|
749
749
|
export {
|
|
750
750
|
run_gateway
|
|
751
751
|
};
|
|
752
|
-
//# sourceMappingURL=gateway-
|
|
752
|
+
//# sourceMappingURL=gateway-XTYDYT67.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ interface Tool {
|
|
|
37
37
|
name: string;
|
|
38
38
|
description: string;
|
|
39
39
|
parameters: JsonSchemaObject;
|
|
40
|
+
/** Per-tool executor timeout override in ms; unset tools get the 30s default. */
|
|
41
|
+
timeout_ms?: number;
|
|
40
42
|
execute(args: Record<string, unknown>, context: ToolContext): Promise<ToolResult>;
|
|
41
43
|
}
|
|
42
44
|
interface Toolset {
|
|
@@ -55,6 +57,12 @@ interface Toolset {
|
|
|
55
57
|
/** Runtime info handed to every hook call. */
|
|
56
58
|
interface HookContext {
|
|
57
59
|
work_dir: string;
|
|
60
|
+
/**
|
|
61
|
+
* This plugin's own per-run state sub-map. Every hook invocation receives
|
|
62
|
+
* a ctx exposing only the invoking plugin's bag; the bag is swapped fresh
|
|
63
|
+
* at each run start. Absent only on hand-built contexts outside the runner.
|
|
64
|
+
*/
|
|
65
|
+
state?: Map<string, unknown>;
|
|
58
66
|
}
|
|
59
67
|
/** Argument passed to before_tool_call hooks. */
|
|
60
68
|
interface BeforeToolCallInfo {
|
|
@@ -69,6 +77,9 @@ interface BeforeToolCallResult {
|
|
|
69
77
|
/** Argument passed to after_tool_call hooks. */
|
|
70
78
|
interface AfterToolCallInfo extends BeforeToolCallInfo {
|
|
71
79
|
result_summary: string;
|
|
80
|
+
/** Structured executor outcome; gate on this, never parse result_summary. */
|
|
81
|
+
ok: boolean;
|
|
82
|
+
error?: string;
|
|
72
83
|
}
|
|
73
84
|
interface RunEndInfo {
|
|
74
85
|
stopped_reason: string;
|
|
@@ -445,6 +456,7 @@ declare class Agent {
|
|
|
445
456
|
private readonly hook_runner;
|
|
446
457
|
constructor(config: AgentConfig, plugins?: readonly LoadedPlugin[]);
|
|
447
458
|
run(options: AgentRunOptions): Promise<AgentRunResult>;
|
|
459
|
+
/** Per-run deps: the built-once ToolContext threads through every tool execution. */
|
|
448
460
|
private loop_deps;
|
|
449
461
|
/** Best-effort on_run_start fan-out; hook errors are logged, never fatal. */
|
|
450
462
|
private call_plugin_run_start;
|
|
@@ -503,7 +515,11 @@ declare function register_builtin_tools(registry: ToolRegistry, context?: ToolCo
|
|
|
503
515
|
*
|
|
504
516
|
* before_tool_call hooks run in registration order and may veto a call (first
|
|
505
517
|
* blocker wins; the wrapped executor is never called). Hook errors are warned
|
|
506
|
-
* and skipped, never fatal. after_tool_call hooks observe the result summary
|
|
518
|
+
* and skipped, never fatal. after_tool_call hooks observe the result summary
|
|
519
|
+
* plus the executor's structured ok/error fields. Every hook invocation
|
|
520
|
+
* receives a ctx exposing only its own plugin's state sub-map: the bag is a
|
|
521
|
+
* module-internal WeakMap keyed on the plugin object, swapped fresh at each
|
|
522
|
+
* call_run_start and shared by the lifecycle and per-tool ctx build sites.
|
|
507
523
|
*/
|
|
508
524
|
|
|
509
525
|
/** Structural ToolRunner shape accepted from the wrapped executor. */
|
|
@@ -511,27 +527,25 @@ interface WrappedToolRunner {
|
|
|
511
527
|
execute(name: string, args: Record<string, unknown>, context?: ToolContext): Promise<ToolResult>;
|
|
512
528
|
}
|
|
513
529
|
/**
|
|
514
|
-
*
|
|
515
|
-
*
|
|
530
|
+
* Hooks stay attached to their plugin (no flattening) so each invocation can
|
|
531
|
+
* be handed a ctx exposing only that plugin's sub-map; hooks always run in
|
|
532
|
+
* plugin registration order.
|
|
516
533
|
*/
|
|
517
534
|
declare class HookedToolRunner {
|
|
518
535
|
private readonly wrapped;
|
|
519
|
-
private readonly
|
|
520
|
-
|
|
521
|
-
private readonly run_start_hooks;
|
|
522
|
-
private readonly run_end_hooks;
|
|
523
|
-
constructor(wrapped: WrappedToolRunner, hooks: readonly PluginHooks[]);
|
|
536
|
+
private readonly hooked_plugins;
|
|
537
|
+
constructor(wrapped: WrappedToolRunner, plugins: readonly Plugin[]);
|
|
524
538
|
/** Run before hooks in order; the first {block: true} verdict wins. */
|
|
525
539
|
private run_before_hooks;
|
|
526
540
|
/** Fire-and-forget in spirit but awaited here so runs settle cleanly. */
|
|
527
541
|
private run_after_hooks;
|
|
528
542
|
execute(name: string, args: Record<string, unknown>, context?: ToolContext): Promise<ToolResult>;
|
|
529
|
-
/** Best-effort on_run_start fan-out used by Agent.run; never throws. */
|
|
543
|
+
/** Best-effort on_run_start fan-out used by Agent.run; never throws. Swaps in a fresh state sub-map per hooked plugin first. */
|
|
530
544
|
call_run_start(info: {
|
|
531
545
|
input_chars: number;
|
|
532
|
-
},
|
|
546
|
+
}, base: HookContext): Promise<void>;
|
|
533
547
|
/** Best-effort on_run_end fan-out used by Agent.run; never throws. */
|
|
534
|
-
call_run_end(info: RunEndInfo,
|
|
548
|
+
call_run_end(info: RunEndInfo, base: HookContext): Promise<void>;
|
|
535
549
|
}
|
|
536
550
|
|
|
537
551
|
/**
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
create_agent,
|
|
6
6
|
truncate_text
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-MLFJW4JU.js";
|
|
8
8
|
|
|
9
9
|
// src/tui.tsx
|
|
10
10
|
import { render } from "ink";
|
|
@@ -430,4 +430,4 @@ async function run_tui(config) {
|
|
|
430
430
|
export {
|
|
431
431
|
run_tui
|
|
432
432
|
};
|
|
433
|
-
//# sourceMappingURL=tui-
|
|
433
|
+
//# sourceMappingURL=tui-VYBJSGRV.js.map
|
|
@@ -28,7 +28,7 @@ flowchart TB
|
|
|
28
28
|
CLIENTS["openai_compat / anthropic / ollama<br/>HTTP clients"]
|
|
29
29
|
EXEC["ToolExecutor<br/>(src/tools/executor.ts)"]
|
|
30
30
|
REG["ToolRegistry<br/>(src/tools/registry.ts)"]
|
|
31
|
-
|
|
31
|
+
BUILTIN["builtin tools<br/>(src/tools/builtin/*)"]
|
|
32
32
|
COMP["ContextCompressor<br/>(src/context/compressor.ts)"]
|
|
33
33
|
SESSION["SessionStore<br/>(src/session/store.ts)"]
|
|
34
34
|
|
|
@@ -59,18 +59,20 @@ structural interfaces ([`src/agent/loop.ts`](../../src/agent/loop.ts)):
|
|
|
59
59
|
|
|
60
60
|
- `ChatFn` - `(messages, tools, options?) => Promise<ChatResult>` (declared in
|
|
61
61
|
`src/context/compressor.ts`, since compression needs the same shape).
|
|
62
|
-
- `ToolRunner` - `{ execute(name, args) => Promise<ToolResult> }`.
|
|
62
|
+
- `ToolRunner` - `{ execute(name, args, context?) => Promise<ToolResult> }`.
|
|
63
63
|
|
|
64
64
|
`run_conversation` receives a `LoopDeps` object holding a `ChatFn`, a
|
|
65
|
-
`ToolRunner`, a `definitions()` callback for tool schemas,
|
|
66
|
-
emitter
|
|
67
|
-
`
|
|
65
|
+
`ToolRunner`, a `definitions()` callback for tool schemas, an optional
|
|
66
|
+
emitter, and an optional per-run `tool_context` threaded to every tool
|
|
67
|
+
execution. The `Agent` class (`src/agent/agent.ts`) is the composition root:
|
|
68
|
+
its `loop_deps()` method wires the real implementations -
|
|
68
69
|
|
|
69
70
|
```ts
|
|
70
71
|
chat: (messages, tools, chat_options) => this.router.chat_with_failover(messages, tools, chat_options),
|
|
71
72
|
tools: this.executor,
|
|
72
73
|
definitions: () => this.registry.definitions(),
|
|
73
74
|
emitter: this.events,
|
|
75
|
+
tool_context,
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
(src/agent/agent.ts, `loop_deps()`)
|
|
@@ -166,7 +168,7 @@ Walkthrough of a single `Agent.run({ input })` call
|
|
|
166
168
|
| `src/tools/guard.ts` | Path confinement, timeouts, clamping, arg coercion. |
|
|
167
169
|
| `src/tools/registry.ts` | Name-keyed tool registry; duplicate rejection. |
|
|
168
170
|
| `src/tools/executor.ts` | Never-throw execution with timeout and abort. |
|
|
169
|
-
| `src/tools/builtin/*` |
|
|
171
|
+
| `src/tools/builtin/*` | Builtin tools, including `run_tests` (see [tools](./tools.md)). |
|
|
170
172
|
| `src/gateway/bus.ts` | Conversation-keyed runner over one shared `Agent`. |
|
|
171
173
|
| `src/gateway/runner.ts` | Adapter construction, signal handling, process lifetime. |
|
|
172
174
|
| `src/gateway/{telegram,discord,twitch,webhook}.ts` | Platform adapters. |
|
|
@@ -23,7 +23,7 @@ flowchart LR
|
|
|
23
23
|
K -- yes --> M["LoadedPlugin collected"]
|
|
24
24
|
M --> N["Agent constructor"]
|
|
25
25
|
N --> O["registry merge:\nplugin tools appended\n(dup tool name → warn+skip)"]
|
|
26
|
-
N --> P["
|
|
26
|
+
N --> P["hooked plugins kept\nwhole (per-plugin state channel)"]
|
|
27
27
|
P --> Q["HookedToolRunner wraps\nToolExecutor when hooks exist"]
|
|
28
28
|
E --> R["warn + continue"]
|
|
29
29
|
H --> R
|
|
@@ -53,14 +53,66 @@ sequenceDiagram
|
|
|
53
53
|
else no blocker
|
|
54
54
|
H->>E: execute(name, args, context?)
|
|
55
55
|
E-->>H: ToolResult
|
|
56
|
-
H->>A: await hook({...info, result_summary}, ctx)
|
|
57
|
-
note over A: summary = 300 chars of output/error
|
|
56
|
+
H->>A: await hook({...info, result_summary, ok, error?}, ctx)
|
|
57
|
+
note over A: summary = 300 chars of output/error; ok/error are structured
|
|
58
58
|
H-->>L: ToolResult unchanged
|
|
59
59
|
end
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Lifecycle fan-outs live on the same wrapper: `Agent.run` calls `call_run_start({input_chars})` before `run_conversation` and `call_run_end({stopped_reason, turns_used})` after it (including the abort/throw path, via `finally`). Both are best-effort: hook throws are logged at `warn` and the run proceeds.
|
|
63
63
|
|
|
64
|
+
## Builtin gatekeeper
|
|
65
|
+
|
|
66
|
+
`Agent` constructs `gatekeeper_plugin` in code, before config plugins, and
|
|
67
|
+
pushes it as a synthetic `LoadedPlugin` through tool registration and the
|
|
68
|
+
hook runner. The config loader never sees it. Construction failure means
|
|
69
|
+
`git_commit` is not in the registry at all. "No gatekeeper → no `git_commit`"
|
|
70
|
+
is that trusted path: with the gatekeeper off, a config plugin may still name
|
|
71
|
+
a tool `git_commit` (the documented plugin-trust floor). While the gatekeeper
|
|
72
|
+
is registered, first-wins keeps its tool.
|
|
73
|
+
|
|
74
|
+
`LICH_ALLOW_SELF_COMMIT` is read from process env at construction. The spec
|
|
75
|
+
value is `1`. Unset or any other value is fail-closed.
|
|
76
|
+
|
|
77
|
+
Per-run state starts `tests_ok=false`, `dirty=true`, `commits=0` (swapped at
|
|
78
|
+
`call_run_start`). `after_tool_call` updates only on structured `ok`:
|
|
79
|
+
|
|
80
|
+
- `write_file` / `edit_file` success → `dirty=true`
|
|
81
|
+
- `run_tests` success → `tests_ok=true`, `dirty=false`
|
|
82
|
+
- `git_commit` success → `commits++`
|
|
83
|
+
|
|
84
|
+
`before_tool_call` vetoes `git_commit` unless
|
|
85
|
+
`allow_self_commit && tests_ok && !dirty && commits < 1`. The reason names
|
|
86
|
+
the failed condition: `self_commit_disabled`, `tests_not_ok`,
|
|
87
|
+
`worktree_dirty`, `commit_budget_exhausted`. The model sees
|
|
88
|
+
`blocked_by_plugin: <reason>`.
|
|
89
|
+
|
|
90
|
+
`terminal` is vetoed on a hardcoded denylist match; the reason is
|
|
91
|
+
`git_denylist: <pattern>`. Patterns: flag-tolerant `commit` and `push`
|
|
92
|
+
(`commit`, `-commit`, `--commit`, `push`, `-push`, `--push`) plus
|
|
93
|
+
any-occurrence `commit-tree` and `update-ref`. No `remote` pattern. One
|
|
94
|
+
commit per run, hardcoded.
|
|
95
|
+
|
|
96
|
+
`git_commit` args are `{message, paths}` with 1–50 paths relative to
|
|
97
|
+
`work_dir`. It rejects `""`, `.`, anything resolving to `work_dir`, and
|
|
98
|
+
secret-ish basenames (`.env`, `.env.local`, `*.pem`, `*.p12`, `id_rsa*`).
|
|
99
|
+
Unreachable `HEAD` is fail-closed. Recipe: scoped `git add -- <paths>`, then
|
|
100
|
+
`git commit --only` with explicit identity `-c` flags. `timeout_ms` is 60000.
|
|
101
|
+
It never pushes.
|
|
102
|
+
|
|
103
|
+
**Attestation:** clean state attests no `write_file`/`edit_file` since the
|
|
104
|
+
last green `run_tests`; it does NOT attest absence of terminal-mediated
|
|
105
|
+
writes — that sits with the documented terminal floor.
|
|
106
|
+
|
|
107
|
+
Floors, stated not closed:
|
|
108
|
+
|
|
109
|
+
- Terminal floor: raw `terminal` can run arbitrary git; the denylist is
|
|
110
|
+
best-effort. The boundary is human review of the local repo; push is
|
|
111
|
+
human-only.
|
|
112
|
+
- Plugin-trust floor: `.lich/config.json` `plugins` is persistent arbitrary
|
|
113
|
+
code at next process start. Review config diffs.
|
|
114
|
+
- One lich process per repo (the `run_tests` mutex is process-local).
|
|
115
|
+
|
|
64
116
|
## Design decisions
|
|
65
117
|
|
|
66
118
|
- **Explicit entries, no directory scan.** v1 loads only the files you list in `config.plugins`. Directory scanning would make runs depend on whatever happens to sit in a folder — non-reproducible, and a footgun for tools that write into `.lich/`. Explicit entries make the agent's tool surface a function of the config alone.
|
|
@@ -74,7 +126,8 @@ Lifecycle fan-outs live on the same wrapper: `Agent.run` calls `call_run_start({
|
|
|
74
126
|
| --- | --- | --- |
|
|
75
127
|
| `Plugin` | type | `{name, version?, tools?, hooks?}` — what a plugin module exports. |
|
|
76
128
|
| `PluginHooks` | type | The four optional lifecycle hooks with their signatures. |
|
|
77
|
-
| `HookContext` | type | `{work_dir}` passed to every hook. |
|
|
129
|
+
| `HookContext` | type | `{work_dir, state?}` passed to every hook; `state` is the invoking plugin's own per-run bag. |
|
|
130
|
+
| `AfterToolCallInfo` | type | Tool name/args plus the 300-char `result_summary` and structured `ok`/`error` fields. |
|
|
78
131
|
| `LoadedPlugin` | type | `{plugin, entry}` — a loaded plugin and its source path. |
|
|
79
132
|
| `load_plugins` | function | `(entries, base_dir) => {plugins, errors}` — dynamic import + shape validation. |
|
|
80
133
|
| `plugin_errors_summary` | function | Joins error entries into one warn-able string. |
|
|
@@ -118,9 +118,10 @@ args, context?)`:
|
|
|
118
118
|
defaults (falling back to `process.cwd()`) plus the configured `env`.
|
|
119
119
|
3. **Cancelled fast path**: if the context signal is already aborted, return
|
|
120
120
|
`{ ok: false, output: "", error: "cancelled" }` without running the tool.
|
|
121
|
-
4. **Signal merge +
|
|
121
|
+
4. **Signal merge + timeout**: a fresh `AbortController` is aborted by
|
|
122
122
|
the external signal (an `abort` listener), by the `with_timeout` deadline
|
|
123
|
-
(
|
|
123
|
+
(the tool's own `timeout_ms` when declared, else
|
|
124
|
+
`DEFAULT_TOOL_TIMEOUT_MS = 30000`), and the merged signal is what the tool
|
|
124
125
|
receives. The external listener is removed in a `finally`.
|
|
125
126
|
5. **Output clamping**: successful results pass through `clamp_result`
|
|
126
127
|
(`clamp_output`, 20 000 chars).
|
|
@@ -137,8 +138,9 @@ form back with `parse_tool_message_content` (src/tui/state.ts).
|
|
|
137
138
|
|
|
138
139
|
## Builtin catalog
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
([`src/tools/builtin/index.ts`](../../src/tools/builtin/index.ts))
|
|
141
|
+
Registered by `register_builtin_tools`
|
|
142
|
+
([`src/tools/builtin/index.ts`](../../src/tools/builtin/index.ts)).
|
|
143
|
+
Docs tools join the list only when a docs root resolves.
|
|
142
144
|
|
|
143
145
|
| Tool | Key args | Implementation insight |
|
|
144
146
|
| --- | --- | --- |
|
|
@@ -154,6 +156,7 @@ Twelve tools, registered by `register_builtin_tools`
|
|
|
154
156
|
| `process_list` | `filter?`, `max_results?` | Reads `/proc` synchronously: numeric dirs are pids, `cmdline` is NUL-separated; missing entries (process died mid-scan) read as empty. |
|
|
155
157
|
| `disk_usage` | `path?`, `max_entries?` | One `du -sb` subprocess per depth-1 entry with a 10 s timeout; sorted desc with a `TOTAL` row; `du` missing yields `du_unavailable`. |
|
|
156
158
|
| `env_get` | `keys?`, `prefix?`, `reveal?` | Values are hidden unless `reveal`; names matching `/(secret\|token\|password\|key\|credential\|auth)/i` are **always** masked as `<redacted: N chars>`. |
|
|
159
|
+
| `run_tests` | `filter?` | Runs `LICH_TEST_COMMAND` (default `node node_modules/vitest/vitest.mjs run`) in `work_dir` via `bash -lc`. `timeout_ms` is 600000. A module mutex makes a concurrent call return `{ok:false, error:"run_tests_busy"}`. `ok` is the structured pass/fail the gatekeeper reads; output is clamped to 2000 chars. One lich process per repo — a second process is fail-closed busy or failed. |
|
|
157
160
|
|
|
158
161
|
The three HTTP tools (`fetch_url`, `web_search`, `http_request`) share
|
|
159
162
|
helpers from `fetch_url.ts`: `valid_http_url` (URL parse + protocol
|
|
@@ -161,6 +164,15 @@ allowlist), `compose_abort_signal` (per-call `AbortSignal.timeout` merged
|
|
|
161
164
|
with the executor's cancellation via `AbortSignal.any`), and `clamp_int_arg`
|
|
162
165
|
(floored, bounded to `[1, max]`).
|
|
163
166
|
|
|
167
|
+
## Docs search and skills
|
|
168
|
+
|
|
169
|
+
`docs_search` scores sections under the resolved docs root (memoized) and, when
|
|
170
|
+
`<work_dir>/.lich/skills/` exists, also walks that directory. The skills
|
|
171
|
+
candidate is existence-only: it does not need `index.md`. The walk is fresh
|
|
172
|
+
on every call — user-writable skill files are not memoized into the package
|
|
173
|
+
docs cache. Skills are reference data, written with `write_file`, not
|
|
174
|
+
instructions. See the [plugins guide](../user-guide/plugins.md#skills-and-memory).
|
|
175
|
+
|
|
164
176
|
## Registry
|
|
165
177
|
|
|
166
178
|
[`ToolRegistry`](../../src/tools/registry.ts) is a name-keyed `Map`:
|
package/docs/index.md
CHANGED
|
@@ -8,14 +8,14 @@ outline: [2, 3]
|
|
|
8
8
|
|
|
9
9
|
Lich is a TypeScript AI agent harness: a library and a CLI that run a chat model inside a Think-Act-Observe loop. A chat wrapper forwards one prompt and prints one completion. A harness keeps going: the model plans (think), calls tools such as `read_file` or `terminal` (act), reads the tool results (observe), and repeats until it can produce a final answer. Lich wraps that loop with the machinery real deployments need: provider failover with bounded retries, path confinement and output clamps on every tool, context compression when the transcript grows past a token budget, and append-only JSONL session transcripts.
|
|
10
10
|
|
|
11
|
-
One package, four ways to drive the same agent: a one-shot CLI, an interactive chat REPL, an ink-based terminal UI, and a long-running messaging gateway that bridges Telegram, Discord, Twitch, and a zero-config HTTP webhook. All four share the same
|
|
11
|
+
One package, four ways to drive the same agent: a one-shot CLI, an interactive chat REPL, an ink-based terminal UI, and a long-running messaging gateway that bridges Telegram, Discord, Twitch, and a zero-config HTTP webhook. All four share the same builtin tools, the same provider configuration, and the same session store.
|
|
12
12
|
|
|
13
13
|
## Feature overview
|
|
14
14
|
|
|
15
15
|
| Capability | What it gives you |
|
|
16
16
|
| --- | --- |
|
|
17
17
|
| Providers | `openai_compat`, `anthropic`, and `ollama` with automatic failover between configured providers; 429/5xx and network errors retry with backoff before failing over. |
|
|
18
|
-
| Tools |
|
|
18
|
+
| Tools | Builtins (file read/write/edit, directory listing, shell, grep, HTTP fetch/request, web search, process list, disk usage, env inspection, `run_tests`), all confined to the working directory. `git_commit` is the gatekeeper's tool, not a config plugin. |
|
|
19
19
|
| Context compression | Transcript summarized in place when estimated tokens cross `compress_threshold` of `context_budget_tokens`; the 8 most recent turns always stay verbatim. |
|
|
20
20
|
| Sessions | Every run persists a `.jsonl` transcript under `.lich/sessions/`, labeled by origin (`tui`, `gw:<platform>:<chat>`). |
|
|
21
21
|
| CLI | One-shot tasks, chat REPL, TUI, gateway, and a `config` template command, all with flag/env/config-file configuration. |
|
|
@@ -33,7 +33,7 @@ One package, four ways to drive the same agent: a one-shot CLI, an interactive c
|
|
|
33
33
|
| [TUI guide](user-guide/tui.md) | Run the terminal UI and use slash commands and the status bar. |
|
|
34
34
|
| [Gateway guide](user-guide/gateway.md) | Wire Telegram, Discord, Twitch, and the HTTP webhook to one agent. |
|
|
35
35
|
| [Library guide](user-guide/library.md) | Embed the agent in TypeScript with events and multi-turn history. |
|
|
36
|
-
| [Plugins guide](user-guide/plugins.md) | Add your own tools and lifecycle hooks
|
|
36
|
+
| [Plugins guide](user-guide/plugins.md) | Add your own tools and lifecycle hooks, and run the self-improvement loop. |
|
|
37
37
|
| [Architecture overview](architecture/overview.md) | Understand how the harness works inside. |
|
|
38
38
|
|
|
39
39
|
## How it works
|
package/docs/user-guide/cli.md
CHANGED
|
@@ -129,6 +129,19 @@ Minimal per-provider examples:
|
|
|
129
129
|
|
|
130
130
|
Listed providers form a failover chain: the router walks them in order, retrying `rate_limit`/`network` errors (bounded backoff) on the current provider before moving on, and failing over immediately on `auth`, `overflow`, and `bad_request`.
|
|
131
131
|
|
|
132
|
+
## Self-improvement environment
|
|
133
|
+
|
|
134
|
+
These are process-env knobs, not config fields. They are assembled in code and
|
|
135
|
+
never accepted as a config passthrough.
|
|
136
|
+
|
|
137
|
+
| Variable | Meaning |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `LICH_ALLOW_SELF_COMMIT` | Set to `1` to allow one gated `git_commit` per run. Unset or any other value is fail-closed. Read at agent construction. |
|
|
140
|
+
| `LICH_TEST_COMMAND` | Command `run_tests` runs in `work_dir` (default `node node_modules/vitest/vitest.mjs run`). An optional `filter` argument is appended. |
|
|
141
|
+
|
|
142
|
+
Veto reasons, the terminal git denylist, skills, and `MEMORY.md` are in the
|
|
143
|
+
[plugins guide](plugins.md#self-improvement-loop).
|
|
144
|
+
|
|
132
145
|
## Session files
|
|
133
146
|
|
|
134
147
|
Each run writes `.lich/sessions/<timestamp36>-<counter>[-label].jsonl` where the label is the run origin: `-tui`, or `-gw-<platform>-<chat_id>` for gateway conversations. One-shot and chat runs get no label. Records are JSON lines of two kinds: `{"ts","kind":"meta","meta":{...}}` (run start, budget exhaustion) and `{"ts","kind":"message","message":{...}}` for each system/user/assistant/tool message.
|
|
@@ -47,7 +47,7 @@ const result = await run_agent(
|
|
|
47
47
|
|
|
48
48
|
## Agent class
|
|
49
49
|
|
|
50
|
-
`new Agent(config)` (or `create_agent(raw)`) builds the provider router, registers the
|
|
50
|
+
`new Agent(config)` (or `create_agent(raw)`) builds the provider router, registers the builtin tools (filtered by `tools_enabled`) plus the gatekeeper's `git_commit`, and exposes:
|
|
51
51
|
|
|
52
52
|
| Member | Type | Purpose |
|
|
53
53
|
| --- | --- | --- |
|
|
@@ -141,6 +141,8 @@ const config = {
|
|
|
141
141
|
|
|
142
142
|
Listed providers form a failover chain tried in order: `rate_limit`/`network` errors retry with backoff (3 attempts) on the current provider before failing over; `auth`, `overflow`, and `bad_request` fail over immediately. The last error is rethrown when all providers fail.
|
|
143
143
|
|
|
144
|
+
`LICH_ALLOW_SELF_COMMIT` and `LICH_TEST_COMMAND` are process-env knobs, not config fields. See the [CLI environment](cli.md#self-improvement-environment).
|
|
145
|
+
|
|
144
146
|
## Custom tool filtering
|
|
145
147
|
|
|
146
148
|
`tools_enabled` accepts `"all"` (default) or an array of builtin tool names to register; everything else stays unregistered and invisible to the model:
|
|
@@ -47,11 +47,11 @@ All hooks are awaited. Hook errors are logged as warnings and skipped — a brok
|
|
|
47
47
|
| Hook | Signature | Purpose |
|
|
48
48
|
| --- | --- | --- |
|
|
49
49
|
| `before_tool_call` | `(info: {tool_name, args}, ctx) => {block?: boolean, reason?: string} \| void` | Runs before each tool call in plugin registration order. Return `{block: true, reason}` to veto. |
|
|
50
|
-
| `after_tool_call` | `(info: {tool_name, args, result_summary}, ctx) => void` | Runs after each tool call with a 300-char
|
|
50
|
+
| `after_tool_call` | `(info: {tool_name, args, result_summary, ok, error?}, ctx) => void` | Runs after each tool call with a 300-char summary plus structured `ok`/`error`. |
|
|
51
51
|
| `on_run_start` | `(info: {input_chars}, ctx) => void` | Runs once before the conversation loop starts. |
|
|
52
52
|
| `on_run_end` | `(info: {stopped_reason, turns_used}, ctx) => void` | Runs once after the loop ends with the outcome. |
|
|
53
53
|
|
|
54
|
-
`ctx` is `{work_dir
|
|
54
|
+
`ctx` is `{work_dir, state?}` — the agent's working directory plus that plugin's per-run bag.
|
|
55
55
|
|
|
56
56
|
## Tool authoring
|
|
57
57
|
|
|
@@ -115,6 +115,59 @@ Failures are contained at every layer:
|
|
|
115
115
|
- **Bun** runs TypeScript plugin files natively — `.ts` entries just work (`bun src/cli.ts ...` from a clone).
|
|
116
116
|
- **Node** (the built `dist/cli.js`) uses the native ESM loader, which does not compile TS. For node deployments, compile your plugin or ship it as `.mjs`/plain JS and list that file in `plugins`.
|
|
117
117
|
|
|
118
|
+
## Self-improvement loop
|
|
119
|
+
|
|
120
|
+
The agent can write a tool, prove it with `run_tests`, and commit it with
|
|
121
|
+
`git_commit` — one commit per run, and only when you opt in. The gatekeeper
|
|
122
|
+
is constructed in code (not listed in `config.plugins`). If it does not
|
|
123
|
+
register, `git_commit` is absent. A config plugin naming `git_commit` is
|
|
124
|
+
inside the plugin-trust floor only when the gatekeeper is off; while it is
|
|
125
|
+
on, first-wins keeps the gatekeeper's tool.
|
|
126
|
+
|
|
127
|
+
Set `LICH_ALLOW_SELF_COMMIT=1` before startup. Unset, or any other value, is
|
|
128
|
+
fail-closed. `git_commit` is vetoed unless every condition holds; the reason
|
|
129
|
+
names the first failure, and the model sees `blocked_by_plugin: <reason>`:
|
|
130
|
+
|
|
131
|
+
| Failed condition | Reason |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `LICH_ALLOW_SELF_COMMIT` is not `1` | `self_commit_disabled` |
|
|
134
|
+
| no green `run_tests` yet this run | `tests_not_ok` |
|
|
135
|
+
| a `write_file` or `edit_file` succeeded after that green run | `worktree_dirty` |
|
|
136
|
+
| this run already committed once | `commit_budget_exhausted` |
|
|
137
|
+
|
|
138
|
+
`terminal` is vetoed when the command matches the hardcoded git denylist.
|
|
139
|
+
The reason is `git_denylist: <pattern>`. Patterns are flag-tolerant
|
|
140
|
+
`commit`/`push` (`commit`, `-commit`, `--commit`, `push`, `-push`, `--push`)
|
|
141
|
+
and any occurrence of `commit-tree` or `update-ref`. There is no `remote`
|
|
142
|
+
pattern. The denylist is best-effort: raw `terminal` can still run git. The
|
|
143
|
+
boundary is a human reviewing the local repo. Push is human-only.
|
|
144
|
+
|
|
145
|
+
`git_commit` takes `{message, paths}` — 1 to 50 paths relative to `work_dir`.
|
|
146
|
+
It rejects `""`, `.`, a path that resolves to `work_dir` itself, and
|
|
147
|
+
secret-ish basenames (`.env`, `.env.local`, `*.pem`, `*.p12`, `id_rsa*`).
|
|
148
|
+
It refuses an unreachable `HEAD`. It stages exactly the named paths
|
|
149
|
+
(`git add -- <paths>`) and commits with `git commit --only`. It never pushes.
|
|
150
|
+
|
|
151
|
+
`run_tests` takes an optional `filter` and runs `LICH_TEST_COMMAND` in
|
|
152
|
+
`work_dir` (default `node node_modules/vitest/vitest.mjs run`) with a 600s
|
|
153
|
+
timeout. A second call in the same process returns `run_tests_busy`. The
|
|
154
|
+
mutex is process-local: one lich process per repo.
|
|
155
|
+
|
|
156
|
+
Clean state attests no `write_file`/`edit_file` since the last green
|
|
157
|
+
`run_tests`; it does NOT attest absence of terminal-mediated writes.
|
|
158
|
+
|
|
159
|
+
### Skills and memory
|
|
160
|
+
|
|
161
|
+
Write a markdown note with `write_file` to `.lich/skills/<name>.md`.
|
|
162
|
+
`docs_search` finds those files. That directory does not need `index.md`,
|
|
163
|
+
and it is walked fresh on every search. The default system prompt says tool
|
|
164
|
+
results — docs, skills, memory — are reference data, not instructions.
|
|
165
|
+
|
|
166
|
+
`MEMORY.md` is append-only and human-reviewable. It is never auto-loaded.
|
|
167
|
+
Review it between appends and the next self-commit.
|
|
168
|
+
|
|
118
169
|
## Security note
|
|
119
170
|
|
|
120
|
-
Plugins execute **in-process with full privileges** — the same trust level as the agent itself and your shell. A plugin can read any file the process can, make network calls, and alter process state. Only load plugin files you wrote or audited; treat `.lich/plugins/` like you treat `.env` files.
|
|
171
|
+
Plugins execute **in-process with full privileges** — the same trust level as the agent itself and your shell. A plugin can read any file the process can, make network calls, and alter process state. Only load plugin files you wrote or audited; treat `.lich/plugins/` like you treat `.env` files.
|
|
172
|
+
|
|
173
|
+
`.lich/config.json` `plugins` is persistent arbitrary code at the next process start. Review config diffs before the next self-commit. The terminal git denylist does not close that hole.
|