sakura-agent 0.1.0 → 0.1.2
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/README.md +207 -77
- package/README.zh-CN.md +208 -78
- package/THIRD_PARTY_NOTICES.md +8 -58
- package/dist/packages/agent/src/index.d.ts +49 -11
- package/dist/packages/agent/src/index.js +50 -55
- 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 +5 -0
- package/dist/packages/agent/src/system-prompt.js +1 -1
- 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 +45 -0
- package/dist/packages/env/src/connect-ssh.js +242 -0
- package/dist/packages/env/src/index.d.ts +6 -1
- package/dist/packages/env/src/index.js +3 -1
- package/dist/packages/env/src/local.d.ts +11 -2
- package/dist/packages/env/src/local.js +42 -10
- package/dist/packages/env/src/remote-stream.d.ts +48 -0
- package/dist/packages/env/src/remote-stream.js +143 -0
- package/dist/packages/env/src/remote-types.d.ts +17 -0
- package/dist/packages/env/src/remote-types.js +10 -0
- package/dist/packages/env/src/remote.d.ts +16 -7
- package/dist/packages/env/src/remote.js +73 -10
- 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/compaction.d.ts +24 -3
- package/dist/packages/kernel/src/compaction.js +23 -16
- package/dist/packages/kernel/src/goal.d.ts +10 -1
- package/dist/packages/kernel/src/goal.js +28 -0
- package/dist/packages/kernel/src/hooks.d.ts +5 -0
- package/dist/packages/kernel/src/hooks.js +21 -1
- package/dist/packages/kernel/src/index.d.ts +3 -2
- package/dist/packages/kernel/src/index.js +1 -0
- package/dist/packages/kernel/src/loop.d.ts +28 -4
- package/dist/packages/kernel/src/loop.js +69 -47
- package/dist/packages/kernel/src/types.d.ts +25 -4
- 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/errors.d.ts +11 -0
- package/dist/packages/llm/src/chat/errors.js +17 -0
- package/dist/packages/llm/src/chat/request.js +22 -4
- package/dist/packages/llm/src/chat/retry.d.ts +1 -1
- package/dist/packages/llm/src/chat/retry.js +1 -1
- package/dist/packages/llm/src/chat/stream.js +2 -1
- package/dist/packages/llm/src/index.js +7 -2
- package/dist/packages/plugins/context/src/index.d.ts +2 -1
- package/dist/packages/plugins/context/src/index.js +2 -1
- package/dist/packages/plugins/limits/src/index.d.ts +4 -1
- package/dist/packages/plugins/redaction/src/index.d.ts +3 -0
- package/dist/packages/plugins/redaction/src/index.js +3 -0
- package/dist/packages/plugins/redaction/src/rules.gen.js +1 -1
- package/dist/packages/plugins/review/src/chain.d.ts +10 -1
- package/dist/packages/plugins/review/src/chain.js +7 -4
- package/dist/packages/plugins/review/src/hard-rules.d.ts +6 -0
- package/dist/packages/plugins/review/src/hard-rules.js +58 -25
- package/dist/packages/plugins/review/src/index.d.ts +21 -0
- package/dist/packages/plugins/review/src/index.js +42 -7
- package/dist/packages/plugins/review/src/reviewer.d.ts +32 -3
- package/dist/packages/plugins/review/src/reviewer.js +115 -27
- package/dist/packages/plugins/tools/src/builtin/bash.js +24 -10
- package/dist/packages/plugins/tools/src/builtin/image.d.ts +11 -0
- package/dist/packages/plugins/tools/src/builtin/image.js +84 -0
- package/dist/packages/plugins/tools/src/builtin/read.js +26 -12
- package/dist/packages/plugins/toolset/src/codemode.d.ts +17 -6
- package/dist/packages/plugins/toolset/src/codemode.js +84 -26
- package/dist/packages/plugins/toolset/src/index.d.ts +1 -1
- package/dist/packages/plugins/toolset/src/index.js +1 -1
- 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 +6 -1
- package/dist/packages/session/src/file-log.js +45 -25
- 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 +2 -0
- package/dist/packages/session/src/index.js +1 -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 +3 -12
- package/dist/packages/session/src/memory-log.js +3 -23
- package/dist/packages/session/src/projection.js +22 -3
- 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/index.d.ts +11 -3
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/testing.d.ts +2 -0
- package/dist/sdk/testing.js +2 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -2,109 +2,215 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) · [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
**Sakura is an
|
|
5
|
+
**Sakura is the Codex-style coding agent as a library.** The whole loop (tools, sessions, compaction, goals, review, sub-agents) lives in code you own, so defining an agent for one specific job, or starting a fleet of them, takes a few lines instead of a configuration project with plugin switches. It is written in TypeScript and published on npm as `sakura-agent`, with an SDK and a small `sakura` command-line tool; the terminal built on the same SDK is [`sakuracode`](code/README.md).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```ts
|
|
8
|
+
import { createAgent } from "sakura-agent";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
const agent = await createAgent({ model }); // this one line
|
|
11
|
+
const run = agent.run("Add tests for parse() and make them pass.");
|
|
12
|
+
```
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
That one line already gives a complete Codex-style agent: the `read`, `write`, `edit` and `bash` tools, project instructions (`AGENTS.md`), automatic compaction when the context fills, automatic recovery from replies that were cut off or came back empty, and budgets you can set. Give it a `sessionFile` to resume, fork and rewind; give it a `goal` to have it keep working toward one.
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
## Why Sakura
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
**You own the complete inside of a Codex-style agent, instead of using a finished product you cannot change.** Everything else follows from that.
|
|
19
|
+
|
|
20
|
+
### 1. One line to start, and the default is complete
|
|
21
|
+
|
|
22
|
+
You do not pick plugins first. The default set is what a command-line agent needs; when you want more (review, redaction, sub-agents, MCP), each is one more line.
|
|
23
|
+
|
|
24
|
+
### 2. Specialising an agent means changing parameters, not flipping switches
|
|
25
|
+
|
|
26
|
+
A purpose is a few parameters: the directory the agent works in, the tools it gets, what it is told, and what it has to achieve.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// a read-only reviewer
|
|
30
|
+
await createAgent({ model, tools: ["read"], rules: ["Review only; do not change any file."] });
|
|
31
|
+
|
|
32
|
+
// an agent with a goal and a check: it may not stop while the check fails
|
|
33
|
+
await createAgent({ model, goal: { objective: "Make all tests pass", verify: "npm test" } });
|
|
34
|
+
|
|
35
|
+
// tools running on another machine, with automatic review of what runs there
|
|
36
|
+
const environment = await createSshEnvironment({ host: "agent@box", cwd: "/srv/job" });
|
|
37
|
+
await createAgent({ model, environment, plugins: [...corePlugins(), reviewPlugin({ llm: reviewerModel })] });
|
|
38
|
+
|
|
39
|
+
// an agent that can hand work to sub-agents (they inherit review and redaction)
|
|
40
|
+
await createAgent({ model, subAgents: {} });
|
|
21
41
|
```
|
|
22
42
|
|
|
23
|
-
|
|
43
|
+
### 3. A fleet of agents is cheap
|
|
44
|
+
|
|
45
|
+
An agent is an object: its own working directory, its own `Environment`, its own session file, and nothing read from the process's current directory. So a fleet is an array and `Promise.all`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const results = await Promise.all(
|
|
49
|
+
jobs.map(async (job) => {
|
|
50
|
+
const agent = await createAgent({ model, cwd: job.dir, confine: true, tools: job.tools, rules: job.rules, sessionFile: job.session });
|
|
51
|
+
const run = agent.run(job.task);
|
|
52
|
+
for await (const _event of run.events) {} // write the events to your own log here
|
|
53
|
+
return (await run.result).reason;
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
```
|
|
24
57
|
|
|
25
|
-
|
|
58
|
+
A complete example you can run is [`examples/fleet.ts`](examples/fleet.ts) (a scripted model when there is no API key, so it works offline: `npm run example:fleet`): three agents for documentation, tests and review, each in its own directory and all running at once; the read-only review agent cannot have written a file.
|
|
26
59
|
|
|
27
|
-
|
|
60
|
+
### 4. Where it can reach, who reviews it and what it may spend are options in your code
|
|
61
|
+
|
|
62
|
+
- **Where it can reach:** `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; any function that can run a shell command can be an environment.
|
|
63
|
+
- **Who reviews it:** 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; every decision is recorded in the session log. Agents can run unattended. An agent with no shell (file tools plus a checker that answers true or false) needs nothing more than its confined directory, so no second model has to run.
|
|
64
|
+
- **What it may spend:** `limits` covers turns, tokens, cost and run time.
|
|
65
|
+
|
|
66
|
+
## How it compares
|
|
67
|
+
|
|
68
|
+
Tools such as Codex CLI and pi are finished products you use as they are; Sakura gives you the parts inside such a product, to take apart, recombine and put into your own product or a fleet of agents. The comparison is from their public documentation (November 2026).
|
|
69
|
+
|
|
70
|
+
| | Codex CLI | pi | Sakura |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| 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: the whole agent is in your code; with a minimal CLI and a separate terminal |
|
|
73
|
+
| 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 |
|
|
74
|
+
| Sub-agents, plan mode | not checked | left out by design | sub-agents built in (opt-in) |
|
|
75
|
+
| Where the tools run | this machine, in a sandbox | not described in its README | this machine, SSH, or any function, chosen per agent |
|
|
76
|
+
| Models | OpenAI models by default | built-in providers via `/login` | any Chat Completions endpoint (tried with DeepSeek, Kimi, OpenCode Go) |
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
## Who it is for
|
|
81
|
+
|
|
82
|
+
- You want to define an agent in code and control its model, session, tools and reach from there.
|
|
83
|
+
- You want many agents, each with a specific job: batch work, remote machines, or an agent inside your own product, running without anyone watching.
|
|
84
|
+
- 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.
|
|
85
|
+
|
|
86
|
+
It is not an all-in-one assistant for your own laptop (for that, use [`sakuracode`](code/README.md): `npm install -g sakuracode`), 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 isolation you provide yourself.
|
|
87
|
+
|
|
88
|
+
## Where an agent can reach
|
|
28
89
|
|
|
29
90
|
```ts
|
|
30
|
-
import {
|
|
31
|
-
import type {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
43
|
-
effect: "read",
|
|
44
|
-
async execute() {
|
|
45
|
-
return { content: [{ type: "text", text: "Build passed." }] };
|
|
46
|
-
},
|
|
47
|
-
});
|
|
91
|
+
import { createAgent, createSshEnvironment, corePlugins, reviewPlugin } from "sakura-agent";
|
|
92
|
+
import type { Tool } from "sakura-agent";
|
|
93
|
+
|
|
94
|
+
// a tool is a plain object
|
|
95
|
+
const checkProof: Tool = {
|
|
96
|
+
name: "check_proof",
|
|
97
|
+
description: "Check proof.txt. Answers true or false.",
|
|
98
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
99
|
+
effect: "read",
|
|
100
|
+
async execute(_args, { env }) {
|
|
101
|
+
const proof = new TextDecoder().decode(await env.readFile("proof.txt"));
|
|
102
|
+
return { content: [{ type: "text", text: String(proof.includes("QED")) }] };
|
|
48
103
|
},
|
|
49
104
|
};
|
|
50
105
|
|
|
106
|
+
// 1. no shell, confined to its directory: no reviewer is needed
|
|
107
|
+
const prover = await createAgent({ model, cwd: "/work/job-17", confine: true, tools: ["read", "write", "edit", checkProof] });
|
|
108
|
+
|
|
109
|
+
// 2. the same loop, with the tools on another machine, and a reviewer for what it runs there
|
|
110
|
+
const environment = await createSshEnvironment({ host: "agent@box", cwd: "/srv/job-17" });
|
|
111
|
+
const remote = await createAgent({ model, environment, plugins: [...corePlugins(), reviewPlugin({ llm: reviewerModel })] });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## What we measured
|
|
115
|
+
|
|
116
|
+
Numbers from real runs, each with the conditions it was taken under. They describe those runs; they are not guarantees, and provider behaviour can change.
|
|
117
|
+
|
|
118
|
+
| What | Result | Conditions |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| Prompt cache | The median request read **98.8 %** of its input from the provider's cache; 95.7 % of all input tokens (97.7 % without the first request after each compaction) | One real coding session on `deepseek-v4.1-flash` via OpenCode Go: 20.8 hours, 1,817 model requests. Compaction and review requests are not counted. Caching is done by the provider |
|
|
121
|
+
| Long unattended run | **20.8 hours**, 1,817 model requests, 1,766 tool calls, no provider error and no crash; the process stayed at about 160 MB | The same session, measured over those first 20.8 hours |
|
|
122
|
+
| Automatic review | **445** reviewed calls in the next 15 hours: **0** technical failures of the reviewer, 2 refusals (a cancelled run, and an `rm -rf` of the working directory) | The same session after the reviewer's conversation was bounded and a failed review is retried once. Before that, 9 reviews failed technically because the reviewer's own conversation had grown too long |
|
|
123
|
+
| Benchmark | No complete Terminal-Bench 2.1 score yet, so none is published | A partial run exists; it is not a score |
|
|
124
|
+
|
|
125
|
+
One limit to know: later in that same session, an error the provider sent inside a stream stopped the run once. Such errors are now classified like the same error as an HTTP status and retried.
|
|
126
|
+
|
|
127
|
+
## At a glance
|
|
128
|
+
|
|
129
|
+
- **What it is:** a library for TypeScript code, plus a minimal CLI (`sakura run`, `sakura resume`) and, in the repository, a terminal.
|
|
130
|
+
- **Runtime:** Node.js 26 or newer. TypeScript sources are run directly in development; the npm package ships compiled JavaScript.
|
|
131
|
+
- **Models:** any OpenAI-style Chat Completions endpoint (tried with DeepSeek, Kimi and OpenCode Go). One wire protocol, with per-model compatibility switches.
|
|
132
|
+
- **Kernel:** a stateless loop, eight hooks, three interfaces (`LlmClient`, `Log`, `Environment`), conversation compaction and goals.
|
|
133
|
+
- **Persistence:** an append-only, tree-shaped JSONL session file. A crashed run continues from the same file.
|
|
134
|
+
- **Plugins on by default:** `tools`, `context`, `resilience`, `limits`. **Opt in:** `review`, `redaction`, sub-agents, MCP client, tool search, code mode.
|
|
135
|
+
- **Safety plugins fail closed:** if the review or the redaction cannot do its job, the action or request does not go ahead.
|
|
136
|
+
- **Tests:** 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.
|
|
137
|
+
- **License:** MIT.
|
|
138
|
+
|
|
139
|
+
## How it is built
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
kernel the loop, 8 hooks, 3 interfaces (LlmClient, Log, Environment), compaction, goals
|
|
143
|
+
session the conversation log: in memory, or a JSONL file (tree-shaped, single writer, repairs a torn last line)
|
|
144
|
+
llm one protocol (Chat Completions): retries, resend after a dropped stream, stall detection
|
|
145
|
+
env where tools run: the local machine, or a remote runner you provide
|
|
146
|
+
agent createAgent(): assembles the above with a default plugin set
|
|
147
|
+
plugins/* tools, context, resilience, limits, review, redaction, agents, toolset
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- **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.
|
|
151
|
+
- **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.
|
|
152
|
+
- **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.
|
|
153
|
+
|
|
154
|
+
| Plugin | What it does | On by default |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `tools` | `read`, `write`, `edit`, `bash` (background commands, `sleep N` waits, long output saved to a file) | yes |
|
|
157
|
+
| `context` | loads `AGENTS.md` as pinned instructions, reloads it after compaction | yes |
|
|
158
|
+
| `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 |
|
|
159
|
+
| `limits` | turn, token, cost and run-time budgets (the run time is checked between turns, not during one); pauses on quota errors | yes (unlimited) |
|
|
160
|
+
| `review` | automatic review of risky tool calls: hard rules, extra authorizers, a reviewer model; fails closed | no |
|
|
161
|
+
| `redaction` | swaps secrets for stable placeholders before a request leaves, restores them in the reply | no |
|
|
162
|
+
| `agents` | `delegate`, `agent_view`, `agent_stop` for sub-agents (they inherit review and redaction); enabled with the `subAgents` option | no |
|
|
163
|
+
| `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 |
|
|
164
|
+
|
|
165
|
+
## Install and run
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
npm install sakura-agent
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { compatPresets, createAgent } from "sakura-agent";
|
|
173
|
+
|
|
51
174
|
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
175
|
cwd: process.cwd(),
|
|
176
|
+
model: { baseUrl: "https://api.deepseek.com", apiKey: process.env.DEEPSEEK_API_KEY!, model: "deepseek-flash", contextWindow: 128_000, compat: compatPresets.deepseek },
|
|
60
177
|
tools: ["read"],
|
|
61
|
-
|
|
62
|
-
...corePlugins().filter((plugin) => plugin.name !== "limits"),
|
|
63
|
-
limitsPlugin({ maxTurns: 8 }),
|
|
64
|
-
projectTools,
|
|
65
|
-
],
|
|
66
|
-
sessionFile: ".sakura/session.jsonl",
|
|
67
|
-
maxTokens: 1_024,
|
|
178
|
+
sessionFile: ".sakura/session.jsonl", // omit to keep the conversation in memory
|
|
68
179
|
});
|
|
69
|
-
|
|
70
180
|
try {
|
|
71
|
-
const run = agent.run("
|
|
181
|
+
const run = agent.run("Summarize README.md.");
|
|
72
182
|
for await (const event of run.events) {
|
|
73
|
-
if (event.type === "tool_start") console.log(event.call.name);
|
|
183
|
+
if (event.type === "tool_start") console.log("tool:", event.call.name);
|
|
74
184
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
75
185
|
for (const part of event.message.content) if (part.type === "text") console.log(part.text);
|
|
76
186
|
}
|
|
77
187
|
}
|
|
78
|
-
console.log(await run.result);
|
|
188
|
+
console.log((await run.result).reason); // "done" | "cancelled" | "paused" | "error"
|
|
79
189
|
} finally {
|
|
80
|
-
await agent.close();
|
|
190
|
+
await agent.close(); // always: it stops commands still running in the background and releases the session file
|
|
81
191
|
}
|
|
82
192
|
```
|
|
83
193
|
|
|
84
|
-
|
|
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. |
|
|
194
|
+
To resume, create an agent with the same `sessionFile` and call `agent.run()` without a prompt.
|
|
93
195
|
|
|
94
|
-
|
|
196
|
+
Turning on the safety plugins and sub-agents:
|
|
95
197
|
|
|
96
198
|
```ts
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
199
|
+
import { corePlugins, createAgent, redactionPlugin, reviewPlugin } from "sakura-agent";
|
|
200
|
+
|
|
201
|
+
const redaction = redactionPlugin({ key: "stable-key" });
|
|
202
|
+
// `model` and `reviewerModel` are model settings or clients, as in the first example
|
|
203
|
+
const agent = await createAgent({
|
|
204
|
+
model,
|
|
205
|
+
plugins: [...corePlugins(), redaction, reviewPlugin({ llm: redaction.wrap(reviewerModel) })], // the reviewer must not see secrets either
|
|
206
|
+
subAgents: {}, // sub-agents get the same plugins
|
|
207
|
+
mcp: { servers: { files: { command: "npx", args: ["some-mcp-server"] } }, expose: "codemode" }, // script calls are reviewed too
|
|
208
|
+
});
|
|
103
209
|
```
|
|
104
210
|
|
|
105
|
-
|
|
211
|
+
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
212
|
|
|
107
|
-
|
|
213
|
+
## The command line
|
|
108
214
|
|
|
109
215
|
```sh
|
|
110
216
|
sakura run "Read README.md and summarize it" --tools read --session .sakura/demo.jsonl
|
|
@@ -112,22 +218,46 @@ sakura resume --tools read --session .sakura/demo.jsonl
|
|
|
112
218
|
sakura --help
|
|
113
219
|
```
|
|
114
220
|
|
|
115
|
-
Add `--json` for JSON lines or
|
|
221
|
+
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.
|
|
222
|
+
|
|
223
|
+
## Frequently asked questions
|
|
224
|
+
|
|
225
|
+
**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.
|
|
116
226
|
|
|
117
|
-
|
|
227
|
+
**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.
|
|
118
228
|
|
|
119
|
-
|
|
229
|
+
**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.
|
|
120
230
|
|
|
121
|
-
|
|
231
|
+
**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.
|
|
122
232
|
|
|
123
|
-
|
|
233
|
+
**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.
|
|
124
234
|
|
|
125
|
-
|
|
235
|
+
**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.
|
|
236
|
+
|
|
237
|
+
**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.
|
|
238
|
+
|
|
239
|
+
**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.
|
|
240
|
+
|
|
241
|
+
**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.
|
|
242
|
+
|
|
243
|
+
**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.
|
|
244
|
+
|
|
245
|
+
**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).
|
|
246
|
+
|
|
247
|
+
## Development
|
|
126
248
|
|
|
127
249
|
```sh
|
|
128
|
-
npm
|
|
250
|
+
npm ci
|
|
251
|
+
npm run check # boundaries, types, lint, format, then all tests
|
|
252
|
+
npm run test:unit # one package at a time (packages/*/test)
|
|
253
|
+
npm run test:contract # one suite run against every implementation of an interface
|
|
254
|
+
npm run test:tools # repository rules: dependency direction, export ceilings, no cycles
|
|
255
|
+
npm run example # the quick-start example with a scripted model
|
|
256
|
+
npm run build # compile the SDK and CLI into dist/
|
|
129
257
|
```
|
|
130
258
|
|
|
259
|
+
None of the tests calls a real model.
|
|
260
|
+
|
|
131
261
|
## License
|
|
132
262
|
|
|
133
|
-
[
|
|
263
|
+
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.
|