sakura-agent 0.1.0 → 0.1.1

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.
Files changed (91) hide show
  1. package/README.md +144 -78
  2. package/README.zh-CN.md +145 -79
  3. package/THIRD_PARTY_NOTICES.md +8 -58
  4. package/dist/packages/agent/src/index.d.ts +49 -11
  5. package/dist/packages/agent/src/index.js +50 -55
  6. package/dist/packages/agent/src/restrict-tools.d.ts +6 -0
  7. package/dist/packages/agent/src/restrict-tools.js +11 -0
  8. package/dist/packages/agent/src/system-prompt.d.ts +5 -0
  9. package/dist/packages/agent/src/system-prompt.js +1 -1
  10. package/dist/packages/agent/src/wake.d.ts +41 -0
  11. package/dist/packages/agent/src/wake.js +71 -0
  12. package/dist/packages/env/src/confine.d.ts +13 -0
  13. package/dist/packages/env/src/confine.js +40 -0
  14. package/dist/packages/env/src/connect-ssh.d.ts +33 -0
  15. package/dist/packages/env/src/connect-ssh.js +132 -0
  16. package/dist/packages/env/src/index.d.ts +5 -1
  17. package/dist/packages/env/src/index.js +3 -1
  18. package/dist/packages/env/src/local.d.ts +11 -2
  19. package/dist/packages/env/src/local.js +42 -10
  20. package/dist/packages/env/src/remote.d.ts +17 -1
  21. package/dist/packages/env/src/remote.js +54 -4
  22. package/dist/packages/evals/src/bfcl.d.ts +24 -0
  23. package/dist/packages/evals/src/bfcl.js +71 -0
  24. package/dist/packages/evals/src/index.d.ts +3 -0
  25. package/dist/packages/evals/src/index.js +2 -0
  26. package/dist/packages/evals/src/png.d.ts +2 -0
  27. package/dist/packages/evals/src/png.js +30 -0
  28. package/dist/packages/evals/src/pool.d.ts +3 -0
  29. package/dist/packages/evals/src/pool.js +22 -0
  30. package/dist/packages/evals/src/providers.d.ts +13 -0
  31. package/dist/packages/evals/src/providers.js +23 -0
  32. package/dist/packages/kernel/src/compaction.d.ts +19 -0
  33. package/dist/packages/kernel/src/compaction.js +12 -2
  34. package/dist/packages/kernel/src/goal.d.ts +10 -1
  35. package/dist/packages/kernel/src/goal.js +28 -0
  36. package/dist/packages/kernel/src/hooks.d.ts +5 -0
  37. package/dist/packages/kernel/src/hooks.js +21 -1
  38. package/dist/packages/kernel/src/index.d.ts +3 -2
  39. package/dist/packages/kernel/src/index.js +1 -0
  40. package/dist/packages/kernel/src/loop.d.ts +28 -4
  41. package/dist/packages/kernel/src/loop.js +69 -47
  42. package/dist/packages/kernel/src/types.d.ts +25 -4
  43. package/dist/packages/kernel/src/usage.d.ts +3 -0
  44. package/dist/packages/kernel/src/usage.js +16 -0
  45. package/dist/packages/llm/src/chat/request.js +22 -4
  46. package/dist/packages/llm/src/chat/retry.d.ts +1 -1
  47. package/dist/packages/llm/src/chat/retry.js +1 -1
  48. package/dist/packages/llm/src/index.js +7 -2
  49. package/dist/packages/plugins/context/src/index.d.ts +2 -1
  50. package/dist/packages/plugins/context/src/index.js +2 -1
  51. package/dist/packages/plugins/limits/src/index.d.ts +4 -1
  52. package/dist/packages/plugins/redaction/src/index.d.ts +3 -0
  53. package/dist/packages/plugins/redaction/src/index.js +3 -0
  54. package/dist/packages/plugins/redaction/src/rules.gen.js +1 -1
  55. package/dist/packages/plugins/review/src/chain.d.ts +2 -0
  56. package/dist/packages/plugins/review/src/chain.js +2 -1
  57. package/dist/packages/plugins/review/src/hard-rules.d.ts +6 -0
  58. package/dist/packages/plugins/review/src/hard-rules.js +58 -25
  59. package/dist/packages/plugins/review/src/index.d.ts +13 -0
  60. package/dist/packages/plugins/review/src/index.js +9 -5
  61. package/dist/packages/plugins/tools/src/builtin/bash.js +24 -10
  62. package/dist/packages/plugins/toolset/src/codemode.d.ts +17 -6
  63. package/dist/packages/plugins/toolset/src/codemode.js +84 -26
  64. package/dist/packages/plugins/toolset/src/index.d.ts +1 -1
  65. package/dist/packages/plugins/toolset/src/index.js +1 -1
  66. package/dist/packages/plugins/trace/src/index.d.ts +27 -0
  67. package/dist/packages/plugins/trace/src/index.js +29 -0
  68. package/dist/packages/plugins/trace/src/modules.d.ts +6 -0
  69. package/dist/packages/plugins/trace/src/modules.js +28 -0
  70. package/dist/packages/session/src/file-log.d.ts +6 -1
  71. package/dist/packages/session/src/file-log.js +45 -25
  72. package/dist/packages/session/src/fork.d.ts +16 -0
  73. package/dist/packages/session/src/fork.js +70 -0
  74. package/dist/packages/session/src/index.d.ts +2 -0
  75. package/dist/packages/session/src/index.js +1 -0
  76. package/dist/packages/session/src/log-core.d.ts +37 -0
  77. package/dist/packages/session/src/log-core.js +47 -0
  78. package/dist/packages/session/src/memory-log.d.ts +3 -12
  79. package/dist/packages/session/src/memory-log.js +3 -23
  80. package/dist/packages/session/src/projection.js +2 -0
  81. package/dist/packages/testing/src/faux.d.ts +36 -0
  82. package/dist/packages/testing/src/faux.js +69 -0
  83. package/dist/packages/testing/src/index.d.ts +4 -0
  84. package/dist/packages/testing/src/index.js +2 -0
  85. package/dist/packages/testing/src/rng.d.ts +7 -0
  86. package/dist/packages/testing/src/rng.js +17 -0
  87. package/dist/sdk/index.d.ts +11 -3
  88. package/dist/sdk/index.js +5 -1
  89. package/dist/sdk/testing.d.ts +2 -0
  90. package/dist/sdk/testing.js +2 -0
  91. package/package.json +5 -1
package/README.md CHANGED
@@ -2,109 +2,151 @@
2
2
 
3
3
  [English](README.md) · [简体中文](README.zh-CN.md)
4
4
 
5
- **Sakura is an embeddable TypeScript SDK for building coding agents with resumable sessions and configurable tool execution.**
5
+ **Sakura is an agent loop you can embed, review and put anywhere.** A small TypeScript kernel (about 1,600 lines, eight hooks) runs the loop; everything else is an optional plugin. You define an agent in code the way you define any object, give it tools that are plain objects, and decide exactly where it may reach: this directory, another machine over SSH, or any place a function can run a command. It ships as the `sakura-agent` npm package with an SDK and a small `sakura` command-line tool, and the repository also contains a Codex-style terminal built on the same SDK.
6
6
 
