@moikapy/lich 0.3.1 → 0.5.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 +42 -0
- package/README.md +12 -2
- package/dist/{chunk-P52U5M3L.js → chunk-CV2YH3FH.js} +449 -71
- package/dist/chunk-CV2YH3FH.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +590 -25
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-CWPVIU3W.js → gateway-W6S43ETE.js} +27 -18
- package/dist/gateway-W6S43ETE.js.map +1 -0
- package/dist/index.d.ts +58 -11
- package/dist/index.js +1 -1
- package/dist/{tui-K3EPRXTV.js → tui-DT7XWDTX.js} +8 -5
- package/dist/tui-DT7XWDTX.js.map +1 -0
- package/docs/architecture/overview.md +8 -6
- package/docs/architecture/plugins.md +57 -4
- package/docs/architecture/tools.md +16 -4
- package/docs/getting-started.md +15 -4
- package/docs/index.md +4 -3
- package/docs/user-guide/cli.md +24 -3
- package/docs/user-guide/godot.md +160 -0
- package/docs/user-guide/library.md +4 -2
- package/docs/user-guide/plugins.md +79 -5
- package/docs/user-guide/tui.md +4 -3
- package/examples/game_bridge/README.md +68 -0
- package/examples/game_bridge/bridge_io.mjs +60 -0
- package/examples/game_bridge/bridge_paths.mjs +11 -0
- package/examples/game_bridge/dungeon_memory.mjs +56 -0
- package/examples/game_bridge/enemy_actions.mjs +44 -0
- package/examples/game_bridge/game_bridge.plugin.mjs +17 -0
- package/examples/game_bridge/meteor_veto.mjs +22 -0
- package/examples/game_bridge/schemas.mjs +48 -0
- package/examples/game_bridge/snapshot.mjs +19 -0
- package/examples/game_bridge/validate_order.mjs +44 -0
- package/package.json +2 -1
- package/dist/chunk-P52U5M3L.js.map +0 -1
- package/dist/gateway-CWPVIU3W.js.map +0 -1
- package/dist/tui-K3EPRXTV.js.map +0 -1
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;
|
|
@@ -201,6 +212,8 @@ declare class ProviderError extends Error {
|
|
|
201
212
|
*/
|
|
202
213
|
|
|
203
214
|
declare const agent_config_schema: z.ZodEffects<z.ZodObject<{
|
|
215
|
+
/** Display name used by the TUI banner. */
|
|
216
|
+
agent_name: z.ZodDefault<z.ZodString>;
|
|
204
217
|
system_prompt: z.ZodOptional<z.ZodString>;
|
|
205
218
|
max_turns: z.ZodDefault<z.ZodNumber>;
|
|
206
219
|
providers: z.ZodArray<z.ZodObject<{
|
|
@@ -244,9 +257,21 @@ declare const agent_config_schema: z.ZodEffects<z.ZodObject<{
|
|
|
244
257
|
terminal_timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
245
258
|
/** Plugin entry module specifiers, relative to work_dir or absolute. */
|
|
246
259
|
plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
260
|
+
gateway: z.ZodOptional<z.ZodObject<{
|
|
261
|
+
platforms: z.ZodDefault<z.ZodArray<z.ZodEnum<["webhook", "telegram", "discord", "twitch"]>, "many">>;
|
|
262
|
+
/** Env-var names that hold tokens. Never store the secrets themselves. */
|
|
263
|
+
token_envs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
264
|
+
}, "strip", z.ZodTypeAny, {
|
|
265
|
+
platforms: ("webhook" | "telegram" | "discord" | "twitch")[];
|
|
266
|
+
token_envs: Record<string, string>;
|
|
267
|
+
}, {
|
|
268
|
+
platforms?: ("webhook" | "telegram" | "discord" | "twitch")[] | undefined;
|
|
269
|
+
token_envs?: Record<string, string> | undefined;
|
|
270
|
+
}>>;
|
|
247
271
|
log_level: z.ZodDefault<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
248
272
|
}, "strip", z.ZodTypeAny, {
|
|
249
273
|
plugins: string[];
|
|
274
|
+
agent_name: string;
|
|
250
275
|
max_turns: number;
|
|
251
276
|
providers: z.objectOutputType<{
|
|
252
277
|
kind: z.ZodEnum<["openai_compat", "anthropic", "ollama"]>;
|
|
@@ -269,6 +294,10 @@ declare const agent_config_schema: z.ZodEffects<z.ZodObject<{
|
|
|
269
294
|
system_prompt?: string | undefined;
|
|
270
295
|
work_dir?: string | undefined;
|
|
271
296
|
session_dir?: string | undefined;
|
|
297
|
+
gateway?: {
|
|
298
|
+
platforms: ("webhook" | "telegram" | "discord" | "twitch")[];
|
|
299
|
+
token_envs: Record<string, string>;
|
|
300
|
+
} | undefined;
|
|
272
301
|
}, {
|
|
273
302
|
providers: z.objectInputType<{
|
|
274
303
|
kind: z.ZodEnum<["openai_compat", "anthropic", "ollama"]>;
|
|
@@ -284,6 +313,7 @@ declare const agent_config_schema: z.ZodEffects<z.ZodObject<{
|
|
|
284
313
|
plugins?: string[] | undefined;
|
|
285
314
|
temperature?: number | undefined;
|
|
286
315
|
max_tokens?: number | undefined;
|
|
316
|
+
agent_name?: string | undefined;
|
|
287
317
|
system_prompt?: string | undefined;
|
|
288
318
|
max_turns?: number | undefined;
|
|
289
319
|
work_dir?: string | undefined;
|
|
@@ -292,12 +322,17 @@ declare const agent_config_schema: z.ZodEffects<z.ZodObject<{
|
|
|
292
322
|
compress_threshold?: number | undefined;
|
|
293
323
|
session_dir?: string | undefined;
|
|
294
324
|
terminal_timeout_ms?: number | undefined;
|
|
325
|
+
gateway?: {
|
|
326
|
+
platforms?: ("webhook" | "telegram" | "discord" | "twitch")[] | undefined;
|
|
327
|
+
token_envs?: Record<string, string> | undefined;
|
|
328
|
+
} | undefined;
|
|
295
329
|
log_level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
296
330
|
}>, {
|
|
297
331
|
work_dir: string;
|
|
298
332
|
providers: ProviderConfig[];
|
|
299
333
|
session_dir: string;
|
|
300
334
|
plugins: string[];
|
|
335
|
+
agent_name: string;
|
|
301
336
|
max_turns: number;
|
|
302
337
|
tools_enabled: string[] | "all";
|
|
303
338
|
context_budget_tokens: number;
|
|
@@ -307,6 +342,10 @@ declare const agent_config_schema: z.ZodEffects<z.ZodObject<{
|
|
|
307
342
|
temperature?: number | undefined;
|
|
308
343
|
max_tokens?: number | undefined;
|
|
309
344
|
system_prompt?: string | undefined;
|
|
345
|
+
gateway?: {
|
|
346
|
+
platforms: ("webhook" | "telegram" | "discord" | "twitch")[];
|
|
347
|
+
token_envs: Record<string, string>;
|
|
348
|
+
} | undefined;
|
|
310
349
|
}, {
|
|
311
350
|
providers: z.objectInputType<{
|
|
312
351
|
kind: z.ZodEnum<["openai_compat", "anthropic", "ollama"]>;
|
|
@@ -322,6 +361,7 @@ declare const agent_config_schema: z.ZodEffects<z.ZodObject<{
|
|
|
322
361
|
plugins?: string[] | undefined;
|
|
323
362
|
temperature?: number | undefined;
|
|
324
363
|
max_tokens?: number | undefined;
|
|
364
|
+
agent_name?: string | undefined;
|
|
325
365
|
system_prompt?: string | undefined;
|
|
326
366
|
max_turns?: number | undefined;
|
|
327
367
|
work_dir?: string | undefined;
|
|
@@ -330,6 +370,10 @@ declare const agent_config_schema: z.ZodEffects<z.ZodObject<{
|
|
|
330
370
|
compress_threshold?: number | undefined;
|
|
331
371
|
session_dir?: string | undefined;
|
|
332
372
|
terminal_timeout_ms?: number | undefined;
|
|
373
|
+
gateway?: {
|
|
374
|
+
platforms?: ("webhook" | "telegram" | "discord" | "twitch")[] | undefined;
|
|
375
|
+
token_envs?: Record<string, string> | undefined;
|
|
376
|
+
} | undefined;
|
|
333
377
|
log_level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
334
378
|
}>;
|
|
335
379
|
type AgentConfig = z.infer<typeof agent_config_schema>;
|
|
@@ -445,6 +489,7 @@ declare class Agent {
|
|
|
445
489
|
private readonly hook_runner;
|
|
446
490
|
constructor(config: AgentConfig, plugins?: readonly LoadedPlugin[]);
|
|
447
491
|
run(options: AgentRunOptions): Promise<AgentRunResult>;
|
|
492
|
+
/** Per-run deps: the built-once ToolContext threads through every tool execution. */
|
|
448
493
|
private loop_deps;
|
|
449
494
|
/** Best-effort on_run_start fan-out; hook errors are logged, never fatal. */
|
|
450
495
|
private call_plugin_run_start;
|
|
@@ -503,7 +548,11 @@ declare function register_builtin_tools(registry: ToolRegistry, context?: ToolCo
|
|
|
503
548
|
*
|
|
504
549
|
* before_tool_call hooks run in registration order and may veto a call (first
|
|
505
550
|
* 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
|
|
551
|
+
* and skipped, never fatal. after_tool_call hooks observe the result summary
|
|
552
|
+
* plus the executor's structured ok/error fields. Every hook invocation
|
|
553
|
+
* receives a ctx exposing only its own plugin's state sub-map: the bag is a
|
|
554
|
+
* module-internal WeakMap keyed on the plugin object, swapped fresh at each
|
|
555
|
+
* call_run_start and shared by the lifecycle and per-tool ctx build sites.
|
|
507
556
|
*/
|
|
508
557
|
|
|
509
558
|
/** Structural ToolRunner shape accepted from the wrapped executor. */
|
|
@@ -511,27 +560,25 @@ interface WrappedToolRunner {
|
|
|
511
560
|
execute(name: string, args: Record<string, unknown>, context?: ToolContext): Promise<ToolResult>;
|
|
512
561
|
}
|
|
513
562
|
/**
|
|
514
|
-
*
|
|
515
|
-
*
|
|
563
|
+
* Hooks stay attached to their plugin (no flattening) so each invocation can
|
|
564
|
+
* be handed a ctx exposing only that plugin's sub-map; hooks always run in
|
|
565
|
+
* plugin registration order.
|
|
516
566
|
*/
|
|
517
567
|
declare class HookedToolRunner {
|
|
518
568
|
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[]);
|
|
569
|
+
private readonly hooked_plugins;
|
|
570
|
+
constructor(wrapped: WrappedToolRunner, plugins: readonly Plugin[]);
|
|
524
571
|
/** Run before hooks in order; the first {block: true} verdict wins. */
|
|
525
572
|
private run_before_hooks;
|
|
526
573
|
/** Fire-and-forget in spirit but awaited here so runs settle cleanly. */
|
|
527
574
|
private run_after_hooks;
|
|
528
575
|
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. */
|
|
576
|
+
/** 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
577
|
call_run_start(info: {
|
|
531
578
|
input_chars: number;
|
|
532
|
-
},
|
|
579
|
+
}, base: HookContext): Promise<void>;
|
|
533
580
|
/** Best-effort on_run_end fan-out used by Agent.run; never throws. */
|
|
534
|
-
call_run_end(info: RunEndInfo,
|
|
581
|
+
call_run_end(info: RunEndInfo, base: HookContext): Promise<void>;
|
|
535
582
|
}
|
|
536
583
|
|
|
537
584
|
/**
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
LICH_VERSION
|
|
3
3
|
} from "./chunk-ZVK3MUPC.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
create_agent_with_plugins,
|
|
6
6
|
truncate_text
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-CV2YH3FH.js";
|
|
8
8
|
|
|
9
9
|
// src/tui.tsx
|
|
10
10
|
import { render } from "ink";
|
|
@@ -90,6 +90,9 @@ function parse_command(raw_input) {
|
|
|
90
90
|
}
|
|
91
91
|
return { kind: "slash", name: body.slice(0, space_index), args: body.slice(space_index + 1).trim() };
|
|
92
92
|
}
|
|
93
|
+
function tui_banner_text(agent_name, version, model, kind) {
|
|
94
|
+
return `${agent_name} v${version} \u2014 ${model} (${kind})`;
|
|
95
|
+
}
|
|
93
96
|
function format_usage(total_tokens) {
|
|
94
97
|
return total_tokens.toLocaleString("en-US");
|
|
95
98
|
}
|
|
@@ -412,7 +415,7 @@ function TuiApp({ agent }) {
|
|
|
412
415
|
);
|
|
413
416
|
const provider = agent.config.providers[0];
|
|
414
417
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", minHeight: 8, children: [
|
|
415
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children:
|
|
418
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: tui_banner_text(agent.config.agent_name, LICH_VERSION, provider?.model ?? "unknown", provider?.kind ?? "unknown") }),
|
|
416
419
|
/* @__PURE__ */ jsx4(MessageView, { blocks, state }),
|
|
417
420
|
/* @__PURE__ */ jsx4(StatusBar, { state, model: provider?.model ?? "unknown" }),
|
|
418
421
|
/* @__PURE__ */ jsx4(CommandBar, { busy: state.phase !== "idle", on_submit: submit })
|
|
@@ -422,7 +425,7 @@ function TuiApp({ agent }) {
|
|
|
422
425
|
// src/tui.tsx
|
|
423
426
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
424
427
|
async function run_tui(config) {
|
|
425
|
-
const agent =
|
|
428
|
+
const agent = await create_agent_with_plugins(config);
|
|
426
429
|
const instance = render(/* @__PURE__ */ jsx5(TuiApp, { agent }));
|
|
427
430
|
await instance.waitUntilExit();
|
|
428
431
|
return 0;
|
|
@@ -430,4 +433,4 @@ async function run_tui(config) {
|
|
|
430
433
|
export {
|
|
431
434
|
run_tui
|
|
432
435
|
};
|
|
433
|
-
//# sourceMappingURL=tui-
|
|
436
|
+
//# sourceMappingURL=tui-DT7XWDTX.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\";\n\nexport async function run_tui(config: AgentConfig): Promise<number> {\n const agent = await create_agent_with_plugins(config);\n const instance = render(<TuiApp agent={agent} />);\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 { 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): 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)];\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): 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, 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 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));\n }, [add_blocks, set_state]);\n\n const start_message_run = useCallback(\n (text: string): void => {\n add_blocks([{ role: \"user\", lines: [`you › ${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)].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],\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(agent: Agent, add_blocks: AddBlocks, set_blocks: SetBlocks, total_tokens: number): (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).then((block) => add_blocks([block]));\n } else {\n add_blocks([unknown_command_block(parsed.name)]);\n }\n },\n [agent, add_blocks, set_blocks, total_tokens],\n );\n return handle;\n}\n\ninterface TuiAppProps {\n readonly agent: Agent;\n}\n\nexport function TuiApp({ agent }: 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, add_blocks, set_state, set_blocks);\n const handle_slash = use_slash_commands(agent, 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(agent.config.agent_name, LICH_VERSION, provider?.model ?? \"unknown\", provider?.kind ?? \"unknown\")}</Text>\n <MessageView blocks={blocks} state={state} />\n <StatusBar state={state} model={provider?.model ?? \"unknown\"} />\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\";\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 line: agent name, version, model, provider kind. */\nexport function tui_banner_text(agent_name: string, version: string, model: string, kind: string): string {\n return `${agent_name} v${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): string[] {\n if (message.role === \"user\") {\n return [`you \\u203a ${message.content}`];\n }\n if (message.role === \"assistant\") {\n const lines = [`lich \\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): 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) };\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) };\n}\n\n/** Keep the newest `cap` non-system messages as renderable blocks. */\nexport function split_history_blocks(messages: readonly Message[], cap: number): 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(format_message_block);\n}\n\nfunction assistant_result_block(message: AssistantMessage): HistoryBlock | undefined {\n if (message.content.length === 0) {\n return undefined;\n }\n return { role: \"lich\", lines: [`lich \\u203a ${message.content}`] };\n}\n\n/** Post-run meta blocks: compression notices, budget, errors, final answer. */\nexport function run_notice_blocks(result: AgentRunResult): HistoryBlock[] {\n const blocks: HistoryBlock[] = [];\n if (result.outcome.stopped_reason === \"budget\") {\n blocks.push({ role: \"error\", lines: [\"\\u00b7 budget exhausted (turn cap reached)\"] });\n }\n const final_block = result.outcome.final === undefined ? undefined : assistant_result_block(result.outcome.final);\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): HistoryBlock {\n return { role: \"meta\", lines: [`\\u00b7 context compressed (summary ${summary_chars} chars)`] };\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[], 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 lines: string[] = [`\\u00b7 sessions (${sorted.length}):`];\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 { format_usage, type UiState } from \"./state.js\";\n\nconst PHASE_LABELS: Record<UiState[\"phase\"], string> = {\n idle: \"idle\",\n thinking: \"thinking\",\n tool: \"tool\",\n};\n\ninterface StatusBarProps {\n readonly state: UiState;\n readonly model: string;\n}\n\nexport function StatusBar({ state, model }: StatusBarProps): React.JSX.Element {\n const phase = 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\"> · 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,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/getting-started.md
CHANGED
|
@@ -42,6 +42,8 @@ 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 and no `LICH_MODEL`, runs a setup wizard and writes `.lich/config.json` once before opening the TUI. `.lich/` is gitignored.
|
|
46
|
+
|
|
45
47
|
### Path C: an explicit config file
|
|
46
48
|
|
|
47
49
|
```sh
|
|
@@ -70,7 +72,8 @@ Exit code `0` means the model produced a final answer; `1` means the turn budget
|
|
|
70
72
|
## Your first TUI session
|
|
71
73
|
|
|
72
74
|
```sh
|
|
73
|
-
lich
|
|
75
|
+
lich # TUI; first run on a TTY opens the setup wizard
|
|
76
|
+
lich tui # same TUI, no wizard
|
|
74
77
|
```
|
|
75
78
|
|
|
76
79
|
Type a message and press Enter. The transcript shows your line, live tool-call rows while the agent works, and the reply; the status bar at the bottom tracks turns, tokens, and the session file path. Slash commands: `/help`, `/model`, `/usage`, `/clear`, `/sessions`, `/exit`. Details in [the TUI guide](user-guide/tui.md).
|
|
@@ -117,7 +120,7 @@ jq -r 'select(.kind=="message") | "\(.message.role): \(.message.content)"' .lich
|
|
|
117
120
|
|
|
118
121
|
| Symptom | Cause and fix |
|
|
119
122
|
| --- | --- |
|
|
120
|
-
| `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 config`). |
|
|
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`). |
|
|
121
124
|
| `lich: config not found: <path>` | `--config` was given a path that does not exist. Check the path or drop the flag to use discovery. |
|
|
122
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. |
|
|
123
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`. |
|
|
@@ -126,13 +129,21 @@ jq -r 'select(.kind=="message") | "\(.message.role): \(.message.content)"' .lich
|
|
|
126
129
|
|
|
127
130
|
## Updating
|
|
128
131
|
|
|
129
|
-
|
|
132
|
+
Check the npm registry and install a newer release with:
|
|
130
133
|
|
|
131
134
|
```sh
|
|
132
|
-
|
|
135
|
+
lich update
|
|
133
136
|
lich --version # -> the version you just installed
|
|
134
137
|
```
|
|
135
138
|
|
|
139
|
+
`lich update` compares the installed version to `npm view @moikapy/lich version`. When the registry copy is newer, it runs the equivalent command:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
npm install -g @moikapy/lich@latest
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Exit any running `lich tui` or `lich gateway` first — npm cannot replace the package while those processes are running. A git clone updates with `git pull` instead; `npx` cannot persist an update.
|
|
146
|
+
|
|
136
147
|
Updates never touch your data: the per-project `.lich/` directory holds your config and session transcripts, installers neither read nor migrate it, and it is gitignored by design so a checkout never collides with it. For what changed between versions, see the [changelog](https://github.com/moikapy/lich/blob/main/CHANGELOG.md).
|
|
137
148
|
|
|
138
149
|
## Development install (from source)
|
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,8 @@ 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
|
+
| [Godot guide](user-guide/godot.md) | Run lich beside a Godot game and drain `.lich/game/` orders each tick. |
|
|
37
38
|
| [Architecture overview](architecture/overview.md) | Understand how the harness works inside. |
|
|
38
39
|
|
|
39
40
|
## How it works
|