sakura-agent 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +198 -2
- package/README.zh-CN.md +199 -0
- package/THIRD_PARTY_NOTICES.md +13 -0
- package/dist/packages/agent/src/compose.d.ts +25 -0
- package/dist/packages/agent/src/compose.js +91 -0
- package/dist/packages/agent/src/index.d.ts +130 -0
- package/dist/packages/agent/src/index.js +161 -0
- package/dist/packages/agent/src/restrict-tools.d.ts +6 -0
- package/dist/packages/agent/src/restrict-tools.js +11 -0
- package/dist/packages/agent/src/system-prompt.d.ts +15 -0
- package/dist/packages/agent/src/system-prompt.js +49 -0
- package/dist/packages/agent/src/wake.d.ts +41 -0
- package/dist/packages/agent/src/wake.js +71 -0
- package/dist/packages/env/src/confine.d.ts +13 -0
- package/dist/packages/env/src/confine.js +40 -0
- package/dist/packages/env/src/connect-ssh.d.ts +33 -0
- package/dist/packages/env/src/connect-ssh.js +132 -0
- package/dist/packages/env/src/index.d.ts +7 -0
- package/dist/packages/env/src/index.js +4 -0
- package/dist/packages/env/src/local.d.ts +12 -0
- package/dist/packages/env/src/local.js +206 -0
- package/dist/packages/env/src/remote.d.ts +31 -0
- package/dist/packages/env/src/remote.js +170 -0
- package/dist/packages/evals/src/bfcl.d.ts +24 -0
- package/dist/packages/evals/src/bfcl.js +71 -0
- package/dist/packages/evals/src/index.d.ts +3 -0
- package/dist/packages/evals/src/index.js +2 -0
- package/dist/packages/evals/src/png.d.ts +2 -0
- package/dist/packages/evals/src/png.js +30 -0
- package/dist/packages/evals/src/pool.d.ts +3 -0
- package/dist/packages/evals/src/pool.js +22 -0
- package/dist/packages/evals/src/providers.d.ts +13 -0
- package/dist/packages/evals/src/providers.js +23 -0
- package/dist/packages/kernel/src/channel.d.ts +7 -0
- package/dist/packages/kernel/src/channel.js +34 -0
- package/dist/packages/kernel/src/compaction.d.ts +44 -0
- package/dist/packages/kernel/src/compaction.js +89 -0
- package/dist/packages/kernel/src/constants.d.ts +2 -0
- package/dist/packages/kernel/src/constants.js +5 -0
- package/dist/packages/kernel/src/goal.d.ts +35 -0
- package/dist/packages/kernel/src/goal.js +65 -0
- package/dist/packages/kernel/src/hooks.d.ts +34 -0
- package/dist/packages/kernel/src/hooks.js +153 -0
- package/dist/packages/kernel/src/index.d.ts +7 -0
- package/dist/packages/kernel/src/index.js +4 -0
- package/dist/packages/kernel/src/loop.d.ts +75 -0
- package/dist/packages/kernel/src/loop.js +556 -0
- package/dist/packages/kernel/src/prompts.d.ts +18 -0
- package/dist/packages/kernel/src/prompts.js +99 -0
- package/dist/packages/kernel/src/system.d.ts +25 -0
- package/dist/packages/kernel/src/system.js +49 -0
- package/dist/packages/kernel/src/types.d.ts +398 -0
- package/dist/packages/kernel/src/types.js +23 -0
- package/dist/packages/kernel/src/usage.d.ts +3 -0
- package/dist/packages/kernel/src/usage.js +16 -0
- package/dist/packages/llm/src/chat/compat.d.ts +19 -0
- package/dist/packages/llm/src/chat/compat.js +16 -0
- package/dist/packages/llm/src/chat/errors.d.ts +8 -0
- package/dist/packages/llm/src/chat/errors.js +44 -0
- package/dist/packages/llm/src/chat/request.d.ts +12 -0
- package/dist/packages/llm/src/chat/request.js +109 -0
- package/dist/packages/llm/src/chat/retry.d.ts +26 -0
- package/dist/packages/llm/src/chat/retry.js +57 -0
- package/dist/packages/llm/src/chat/stream.d.ts +17 -0
- package/dist/packages/llm/src/chat/stream.js +121 -0
- package/dist/packages/llm/src/index.d.ts +45 -0
- package/dist/packages/llm/src/index.js +120 -0
- package/dist/packages/plugins/agents/src/index.d.ts +33 -0
- package/dist/packages/plugins/agents/src/index.js +206 -0
- package/dist/packages/plugins/agents/src/view.d.ts +24 -0
- package/dist/packages/plugins/agents/src/view.js +60 -0
- package/dist/packages/plugins/context/src/index.d.ts +27 -0
- package/dist/packages/plugins/context/src/index.js +67 -0
- package/dist/packages/plugins/limits/src/cost.d.ts +9 -0
- package/dist/packages/plugins/limits/src/cost.js +3 -0
- package/dist/packages/plugins/limits/src/index.d.ts +31 -0
- package/dist/packages/plugins/limits/src/index.js +31 -0
- package/dist/packages/plugins/redaction/src/engine.d.ts +24 -0
- package/dist/packages/plugins/redaction/src/engine.js +110 -0
- package/dist/packages/plugins/redaction/src/index.d.ts +28 -0
- package/dist/packages/plugins/redaction/src/index.js +69 -0
- package/dist/packages/plugins/redaction/src/restore.d.ts +6 -0
- package/dist/packages/plugins/redaction/src/restore.js +44 -0
- package/dist/packages/plugins/redaction/src/rules.gen.d.ts +14 -0
- package/dist/packages/plugins/redaction/src/rules.gen.js +2059 -0
- package/dist/packages/plugins/resilience/src/index.d.ts +31 -0
- package/dist/packages/plugins/resilience/src/index.js +86 -0
- package/dist/packages/plugins/review/src/chain.d.ts +33 -0
- package/dist/packages/plugins/review/src/chain.js +25 -0
- package/dist/packages/plugins/review/src/hard-rules.d.ts +13 -0
- package/dist/packages/plugins/review/src/hard-rules.js +156 -0
- package/dist/packages/plugins/review/src/index.d.ts +58 -0
- package/dist/packages/plugins/review/src/index.js +62 -0
- package/dist/packages/plugins/review/src/policy.d.ts +15 -0
- package/dist/packages/plugins/review/src/policy.js +65 -0
- package/dist/packages/plugins/review/src/reviewer.d.ts +39 -0
- package/dist/packages/plugins/review/src/reviewer.js +119 -0
- package/dist/packages/plugins/review/src/shell.d.ts +10 -0
- package/dist/packages/plugins/review/src/shell.js +124 -0
- package/dist/packages/plugins/tools/src/artifacts.d.ts +27 -0
- package/dist/packages/plugins/tools/src/artifacts.js +61 -0
- package/dist/packages/plugins/tools/src/builtin/args.d.ts +11 -0
- package/dist/packages/plugins/tools/src/builtin/args.js +46 -0
- package/dist/packages/plugins/tools/src/builtin/bash.d.ts +11 -0
- package/dist/packages/plugins/tools/src/builtin/bash.js +110 -0
- package/dist/packages/plugins/tools/src/builtin/edit.d.ts +2 -0
- package/dist/packages/plugins/tools/src/builtin/edit.js +71 -0
- package/dist/packages/plugins/tools/src/builtin/read.d.ts +2 -0
- package/dist/packages/plugins/tools/src/builtin/read.js +75 -0
- package/dist/packages/plugins/tools/src/builtin/write.d.ts +2 -0
- package/dist/packages/plugins/tools/src/builtin/write.js +35 -0
- package/dist/packages/plugins/tools/src/edit-match.d.ts +21 -0
- package/dist/packages/plugins/tools/src/edit-match.js +159 -0
- package/dist/packages/plugins/tools/src/fold.d.ts +16 -0
- package/dist/packages/plugins/tools/src/fold.js +65 -0
- package/dist/packages/plugins/tools/src/index.d.ts +10 -0
- package/dist/packages/plugins/tools/src/index.js +10 -0
- package/dist/packages/plugins/toolset/src/bm25.d.ts +8 -0
- package/dist/packages/plugins/toolset/src/bm25.js +35 -0
- package/dist/packages/plugins/toolset/src/codemode.d.ts +47 -0
- package/dist/packages/plugins/toolset/src/codemode.js +226 -0
- package/dist/packages/plugins/toolset/src/index.d.ts +6 -0
- package/dist/packages/plugins/toolset/src/index.js +3 -0
- package/dist/packages/plugins/toolset/src/mcp.d.ts +27 -0
- package/dist/packages/plugins/toolset/src/mcp.js +173 -0
- package/dist/packages/plugins/toolset/src/search.d.ts +12 -0
- package/dist/packages/plugins/toolset/src/search.js +57 -0
- package/dist/packages/plugins/trace/src/index.d.ts +27 -0
- package/dist/packages/plugins/trace/src/index.js +29 -0
- package/dist/packages/plugins/trace/src/modules.d.ts +6 -0
- package/dist/packages/plugins/trace/src/modules.js +28 -0
- package/dist/packages/session/src/file-log.d.ts +25 -0
- package/dist/packages/session/src/file-log.js +144 -0
- package/dist/packages/session/src/fork.d.ts +16 -0
- package/dist/packages/session/src/fork.js +70 -0
- package/dist/packages/session/src/index.d.ts +6 -0
- package/dist/packages/session/src/index.js +3 -0
- package/dist/packages/session/src/log-core.d.ts +37 -0
- package/dist/packages/session/src/log-core.js +47 -0
- package/dist/packages/session/src/memory-log.d.ts +7 -0
- package/dist/packages/session/src/memory-log.js +7 -0
- package/dist/packages/session/src/projection.d.ts +2 -0
- package/dist/packages/session/src/projection.js +90 -0
- package/dist/packages/session/src/tree.d.ts +13 -0
- package/dist/packages/session/src/tree.js +35 -0
- package/dist/packages/testing/src/faux.d.ts +36 -0
- package/dist/packages/testing/src/faux.js +69 -0
- package/dist/packages/testing/src/index.d.ts +4 -0
- package/dist/packages/testing/src/index.js +2 -0
- package/dist/packages/testing/src/rng.d.ts +7 -0
- package/dist/packages/testing/src/rng.js +17 -0
- package/dist/sdk/cli.d.ts +2 -0
- package/dist/sdk/cli.js +118 -0
- package/dist/sdk/index.d.ts +18 -0
- package/dist/sdk/index.js +10 -0
- package/dist/sdk/testing.d.ts +2 -0
- package/dist/sdk/testing.js +2 -0
- package/licenses/codex-Apache-2.0.txt +201 -0
- package/licenses/gitleaks-MIT.txt +21 -0
- package/licenses/pi-mono-MIT.txt +21 -0
- package/package.json +78 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025–2026 Vist233
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,199 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sakura
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) · [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Why Sakura
|
|
8
|
+
|
|
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.
|
|
15
|
+
|
|
16
|
+
## How it compares
|
|
17
|
+
|
|
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.
|
|
19
|
+
|
|
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
|
|
31
|
+
|
|
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.
|
|
35
|
+
|
|
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.
|
|
37
|
+
|
|
38
|
+
## Where an agent can reach
|
|
39
|
+
|
|
40
|
+
```ts
|
|
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")) }] };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
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
|
+
|
|
110
|
+
const agent = await createAgent({
|
|
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 },
|
|
113
|
+
tools: ["read"],
|
|
114
|
+
sessionFile: ".sakura/session.jsonl", // omit to keep the conversation in memory
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
const run = agent.run("Summarize README.md.");
|
|
118
|
+
for await (const event of run.events) {
|
|
119
|
+
if (event.type === "tool_start") console.log("tool:", event.call.name);
|
|
120
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
121
|
+
for (const part of event.message.content) if (part.type === "text") console.log(part.text);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log((await run.result).reason); // "done" | "cancelled" | "paused" | "error"
|
|
125
|
+
} finally {
|
|
126
|
+
await agent.close(); // always: it stops commands still running in the background and releases the session file
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
To resume, create an agent with the same `sessionFile` and call `agent.run()` without a prompt.
|
|
131
|
+
|
|
132
|
+
Turning on the safety plugins and sub-agents:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
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
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
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.
|
|
148
|
+
|
|
149
|
+
## The command line
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
sakura run "Read README.md and summarize it" --tools read --session .sakura/demo.jsonl
|
|
153
|
+
sakura resume --tools read --session .sakura/demo.jsonl
|
|
154
|
+
sakura --help
|
|
155
|
+
```
|
|
156
|
+
|
|
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
|
|
160
|
+
|
|
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.
|
|
162
|
+
|
|
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.
|
|
164
|
+
|
|
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.
|
|
166
|
+
|
|
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.
|
|
168
|
+
|
|
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
|
|
184
|
+
|
|
185
|
+
```sh
|
|
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/
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
None of the tests calls a real model.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
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
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Sakura
|
|
2
|
+
|
|
3
|
+
[English](README.md) · [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
**Sakura 是一个能嵌入、能审核、能放在任何地方的 Agent 循环。** 一个小小的 TypeScript 内核(约 1600 行、8 个钩子)负责循环,其余的一切都是可以不装的插件。你像定义一个对象那样用代码定义 Agent,工具就是普通对象,并且精确决定它能触到哪里:这个目录、通过 SSH 连上的另一台机器,或者任何能执行命令的函数。它以 `sakura-agent` 这个 npm 包发布,包含 SDK 和一个小的 `sakura` 命令行工具;仓库里还有一个基于同一个 SDK 的、类似 Codex 的终端。
|
|
6
|
+
|
|
7
|
+
## 为什么用 Sakura
|
|
8
|
+
|
|
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 服务器的实测。
|
|
15
|
+
|
|
16
|
+
## 和别的工具比
|
|
17
|
+
|
|
18
|
+
我对比过的两个工具,依据它们公开的文档(2026 年 11 月)。它们是给坐在键盘前的人用的产品;Sakura 是可以拆开、嵌进去的那一部分。
|
|
19
|
+
|
|
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
|
+
## 适合谁
|
|
31
|
+
|
|
32
|
+
- 你让 Agent **无人看着也能运行**:批处理、很多个 Agent、远程机器,或者放在你自己的产品里。
|
|
33
|
+
- 你想用代码定义 Agent,并从那里控制它的模型、会话、工具和能触到的范围。
|
|
34
|
+
- 你想要把审核和密钥脱敏做成独立、可测试的部件,而不是一个大程序里的开关,以及一个坐下来一次就能读完的循环。
|
|
35
|
+
|
|
36
|
+
它不是给你自己笔记本用的全能助手,也不带沙箱:默认的本机环境使用调用进程的权限。`confine` 是文件工具的边界,不是 shell 的边界:shell 命令仍然能触到任何地方,所以给 Agent 一个 shell 时要同时开启审核,或者把不可信的任务放在你自己的隔离环境里运行。
|
|
37
|
+
|
|
38
|
+
## Agent 能触到哪里
|
|
39
|
+
|
|
40
|
+
```ts
|
|
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")) }] };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
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
|
+
|
|
110
|
+
const agent = await createAgent({
|
|
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 },
|
|
113
|
+
tools: ["read"],
|
|
114
|
+
sessionFile: ".sakura/session.jsonl", // 不写则会话只放在内存里
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
const run = agent.run("总结一下 README.md。");
|
|
118
|
+
for await (const event of run.events) {
|
|
119
|
+
if (event.type === "tool_start") console.log("tool:", event.call.name);
|
|
120
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
121
|
+
for (const part of event.message.content) if (part.type === "text") console.log(part.text);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log((await run.result).reason); // "done" | "cancelled" | "paused" | "error"
|
|
125
|
+
} finally {
|
|
126
|
+
await agent.close(); // 一定要调用:它会停掉还在后台运行的命令,并释放会话文件锁
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
要继续一个会话,用同一个 `sessionFile` 创建 Agent,再不带提示词调用 `agent.run()`。
|
|
131
|
+
|
|
132
|
+
打开安全类插件和子 Agent:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
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
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
插件可以包含插件:`bundle("safety", [redaction, review])` 是由两个插件组成的一个插件。`createAgent` 会把每个插件只装一次,按 `requires` 排好顺序,缺依赖、成环或重名时在创建阶段就报错。
|
|
148
|
+
|
|
149
|
+
## 命令行
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
sakura run "读 README.md 并总结" --tools read --session .sakura/demo.jsonl
|
|
153
|
+
sakura resume --tools read --session .sakura/demo.jsonl
|
|
154
|
+
sakura --help
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
先设置 `DEEPSEEK_API_KEY` 或 `SAKURA_API_KEY`。不传 `--tools` 时命令行不带任何内置工具。加 `--json` 输出 JSON 行,用 `--cwd`、`--base-url`、`--model`、`--context-window` 配置运行。
|
|
158
|
+
|
|
159
|
+
## 常见问题
|
|
160
|
+
|
|
161
|
+
**崩溃后怎么恢复?** 会话文件只追加,一条消息完整结束才写一行。在同一个文件上重新运行即可:只读调用如果当时没完成,就直接再跑一次;可能跑了一半的命令得到一条"被中断,请先检查现状"的结果而不是再跑一遍;被截断的最后一行在打开文件时会修复。
|
|
162
|
+
|
|
163
|
+
**能自己手动压缩对话吗?** 能。上下文进入模型窗口剩余 17%(且不少于 16384 个 token)时会自动压缩;`agent.run(undefined, { compact: true })` 则立刻压缩,不管多大:把较早的部分写成摘要,较新的部分原样保留,并用 `compaction` 事件告诉你(没有值得总结的内容时没有这个事件)。`agent.run("下一个问题", { compact: true })` 先压缩再回答,新问题不会被写进摘要。
|
|
164
|
+
|
|
165
|
+
**能回到对话的某个早先位置吗?** 能,有两种办法。`agent.rewindTo(条目 id)` 在同一个会话文件里回退:接下来说的话开出新分支,旧分支仍在文件里。`agent.fork(路径, 条目 id)` 把到那个点为止的对话复制成一个新的会话文件,用 `createAgent({ sessionFile: 路径 })` 打开;原会话不变,两边都可以继续。分叉不会把一个回合切成两半:最后一条消息里的工具调用的结果会一起带上。
|
|
166
|
+
|
|
167
|
+
**模型能让命令在后台运行吗?** 能。`bash` 有 `background: true` 参数,任何命令跑过 120 秒也会自动转后台。模型会被告知进程号和输出文件,命令结束时也会被告知。模型可以要求命令结束时叫醒自己(`bash` 调用里写 `wake: true`,不需要就写 `wake: false`);应用用 `createAgent` 的 `wake` 选项设置默认值。一定要调用 `agent.close()`:留着没结束的命令会让 Node 进程一直不退出,`close()`、Ctrl-C 和 `process.exit` 都会停掉它。
|
|
168
|
+
|
|
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
|
+
## 开发
|
|
184
|
+
|
|
185
|
+
```sh
|
|
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/
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
所有测试都不调用真实模型。
|
|
196
|
+
|
|
197
|
+
## 许可
|
|
198
|
+
|
|
199
|
+
MIT,见 [LICENSE](LICENSE)。取自其他项目的代码和文字(Pi、OpenAI Codex、gitleaks)登记在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md),许可证原文在 `licenses/`。早先的 Python 源码保留在仓库历史里。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Sakura borrows from a few other projects. Where code or text was copied or adapted, the source file says so in its header, and the licence text is kept in `licenses/`.
|
|
4
|
+
|
|
5
|
+
| Project | Licence | What Sakura took |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| [Pi](https://github.com/badlogic/pi-mono) (Mario Zechner) | MIT | The edit-matching code in `packages/plugins/tools/src/edit-match.ts`, and the wording of the `read`, `write`, `edit` and `bash` tool descriptions, adapted. The structure and some rules of the system prompt follow Pi's. |
|
|
8
|
+
| [gitleaks](https://github.com/gitleaks/gitleaks) (Zachary Rice) | MIT | The secret-detection rules in `packages/plugins/redaction/src/rules.gen.ts`, converted to TypeScript. |
|
|
9
|
+
| [OpenAI Codex](https://github.com/openai/codex) | Apache-2.0 | Ideas only: some task-execution principles in the system prompt, reworded. No code or text is copied. |
|
|
10
|
+
|
|
11
|
+
Dependency: [`quickjs-emscripten`](https://github.com/justjake/quickjs-emscripten) (MIT) runs the code-mode scripts. It is installed from npm, not copied.
|
|
12
|
+
|
|
13
|
+
Licence texts: `licenses/pi-mono-MIT.txt`, `licenses/gitleaks-MIT.txt`, `licenses/codex-Apache-2.0.txt`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Plugin } from "../../kernel/src/index.js";
|
|
2
|
+
/** A plugin that may say what it needs and what it offers. The kernel ignores the two extra fields. */
|
|
3
|
+
export interface Composable extends Plugin {
|
|
4
|
+
/** Names (of plugins or of capabilities that plugins provide) that must be installed before this one. */
|
|
5
|
+
requires?: string[];
|
|
6
|
+
/** Capability names this plugin offers besides its own name, so others can depend on "what" rather than "who". */
|
|
7
|
+
provides?: string[];
|
|
8
|
+
/** Set by bundle(): lets compose() see through the bundle to the plugins inside it. */
|
|
9
|
+
children?: Plugin[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One plugin made of several: installing it installs them all, in the order given. All three entry points are passed on
|
|
13
|
+
* (setup, prepare, afterCompaction); passing on only setup would silently drop the other two. Bundles may contain bundles.
|
|
14
|
+
* Give a bundle to createAgent (or to compose) and a plugin that is in two bundles is installed once.
|
|
15
|
+
*/
|
|
16
|
+
export declare function bundle(name: string, children: Plugin[]): Composable;
|
|
17
|
+
export declare class CompositionError extends Error {
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Turns a list of plugins that may contain and depend on each other into the flat, ordered list the kernel wants: bundles opened up,
|
|
22
|
+
* each plugin once, every plugin after the ones it requires. Throws before anything runs when a requirement is missing, the requirements
|
|
23
|
+
* form a cycle, or two different plugins share a name. Plugins keep the order they were listed in unless a requirement says otherwise.
|
|
24
|
+
*/
|
|
25
|
+
export declare function compose(listed: Plugin[]): Plugin[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const unique = (items) => [...new Set(items)];
|
|
2
|
+
/**
|
|
3
|
+
* One plugin made of several: installing it installs them all, in the order given. All three entry points are passed on
|
|
4
|
+
* (setup, prepare, afterCompaction); passing on only setup would silently drop the other two. Bundles may contain bundles.
|
|
5
|
+
* Give a bundle to createAgent (or to compose) and a plugin that is in two bundles is installed once.
|
|
6
|
+
*/
|
|
7
|
+
export function bundle(name, children) {
|
|
8
|
+
const compensates = children.flatMap((child) => (child.compensates ? [child.compensates] : []));
|
|
9
|
+
const withPrepare = children.filter((child) => child.prepare);
|
|
10
|
+
const withAfter = children.filter((child) => child.afterCompaction);
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
children,
|
|
14
|
+
...(compensates.length > 0 ? { compensates: compensates.join("; ") } : {}),
|
|
15
|
+
requires: unique(children.flatMap((child) => child.requires ?? [])),
|
|
16
|
+
provides: unique(children.flatMap((child) => [child.name, ...(child.provides ?? [])])),
|
|
17
|
+
setup(api) {
|
|
18
|
+
for (const child of children)
|
|
19
|
+
child.setup(api);
|
|
20
|
+
},
|
|
21
|
+
...(withPrepare.length > 0
|
|
22
|
+
? {
|
|
23
|
+
async prepare(context) {
|
|
24
|
+
for (const child of withPrepare)
|
|
25
|
+
await child.prepare?.(context);
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
: {}),
|
|
29
|
+
...(withAfter.length > 0
|
|
30
|
+
? {
|
|
31
|
+
async afterCompaction(context) {
|
|
32
|
+
for (const child of withAfter)
|
|
33
|
+
await child.afterCompaction?.(context);
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export class CompositionError extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "CompositionError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Turns a list of plugins that may contain and depend on each other into the flat, ordered list the kernel wants: bundles opened up,
|
|
47
|
+
* each plugin once, every plugin after the ones it requires. Throws before anything runs when a requirement is missing, the requirements
|
|
48
|
+
* form a cycle, or two different plugins share a name. Plugins keep the order they were listed in unless a requirement says otherwise.
|
|
49
|
+
*/
|
|
50
|
+
export function compose(listed) {
|
|
51
|
+
const providers = new Map(); // capability -> names of the plugins that offer it
|
|
52
|
+
const byName = new Map();
|
|
53
|
+
const open = (plugin, groups) => {
|
|
54
|
+
if (plugin.children) {
|
|
55
|
+
for (const child of plugin.children)
|
|
56
|
+
open(child, [...groups, plugin.name]);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const known = byName.get(plugin.name);
|
|
60
|
+
if (known && known !== plugin)
|
|
61
|
+
throw new CompositionError(`two different plugins are named '${plugin.name}'`);
|
|
62
|
+
byName.set(plugin.name, plugin);
|
|
63
|
+
for (const capability of [plugin.name, ...(plugin.provides ?? []), ...groups])
|
|
64
|
+
providers.set(capability, (providers.get(capability) ?? new Set()).add(plugin.name));
|
|
65
|
+
};
|
|
66
|
+
for (const plugin of listed)
|
|
67
|
+
open(plugin, []);
|
|
68
|
+
const ordered = [];
|
|
69
|
+
const state = new Map();
|
|
70
|
+
const visit = (name, path) => {
|
|
71
|
+
if (state.get(name) === "done")
|
|
72
|
+
return;
|
|
73
|
+
if (state.get(name) === "visiting")
|
|
74
|
+
throw new CompositionError(`dependency cycle: ${[...path, name].join(" -> ")}`);
|
|
75
|
+
state.set(name, "visiting");
|
|
76
|
+
const plugin = byName.get(name);
|
|
77
|
+
for (const need of plugin.requires ?? []) {
|
|
78
|
+
const offered = providers.get(need);
|
|
79
|
+
if (!offered)
|
|
80
|
+
throw new CompositionError(`plugin '${name}' requires '${need}', which nothing provides`);
|
|
81
|
+
for (const provider of offered)
|
|
82
|
+
if (provider !== name)
|
|
83
|
+
visit(provider, [...path, name]);
|
|
84
|
+
}
|
|
85
|
+
state.set(name, "done");
|
|
86
|
+
ordered.push(plugin);
|
|
87
|
+
};
|
|
88
|
+
for (const name of byName.keys())
|
|
89
|
+
visit(name, []);
|
|
90
|
+
return ordered;
|
|
91
|
+
}
|