7
- Sakura grew out of a need to run an agent as part of an application and control its models, sessions, and tools from code. The `sakura` CLI uses the same SDK for terminal and script workflows.
7
+ ## Why Sakura
8
8
 
9
- ## Run the examples
9
+ - **An agent is a few lines of code.** `createAgent({ model, tools, rules, plugins })`; tools are plain objects (a name, a JSON schema, an `execute` function); a Python or shell tool is a command the `bash` tool or the environment runs. Nothing to install, no config files to generate.
10
+ - **What you expect from a CLI agent, assembled from code.** Resumable sessions (fork, rewind, crash recovery), conversation compaction, goals, steer / queue / cancel while it works, project instructions (`AGENTS.md`), sub-agents, MCP, tool search and a code mode. Each is a plugin or an option; leave out what you do not need.
11
+ - **You decide where it can reach.** Every agent has its own working directory and its own `Environment`; nothing is read from the process's current directory. `confine: true` keeps the file tools inside the directory, however a path is written (absolute, `..`, a link). `createSshEnvironment` runs the tools on another machine over one SSH login and tells the model that it is there. Any function that runs a shell command can be an environment, so the loop can run where your key is and the hands where the work is.
12
+ - **A reviewer in the loop instead of a person.** The `review` plugin judges risky tool calls with hard rules, then a reviewer model, and a circuit breaker stops a run that keeps being refused. Agents can run unattended. And when an agent has no shell (file tools plus a checker that answers true or false, say), a confined directory is all the safety it needs, and no second model has to run.
13
+ - **A terminal when you want one.** `terminal/` is a Codex-style terminal (Markdown output, slash commands, resume, fork, `exec --json`) written only against the public SDK. It is in the repository, not in the npm package.
14
+ - **Small enough to read, tested like a product.** Over 5,000 tests in the public tree, contract suites that run one test set against every implementation of an interface, and live tests against real models and a real SSH server.
10
15
 
11
- Use Node.js 26 and npm.
16
+ ## How it compares
12
17
 
13
- Install a published version with `npm install sakura-agent`. The commands below run the examples from source.
18
+ Two tools I compared it with, from their public documentation (November 2026). They are products for a person at a keyboard; Sakura is the part you can take apart and embed.
14
19
 
