@moikapy/lich 0.5.1 → 0.7.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 +33 -0
- package/README.md +84 -4
- package/dist/chunk-JYURFAGB.js +92 -0
- package/dist/chunk-JYURFAGB.js.map +1 -0
- package/dist/{chunk-CV2YH3FH.js → chunk-QVJCIZIF.js} +748 -13
- package/dist/chunk-QVJCIZIF.js.map +1 -0
- package/dist/chunk-SAEB3QL3.js +58 -0
- package/dist/chunk-SAEB3QL3.js.map +1 -0
- package/dist/cli.js +263 -22
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-W6S43ETE.js → gateway-5BG3YCZF.js} +2 -2
- package/dist/index.d.ts +229 -118
- package/dist/index.js +5 -3
- package/dist/{tui-2VO6LAFM.js → tui-L6RABP2J.js} +43 -41
- package/dist/tui-L6RABP2J.js.map +1 -0
- package/docs/.vitepress/config.mts +6 -1
- package/docs/architecture/agent-loop.md +5 -3
- package/docs/architecture/overview.md +36 -22
- package/docs/architecture/plugins.md +1 -1
- package/docs/architecture/tools.md +7 -2
- package/docs/getting-started.md +9 -7
- package/docs/index.md +6 -4
- package/docs/user-guide/cli.md +24 -9
- package/docs/user-guide/games.md +93 -0
- package/docs/user-guide/godot.md +3 -1
- package/docs/user-guide/library.md +8 -2
- package/docs/user-guide/plugins.md +2 -2
- package/docs/user-guide/redot.md +93 -0
- package/docs/user-guide/tui.md +11 -11
- package/examples/game_bridge/README.md +2 -0
- package/optional-mcps/godot/manifest.json +6 -0
- package/optional-mcps/redot/manifest.json +18 -0
- package/package.json +2 -1
- package/dist/chunk-6M6OAQGN.js +0 -17
- package/dist/chunk-6M6OAQGN.js.map +0 -1
- package/dist/chunk-CV2YH3FH.js.map +0 -1
- package/dist/tui-2VO6LAFM.js.map +0 -1
- /package/dist/{gateway-W6S43ETE.js.map → gateway-5BG3YCZF.js.map} +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fill_template,
|
|
3
|
+
load_theme
|
|
4
|
+
} from "./chunk-JYURFAGB.js";
|
|
1
5
|
import {
|
|
2
6
|
LICH_VERSION
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-SAEB3QL3.js";
|
|
4
8
|
import {
|
|
5
9
|
create_agent_with_plugins,
|
|
6
10
|
truncate_text
|
|
7
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QVJCIZIF.js";
|
|
8
12
|
|
|
9
13
|
// src/tui.tsx
|
|
10
14
|
import { render } from "ink";
|
|
@@ -90,24 +94,24 @@ function parse_command(raw_input) {
|
|
|
90
94
|
}
|
|
91
95
|
return { kind: "slash", name: body.slice(0, space_index), args: body.slice(space_index + 1).trim() };
|
|
92
96
|
}
|
|
93
|
-
function tui_banner_text(
|
|
94
|
-
return
|
|
97
|
+
function tui_banner_text(theme, version, model, kind) {
|
|
98
|
+
return fill_template(theme.welcome, { version, model, kind });
|
|
95
99
|
}
|
|
96
100
|
function format_usage(total_tokens) {
|
|
97
101
|
return total_tokens.toLocaleString("en-US");
|
|
98
102
|
}
|
|
99
|
-
function assistant_result_block(message) {
|
|
103
|
+
function assistant_result_block(message, theme) {
|
|
100
104
|
if (message.content.length === 0) {
|
|
101
105
|
return void 0;
|
|
102
106
|
}
|
|
103
|
-
return { role: "lich", lines: [
|
|
107
|
+
return { role: "lich", lines: [`${theme.response_label} \u203A ${message.content}`] };
|
|
104
108
|
}
|
|
105
|
-
function run_notice_blocks(result) {
|
|
109
|
+
function run_notice_blocks(result, theme) {
|
|
106
110
|
const blocks = [];
|
|
107
111
|
if (result.outcome.stopped_reason === "budget") {
|
|
108
|
-
blocks.push({ role: "error", lines: [
|
|
112
|
+
blocks.push({ role: "error", lines: [`\xB7 ${fill_template(theme.notices.budget_exhausted, {})}`] });
|
|
109
113
|
}
|
|
110
|
-
const final_block = result.outcome.final === void 0 ? void 0 : assistant_result_block(result.outcome.final);
|
|
114
|
+
const final_block = result.outcome.final === void 0 ? void 0 : assistant_result_block(result.outcome.final, theme);
|
|
111
115
|
if (final_block !== void 0) {
|
|
112
116
|
blocks.push(final_block);
|
|
113
117
|
}
|
|
@@ -124,8 +128,9 @@ function tool_result_block(call, ok, result_content) {
|
|
|
124
128
|
];
|
|
125
129
|
return { role: ok === true ? "tool" : "error", lines };
|
|
126
130
|
}
|
|
127
|
-
function compress_notice_block(summary_chars) {
|
|
128
|
-
|
|
131
|
+
function compress_notice_block(summary_chars, theme) {
|
|
132
|
+
const line = fill_template(theme.notices.compressed, { chars: summary_chars });
|
|
133
|
+
return { role: "meta", lines: [`\xB7 ${line}`] };
|
|
129
134
|
}
|
|
130
135
|
function error_notice_block(message) {
|
|
131
136
|
return { role: "error", lines: [`\xB7 error: ${truncate_text(message, ERROR_PREVIEW_CHARS)}`] };
|
|
@@ -150,12 +155,13 @@ function usage_notice_block(total_tokens) {
|
|
|
150
155
|
function unknown_command_block(name) {
|
|
151
156
|
return { role: "error", lines: [`\xB7 unknown command: /${name} (try /help)`] };
|
|
152
157
|
}
|
|
153
|
-
function session_list_block(entries, cap = 10) {
|
|
158
|
+
function session_list_block(entries, theme, cap = 10) {
|
|
154
159
|
const sorted = [...entries].sort((a, b) => b.mtime_ms - a.mtime_ms).slice(0, cap);
|
|
155
160
|
if (sorted.length === 0) {
|
|
156
161
|
return { role: "meta", lines: ["\xB7 no session files yet"] };
|
|
157
162
|
}
|
|
158
|
-
const
|
|
163
|
+
const label = fill_template(theme.notices.sessions, { count: sorted.length });
|
|
164
|
+
const lines = [`\xB7 ${label}`];
|
|
159
165
|
for (const entry of sorted) {
|
|
160
166
|
lines.push(` ${entry.name} (${format_usage(entry.size_bytes)} bytes)`);
|
|
161
167
|
}
|
|
@@ -211,20 +217,15 @@ function MessageView({ blocks, state }) {
|
|
|
211
217
|
// src/tui/status_bar.tsx
|
|
212
218
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
213
219
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
thinking: "thinking",
|
|
217
|
-
tool: "tool"
|
|
218
|
-
};
|
|
219
|
-
function StatusBar({ state, model }) {
|
|
220
|
-
const phase = PHASE_LABELS[state.phase];
|
|
220
|
+
function StatusBar({ state, model, theme }) {
|
|
221
|
+
const phase = theme.phase_labels[state.phase];
|
|
221
222
|
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
222
223
|
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
223
224
|
`model ${model} \xB7 turns ${state.turns_used} \xB7 tokens ${format_usage(state.usage.total_tokens)} \xB7 [${phase}]`,
|
|
224
225
|
state.compress_count > 0 ? ` \xB7 compressed ${state.compress_count}` : "",
|
|
225
226
|
state.session_path !== void 0 ? ` \xB7 ${state.session_path}` : ""
|
|
226
227
|
] }),
|
|
227
|
-
state.budget_exhausted ? /* @__PURE__ */ jsx2(Text2, { color: "red", children:
|
|
228
|
+
state.budget_exhausted ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: ` \xB7 ${theme.notices.budget_exhausted}` }) : null
|
|
228
229
|
] });
|
|
229
230
|
}
|
|
230
231
|
|
|
@@ -292,12 +293,12 @@ function CommandBar({ busy, on_submit }) {
|
|
|
292
293
|
// src/tui/app.tsx
|
|
293
294
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
294
295
|
var SESSION_LIST_CAP = 10;
|
|
295
|
-
function event_blocks(event) {
|
|
296
|
+
function event_blocks(event, theme) {
|
|
296
297
|
if (event.type === "tool_call_end") {
|
|
297
298
|
return [tool_result_block(event.call, event.result.ok === true, event.result.output)];
|
|
298
299
|
}
|
|
299
300
|
if (event.type === "compress_end") {
|
|
300
|
-
return [compress_notice_block(event.summary_chars)];
|
|
301
|
+
return [compress_notice_block(event.summary_chars, theme)];
|
|
301
302
|
}
|
|
302
303
|
if (event.type === "error") {
|
|
303
304
|
return [error_notice_block(event.error instanceof Error ? event.error.message : String(event.error))];
|
|
@@ -313,7 +314,7 @@ async function run_agent_turn(agent, history, input, on_event, on_done, signal)
|
|
|
313
314
|
stop_listening();
|
|
314
315
|
}
|
|
315
316
|
}
|
|
316
|
-
async function sessions_block(config) {
|
|
317
|
+
async function sessions_block(config, theme) {
|
|
317
318
|
try {
|
|
318
319
|
const dir_entries = await readdir(config.session_dir, { withFileTypes: true });
|
|
319
320
|
const entries = [];
|
|
@@ -324,7 +325,7 @@ async function sessions_block(config) {
|
|
|
324
325
|
const info = await stat(`${config.session_dir}/${entry.name}`);
|
|
325
326
|
entries.push({ name: entry.name, size_bytes: info.size, mtime_ms: info.mtimeMs });
|
|
326
327
|
}
|
|
327
|
-
return session_list_block(entries, SESSION_LIST_CAP);
|
|
328
|
+
return session_list_block(entries, theme, SESSION_LIST_CAP);
|
|
328
329
|
} catch {
|
|
329
330
|
return { role: "meta", lines: ["\xB7 no session files yet"] };
|
|
330
331
|
}
|
|
@@ -332,23 +333,23 @@ async function sessions_block(config) {
|
|
|
332
333
|
function run_error_text(error) {
|
|
333
334
|
return error instanceof Error ? error.message : String(error);
|
|
334
335
|
}
|
|
335
|
-
function use_agent_run(agent, add_blocks, set_state, set_blocks) {
|
|
336
|
+
function use_agent_run(agent, theme, add_blocks, set_state, set_blocks) {
|
|
336
337
|
const history_ref = useRef([]);
|
|
337
338
|
const controller_ref = useRef(void 0);
|
|
338
339
|
const finish_run = useCallback((result) => {
|
|
339
340
|
history_ref.current = result.messages;
|
|
340
341
|
set_state((current) => apply_run_result(current, result));
|
|
341
|
-
add_blocks(run_notice_blocks(result));
|
|
342
|
-
}, [add_blocks, set_state]);
|
|
342
|
+
add_blocks(run_notice_blocks(result, theme));
|
|
343
|
+
}, [add_blocks, set_state, theme]);
|
|
343
344
|
const start_message_run = useCallback(
|
|
344
345
|
(text) => {
|
|
345
|
-
add_blocks([{ role: "user", lines: [
|
|
346
|
+
add_blocks([{ role: "user", lines: [`${theme.user_label} \u203A ${text}`] }]);
|
|
346
347
|
set_state((current) => ({ ...current, phase: "thinking", active_tool: void 0 }));
|
|
347
348
|
const controller = new AbortController();
|
|
348
349
|
controller_ref.current = controller;
|
|
349
350
|
const on_event = (event) => {
|
|
350
351
|
set_state((current) => apply_event(current, event));
|
|
351
|
-
set_blocks((current) => [...current, ...event_blocks(event)].slice(-HISTORY_CAP));
|
|
352
|
+
set_blocks((current) => [...current, ...event_blocks(event, theme)].slice(-HISTORY_CAP));
|
|
352
353
|
};
|
|
353
354
|
void run_agent_turn(agent, history_ref.current, text, on_event, finish_run, controller.signal).catch((error) => {
|
|
354
355
|
add_blocks([error_notice_block(run_error_text(error))]);
|
|
@@ -359,12 +360,12 @@ function use_agent_run(agent, add_blocks, set_state, set_blocks) {
|
|
|
359
360
|
}
|
|
360
361
|
});
|
|
361
362
|
},
|
|
362
|
-
[agent, add_blocks, finish_run, set_blocks, set_state]
|
|
363
|
+
[agent, add_blocks, finish_run, set_blocks, set_state, theme]
|
|
363
364
|
);
|
|
364
365
|
useEffect2(() => () => controller_ref.current?.abort(), []);
|
|
365
366
|
return start_message_run;
|
|
366
367
|
}
|
|
367
|
-
function use_slash_commands(agent, add_blocks, set_blocks, total_tokens) {
|
|
368
|
+
function use_slash_commands(agent, theme, add_blocks, set_blocks, total_tokens) {
|
|
368
369
|
const handle = useCallback(
|
|
369
370
|
(parsed) => {
|
|
370
371
|
if (parsed.name === "exit" || parsed.name === "quit" || parsed.name === "q") {
|
|
@@ -380,16 +381,16 @@ function use_slash_commands(agent, add_blocks, set_blocks, total_tokens) {
|
|
|
380
381
|
} else if (parsed.name === "clear") {
|
|
381
382
|
set_blocks([]);
|
|
382
383
|
} else if (parsed.name === "sessions") {
|
|
383
|
-
void sessions_block(agent.config).then((block) => add_blocks([block]));
|
|
384
|
+
void sessions_block(agent.config, theme).then((block) => add_blocks([block]));
|
|
384
385
|
} else {
|
|
385
386
|
add_blocks([unknown_command_block(parsed.name)]);
|
|
386
387
|
}
|
|
387
388
|
},
|
|
388
|
-
[agent, add_blocks, set_blocks, total_tokens]
|
|
389
|
+
[agent, add_blocks, set_blocks, theme, total_tokens]
|
|
389
390
|
);
|
|
390
391
|
return handle;
|
|
391
392
|
}
|
|
392
|
-
function TuiApp({ agent }) {
|
|
393
|
+
function TuiApp({ agent, theme }) {
|
|
393
394
|
const [blocks, set_blocks] = useState3([]);
|
|
394
395
|
const [state, set_state] = useState3(INITIAL_UI_STATE);
|
|
395
396
|
const add_blocks = useCallback((added) => {
|
|
@@ -398,8 +399,8 @@ function TuiApp({ agent }) {
|
|
|
398
399
|
}
|
|
399
400
|
set_blocks((current) => [...current, ...added].slice(-HISTORY_CAP));
|
|
400
401
|
}, []);
|
|
401
|
-
const start_message_run = use_agent_run(agent, add_blocks, set_state, set_blocks);
|
|
402
|
-
const handle_slash = use_slash_commands(agent, add_blocks, set_blocks, state.usage.total_tokens);
|
|
402
|
+
const start_message_run = use_agent_run(agent, theme, add_blocks, set_state, set_blocks);
|
|
403
|
+
const handle_slash = use_slash_commands(agent, theme, add_blocks, set_blocks, state.usage.total_tokens);
|
|
403
404
|
const submit = useCallback(
|
|
404
405
|
(text) => {
|
|
405
406
|
const parsed = parse_command(text);
|
|
@@ -415,9 +416,9 @@ function TuiApp({ agent }) {
|
|
|
415
416
|
);
|
|
416
417
|
const provider = agent.config.providers[0];
|
|
417
418
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", minHeight: 8, children: [
|
|
418
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: tui_banner_text(
|
|
419
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: tui_banner_text(theme, LICH_VERSION, provider?.model ?? "unknown", provider?.kind ?? "unknown") }),
|
|
419
420
|
/* @__PURE__ */ jsx4(MessageView, { blocks, state }),
|
|
420
|
-
/* @__PURE__ */ jsx4(StatusBar, { state, model: provider?.model ?? "unknown" }),
|
|
421
|
+
/* @__PURE__ */ jsx4(StatusBar, { state, model: provider?.model ?? "unknown", theme }),
|
|
421
422
|
/* @__PURE__ */ jsx4(CommandBar, { busy: state.phase !== "idle", on_submit: submit })
|
|
422
423
|
] });
|
|
423
424
|
}
|
|
@@ -426,11 +427,12 @@ function TuiApp({ agent }) {
|
|
|
426
427
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
427
428
|
async function run_tui(config) {
|
|
428
429
|
const agent = await create_agent_with_plugins(config);
|
|
429
|
-
const
|
|
430
|
+
const theme = load_theme(config.theme);
|
|
431
|
+
const instance = render(/* @__PURE__ */ jsx5(TuiApp, { agent, theme }));
|
|
430
432
|
await instance.waitUntilExit();
|
|
431
433
|
return 0;
|
|
432
434
|
}
|
|
433
435
|
export {
|
|
434
436
|
run_tui
|
|
435
437
|
};
|
|
436
|
-
//# sourceMappingURL=tui-
|
|
438
|
+
//# sourceMappingURL=tui-L6RABP2J.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tui.tsx","../src/tui/app.tsx","../src/tui/state.ts","../src/tui/message_view.tsx","../src/tui/status_bar.tsx","../src/tui/command_bar.tsx"],"sourcesContent":["/**\n * TUI entry: builds the agent from the parsed config and renders the ink app\n * until exit. The CLI dynamic-imports this module for `lich tui`.\n */\nimport { render } from \"ink\";\nimport { create_agent_with_plugins } from \"./agent/agent.js\";\nimport type { AgentConfig } from \"./agent/config.js\";\nimport { TuiApp } from \"./tui/app.js\";\nimport { load_theme } from \"./util/theme.js\";\n\nexport async function run_tui(config: AgentConfig): Promise<number> {\n const agent = await create_agent_with_plugins(config);\n const theme = load_theme(config.theme);\n const instance = render(<TuiApp agent={agent} theme={theme} />);\n await instance.waitUntilExit();\n return 0;\n}","/**\n * Root ink component for the lich TUI: wires agent events into the UI state\n * machine, drives agent.run with history continuity, and lays out header,\n * transcript, status bar, and the command input row.\n */\nimport { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from \"react\";\nimport { Box, Text } from \"ink\";\nimport type { Agent, AgentRunResult } from \"../agent/agent.js\";\nimport type { AgentEvent } from \"../agent/events.js\";\nimport type { AgentConfig } from \"../agent/config.js\";\nimport type { Message } from \"../providers/types.js\";\nimport { LICH_VERSION } from \"../index.js\";\nimport type { ThemeSpec } from \"../util/lore.js\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport {\n apply_event,\n apply_run_result,\n compress_notice_block,\n error_notice_block,\n help_block,\n HISTORY_CAP,\n INITIAL_UI_STATE,\n model_label_block,\n parse_command,\n tui_banner_text,\n run_notice_blocks,\n session_list_block,\n tool_result_block,\n unknown_command_block,\n usage_notice_block,\n type HistoryBlock,\n type ParsedInput,\n type SessionEntryInfo,\n type UiState,\n} from \"./state.js\";\nimport { MessageView } from \"./message_view.js\";\nimport { StatusBar } from \"./status_bar.js\";\nimport { CommandBar } from \"./command_bar.js\";\n\nconst SESSION_LIST_CAP = 10;\n\ntype SlashInput = Extract<ParsedInput, { kind: \"slash\" }>;\ntype AddBlocks = (added: readonly HistoryBlock[]) => void;\ntype SetUiState = Dispatch<SetStateAction<UiState>>;\ntype SetBlocks = Dispatch<SetStateAction<readonly HistoryBlock[]>>;\n\n/** Map one agent event to optional transcript blocks (tool rows, notices). */\nfunction event_blocks(event: AgentEvent, theme: ThemeSpec): readonly HistoryBlock[] {\n if (event.type === \"tool_call_end\") {\n return [tool_result_block(event.call, event.result.ok === true, event.result.output)];\n }\n if (event.type === \"compress_end\") {\n return [compress_notice_block(event.summary_chars, theme)];\n }\n if (event.type === \"error\") {\n return [error_notice_block(event.error instanceof Error ? event.error.message : String(event.error))];\n }\n return [];\n}\n\n/** One agent turn: subscribe to events, run, unsubscribe in finally. */\nasync function run_agent_turn(\n agent: Agent,\n history: readonly Message[],\n input: string,\n on_event: (event: AgentEvent) => void,\n on_done: (result: AgentRunResult) => void,\n signal: AbortSignal,\n): Promise<void> {\n const stop_listening = agent.events.on(on_event);\n try {\n const result = await agent.run({ input, history, signal, label: \"tui\" });\n on_done(result);\n } finally {\n stop_listening();\n }\n}\n\n/** Async /sessions listing as a meta block (never throws). */\nasync function sessions_block(config: AgentConfig, theme: ThemeSpec): Promise<HistoryBlock> {\n try {\n const dir_entries = await readdir(config.session_dir, { withFileTypes: true });\n const entries: SessionEntryInfo[] = [];\n for (const entry of dir_entries) {\n if (entry.isFile() === false || entry.name.endsWith(\".jsonl\") === false) {\n continue;\n }\n const info = await stat(`${config.session_dir}/${entry.name}`);\n entries.push({ name: entry.name, size_bytes: info.size, mtime_ms: info.mtimeMs });\n }\n return session_list_block(entries, theme, SESSION_LIST_CAP);\n } catch {\n return { role: \"meta\", lines: [\"· no session files yet\"] };\n }\n}\n\nfunction run_error_text(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Runs one message exchange; owns history continuity and abort wiring. */\nfunction use_agent_run(\n agent: Agent,\n theme: ThemeSpec,\n add_blocks: AddBlocks,\n set_state: SetUiState,\n set_blocks: SetBlocks,\n): (text: string) => void {\n const history_ref = useRef<readonly Message[]>([]);\n const controller_ref = useRef<AbortController | undefined>(undefined);\n\n const finish_run = useCallback((result: AgentRunResult): void => {\n history_ref.current = result.messages;\n set_state((current) => apply_run_result(current, result));\n add_blocks(run_notice_blocks(result, theme));\n }, [add_blocks, set_state, theme]);\n\n const start_message_run = useCallback(\n (text: string): void => {\n add_blocks([{ role: \"user\", lines: [`${theme.user_label} › ${text}`] }]);\n set_state((current) => ({ ...current, phase: \"thinking\", active_tool: undefined }));\n const controller = new AbortController();\n controller_ref.current = controller;\n const on_event = (event: AgentEvent): void => {\n set_state((current) => apply_event(current, event));\n set_blocks((current) => [...current, ...event_blocks(event, theme)].slice(-HISTORY_CAP));\n };\n void run_agent_turn(agent, history_ref.current, text, on_event, finish_run, controller.signal)\n .catch((error: unknown) => {\n add_blocks([error_notice_block(run_error_text(error))]);\n set_state((current) => ({ ...current, phase: \"idle\" }));\n })\n .finally(() => {\n if (controller_ref.current === controller) {\n controller_ref.current = undefined;\n }\n });\n },\n [agent, add_blocks, finish_run, set_blocks, set_state, theme],\n );\n\n useEffect(() => () => controller_ref.current?.abort(), []);\n\n return start_message_run;\n}\n\n/** Slash-command dispatch: pure client-side actions, never hits the agent. */\nfunction use_slash_commands(\n agent: Agent,\n theme: ThemeSpec,\n add_blocks: AddBlocks,\n set_blocks: SetBlocks,\n total_tokens: number,\n): (parsed: SlashInput) => void {\n const handle = useCallback(\n (parsed: SlashInput): void => {\n if (parsed.name === \"exit\" || parsed.name === \"quit\" || parsed.name === \"q\") {\n process.exit(0);\n return;\n }\n if (parsed.name === \"help\") {\n add_blocks([help_block()]);\n } else if (parsed.name === \"model\") {\n add_blocks([model_label_block(agent.config)]);\n } else if (parsed.name === \"usage\") {\n add_blocks([usage_notice_block(total_tokens)]);\n } else if (parsed.name === \"clear\") {\n set_blocks([]);\n } else if (parsed.name === \"sessions\") {\n void sessions_block(agent.config, theme).then((block) => add_blocks([block]));\n } else {\n add_blocks([unknown_command_block(parsed.name)]);\n }\n },\n [agent, add_blocks, set_blocks, theme, total_tokens],\n );\n return handle;\n}\n\ninterface TuiAppProps {\n readonly agent: Agent;\n readonly theme: ThemeSpec;\n}\n\nexport function TuiApp({ agent, theme }: TuiAppProps): React.JSX.Element {\n const [blocks, set_blocks] = useState<readonly HistoryBlock[]>([]);\n const [state, set_state] = useState(INITIAL_UI_STATE);\n\n const add_blocks = useCallback<AddBlocks>((added: readonly HistoryBlock[]): void => {\n if (added.length === 0) {\n return;\n }\n set_blocks((current) => [...current, ...added].slice(-HISTORY_CAP));\n }, []);\n\n const start_message_run = use_agent_run(agent, theme, add_blocks, set_state, set_blocks);\n const handle_slash = use_slash_commands(agent, theme, add_blocks, set_blocks, state.usage.total_tokens);\n\n const submit = useCallback(\n (text: string): void => {\n const parsed = parse_command(text);\n if (parsed.kind === \"message\") {\n if (parsed.text.length > 0) {\n start_message_run(parsed.text);\n }\n return;\n }\n handle_slash(parsed);\n },\n [handle_slash, start_message_run],\n );\n\n const provider = agent.config.providers[0];\n return (\n <Box flexDirection=\"column\" minHeight={8}>\n <Text dimColor>{tui_banner_text(theme, LICH_VERSION, provider?.model ?? \"unknown\", provider?.kind ?? \"unknown\")}</Text>\n <MessageView blocks={blocks} state={state} />\n <StatusBar state={state} model={provider?.model ?? \"unknown\"} theme={theme} />\n <CommandBar busy={state.phase !== \"idle\"} on_submit={submit} />\n </Box>\n );\n}","/**\n * Pure state logic for the ink TUI: UI-state transitions from agent events,\n * slash-command parsing, transcript block mapping, and display formatters.\n * No ink/react imports here — this module is unit-tested without a TTY.\n */\nimport type { AgentEvent } from \"../agent/events.js\";\nimport type { AgentRunResult } from \"../agent/agent.js\";\nimport type { AssistantMessage, Message, ToolCall, Usage } from \"../providers/types.js\";\nimport { safe_json_parse, truncate_text } from \"../util/json.js\";\nimport type { ThemeSpec } from \"../util/lore.js\";\nimport { fill_template } from \"../util/theme.js\";\n\nexport type UiPhase = \"idle\" | \"thinking\" | \"tool\";\n\nexport interface UiState {\n readonly phase: UiPhase;\n readonly turns_used: number;\n readonly usage: Usage;\n readonly session_path: string | undefined;\n readonly compress_count: number;\n readonly budget_exhausted: boolean;\n readonly last_error: string | undefined;\n readonly active_tool: ToolCall | undefined;\n}\n\nexport const INITIAL_UI_STATE: UiState = {\n phase: \"idle\",\n turns_used: 0,\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n session_path: undefined,\n compress_count: 0,\n budget_exhausted: false,\n last_error: undefined,\n active_tool: undefined,\n};\n\nexport const HISTORY_CAP = 50;\nconst TOOL_ARGS_PREVIEW_CHARS = 80;\nconst TOOL_OUTPUT_PREVIEW_CHARS = 120;\nconst ERROR_PREVIEW_CHARS = 300;\n\nfunction error_text(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n return String(error);\n}\n\n/** Reducer over UiState; one pure mapping per AgentEvent variant. */\nexport function apply_event(state: UiState, event: AgentEvent): UiState {\n switch (event.type) {\n case \"llm_start\":\n return { ...state, phase: \"thinking\", active_tool: undefined };\n case \"llm_end\":\n return {\n ...state,\n usage: {\n prompt_tokens: state.usage.prompt_tokens + event.result.usage.prompt_tokens,\n completion_tokens: state.usage.completion_tokens + event.result.usage.completion_tokens,\n total_tokens: state.usage.total_tokens + event.result.usage.total_tokens,\n },\n };\n case \"tool_call_start\":\n return { ...state, phase: \"tool\", active_tool: event.call };\n case \"tool_call_end\":\n return {\n ...state,\n phase: \"thinking\",\n active_tool: undefined,\n last_error: event.result.ok === true ? state.last_error : (event.result.error ?? \"tool failed\"),\n };\n case \"turn_end\":\n return { ...state, turns_used: event.turn };\n case \"compress_start\":\n return { ...state, compress_count: state.compress_count + 1 };\n case \"budget_exhausted\":\n return { ...state, budget_exhausted: true };\n case \"error\":\n return { ...state, last_error: error_text(event.error) };\n default:\n return state;\n }\n}\n\n/** Fold an AgentRunResult back into UiState after the run promise resolves. */\nexport function apply_run_result(state: UiState, result: AgentRunResult): UiState {\n return {\n ...state,\n phase: \"idle\",\n turns_used: result.outcome.turns_used,\n session_path: result.session_path ?? state.session_path,\n last_error: result.outcome.stopped_reason === \"aborted\" ? \"run aborted\" : state.last_error,\n };\n}\n\nexport type ParsedInput = { kind: \"slash\"; name: string; args: string } | { kind: \"message\"; text: string };\n\n/** Split trimmed input into slash command vs plain message (empty input = message). */\nexport function parse_command(raw_input: string): ParsedInput {\n const text = raw_input.trim();\n if (text.startsWith(\"/\") === false) {\n return { kind: \"message\", text };\n }\n const body = text.slice(1);\n const space_index = body.indexOf(\" \");\n if (space_index === -1) {\n return { kind: \"slash\", name: body, args: \"\" };\n }\n return { kind: \"slash\", name: body.slice(0, space_index), args: body.slice(space_index + 1).trim() };\n}\n\n/** Dim header: the theme welcome string, the one tagline placement. */\nexport function tui_banner_text(theme: ThemeSpec, version: string, model: string, kind: string): string {\n return fill_template(theme.welcome, { version, model, kind });\n}\n\n/** 1234567 -> \"1,234,567\" (US grouping, matching the status bar style). */\nexport function format_usage(total_tokens: number): string {\n return total_tokens.toLocaleString(\"en-US\");\n}\n\nexport type BlockRole = \"user\" | \"lich\" | \"tool\" | \"meta\" | \"error\";\n\nexport interface HistoryBlock {\n readonly role: BlockRole;\n readonly lines: readonly string[];\n}\n\nfunction assistant_tool_line(call: ToolCall): string {\n const args_json = JSON.stringify(call.args) ?? \"{}\";\n return ` \\u23bf ${truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS)}`;\n}\n\nfunction format_message_lines(message: Message, theme: ThemeSpec): string[] {\n if (message.role === \"user\") {\n return [`${theme.user_label} \\u203a ${message.content}`];\n }\n if (message.role === \"assistant\") {\n const lines = [`${theme.response_label} \\u203a ${message.content}`];\n for (const call of message.tool_calls ?? []) {\n lines.push(assistant_tool_line(call));\n }\n return lines;\n }\n if (message.role === \"tool\") {\n const flag = message.is_error === true ? \"error\" : \"ok\";\n return [` \\u23bf ${message.name}: ${flag} (${truncate_text(message.content, TOOL_OUTPUT_PREVIEW_CHARS)})`];\n }\n return [`\\u00b7 system: ${message.content}`];\n}\n\n/** Map one transcript Message to display lines with its role tag. */\nexport function format_message_block(message: Message, theme: ThemeSpec): HistoryBlock {\n if (message.role === \"tool\") {\n const ok = message.is_error !== true;\n return { role: ok === true ? \"tool\" : \"error\", lines: format_message_lines(message, theme) };\n }\n const roles: Record<Exclude<Message[\"role\"], \"tool\">, BlockRole> = {\n system: \"meta\",\n user: \"user\",\n assistant: \"lich\",\n };\n return { role: roles[message.role], lines: format_message_lines(message, theme) };\n}\n\n/** Keep the newest `cap` non-system messages as renderable blocks. */\nexport function split_history_blocks(messages: readonly Message[], cap: number, theme: ThemeSpec): HistoryBlock[] {\n const visible = messages.filter((message) => message.role !== \"system\");\n const start = Math.max(0, visible.length - cap);\n return visible.slice(start).map((message) => format_message_block(message, theme));\n}\n\nfunction assistant_result_block(message: AssistantMessage, theme: ThemeSpec): HistoryBlock | undefined {\n if (message.content.length === 0) {\n return undefined;\n }\n return { role: \"lich\", lines: [`${theme.response_label} \\u203a ${message.content}`] };\n}\n\n/** Post-run meta blocks: compression notices, budget, errors, final answer. */\nexport function run_notice_blocks(result: AgentRunResult, theme: ThemeSpec): HistoryBlock[] {\n const blocks: HistoryBlock[] = [];\n if (result.outcome.stopped_reason === \"budget\") {\n blocks.push({ role: \"error\", lines: [`\\u00b7 ${fill_template(theme.notices.budget_exhausted, {})}`] });\n }\n const final_block = result.outcome.final === undefined ? undefined : assistant_result_block(result.outcome.final, theme);\n if (final_block !== undefined) {\n blocks.push(final_block);\n }\n return blocks;\n}\n\nfunction truncate_tool_preview(result_content: string, ok: boolean): string {\n return ` \\u23bf ${ok === true ? \"ok\" : \"error\"} (${truncate_text(result_content, TOOL_OUTPUT_PREVIEW_CHARS)})`;\n}\n\n/** One finalized live tool row; falls back to the transcript copy when absent. */\nexport function tool_result_block(call: ToolCall, ok: boolean, result_content: string): HistoryBlock {\n const args_json = JSON.stringify(call.args) ?? \"{}\";\n const lines = [\n `\\u23fa ${call.name}(${truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS)})`,\n truncate_tool_preview(result_content, ok),\n ];\n return { role: ok === true ? \"tool\" : \"error\", lines };\n}\n\nexport function parse_tool_message_content(content: string): { ok: boolean; output: string } {\n const parsed = safe_json_parse<{ ok?: unknown; output?: unknown }>(content);\n if (parsed !== undefined && typeof parsed.ok === \"boolean\" && typeof parsed.output === \"string\") {\n return { ok: parsed.ok, output: parsed.output };\n }\n return { ok: true, output: content };\n}\n\nexport function compress_notice_block(summary_chars: number, theme: ThemeSpec): HistoryBlock {\n const line = fill_template(theme.notices.compressed, { chars: summary_chars });\n return { role: \"meta\", lines: [`\\u00b7 ${line}`] };\n}\n\nexport function error_notice_block(message: string): HistoryBlock {\n return { role: \"error\", lines: [`\\u00b7 error: ${truncate_text(message, ERROR_PREVIEW_CHARS)}`] };\n}\n\nexport function tool_args_preview(args: Record<string, unknown>): string {\n const args_json = JSON.stringify(args) ?? \"{}\";\n return truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS);\n}\n\nexport function help_block(): HistoryBlock {\n return { role: \"meta\", lines: [...HELP_LINES] };\n}\n\nexport function model_label_block(config: { providers: readonly { model: string; kind: string }[] }): HistoryBlock {\n const provider = config.providers[0];\n return {\n role: \"meta\",\n lines: [`\\u00b7 model: ${provider?.model ?? \"unknown\"} \\u00b7 provider: ${provider?.kind ?? \"unknown\"}`],\n };\n}\n\nexport function usage_notice_block(total_tokens: number): HistoryBlock {\n return { role: \"meta\", lines: [`\\u00b7 tokens used this session: ${format_usage(total_tokens)}`] };\n}\n\nexport function unknown_command_block(name: string): HistoryBlock {\n return { role: \"error\", lines: [`\\u00b7 unknown command: /${name} (try /help)`] };\n}\n\nexport interface SessionEntryInfo {\n readonly name: string;\n readonly size_bytes: number;\n readonly mtime_ms: number;\n}\n\n/** Newest-first session listing, capped at `cap` entries. */\nexport function session_list_block(entries: readonly SessionEntryInfo[], theme: ThemeSpec, cap: number = 10): HistoryBlock {\n const sorted = [...entries].sort((a, b) => b.mtime_ms - a.mtime_ms).slice(0, cap);\n if (sorted.length === 0) {\n return { role: \"meta\", lines: [\"\\u00b7 no session files yet\"] };\n }\n const label = fill_template(theme.notices.sessions, { count: sorted.length });\n const lines: string[] = [`\\u00b7 ${label}`];\n for (const entry of sorted) {\n lines.push(` ${entry.name} (${format_usage(entry.size_bytes)} bytes)`);\n }\n return { role: \"meta\", lines };\n}\n\nexport const SLASH_COMMAND_NAMES: readonly string[] = [\n \"exit\",\n \"quit\",\n \"q\",\n \"help\",\n \"model\",\n \"usage\",\n \"clear\",\n \"sessions\",\n];\n\nexport const HELP_LINES: readonly string[] = [\n \"commands: /help /model /usage /clear /sessions /exit (aliases: /quit /q)\",\n \"enter submits \\u00b7 backspace deletes \\u00b7 up/down recalls history \\u00b7 pasted newlines become spaces\",\n];","/**\n * Transcript rendering: maps HistoryBlock descriptors to ink elements with\n * role-based colors, and shows an animated braille spinner while thinking.\n * Blocks are pre-capped by the app, so a plain flex column is sufficient.\n */\nimport { useEffect, useState } from \"react\";\nimport { Box, Text } from \"ink\";\nimport { tool_args_preview, type HistoryBlock, type UiState } from \"./state.js\";\n\nconst SPINNER_FRAMES: readonly string[] = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst SPINNER_INTERVAL_MS = 80;\n\n/** Braille spinner frames on an 80ms interval; clears on unmount. */\nfunction use_spinner(): string {\n const [frame, set_frame] = useState(SPINNER_FRAMES[0] ?? \"⠋\");\n useEffect(() => {\n const timer = setInterval(() => {\n const next = SPINNER_FRAMES[(SPINNER_FRAMES.indexOf(frame) + 1) % SPINNER_FRAMES.length];\n set_frame(next ?? \"⠋\");\n }, SPINNER_INTERVAL_MS);\n return () => {\n clearInterval(timer);\n };\n }, [frame]);\n return frame;\n}\n\nfunction ThinkingLine(): React.JSX.Element {\n const frame = use_spinner();\n return <Text dimColor>{`${frame} thinking…`}</Text>;\n}\n\nconst ROLE_COLORS: Record<HistoryBlock[\"role\"], string | undefined> = {\n user: \"white\",\n lich: \"green\",\n tool: \"cyan\",\n meta: undefined,\n error: \"red\",\n};\n\nfunction BlockLines({ block }: { block: HistoryBlock }): React.JSX.Element {\n const color = ROLE_COLORS[block.role];\n return (\n <>\n {block.lines.map((line, index) => (\n <Text key={index} color={color} dimColor={color === undefined}>{line}</Text>\n ))}\n </>\n );\n}\n\ninterface MessageViewProps {\n readonly blocks: readonly HistoryBlock[];\n readonly state: UiState;\n}\n\n/** Transcript column plus the live phase line (spinner / running tool row). */\nexport function MessageView({ blocks, state }: MessageViewProps): React.JSX.Element {\n return (\n <Box flexDirection=\"column\" flexGrow={1}>\n {blocks.map((block, index) => (\n <Box key={index} flexDirection=\"column\">\n <BlockLines block={block} />\n </Box>\n ))}\n {state.phase === \"thinking\" ? <ThinkingLine /> : null}\n {state.phase === \"tool\" && state.active_tool !== undefined ? (\n <Text color=\"cyan\">{`⏺ ${state.active_tool.name}(${tool_args_preview(state.active_tool.args)})`}</Text>\n ) : null}\n </Box>\n );\n}","/**\n * Bottom status line: model, turns, token totals, phase tag, compression\n * count, and the session path once the agent has persisted a transcript.\n */\nimport { Box, Text } from \"ink\";\nimport type { ThemeSpec } from \"../util/lore.js\";\nimport { format_usage, type UiState } from \"./state.js\";\n\ninterface StatusBarProps {\n readonly state: UiState;\n readonly model: string;\n readonly theme: ThemeSpec;\n}\n\nexport function StatusBar({ state, model, theme }: StatusBarProps): React.JSX.Element {\n const phase = theme.phase_labels[state.phase];\n return (\n <Box>\n <Text dimColor>\n {`model ${model} · turns ${state.turns_used} · tokens ${format_usage(state.usage.total_tokens)} · [${phase}]`}\n {state.compress_count > 0 ? ` · compressed ${state.compress_count}` : \"\"}\n {state.session_path !== undefined ? ` · ${state.session_path}` : \"\"}\n </Text>\n {state.budget_exhausted ? <Text color=\"red\">{` · ${theme.notices.budget_exhausted}`}</Text> : null}\n </Box>\n );\n}","/**\n * Input row: printable characters accumulate in a buffer, Enter submits,\n * Backspace/Delete edits, Up/Down walk a 20-entry recall ring, and pasted\n * newlines collapse to spaces. Ctrl+C is left to ink's default handling.\n */\nimport { useState } from \"react\";\nimport { Box, Text, useInput } from \"ink\";\n\nconst INPUT_HISTORY_CAP = 20;\n\ninterface CommandBarProps {\n readonly busy: boolean;\n readonly on_submit: (text: string) => void;\n}\n\n/** Push onto a capped ring (newest first) without mutating the source. */\nfunction push_history(ring: readonly string[], entry: string): readonly string[] {\n return [entry, ...ring.filter((item) => item !== entry)].slice(0, INPUT_HISTORY_CAP);\n}\n\nexport function CommandBar({ busy, on_submit }: CommandBarProps): React.JSX.Element {\n const [buffer, set_buffer] = useState(\"\");\n const [recall_ring, set_recall_ring] = useState<readonly string[]>([]);\n const [recall_index, set_recall_index] = useState<number | undefined>(undefined);\n\n const submit_buffer = (): void => {\n const text = buffer.trim();\n set_buffer(\"\");\n set_recall_index(undefined);\n if (text.length > 0) {\n set_recall_ring((current) => push_history(current, text));\n on_submit(text);\n }\n };\n\n const walk_recall = (direction: 1 | -1): void => {\n if (recall_ring.length === 0) {\n return;\n }\n const current = recall_index ?? -direction;\n const next = Math.min(Math.max(current + direction, 0), recall_ring.length - 1);\n set_recall_index(next);\n set_buffer(recall_ring[next] ?? \"\");\n };\n\n useInput((input, key) => {\n if (key.return === true) {\n submit_buffer();\n return;\n }\n if (key.upArrow === true) {\n walk_recall(-1);\n return;\n }\n if (key.downArrow === true) {\n walk_recall(1);\n return;\n }\n if (key.backspace === true || key.delete === true) {\n set_buffer((current) => current.slice(0, -1));\n return;\n }\n if (key.ctrl === true || key.escape === true || key.tab === true || key.meta === true) {\n return;\n }\n if (input.length > 0) {\n set_buffer((current) => current + input.replaceAll(\"\\n\", \" \").replaceAll(\"\\r\", \" \"));\n }\n });\n\n return (\n <Box>\n <Text dimColor>{busy ? \" … \" : \"› \"}</Text>\n <Text>{buffer}</Text>\n <Text dimColor>▌</Text>\n </Box>\n );\n}"],"mappings":"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","names":["useEffect","useState","Box","Text","Box","Text","jsx","jsxs","Box","Text","useState","Box","Text","jsx","jsxs","jsx","jsxs","useEffect","useState","Box","Text","jsx"]}
|
|
@@ -28,7 +28,11 @@ export default defineConfig({
|
|
|
28
28
|
{ text: 'CLI reference', link: '/user-guide/cli' },
|
|
29
29
|
{ text: 'TUI guide', link: '/user-guide/tui' },
|
|
30
30
|
{ text: 'Gateway guide', link: '/user-guide/gateway' },
|
|
31
|
-
{ text: 'Library guide', link: '/user-guide/library' }
|
|
31
|
+
{ text: 'Library guide', link: '/user-guide/library' },
|
|
32
|
+
{ text: 'Plugins guide', link: '/user-guide/plugins' },
|
|
33
|
+
{ text: 'Godot guide', link: '/user-guide/godot' },
|
|
34
|
+
{ text: 'Redot guide', link: '/user-guide/redot' },
|
|
35
|
+
{ text: 'Games guide', link: '/user-guide/games' }
|
|
32
36
|
]
|
|
33
37
|
},
|
|
34
38
|
{
|
|
@@ -38,6 +42,7 @@ export default defineConfig({
|
|
|
38
42
|
{ text: 'Agent loop', link: '/architecture/agent-loop' },
|
|
39
43
|
{ text: 'Providers', link: '/architecture/providers' },
|
|
40
44
|
{ text: 'Tools', link: '/architecture/tools' },
|
|
45
|
+
{ text: 'Plugins', link: '/architecture/plugins' },
|
|
41
46
|
{ text: 'Extending', link: '/architecture/extending' }
|
|
42
47
|
]
|
|
43
48
|
}
|
|
@@ -187,9 +187,11 @@ coarse - see the trade-off note in [overview](./overview.md#design-trade-offs).
|
|
|
187
187
|
|
|
188
188
|
What gets persisted, per run (`Agent.persist_session`): a `run_start` meta
|
|
189
189
|
record, one `message` record per outcome message (including the seeded system
|
|
190
|
-
message and tool messages),
|
|
191
|
-
stopped on budget
|
|
192
|
-
|
|
190
|
+
message and tool messages), a `budget_exhausted` meta record when the run
|
|
191
|
+
stopped on budget, and a `run_end` meta record (`stopped_reason`, `usage`)
|
|
192
|
+
on every completed run. `usage` is the run's `usage_total`. Everything is
|
|
193
|
+
best-effort: any error logs a warning and returns `session_path: undefined`
|
|
194
|
+
instead of failing the run.
|
|
193
195
|
|
|
194
196
|
`read_session_messages(path)` parses a file back into `Message[]`: per line it
|
|
195
197
|
JSON-parses leniently, accepts only records with a `kind: "message"`-shaped
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
# Architecture Overview
|
|
2
2
|
|
|
3
|
-
Lich
|
|
3
|
+
Lich is a small TypeScript AI agent harness: it drives a chat model in a
|
|
4
4
|
think-act-observe loop, lets the model call tools, compresses history when the
|
|
5
|
-
context budget demands it, and persists transcripts.
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
context budget demands it, and persists transcripts. The published npm package
|
|
6
|
+
is 0.6.0. This tree also includes unreleased editor MCP (changelog 0.7.0);
|
|
7
|
+
`package.json` is still 0.6.0, so `lich --version` prints `0.6.0`. It runs on
|
|
8
|
+
Node >= 20 and on Bun, is ESM with NodeNext resolution, and its runtime
|
|
9
|
+
dependencies are `zod` (config validation), `ink`, and `react` (the TUI).
|
|
10
|
+
Everything else is Node/Bun built-ins.
|
|
8
11
|
|
|
9
12
|
This page is the map. The follow-up pages go deep on each area:
|
|
10
13
|
[agent loop](./agent-loop.md), [providers](./providers.md),
|
|
11
|
-
[tools](./tools.md), and [extending](./extending.md).
|
|
14
|
+
[tools](./tools.md), [plugins](./plugins.md), and [extending](./extending.md).
|
|
12
15
|
|
|
13
16
|
## Layer diagram
|
|
14
17
|
|
|
15
18
|
```mermaid
|
|
16
19
|
flowchart TB
|
|
17
20
|
subgraph entry["Entry surfaces"]
|
|
18
|
-
CLI["src/cli.ts<br/>one-shot
|
|
21
|
+
CLI["src/cli.ts<br/>one-shot, chat, tui,<br/>gateway, mcp"]
|
|
19
22
|
TUI["src/tui/app.tsx<br/>ink TUI"]
|
|
20
23
|
GW["src/gateway/runner.ts<br/>webhook, telegram,<br/>discord, twitch"]
|
|
21
24
|
LIB["src/index.ts<br/>library exports"]
|
|
@@ -94,35 +97,42 @@ Why this matters:
|
|
|
94
97
|
Walkthrough of a single `Agent.run({ input })` call
|
|
95
98
|
([`src/agent/agent.ts`](../../src/agent/agent.ts)):
|
|
96
99
|
|
|
97
|
-
1. **
|
|
100
|
+
1. **MCP attach, once.** Before the first model call, enabled `mcp_servers`
|
|
101
|
+
are connected (`initialize`, `notifications/initialized`, then `tools/list`)
|
|
102
|
+
and registered as `mcp_<server>_<tool>`. An empty `tools_enabled` never
|
|
103
|
+
connects. Disabled servers are skipped. A connect failure logs a warning
|
|
104
|
+
and the run continues. This client is in this source only (0.7.0 unreleased).
|
|
105
|
+
2. **Usage collector attached.** `run()` subscribes a `collect_usage` handler
|
|
98
106
|
on `agent.events`; every `llm_end` event adds the call's token usage into a
|
|
99
107
|
per-run `Usage` total. The subscription is removed in a `finally` block.
|
|
100
|
-
|
|
108
|
+
3. **Seed messages.** The caller's `history` (if any) is copied into a fresh
|
|
101
109
|
array and the new user message is appended. The caller's array is never
|
|
102
110
|
mutated.
|
|
103
|
-
|
|
111
|
+
4. **Loop starts.** `run_conversation(deps, seed, params)` first applies the
|
|
104
112
|
system prompt via `seed_system_prompt` (prepend, or replace an existing
|
|
105
113
|
system message if its content differs) and then enters the turn loop
|
|
106
114
|
described in [agent loop](./agent-loop.md).
|
|
107
|
-
|
|
115
|
+
5. **Each turn.** Abort check at the top of the turn, optional compression
|
|
108
116
|
check, then one LLM call through the router (`chat_with_failover`, which
|
|
109
117
|
walks providers with bounded in-place retries). The assistant message is
|
|
110
118
|
pushed onto the history.
|
|
111
|
-
|
|
112
|
-
through the `ToolExecutor` (30 s
|
|
113
|
-
and a `tool` message is appended per call.
|
|
119
|
+
6. **Tools.** If the assistant message carries `tool_calls`, each call runs
|
|
120
|
+
through the `ToolExecutor` (per-tool `timeout_ms`, else 30 s; abort linking,
|
|
121
|
+
output clamping) and a `tool` message is appended per call. `terminal` sets
|
|
122
|
+
300000 ms; `run_tests` sets 600000. The loop then starts the next
|
|
114
123
|
turn. A turn with no tool calls is the final turn.
|
|
115
|
-
|
|
124
|
+
7. **Outcome.** The loop returns a `LoopOutcome`: the full `messages` array,
|
|
116
125
|
the final assistant message (or the last one seen), the last `ChatResult`
|
|
117
126
|
on a real final, `turns_used`, and a `stopped_reason` of `final`, `budget`,
|
|
118
127
|
or `aborted`.
|
|
119
|
-
|
|
128
|
+
8. **Session persist.** `persist_session()` appends one `meta` record
|
|
120
129
|
(`run_start`), then one `message` record per outcome message, then a
|
|
121
|
-
`budget_exhausted` meta record if the budget stopped the run,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
`
|
|
125
|
-
|
|
130
|
+
`budget_exhausted` meta record if the budget stopped the run, then a
|
|
131
|
+
`run_end` meta record (`stopped_reason`, `usage`) for every completed
|
|
132
|
+
run, to a JSONL file under `session_dir` (default
|
|
133
|
+
`<work_dir>/.lich/sessions`). Persistence is best-effort: failures are
|
|
134
|
+
logged and the run still succeeds with `session_path: undefined`.
|
|
135
|
+
9. **Return.** `AgentRunResult` bundles the outcome, the full transcript
|
|
126
136
|
(prior history plus the new exchange), the collected `usage_total`, and the
|
|
127
137
|
session path.
|
|
128
138
|
|
|
@@ -149,8 +159,11 @@ Walkthrough of a single `Agent.run({ input })` call
|
|
|
149
159
|
| Path | Responsibility |
|
|
150
160
|
| --- | --- |
|
|
151
161
|
| `src/index.ts` | Public library surface; pure re-exports plus `LICH_VERSION`. |
|
|
152
|
-
| `src/cli.ts` |
|
|
153
|
-
| `src/cli_config.ts` | Config file discovery, loading, flag overrides, template. |
|
|
162
|
+
| `src/cli.ts` | CLI: one-shot, `chat`, `tui`, `gateway`, `init`, `config`, `update`, `mcp`. |
|
|
163
|
+
| `src/cli_config.ts` | Config file discovery, loading, flag overrides, template, writer. |
|
|
164
|
+
| `src/cli_update.ts` | `lich update`: npm view, then `npm install -g` when newer. Git clones are told to `git pull`. |
|
|
165
|
+
| `src/cli_mcp.ts` | `lich mcp` list/add/enable/disable/remove against work-dir config. |
|
|
166
|
+
| `src/mcp/*` | MCP client: catalog, stdio/loopback HTTP, tool registration. |
|
|
154
167
|
| `src/agent/agent.ts` | `Agent`: wires router, registry, executor; sessions; usage. |
|
|
155
168
|
| `src/agent/loop.ts` | `run_conversation`: the think-act-observe loop. |
|
|
156
169
|
| `src/agent/config.ts` | Zod config schema, defaults, derived `session_dir`, freeze. |
|
|
@@ -169,6 +182,7 @@ Walkthrough of a single `Agent.run({ input })` call
|
|
|
169
182
|
| `src/tools/registry.ts` | Name-keyed tool registry; duplicate rejection. |
|
|
170
183
|
| `src/tools/executor.ts` | Never-throw execution with timeout and abort. |
|
|
171
184
|
| `src/tools/builtin/*` | Builtin tools, including `run_tests` (see [tools](./tools.md)). |
|
|
185
|
+
| `src/plugins/*` | Plugin loader and hooks. Gatekeeper registers `git_commit` in code. |
|
|
172
186
|
| `src/gateway/bus.ts` | Conversation-keyed runner over one shared `Agent`. |
|
|
173
187
|
| `src/gateway/runner.ts` | Adapter construction, signal handling, process lifetime. |
|
|
174
188
|
| `src/gateway/{telegram,discord,twitch,webhook}.ts` | Platform adapters. |
|
|
@@ -59,7 +59,7 @@ sequenceDiagram
|
|
|
59
59
|
end
|
|
60
60
|
```
|
|
61
61
|
|
|
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})`
|
|
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})` in `finally` only when the loop returned an outcome (`final`, `budget`, or `aborted`). A provider throw leaves `outcome` undefined, so `on_run_end` is skipped and no `run_end` session record is written. Both fan-outs are best-effort: hook throws are logged at `warn` and the run proceeds.
|
|
63
63
|
|
|
64
64
|
## Builtin gatekeeper
|
|
65
65
|
|
|
@@ -39,7 +39,9 @@ result either way.
|
|
|
39
39
|
**`ToolContext`** gives each execution a working directory (`work_dir`, the
|
|
40
40
|
confinement root), a process environment map (the agent injects
|
|
41
41
|
`LICH_TERMINAL_TIMEOUT_MS`), and an abort `signal` that fires on caller abort
|
|
42
|
-
**or** the executor
|
|
42
|
+
**or** the executor deadline (`tool.timeout_ms`, else
|
|
43
|
+
`DEFAULT_TOOL_TIMEOUT_MS` = 30000). `terminal` sets 300000, `run_tests` sets
|
|
44
|
+
600000, and registered MCP tools set 120000.
|
|
43
45
|
|
|
44
46
|
**Parameter schemas.** `parameters` is a `JsonSchemaObject`
|
|
45
47
|
(`src/util/json_schema.ts`) passed through verbatim into provider requests.
|
|
@@ -187,6 +189,9 @@ instructions. See the [plugins guide](../user-guide/plugins.md#skills-and-memory
|
|
|
187
189
|
- **Enabling a subset** is done by rebuilding a fresh registry: `Agent`'s
|
|
188
190
|
`filter_registry` (src/agent/agent.ts) iterates `base.list()` and registers
|
|
189
191
|
only allowed names onto a new `ToolRegistry` when `tools_enabled` is a list
|
|
190
|
-
(`"all"` returns the base registry untouched).
|
|
192
|
+
(`"all"` returns the base registry untouched). Plugin tools, including the
|
|
193
|
+
gatekeeper's `git_commit`, register after that filter, so `tools_enabled: []`
|
|
194
|
+
still leaves `git_commit`. MCP tools register later, on first `run()`, and
|
|
195
|
+
only when the allowlist is `"all"` or names an `mcp_` tool.
|
|
191
196
|
|
|
192
197
|
For building your own tool, see [extending](./extending.md#add-a-builtin-tool).
|
package/docs/getting-started.md
CHANGED
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
npm install -g @moikapy/lich
|
|
13
|
-
lich --version # -> 0.
|
|
13
|
+
lich --version # -> 0.6.0 (reads package.json)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Choose a configuration path
|
|
17
17
|
|
|
18
|
-
Lich needs exactly one thing before it runs: a model. You can provide it three ways
|
|
18
|
+
Lich needs exactly one thing before it runs: a model. You can provide it three ways. Flags override a config file and override `LICH_*` env vars. Those env vars apply only when no config file is found; they are not merged into a discovered file.
|
|
19
19
|
|
|
20
20
|
### Path A: environment variables only
|
|
21
21
|
|
|
@@ -42,7 +42,7 @@ lich config > .lich/config.json
|
|
|
42
42
|
|
|
43
43
|
`lich config` honors `LICH_PROVIDER_KIND` and `LICH_MODEL` when you have them set, and otherwise prints an ollama-oriented template. The file is picked up automatically from `.lich/config.json` in the working directory (or `~/.config/lich/config.json` as a fallback) — after this, plain `lich "task"` needs no env vars.
|
|
44
44
|
|
|
45
|
-
`lich init` writes that same starter file for you (it creates `.lich/` and never overwrites an existing `.lich/config.json`). Bare `lich` on a TTY, with no config in that search chain
|
|
45
|
+
`lich init` writes that same starter file for you (it creates `.lich/` and never overwrites an existing `.lich/config.json`). Bare `lich` on a TTY, with no config in that search chain, runs a setup wizard and writes `.lich/config.json` once before opening the TUI. `LICH_MODEL` / `--model` prefills the model prompt; it does not skip the wizard. Non-TTY stdin skips the wizard. `.lich/` is gitignored.
|
|
46
46
|
|
|
47
47
|
### Path C: an explicit config file
|
|
48
48
|
|
|
@@ -121,10 +121,10 @@ jq -r 'select(.kind=="message") | "\(.message.role): \(.message.content)"' .lich
|
|
|
121
121
|
| Symptom | Cause and fix |
|
|
122
122
|
| --- | --- |
|
|
123
123
|
| `no model configured: set LICH_MODEL, pass --model, or create .lich/config.json` | No provider was resolvable. Set `LICH_MODEL`, pass `--model`, or save a config file (`lich init` or `lich config`). |
|
|
124
|
-
| `lich: config
|
|
124
|
+
| `lich: cannot use config file <path>: ...` | `--config` was given a path that does not exist or is not readable JSON. Check the path or drop the flag to use discovery. |
|
|
125
125
|
| Provider error `kind=auth`, http 401/403 | The api key is missing or wrong. Verify the env var named by `LICH_API_KEY_ENV` (default `OPENAI_API_KEY`/`ANTHROPIC_API_KEY`) is exported in the same shell. |
|
|
126
126
|
| `fetch failed` / connection refused | The endpoint is unreachable. For ollama, check `ollama serve` is running on `http://localhost:11434`; for remote APIs, check `LICH_BASE_URL`. |
|
|
127
|
-
| `[lich] budget exhausted after N turns` | The task did not finish within `max_turns` (default 25). Raise
|
|
127
|
+
| `[lich] budget exhausted after N turns — the ritual is spent` | The task did not finish within `max_turns` (default 25). The `budget exhausted` keyword stays; the flavor suffix comes from the theme. Raise the cap with `--max-turns 50` or in config. |
|
|
128
128
|
| `unknown flag: --foo` | Flag typo, or the flag was placed where a subcommand is expected. Run `lich --help`. |
|
|
129
129
|
|
|
130
130
|
## Updating
|
|
@@ -153,7 +153,7 @@ To hack on Lich itself, run the CLI straight from a clone instead of the npm pac
|
|
|
153
153
|
```sh
|
|
154
154
|
git clone https://github.com/Moikapy/lich.git && cd lich
|
|
155
155
|
bun install
|
|
156
|
-
bun src/cli.ts --version # -> 0.
|
|
156
|
+
bun src/cli.ts --version # -> 0.6.0 (package.json; MCP in this tree is unreleased)
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
`bun src/cli.ts` accepts the same arguments as the installed `lich` binary, so every command on this page works unchanged.
|
|
@@ -163,4 +163,6 @@ bun src/cli.ts --version # -> 0.3.0
|
|
|
163
163
|
- All four CLI modes, flags, and provider resolution: [CLI reference](user-guide/cli.md).
|
|
164
164
|
- Slash commands and the status bar: [TUI guide](user-guide/tui.md).
|
|
165
165
|
- Telegram, Discord, Twitch, and webhook setup: [Gateway guide](user-guide/gateway.md).
|
|
166
|
-
- Embedding the agent in your own TypeScript: [Library guide](user-guide/library.md).
|
|
166
|
+
- Embedding the agent in your own TypeScript: [Library guide](user-guide/library.md).
|
|
167
|
+
- Session JSONL as a combat log: [Games guide](user-guide/games.md).
|
|
168
|
+
- Editor MCP (`lich mcp`, unreleased in the published 0.6.0 package): [Redot guide](user-guide/redot.md). Play is still the [Godot guide](user-guide/godot.md).
|
package/docs/index.md
CHANGED
|
@@ -8,7 +8,7 @@ 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 builtin tools, the same provider configuration, and the same session store.
|
|
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. `lich init`, `lich config`, `lich update`, and `lich mcp` do not start that loop.
|
|
12
12
|
|
|
13
13
|
## Feature overview
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ One package, four ways to drive the same agent: a one-shot CLI, an interactive c
|
|
|
18
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
|
-
| CLI | One-shot tasks, chat REPL, TUI, gateway,
|
|
21
|
+
| CLI | One-shot tasks, chat REPL, TUI, gateway, `init`, `update`, `config`, and (in this source) `lich mcp`. Flag, env, and config-file configuration. |
|
|
22
22
|
| TUI | Live ink transcript with tool-call rows, status bar (model, turns, tokens, session path), and slash commands. |
|
|
23
23
|
| Gateway | One shared agent behind webhook/Telegram/Discord/Twitch with per-conversation memory (40-message history cap) and per-platform message splitting. |
|
|
24
24
|
| Library | `create_agent` / `run_agent` with typed events (`AgentEmitter`), multi-turn history, and `ProviderError` kinds for error handling. |
|
|
@@ -29,12 +29,14 @@ One package, four ways to drive the same agent: a one-shot CLI, an interactive c
|
|
|
29
29
|
| Page | Read it to |
|
|
30
30
|
| --- | --- |
|
|
31
31
|
| [Getting started](getting-started.md) | Install, configure a provider, and get your first reply in any mode. |
|
|
32
|
-
| [CLI reference](user-guide/cli.md) |
|
|
32
|
+
| [CLI reference](user-guide/cli.md) | Modes, flags, provider resolution, config files, and `lich mcp`. |
|
|
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
36
|
| [Plugins guide](user-guide/plugins.md) | Add your own tools and lifecycle hooks, and run the self-improvement loop. |
|
|
37
37
|
| [Godot guide](user-guide/godot.md) | Run lich beside a Godot game and drain `.lich/game/` orders each tick. |
|
|
38
|
+
| [Redot guide](user-guide/redot.md) | Add editor MCP servers (Redot catalog entry). Play still uses the game bridge, the opposite direction. |
|
|
39
|
+
| [Games guide](user-guide/games.md) | Replay session JSONL as a combat log, including token totals. |
|
|
38
40
|
| [Architecture overview](architecture/overview.md) | Understand how the harness works inside. |
|
|
39
41
|
|
|
40
42
|
## How it works
|
|
@@ -66,4 +68,4 @@ Working from a clone of the repository? `bun install`, then run the same command
|
|
|
66
68
|
|
|
67
69
|
## Version compatibility
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
The published npm package is **0.6.0**. `lich --version` reads `package.json`, so this tree also prints `0.6.0`. Editor MCP (`mcp_servers`, `lich mcp`) is in this source under changelog **0.7.0 (unreleased)** and is not in the published package. Node >= 20 (`engines` in `package.json`); Bun is the recommended runtime for development from a clone (`bun src/cli.ts ...`). The TUI needs a TTY; the gateway and library run headless on both runtimes.
|