Files
laya-hook/README.md
2026-09-24 14:27:07 +08:00

165 lines
8.5 KiB
Markdown

# Laya Router Hook
[中文说明](README.zh-CN.md)
A local [Laya](https://github.com/NandhaKishorM/laya) multilingual service that scores each coding prompt before Codex or Claude Code sees it. The hook adds a System-1 hint: task level, and whether the agent should plan, read the repo, look up docs, or reason carefully.
It does not switch models. Codex and Claude Code keep the model already configured for that session.
```text
Prompt
-> UserPromptSubmit hook (stdlib only, fail-open)
-> http://127.0.0.1:8765
-> Laya multilingual
-> additionalContext
-> Codex / Claude Code
```
A browser dashboard at [http://127.0.0.1:8765/](http://127.0.0.1:8765/) shows each decision, grouped by session, and updates over server-sent events.
![Dashboard](docs/assets/dashboard.png)
The left rail lists sessions with their latest prompt, client, infer count, and the System-1 level tag. The main panel expands one session across calls; each call shows the question that was asked, the per-axis probabilities, and the model's plain-language verdict. SSE pushes new rows without a refresh.
Cursor loads Claude Code hooks, but its prompt event cannot inject context. The hook sees `CURSOR_VERSION` and returns immediately.
## Install
macOS, [uv](https://docs.astral.sh/uv/getting-started/installation/), and Python 3.10+.
```bash
./install.sh
```
The script:
1. Creates `~/.laya-router/.venv` and installs this repo into it
2. Downloads the `multilingual` weights of `convaiinnovations/laya`
3. Registers a launchd agent, `com.laya-router`, that starts at login and restarts if it exits
4. Writes a hook when Codex or Claude Code is already installed
The install is a normal copy, not an editable install. launchd blocks forever if it has to import a checkout under `~/Documents`. `laya-router restart` copies the repo into the venv again and restarts the service.
One client only:
```bash
./install.sh --only codex
./install.sh --only claude
```
The first Codex session must trust the hook. Start `codex` and run `/hooks`. Editing the Python files does not change the command string, so Codex does not ask for review again.
## Commands
After install, the binaries live in `~/.laya-router/.venv/bin`.
```bash
laya-router status
laya-router logs
laya-router logs -f
laya-router route "Rename the variables in this function"
laya-router restart
laya-router stop
laya-router start
```
Remove the hooks and the launchd agent, and keep the runtime directory:
```bash
./uninstall.sh
```
Also delete config, logs, and the virtualenv:
```bash
./uninstall.sh --purge
```
## Configuration
`~/.laya-router/config.toml` is written on first install and is not overwritten later.
| Key | Meaning |
| --- | --- |
| `host` | Bind address. Keep `127.0.0.1`. |
| `port` | Default `8765`. |
| `device` | `auto`, `mps`, `cuda`, or `cpu`. `auto` falls through if warmup fails. |
| `log_prompts` | Write the raw prompt into `data/routes.jsonl`. |
| `request_timeout` | Seconds the prompt hook waits for the service. Default `2.5`. |
| `event_timeout` | Seconds a tool hook waits. Default `1.0`. |
| `max_prompt_chars` | Max characters sent to the model. Longer prompts keep the head and tail. |
| `trajectory` | Inject a short hint after tool calls, and before an edit of unread code. |
| `gate` | Tighten risky Bash and edit calls. Never returns allow. |
| `stop_check` | Ask the agent to verify after edits before it stops. |
| `skills` | Suggest at most one installed skill on each user prompt. |
| `skill_inject` | `suggest` names the skill. `content` attaches `SKILL.md` once per session. |
Restart after config or server changes: `laya-router restart`.
## How a decision is made
One forward pass asks five questions:
- `task_level`: `simple`, `normal`, or `complex`
- `needs_plan`, `needs_repo`, `needs_web`, `needs_deep_reasoning`: probabilities from 0 to 1
`laya-multilingual` ships without fitted temperatures, so a displayed 0.90 is not a calibrated 90%. The injected text says the numbers are hints. Ordinal `score` is not used; Laya's own benchmarks show it is the weakest primitive.
Every call is appended to `~/.laya-router/data/routes.jsonl` (timestamp, client, working directory, session, prompt unless disabled, scores, latency, question version). That file is the material for a later coding-specific fine-tune.
If the service is down, the hook prints `{}` and exits 0. Codex and Claude Code continue. The error is appended to `~/.laya-router/logs/hook.log`.
## During a turn
The same hook command also listens for `PreToolUse`, `PostToolUse`, and `Stop`. The resident service keeps the session trace in memory.
- `PreToolUse` on Bash or an edit: a high risk is denied; a medium risk asks on Claude Code and warns on Codex, because Codex rejects `permissionDecision: ask`. An edit of a file that was not read gets a short hint.
- `PostToolUse`: every Bash or edit checkpoint gets a short hint. The same hint is not repeated. Two failures in a row, or a high `is_stuck`, tells the agent to stop and replan. `Read`, `Grep`, and `Glob` are recorded and stay quiet.
- `Stop`: if code changed and nothing verified it, the hook blocks once so the agent keeps going. `stop_hook_active` is always allowed through.
On `UserPromptSubmit`, a second pair of questions can name one installed skill. The roster is every `SKILL.md` under `~/.claude/skills`, `~/.codex/skills`, `~/.agents/skills`, the project skill directories, and Claude plugin install paths. A skill with `disable-model-invocation: true` is skipped. The first pass ranks the roster and gates on whether a skill is warranted. The second pass re-reads the top three. Either threshold below 0.30 suggests nothing.
`laya-mcp` exposes `laya_judge` over stdio. Allow `mcp__laya` in Claude Code if you want the agent to call it. Install registers it with `claude mcp add` and `codex mcp add` when those CLIs are on `PATH`.
The first Codex session must trust the new hook entries. Run `/hooks` again after upgrading.
## Develop
```bash
PYTHONPATH=src uv run --no-project --with pytest python -m pytest
```
Unit tests do not load the model. After the service is up, `laya-router route` runs a real inference.
| File | Role | Picked up |
| --- | --- | --- |
| `src/laya_router/questions.py` | Question text. Bump `QUESTIONS_VERSION` when the wording changes. | `laya-router restart` |
| `src/laya_router/render.py` | Text injected into the agent | `laya-router restart`, then the next prompt |
| `src/laya_router/hook.py` | Prompt and tool events, timeout, fail-open | `laya-router restart`, then the next prompt |
| `src/laya_router/server.py` | Model load, `/route`, dashboard | `laya-router restart` |
| `src/laya_router/dashboard.html` | Browser UI | `laya-router restart` |
Do not run the server with `--reload`. A reloader starts extra processes and loads the model more than once.
## Security
This repository does not contain API keys, account tokens, or model weights. The installer never uploads `~/.claude/settings.json` or `~/.codex`.
The service has no authentication. It listens on `127.0.0.1` only. `/route` accepts a prompt, and the dashboard shows stored prompts. Binding any other address makes both reachable by anyone who can open that port. The process prints a warning if `host` is not a loopback address. Do not put this port on a public interface, and do not reverse-proxy it without adding auth yourself.
`routes.jsonl` stores the raw prompt by default, including anything you paste into Codex or Claude Code. Set `log_prompts = false` to keep only a SHA-256. The runtime directory is `~/.laya-router/`, outside this repo. Do not commit it.
The installer rewrites `~/.codex/hooks.json` and merges `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` entries into `~/.claude/settings.json`. That Claude file often holds API credentials. The tool preserves existing keys, writes a timestamped `*.bak-*` copy before changing a file, and removes only its own hook on uninstall. Read the diff before you publish a dotfile.
A web page on another origin cannot read these responses: the server does not send `Access-Control-Allow-Origin`. Keep the default bind address anyway.
The hook adds a hint. It does not copy the user prompt back into the injected instructions.
## Limits
The multilingual base checkpoint is a fast model to specialize, not a zero-shot coding router. Public typed-decision numbers for the base checkpoints sit near chance until a domain fine-tune. Expect some complex requests to be labeled `simple`. Use the log, then fine-tune, before trusting the scores to choose a model.
## License
Apache License 2.0. The Laya model and library are also Apache-2.0.