15
- ```sh
16
- git clone https://github.com/Vist233/Sakura.git
17
- cd Sakura
18
- npm ci
19
- npm run example
20
- npm run example:tools
21
- ```
20
+ | | Codex CLI | pi | Sakura |
21
+ |---|---|---|---|
22
+ | What it is | a terminal agent product; `exec --json` for automation; can run as an MCP server | a "minimal, extensible agent harness"; interactive, print/JSON and RPC modes, a TypeScript SDK | a library first, with a minimal CLI and a separate terminal |
23
+ | Keeping actions safe | sandbox modes (read-only, workspace-write, full access) and approval policies | not described in its README | a reviewer with hard rules and a breaker, no person needed; `confine` for the file tools; no sandbox of its own |
24
+ | Sub-agents, plan mode | not checked | left out by design | sub-agents built in (opt-in) |
25
+ | Where the tools run | this machine, in a sandbox | not described in its README | this machine, SSH, or any function, chosen per agent |
26
+ | Models | OpenAI models by default | built-in providers via `/login` | any Chat Completions endpoint (tried with DeepSeek, Kimi, OpenCode Go) |
27
+
28
+ Sources: [Codex CLI documentation](https://developers.openai.com/codex), [pi README](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent). Check them for current details; both move quickly.
29
+
30
+ ## Who it is for
22
31
 
23
- The examples use a scripted model by default. Set `DEEPSEEK_API_KEY` to run them with DeepSeek. The [tool example](examples/tools.ts) registers a function and shows the agent calling it.
32
+ - You run agents **without watching them**: batch jobs, many agents, remote machines, or an agent inside your own product.
33
+ - You want to define an agent in code and control its model, session, tools and reach from there.
34
+ - You want review and secret redaction as separate, testable pieces rather than flags inside a monolith, and a loop you can read in one sitting.
24
35
 
25
- ## Create an agent and add tools
36
+ It is not an all-in-one assistant for your own laptop, and it does not ship a sandbox: the default local environment uses the permissions of the calling process. `confine` is a boundary for the file tools, not for a shell: a shell command can still reach anything, so give an agent a shell only together with the review, or run untrusted work in an isolated environment of your own.
26
37
 
27
- Import from `sakura-agent` in an installed package. In this repository, the same API is available from `sdk/index.ts`. The following example expects `DEEPSEEK_API_KEY` in the environment.
38
+ ## Where an agent can reach
28
39
 
29
40
  ```ts
30
- import { compatPresets, corePlugins, createAgent, limitsPlugin } from "sakura-agent";
31
- import type { Plugin } from "sakura-agent";
32
-
33
- const apiKey = process.env.DEEPSEEK_API_KEY;
34
- if (!apiKey) throw new Error("Set DEEPSEEK_API_KEY.");
35
-
36
- const projectTools: Plugin = {
37
- name: "project-tools",
38
- setup(api) {
39
- api.registerTool({
40
- name: "project_status",
41
- description: "Return the status of the sample project.",
42
- parameters: { type: "object", properties: {}, additionalProperties: false },
43
- effect: "read",
44
- async execute() {
45
- return { content: [{ type: "text", text: "Build passed." }] };
46
- },
47
- });
41
+ import { createAgent, createSshEnvironment, corePlugins, reviewPlugin } from "sakura-agent";
42
+ import type { Tool } from "sakura-agent";
43
+
44
+ // a tool is a plain object
45
+ const checkProof: Tool = {
46
+ name: "check_proof",
47
+ description: "Check proof.txt. Answers true or false.",
48
+ parameters: { type: "object", properties: {}, additionalProperties: false },
49
+ effect: "read",
50
+ async execute(_args, { env }) {
51
+ const proof = new TextDecoder().decode(await env.readFile("proof.txt"));
52
+ return { content: [{ type: "text", text: String(proof.includes("QED")) }] };
48
53
  },
49
54
  };
50
55
 
56
+ // 1. no shell, confined to its directory: no reviewer is needed
57
+ const prover = await createAgent({ model, cwd: "/work/job-17", confine: true, tools: ["read", "write", "edit", checkProof] });
58
+
59
+ // 2. the same loop, with the tools on another machine, and a reviewer for what it runs there
60
+ const environment = await createSshEnvironment({ host: "agent@box", cwd: "/srv/job-17" });
61
+ const remote = await createAgent({ model, environment, plugins: [...corePlugins(), reviewPlugin({ llm: reviewerModel })] });
62
+ ```
63
+
64
+ ## At a glance
65
+
66
+ - **What it is:** a library for TypeScript code, plus a minimal CLI (`sakura run`, `sakura resume`) and, in the repository, a terminal.
67
+ - **Runtime:** Node.js 26 or newer. TypeScript sources are run directly in development; the npm package ships compiled JavaScript.
68
+ - **Models:** any OpenAI-style Chat Completions endpoint (tried with DeepSeek, Kimi and OpenCode Go). One wire protocol, with per-model compatibility switches.
69
+ - **Kernel:** a stateless loop, eight hooks, three interfaces (`LlmClient`, `Log`, `Environment`), conversation compaction and goals.
70
+ - **Persistence:** an append-only, tree-shaped JSONL session file. A crashed run continues from the same file.
71
+ - **Plugins on by default:** `tools`, `context`, `resilience`, `limits`. **Opt in:** `review`, `redaction`, sub-agents, MCP client, tool search, code mode.
72
+ - **Safety plugins fail closed:** if the review or the redaction cannot do its job, the action or request does not go ahead.
73
+ - **License:** MIT.
74
+
75
+ ## How it is built
76
+
77
+ ```
78
+ kernel the loop, 8 hooks, 3 interfaces (LlmClient, Log, Environment), compaction, goals
79
+ session the conversation log: in memory, or a JSONL file (tree-shaped, single writer, repairs a torn last line)
80
+ llm one protocol (Chat Completions): retries, resend after a dropped stream, stall detection
81
+ env where tools run: the local machine, or a remote runner you provide
82
+ agent createAgent(): assembles the above with a default plugin set
83
+ plugins/* tools, context, resilience, limits, review, redaction, agents, toolset
84
+ ```
85
+
86
+ - **The kernel keeps no state.** It reads the log, asks the model, runs tools, appends to the log. A crash is recovered by running again on the same log. Tools marked `replay: "never"` get an "interrupted" result instead of being run twice.
87
+ - **Eight hooks are all a plugin gets:** `context`, `request`, `tool.before`, `tool.run`, `tool.after`, `turn.end`, `error`, `event`. A plugin is `{ name, compensates?, setup(api), prepare?, afterCompaction? }`. `compensates` says which weakness of today's models the plugin works around, so it can be deleted when that weakness is gone.
88
+ - **Packages depend in one direction** (kernel ← infrastructure ← plugins ← agent) and plugins never import each other. A script checks this (`npm run check:boundaries`), together with a ceiling on how many names a package exports and a ban on import cycles.
89
+
90
+ | Plugin | What it does | On by default |
91
+ |---|---|---|
92
+ | `tools` | `read`, `write`, `edit`, `bash` (background commands, `sleep N` waits, long output saved to a file) | yes |
93
+ | `context` | loads `AGENTS.md` as pinned instructions, reloads it after compaction | yes |
94
+ | `resilience` | asks the model again when a reply was cut off before it wrote anything (a reasoning model that used its whole output limit thinking), came back empty, or (optionally) was cut in the middle of its text. Each fix can be switched off | yes |
95
+ | `limits` | turn, token, cost and run-time budgets (the run time is checked between turns, not during one); pauses on quota errors | yes (unlimited) |
96
+ | `review` | automatic review of risky tool calls: hard rules, extra authorizers, a reviewer model; fails closed | no |
97
+ | `redaction` | swaps secrets for stable placeholders before a request leaves, restores them in the reply | no |
98
+ | `agents` | `delegate`, `agent_view`, `agent_stop` for sub-agents (they inherit review and redaction); enabled with the `subAgents` option | no |
99
+ | `toolset` | BM25 tool search, an MCP client (stdio), code mode (scripts that call several tools, run in an isolated JavaScript runtime); enabled with the `mcp` option | no |
100
+
101
+ ## Install and run
102
+
103
+ ```sh
104
+ npm install sakura-agent
105
+ ```
106
+
107
+ ```ts
108
+ import { compatPresets, createAgent } from "sakura-agent";
109
+
51
110
  const agent = await createAgent({
52
- model: {
53
- baseUrl: "https://api.deepseek.com",
54
- apiKey,
55
- model: "deepseek-flash",
56
- contextWindow: 128_000,
57
- compat: compatPresets.deepseek,
58
- },
59
111
  cwd: process.cwd(),
112
+ model: { baseUrl: "https://api.deepseek.com", apiKey: process.env.DEEPSEEK_API_KEY!, model: "deepseek-flash", contextWindow: 128_000, compat: compatPresets.deepseek },
60
113
  tools: ["read"],
61
- plugins: [
62
- ...corePlugins().filter((plugin) => plugin.name !== "limits"),
63
- limitsPlugin({ maxTurns: 8 }),
64
- projectTools,
65
- ],
66
- sessionFile: ".sakura/session.jsonl",
67
- maxTokens: 1_024,
114
+ sessionFile: ".sakura/session.jsonl", // omit to keep the conversation in memory
68
115
  });
69
-
70
116
  try {
71
- const run = agent.run("Call project_status and tell me the result.");
117
+ const run = agent.run("Summarize README.md.");
72
118
  for await (const event of run.events) {
73
- if (event.type === "tool_start") console.log(event.call.name);
119
+ if (event.type === "tool_start") console.log("tool:", event.call.name);
74
120
  if (event.type === "message_end" && event.message.role === "assistant") {
75
121
  for (const part of event.message.content) if (part.type === "text") console.log(part.text);
76
122
  }
77
123
  }
78
- console.log(await run.result);
124
+ console.log((await run.result).reason); // "done" | "cancelled" | "paused" | "error"
79
125
  } finally {
80
- await agent.close();
126
+ await agent.close(); // always: it stops commands still running in the background and releases the session file
81
127
  }
82
128
  ```
83
129
 
84
- | Configuration | What it controls |
85
- | --- | --- |
86
- | `model` | Provider URL, API key, model name, and context window. You can also pass an `LlmClient`. |
87
- | `tools` | Selects built-in tools from `read`, `write`, `edit`, and `bash`. Pass `[]` to disable them. |
88
- | `plugins` | Registers custom tools or adds behavior. This list replaces the defaults, so include the desired `corePlugins()`. Validate tool arguments in `execute()`. |
89
- | `mcp` | Starts stdio MCP servers and exposes their tools to the agent. |
90
- | `cwd`, `environment` | Choose the tool working directory or supply a custom execution environment. |
91
- | `sessionFile` | Saves the session as JSONL. To resume, create an agent with the same file and call `agent.run()` without a prompt. |
92
- | `maxTokens`, `limitsPlugin()` | Set a per-request output limit and session or run budgets. |
130
+ To resume, create an agent with the same `sessionFile` and call `agent.run()` without a prompt.
93
131
 
94
- For an existing stdio MCP server, add this property to the same configuration. Use your own server command and arguments. Check `agent.mcp` for startup status.
132
+ Turning on the safety plugins and sub-agents:
95
133
 
96
134
  ```ts
97
- mcp: {
98
- servers: {
99
- project: { command: "node", args: ["./your-mcp-server.mjs"] },
100
- },
101
- expose: "direct",
102
- }
135
+ import { corePlugins, createAgent, redactionPlugin, reviewPlugin } from "sakura-agent";
136
+
137
+ const redaction = redactionPlugin({ key: "stable-key" });
138
+ // `model` and `reviewerModel` are model settings or clients, as in the first example
139
+ const agent = await createAgent({
140
+ model,
141
+ plugins: [...corePlugins(), redaction, reviewPlugin({ llm: redaction.wrap(reviewerModel) })], // the reviewer must not see secrets either
142
+ subAgents: {}, // sub-agents get the same plugins
143
+ mcp: { servers: { files: { command: "npx", args: ["some-mcp-server"] } }, expose: "codemode" }, // script calls are reviewed too
144
+ });
103
145
  ```
104
146
 
105
- ## Use the CLI
147
+ Plugins can contain plugins: `bundle("safety", [redaction, review])` is one plugin made of two, and `createAgent` installs each plugin once, orders them by `requires`, and fails at creation on a missing requirement, a cycle or a duplicate name.
106
148
 
107
- From source, run `npm run build` and use `node dist/sdk/cli.js` in place of `sakura`. An installed package provides the `sakura` executable. Set `DEEPSEEK_API_KEY` or `SAKURA_API_KEY` first.
149
+ ## The command line
108
150
 
109
151
  ```sh
110
152
  sakura run "Read README.md and summarize it" --tools read --session .sakura/demo.jsonl
@@ -112,22 +154,46 @@ sakura resume --tools read --session .sakura/demo.jsonl
112
154
  sakura --help
113
155
  ```
114
156
 
115
- Add `--json` for JSON lines or use `--cwd`, `--base-url`, `--model`, and `--context-window` to configure the run. The CLI starts with no built-in tools unless you select them.
157
+ Set `DEEPSEEK_API_KEY` or `SAKURA_API_KEY` first. The CLI starts with no built-in tools unless you pass `--tools`. Add `--json` for JSON lines, or `--cwd`, `--base-url`, `--model` and `--context-window` to configure the run.
158
+
159
+ ## Frequently asked questions
116
160
 
117
- ## Choose where tools run
161
+ **How does it recover from a crash?** The session file is append-only and a message is written only when it is complete. Run the agent again on the same file: a read-only call that was open is simply run again, a command that may have half run gets an "interrupted, check the current state" result instead of running twice, and a torn last line is repaired when the file is opened.
118
162
 
119
- The SDK manages the agent loop, model requests, and session state. The execution environment handles files and shell commands. Use `createLocalEnvironment()` for local execution or `createRemoteEnvironment({ cwd, run })` with a runner you provide. For example, a local application can manage sessions while a Linux worker runs builds with its own dependencies.
163
+ **Can I compact the conversation myself?** Yes. Compaction starts by itself when the context is within 17% of the model's window (and never less than 16384 tokens from it), and `agent.run(undefined, { compact: true })` does it now, whatever the size: it summarises the older part, keeps the recent part word for word, and says so with a `compaction` event (there is none when there was nothing worth summarising). `agent.run("next question", { compact: true })` compacts first and then answers, and the new question is not part of the summary.
120
164
 
121
- The default local environment uses the calling process's permissions. `cwd` selects a working directory. Use an isolated runner for untrusted workloads. Session files and JSON event logs can contain sensitive content. Review and redaction are opt-in, and a separate reviewer client needs its own redaction wrapper.
165
+ **Can I go back to an earlier point in a conversation?** Yes, in two ways. `agent.rewindTo(entryId)` goes back inside the same session file: what is said next starts a new branch, and the old one stays in the file. `agent.fork(path, entryId)` copies the conversation up to that point into a new session file, which you open with `createAgent({ sessionFile: path })`; the original is not changed and both can go on. A fork never cuts a turn in two: the results of the tool calls of the last message come with it.
122
166
 
123
- ## Validation
167
+ **Can the model run a command in the background?** Yes. `bash` takes `background: true`, and any command still running after 120 seconds moves to the background by itself. The model is told the process id and the output file, and is told when the command ends. It can ask to be woken up when that happens (`wake: true` on the `bash` call, or `wake: false` to say it does not need to be); the application sets the default with the `wake` option of `createAgent`. Always call `agent.close()`: a command left running keeps the Node process alive, and `close()`, Ctrl-C and `process.exit` stop it.
124
168
 
125
- The public CI runs offline checks on Linux and macOS. In a separate local evaluation with DeepSeek Flash, Sakura passed 10 Lean proof checks and 20 append-log checks. Across both tasks it passed 96 background checks. The evaluation used an external isolation layer and resumed from checkpoints after transport failures.
169
+ **How are secrets kept from the model?** The `redaction` plugin replaces secrets (a rule table derived from gitleaks plus a few generic rules) with stable placeholders in every request, restores them in the reply before a tool runs, and refuses to send a request if scanning fails. `redaction.wrap(llm)` gives the reviewer model the same protection. It protects what is sent to the model provider, not what is stored on this machine: the session file, the events and any trace keep the real values (the tools need them, and a resumed session is read from that file), so mask them with `redaction.scrub(text)` before you export or share them. It does not stop a command you let the agent run from reading the environment of its own process.
170
+
171
+ **What does automatic review check?** Hard rules first (read-only commands that stay inside the working directory pass, catastrophic ones such as deleting the root are refused; a read-only command that points outside the working directory goes on to the reviewer), then optional extra authorizers, then a reviewer model that scores risk against the user's authorization. A refusal goes back to the agent as a tool error so it can try another way. Any failure of the review itself is a refusal. Review is a safety net, not a sandbox: it reads the text of a command, so it cannot follow a symbolic link and does not confine file access.
172
+
173
+ **What if a reasoning model uses its whole output limit thinking?** The reply comes back cut off (`finish_reason: length`) with no text. The `resilience` plugin, on by default, notices this, asks the model to continue and raises `max_tokens` (doubling, up to 32768) for the next request. After two retries it lets the run end. Empty replies are asked once more. `resiliencePlugin({ cutEmpty: false })` switches a fix off.
174
+
175
+ **Does it support MCP?** As a client over stdio. Tools can be declared directly, found through BM25 search, or called from a code-mode script.
176
+
177
+ **What is code mode, and why does a script's call need review when the script already runs in an isolated runtime?** In code mode the model writes one small JavaScript program that calls several tools, instead of making one call per request. The program runs in an isolated JavaScript runtime (QuickJS compiled to WebAssembly), which keeps the *program itself* away from the machine. This is not a sandbox for commands. The tools it calls (a shell command, a file write, an MCP tool) run outside that runtime with their full power, so a call made from a script is exactly as risky as the same call made directly. Calls from a script do not pass the loop's hooks, so the review is asked explicitly: with the `review` plugin installed every call a script makes is reviewed one by one (the script as a whole is not reviewed again), and a refusal becomes an error inside the script. Without it, every call simply runs, as a direct call would.
178
+
179
+ **Where do tools run?** In an `Environment`. `createLocalEnvironment()` uses the calling process's permissions. `createRemoteEnvironment({ cwd, run })` sends commands to a runner you provide, for example a container. It is an adapter, not a sandbox. `createSshEnvironment({ host, cwd })` is that adapter for a machine you can reach with ssh: it logs in once, runs every command over that connection, and tells the model in the system prompt that its tools run elsewhere and that the connection can fail (when it does, the tool result says so and where the command's output is; it never reports a lost connection as a finished command). Authentication and host keys are whatever your ssh configuration does; the remote machine needs `bash`, `base64` and `setsid`, so Linux rather than macOS. On this machine a timeout, a cancel and exit stop a command together with everything it started, but not a process that starts a session of its own (with `setsid`, for example); to stop everything, run the commands in a container. Long command output and remote scripts are saved under `.sakura/` in the working directory.
180
+
181
+ **Which models work?** Anything that speaks Chat Completions, including streaming and tool calls. Compatibility switches cover the differences between providers (field names, reasoning content, image handling).
182
+
183
+ ## Development
126
184
 
127
185
  ```sh
128
- npm run check
186
+ npm ci
187
+ npm run check # boundaries, types, lint, format, then all tests
188
+ npm run test:unit # one package at a time (packages/*/test)
189
+ npm run test:contract # one suite run against every implementation of an interface
190
+ npm run test:tools # repository rules: dependency direction, export ceilings, no cycles
191
+ npm run example # the quick-start example with a scripted model
192
+ npm run build # compile the SDK and CLI into dist/
129
193
  ```
130
194
 
195
+ None of the tests calls a real model.
196
+
131
197
  ## License
132
198
 
133
- [MIT](LICENSE) · [Third-party notices](THIRD_PARTY_NOTICES.md) · [Previous Python source](https://github.com/Vist233/Sakura/tree/3e9b52cca7ff9e69fc6fc5d4b865b5e44ac338dd)
199
+ MIT, see [LICENSE](LICENSE). Code and text taken from other projects (Pi, OpenAI Codex, gitleaks) are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) with their licences in `licenses/`. The earlier Python source is kept in the repository history.
package/README.zh-CN.md CHANGED
@@ -2,132 +2,198 @@
2
2
 
3
3
  [English](README.md) · [简体中文](README.zh-CN.md)
4
4
 
5
- **Sakura 是一个可嵌入应用的 TypeScript SDK,用于构建支持会话恢复和可配置工具执行的编程 Agent。**
5
+ **Sakura 是一个能嵌入、能审核、能放在任何地方的 Agent 循环。** 一个小小的 TypeScript 内核(约 1600 行、8 个钩子)负责循环,其余的一切都是可以不装的插件。你像定义一个对象那样用代码定义 Agent,工具就是普通对象,并且精确决定它能触到哪里:这个目录、通过 SSH 连上的另一台机器,或者任何能执行命令的函数。它以 `sakura-agent` 这个 npm 包发布,包含 SDK 和一个小的 `sakura` 命令行工具;仓库里还有一个基于同一个 SDK 的、类似 Codex 的终端。
6
6
 
7
- Sakura 源于一个实际需求:让 Agent 在自己的应用里运行,从代码控制模型、会话和工具。`sakura` CLI 使用同一套 SDK,可在终端或脚本里调用。
7
+ ## 为什么用 Sakura
8
8
 
9
- ## 运行示例
9
+ - **一个 Agent 就是几行代码。** `createAgent({ model, tools, rules, plugins })`;工具是普通对象(名字、JSON Schema、一个 `execute` 函数);Python 或 shell 工具就是让 `bash` 工具或环境执行的一条命令。不用安装什么,也不用生成配置文件。
10
+ - **命令行 Agent 该有的,用代码拼出来。** 可恢复的会话(分叉、回退、崩溃恢复)、对话压缩、目标、运行中引导 / 排队 / 取消、项目说明(`AGENTS.md`)、子 Agent、MCP、工具搜索和代码模式。每一项都是插件或选项,不需要的就不要。
11
+ - **你决定它能触到哪里。** 每个 Agent 有自己的工作目录和自己的 `Environment`,不读进程的当前目录。`confine: true` 让文件工具出不了这个目录,不管路径怎么写(绝对路径、`..`、链接)。`createSshEnvironment` 用一次 SSH 登录在另一台机器上运行工具,并告诉模型它在那里。任何能执行 shell 命令的函数都可以做环境,所以循环可以跑在放密钥的地方,工具跑在干活的地方。
12
+ - **用审核代替人在旁边。** `review` 插件先用硬规则、再用审核模型判断有风险的工具调用,熔断器会让一直被拒绝的运行停下来。Agent 可以无人值守。而且当一个 Agent 没有 shell(比如只有文件工具和一个回答真或假的检查器)时,限定的目录就是它需要的全部安全措施,不用再运行第二个模型。
13
+ - **想要终端也有。** `terminal/` 是一个类似 Codex 的终端(Markdown 输出、斜杠命令、续接、分叉、`exec --json`),只用公开的 SDK 写成。它在仓库里,不在 npm 包里。
14
+ - **小到能读完,按产品标准测试。** 公开仓库里有 5000 多个测试,有对每个接口的所有实现跑同一套用例的契约测试,还有对真实模型和真实 SSH 服务器的实测。
10
15
 
11
- 使用 Node.js 26 和 npm。
16
+ ## 和别的工具比
12
17
 
13
- 已发布的版本可用 `npm install sakura-agent` 安装。下面的命令从源码运行示例。
18
+ 我对比过的两个工具,依据它们公开的文档(2026 年 11 月)。它们是给坐在键盘前的人用的产品;Sakura 是可以拆开、嵌进去的那一部分。
14
19
 
15
- ```sh
16
- git clone https://github.com/Vist233/Sakura.git
17
- cd Sakura
18
- npm ci
19
- npm run example
20
- npm run example:tools
21
- ```
20
+ | | Codex CLI | pi | Sakura |
21
+ |---|---|---|---|
22
+ | 是什么 | 终端 Agent 产品;`exec --json` 用于自动化;可以作为 MCP 服务运行 | “极简、可扩展的 Agent 工具集”;交互、打印 / JSON、RPC 三种模式,有 TypeScript SDK | 以库为主,带一个最小命令行和一个单独的终端 |
23
+ | 怎么保证动作安全 | 沙箱模式(只读、工作区可写、完全访问)加批准策略 | 其 README 没有说明 | 带硬规则和熔断器的审核,不需要人;文件工具有 `confine`;自己不带沙箱 |
24
+ | 子 Agent、计划模式 | 没有核对 | 有意不做 | 内置子 Agent(按需开启) |
25
+ | 工具在哪里运行 | 本机,在沙箱里 | 其 README 没有说明 | 本机、SSH,或任何函数,每个 Agent 各自选择 |
26
+ | 模型 | 默认是 OpenAI 的模型 | 用 `/login` 连接内置的服务商 | 任何 Chat Completions 接口(用 DeepSeek、Kimi、OpenCode Go 试过) |
27
+
28
+ 来源:[Codex CLI 文档](https://developers.openai.com/codex)、[pi README](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)。细节请以它们当前的文档为准,两者都变得很快。
29
+
30
+ ## 适合谁
22
31
 
23
- 示例默认使用脚本模型。设置 `DEEPSEEK_API_KEY` 后可用 DeepSeek 运行。[工具示例](examples/tools.ts) 会注册一个函数,并显示 Agent 对它的调用。
32
+ - 你让 Agent **无人看着也能运行**:批处理、很多个 Agent、远程机器,或者放在你自己的产品里。
33
+ - 你想用代码定义 Agent,并从那里控制它的模型、会话、工具和能触到的范围。
34
+ - 你想要把审核和密钥脱敏做成独立、可测试的部件,而不是一个大程序里的开关,以及一个坐下来一次就能读完的循环。
24
35
 
25
- ## 创建 Agent 并添加工具
36
+ 它不是给你自己笔记本用的全能助手,也不带沙箱:默认的本机环境使用调用进程的权限。`confine` 是文件工具的边界,不是 shell 的边界:shell 命令仍然能触到任何地方,所以给 Agent 一个 shell 时要同时开启审核,或者把不可信的任务放在你自己的隔离环境里运行。
26
37
 
27
- 安装包后从 `sakura-agent` 导入。本仓库中的相同接口位于 `sdk/index.ts`。下面的示例使用环境变量 `DEEPSEEK_API_KEY`。
38
+ ## Agent 能触到哪里
28
39
 
29
40
  ```ts
30
- import { compatPresets, corePlugins, createAgent, limitsPlugin } from "sakura-agent";
31
- import type { Plugin } from "sakura-agent";
32
-
33
- const apiKey = process.env.DEEPSEEK_API_KEY;
34
- if (!apiKey) throw new Error("Set DEEPSEEK_API_KEY.");
35
-
36
- const projectTools: Plugin = {
37
- name: "project-tools",
38
- setup(api) {
39
- api.registerTool({
40
- name: "project_status",
41
- description: "Return the status of the sample project.",
42
- parameters: { type: "object", properties: {}, additionalProperties: false },
43
- effect: "read",
44
- async execute() {
45
- return { content: [{ type: "text", text: "Build passed." }] };
46
- },
47
- });
41
+ import { createAgent, createSshEnvironment, corePlugins, reviewPlugin } from "sakura-agent";
42
+ import type { Tool } from "sakura-agent";
43
+
44
+ // 工具就是普通对象
45
+ const checkProof: Tool = {
46
+ name: "check_proof",
47
+ description: "检查 proof.txt,回答 true 或 false。",
48
+ parameters: { type: "object", properties: {}, additionalProperties: false },
49
+ effect: "read",
50
+ async execute(_args, { env }) {
51
+ const proof = new TextDecoder().decode(await env.readFile("proof.txt"));
52
+ return { content: [{ type: "text", text: String(proof.includes("QED")) }] };
48
53
  },
49
54
  };
50
55
 
56
+ // 1. 没有 shell,限定在自己的目录里:不需要审核
57
+ const prover = await createAgent({ model, cwd: "/work/job-17", confine: true, tools: ["read", "write", "edit", checkProof] });
58
+
59
+ // 2. 同一个循环,工具跑在另一台机器上,并对在那里运行的命令开启审核
60
+ const environment = await createSshEnvironment({ host: "agent@box", cwd: "/srv/job-17" });
61
+ const remote = await createAgent({ model, environment, plugins: [...corePlugins(), reviewPlugin({ llm: reviewerModel })] });
62
+ ```
63
+
64
+ ## 一眼看懂
65
+
66
+ - **是什么:** 供 TypeScript 代码调用的库,外加一个最小的命令行(`sakura run`、`sakura resume`),仓库里还有一个终端。
67
+ - **运行环境:** Node.js 26 及以上。开发时直接运行 TypeScript 源码;npm 包里是编译好的 JavaScript。
68
+ - **模型:** 任何 OpenAI 风格的 Chat Completions 接口(用 DeepSeek、Kimi、OpenCode Go 试过)。只有一种线协议,按模型配置兼容开关。
69
+ - **内核:** 无状态循环、8 个钩子、3 个接口(`LlmClient`、`Log`、`Environment`)、对话压缩和目标。
70
+ - **持久化:** 只追加的树形 JSONL 会话文件;崩溃的运行可以在同一个文件上继续。
71
+ - **默认启用的插件:** `tools`、`context`、`resilience`、`limits`。**按需开启:** `review`、`redaction`、子 Agent、MCP 客户端、工具搜索、代码模式。
72
+ - **安全类插件出错即拒绝:** 审核或脱敏没能完成工作时,动作或请求不会放行。
73
+ - **许可:** MIT。
74
+
75
+ ## 怎么搭的
76
+
77
+ ```
78
+ kernel 循环、8 个钩子、3 个接口(LlmClient、Log、Environment)、压缩、目标
79
+ session 会话日志:内存版,或 JSONL 文件(树形、单写者、会修复被截断的最后一行)
80
+ llm 一种协议(Chat Completions):重试、断流重发、卡住检测
81
+ env 工具在哪里运行:本机,或你提供的远程执行器
82
+ agent createAgent():把以上装配起来,带一套默认插件
83
+ plugins/* tools、context、resilience、limits、review、redaction、agents、toolset
84
+ ```
85
+
86
+ - **内核不保存状态。** 它读日志、问模型、跑工具、写日志。崩溃后在同一份日志上重新运行就能恢复;标成 `replay: "never"` 的工具不会被重复执行,而是得到一条"被中断"的结果。
87
+ - **插件只有 8 个钩子可用:** `context`、`request`、`tool.before`、`tool.run`、`tool.after`、`turn.end`、`error`、`event`。插件的形状是 `{ name, compensates?, setup(api), prepare?, afterCompaction? }`;`compensates` 写明它在弥补当今模型的哪个弱点,这样弱点消失后可以删掉它。
88
+ - **包的依赖只有一个方向**(内核 ← 基础设施 ← 插件 ← agent),插件之间不互相引用。有脚本检查(`npm run check:boundaries`),同时还限制每个包的导出名字数量、禁止循环引用。
89
+
90
+ | 插件 | 做什么 | 默认启用 |
91
+ |---|---|---|
92
+ | `tools` | `read`、`write`、`edit`、`bash`(后台命令、`sleep N` 等待、长输出存成文件) | 是 |
93
+ | `context` | 读取 `AGENTS.md` 作为置顶说明,压缩后重读 | 是 |
94
+ | `resilience` | 模型回复在写出任何内容之前就被截断(思考型模型把输出上限全用来思考)、回复为空、或(可选)文字写到一半被截断时,让模型再答一次;每一项都可以单独关掉 | 是 |
95
+ | `limits` | 轮数、token、费用、运行时间预算(运行时间在每个回合结束时检查,不是回合进行中);额度不足时暂停 | 是(默认不限) |
96
+ | `review` | 对有风险的工具调用做自动审核:硬规则、额外授权方、审核模型;出错一律拒绝 | 否 |
97
+ | `redaction` | 请求发出前把密钥换成稳定的占位符,回复里再还原 | 否 |
98
+ | `agents` | `delegate`、`agent_view`、`agent_stop` 三个子 Agent 工具(子 Agent 继承审核和脱敏);用 `subAgents` 选项开启 | 否 |
99
+ | `toolset` | BM25 工具搜索、MCP 客户端(stdio)、代码模式(让模型写脚本一次调用多个工具,脚本在隔离的 JavaScript 运行环境里执行);用 `mcp` 选项开启 | 否 |
100
+
101
+ ## 安装和运行
102
+
103
+ ```sh
104
+ npm install sakura-agent
105
+ ```
106
+
107
+ ```ts
108
+ import { compatPresets, createAgent } from "sakura-agent";
109
+
51
110
  const agent = await createAgent({
52
- model: {
53
- baseUrl: "https://api.deepseek.com",
54
- apiKey,
55
- model: "deepseek-flash",
56
- contextWindow: 128_000,
57
- compat: compatPresets.deepseek,
58
- },
59
111
  cwd: process.cwd(),
112
+ model: { baseUrl: "https://api.deepseek.com", apiKey: process.env.DEEPSEEK_API_KEY!, model: "deepseek-flash", contextWindow: 128_000, compat: compatPresets.deepseek },
60
113
  tools: ["read"],
61
- plugins: [
62
- ...corePlugins().filter((plugin) => plugin.name !== "limits"),
63
- limitsPlugin({ maxTurns: 8 }),
64
- projectTools,
65
- ],
66
- sessionFile: ".sakura/session.jsonl",
67
- maxTokens: 1_024,
114
+ sessionFile: ".sakura/session.jsonl", // 不写则会话只放在内存里
68
115
  });
69
-
70
116
  try {
71
- const run = agent.run("Call project_status and tell me the result.");
117
+ const run = agent.run("总结一下 README.md。");
72
118
  for await (const event of run.events) {
73
- if (event.type === "tool_start") console.log(event.call.name);
119
+ if (event.type === "tool_start") console.log("tool:", event.call.name);
74
120
  if (event.type === "message_end" && event.message.role === "assistant") {
75
121
  for (const part of event.message.content) if (part.type === "text") console.log(part.text);
76
122
  }
77
123
  }
78
- console.log(await run.result);
124
+ console.log((await run.result).reason); // "done" | "cancelled" | "paused" | "error"
79
125
  } finally {
80
- await agent.close();
126
+ await agent.close(); // 一定要调用:它会停掉还在后台运行的命令,并释放会话文件锁
81
127
  }
82
128
  ```
83
129
 
84
- | 配置 | 作用 |
85
- | --- | --- |
86
- | `model` | 设置服务地址、密钥、模型名和上下文窗口,也可传入 `LlmClient`。 |
87
- | `tools` | 从 `read`、`write`、`edit`、`bash` 中选择内置工具。传入 `[]` 可关闭。 |
88
- | `plugins` | 注册自定义工具或添加行为。这份列表会替换默认配置,需要的默认插件可从 `corePlugins()` 加回。在 `execute()` 中校验工具参数。 |
89
- | `mcp` | 启动 stdio MCP 服务,并向 Agent 暴露工具。 |
90
- | `cwd`、`environment` | 选择工具的工作目录,或提供自定义执行环境。 |
91
- | `sessionFile` | 把会话保存为 JSONL。恢复时用同一个文件创建 Agent,再调用无参数的 `agent.run()`。 |
92
- | `maxTokens`、`limitsPlugin()` | 设置单次请求的输出上限,以及会话或运行预算。 |
130
+ 要继续一个会话,用同一个 `sessionFile` 创建 Agent,再不带提示词调用 `agent.run()`。
93
131
 
94
- 接入已有 stdio MCP 服务时,把下面的属性加入同一份配置。换成实际的启动命令和参数,再通过 `agent.mcp` 检查启动状态。
132
+ 打开安全类插件和子 Agent:
95
133
 
96
134
  ```ts
97
- mcp: {
98
- servers: {
99
- project: { command: "node", args: ["./your-mcp-server.mjs"] },
100
- },
101
- expose: "direct",
102
- }
135
+ import { corePlugins, createAgent, redactionPlugin, reviewPlugin } from "sakura-agent";
136
+
137
+ const redaction = redactionPlugin({ key: "stable-key" });
138
+ // `model` 和 `reviewerModel` 是模型配置或客户端,写法同上一个例子
139
+ const agent = await createAgent({
140
+ model,
141
+ plugins: [...corePlugins(), redaction, reviewPlugin({ llm: redaction.wrap(reviewerModel) })], // 审核模型也不能看到密钥
142
+ subAgents: {}, // 子 Agent 拿到同样的插件
143
+ mcp: { servers: { files: { command: "npx", args: ["some-mcp-server"] } }, expose: "codemode" }, // 脚本里的调用同样会被审核
144
+ });
103
145
  ```
104
146
 
105
- ## 使用 CLI
147
+ 插件可以包含插件:`bundle("safety", [redaction, review])` 是由两个插件组成的一个插件。`createAgent` 会把每个插件只装一次,按 `requires` 排好顺序,缺依赖、成环或重名时在创建阶段就报错。
106
148
 
107
- 从源码运行时,先执行 `npm run build`,再用 `node dist/sdk/cli.js` 代替 `sakura`。安装包提供 `sakura` 命令。先设置 `DEEPSEEK_API_KEY` 或 `SAKURA_API_KEY`。
149
+ ## 命令行
108
150
 
109
151
  ```sh
110
- sakura run "Read README.md and summarize it" --tools read --session .sakura/demo.jsonl
152
+ sakura run "读 README.md 并总结" --tools read --session .sakura/demo.jsonl
111
153
  sakura resume --tools read --session .sakura/demo.jsonl
112
154
  sakura --help
113
155
  ```
114
156
 
115
- 加 `--json` 可按行输出 JSON。用 `--cwd`、`--base-url`、`--model`、`--context-window` 配置运行。CLI 默认不开放内置工具,使用时按需选择。
157
+ 先设置 `DEEPSEEK_API_KEY` 或 `SAKURA_API_KEY`。不传 `--tools` 时命令行不带任何内置工具。加 `--json` 输出 JSON 行,用 `--cwd`、`--base-url`、`--model`、`--context-window` 配置运行。
158
+
159
+ ## 常见问题
116
160
 
117
- ## 选择工具执行位置
161
+ **崩溃后怎么恢复?** 会话文件只追加,一条消息完整结束才写一行。在同一个文件上重新运行即可:只读调用如果当时没完成,就直接再跑一次;可能跑了一半的命令得到一条"被中断,请先检查现状"的结果而不是再跑一遍;被截断的最后一行在打开文件时会修复。
118
162
 
119
- SDK 管理 Agent 循环、模型请求和会话状态。执行环境负责文件与 shell。用 `createLocalEnvironment()` 在本地执行,或给 `createRemoteEnvironment({ cwd, run })` 传入自己的执行器。例如,本地应用可以管理会话,把构建交给装好依赖的 Linux 工作环境。
163
+ **能自己手动压缩对话吗?** 能。上下文进入模型窗口剩余 17%(且不少于 16384 个 token)时会自动压缩;`agent.run(undefined, { compact: true })` 则立刻压缩,不管多大:把较早的部分写成摘要,较新的部分原样保留,并用 `compaction` 事件告诉你(没有值得总结的内容时没有这个事件)。`agent.run("下一个问题", { compact: true })` 先压缩再回答,新问题不会被写进摘要。
120
164
 
121
- 默认本地环境沿用进程权限。`cwd` 用于选择工作目录。不受信任的任务应使用隔离执行器。会话文件和 JSON 事件日志可能包含敏感内容。审核与脱敏按需启用,独立的审核客户端需要单独包装脱敏。
165
+ **能回到对话的某个早先位置吗?** 能,有两种办法。`agent.rewindTo(条目 id)` 在同一个会话文件里回退:接下来说的话开出新分支,旧分支仍在文件里。`agent.fork(路径, 条目 id)` 把到那个点为止的对话复制成一个新的会话文件,用 `createAgent({ sessionFile: 路径 })` 打开;原会话不变,两边都可以继续。分叉不会把一个回合切成两半:最后一条消息里的工具调用的结果会一起带上。
122
166
 
123
- ## 验证
167
+ **模型能让命令在后台运行吗?** 能。`bash` 有 `background: true` 参数,任何命令跑过 120 秒也会自动转后台。模型会被告知进程号和输出文件,命令结束时也会被告知。模型可以要求命令结束时叫醒自己(`bash` 调用里写 `wake: true`,不需要就写 `wake: false`);应用用 `createAgent` 的 `wake` 选项设置默认值。一定要调用 `agent.close()`:留着没结束的命令会让 Node 进程一直不退出,`close()`、Ctrl-C 和 `process.exit` 都会停掉它。
124
168
 
125
- 公开 CI 在 Linux 和 macOS 跑离线检查。在一轮独立的本地 DeepSeek Flash 评测中,Sakura 通过了 10 项 Lean 证明检查和 20 项追加日志检查,两项任务合计通过 96 次后台校验。评测使用外部隔离,传输故障后从检查点续跑。
169
+ **怎么不让模型看到密钥?** `redaction` 插件在每个请求里把密钥(基于 gitleaks 的规则表加几条通用规则)换成稳定的占位符,在工具运行前把回复里的占位符还原,扫描失败时拒绝发出请求。`redaction.wrap(llm)` 让审核模型得到同样的保护。它保护的是发给模型服务商的内容,不是保存在本机的内容:会话文件、事件和追踪里保留真实值(工具要用它们,恢复会话也是从那个文件读),导出或分享前请先用 `redaction.scrub(text)` 打码。它不能阻止你放行的命令去读它自己进程的环境变量。
170
+
171
+ **自动审核检查什么?** 先是硬规则(留在工作目录内的只读命令放行,删除根目录这类灾难性命令拒绝;指向工作目录之外的只读命令交给审核模型),再是可选的额外授权方,最后是审核模型,按风险和用户授权打分。拒绝会作为工具错误回给 Agent,让它换条路;审核本身出任何故障都按拒绝处理。审核是一道安全网,不是沙箱:它只读命令的文字,跟不到符号链接,也不限制文件访问范围。
172
+
173
+ **思考型模型把输出上限全用来思考怎么办?** 回复会以 `finish_reason: length` 被截断,而且没有文字。默认启用的 `resilience` 插件会发现这种情况,让模型继续,并把下一次请求的 `max_tokens` 加大(翻倍,封顶 32768);重试两次之后让运行结束。空回复会再问一次。`resiliencePlugin({ cutEmpty: false })` 可以关掉其中一项。
174
+
175
+ **支持 MCP 吗?** 作为客户端,走 stdio。工具可以直接声明、通过 BM25 搜索找到,或在代码模式的脚本里调用。
176
+
177
+ **什么是代码模式?脚本已经在隔离的运行环境里了,为什么脚本里的调用还要审核?** 代码模式是让模型写一小段 JavaScript,在一段程序里调用多个工具,而不是每次请求只调一个。这段程序在隔离的 JavaScript 运行环境里执行(QuickJS 编译成 WebAssembly),它只管住**程序本身**碰不到机器,不是命令的沙箱。它调用的工具(一条 shell 命令、一次写文件、一个 MCP 工具)是在这个运行环境之外以完整权限运行的,所以脚本里的一次调用和直接发起同一次调用风险一样。脚本里的调用不经过循环的钩子,所以要明确地去请求审核:装了 `review` 插件,脚本里的每一次调用都会逐个被审核(脚本整体不再重复审核),被拒绝就在脚本里变成一个错误;没装,所有调用直接执行,和没有审核时的直接调用一样。
178
+
179
+ **工具在哪里运行?** 在 `Environment` 里。`createLocalEnvironment()` 使用调用进程的权限;`createRemoteEnvironment({ cwd, run })` 把命令交给你提供的执行器(比如一个容器)。它只是适配器,不是沙箱。`createSshEnvironment({ host, cwd })` 是用于能通过 ssh 到达的机器的适配器:登录一次,所有命令都走这一条连接,并在系统提示里告诉模型它的工具在别处执行、连接可能出问题(出问题时工具结果会如实说明,并给出命令输出所在的位置,绝不会把连接断了说成命令结束了)。认证和主机密钥完全由你的 ssh 配置决定;远端机器需要 `bash`、`base64` 和 `setsid`,所以应当是 Linux 而不是 macOS。在本机,超时、取消和退出会停掉命令以及它启动的所有进程,但停不掉自己开了新会话的进程(比如用 `setsid`);要停掉所有进程,请把命令放进容器。长命令输出和远程脚本保存在工作目录下的 `.sakura/` 里。
180
+
181
+ **哪些模型能用?** 任何支持 Chat Completions(含流式和工具调用)的模型。兼容开关覆盖各家的差异(字段名、推理内容、图片处理)。
182
+
183
+ ## 开发
126
184
 
127
185
  ```sh
128
- npm run check
186
+ npm ci
187
+ npm run check # 边界、类型、lint、格式,然后跑全部测试
188
+ npm run test:unit # 一次一个包(packages/*/test)
189
+ npm run test:contract # 同一套测试跑遍一个接口的所有实现
190
+ npm run test:tools # 仓库规则:依赖方向、导出数量上限、无循环引用
191
+ npm run example # 用脚本化模型跑的快速开始示例
192
+ npm run build # 把 SDK 和命令行编译到 dist/
129
193
  ```
130
194
 
195
+ 所有测试都不调用真实模型。
196
+
131
197
  ## 许可
132
198
 
133
- [MIT](LICENSE) · [第三方许可](THIRD_PARTY_NOTICES.md) · [旧 Python 源码](https://github.com/Vist233/Sakura/tree/3e9b52cca7ff9e69fc6fc5d4b865b5e44ac338dd)
199
+ MIT,见 [LICENSE](LICENSE)。取自其他项目的代码和文字(Pi、OpenAI Codex、gitleaks)登记在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md),许可证原文在 `licenses/`。早先的 Python 源码保留在仓库历史里。