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.
Files changed (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -2
  3. package/README.zh-CN.md +199 -0
  4. package/THIRD_PARTY_NOTICES.md +13 -0
  5. package/dist/packages/agent/src/compose.d.ts +25 -0
  6. package/dist/packages/agent/src/compose.js +91 -0
  7. package/dist/packages/agent/src/index.d.ts +130 -0
  8. package/dist/packages/agent/src/index.js +161 -0
  9. package/dist/packages/agent/src/restrict-tools.d.ts +6 -0
  10. package/dist/packages/agent/src/restrict-tools.js +11 -0
  11. package/dist/packages/agent/src/system-prompt.d.ts +15 -0
  12. package/dist/packages/agent/src/system-prompt.js +49 -0
  13. package/dist/packages/agent/src/wake.d.ts +41 -0
  14. package/dist/packages/agent/src/wake.js +71 -0
  15. package/dist/packages/env/src/confine.d.ts +13 -0
  16. package/dist/packages/env/src/confine.js +40 -0
  17. package/dist/packages/env/src/connect-ssh.d.ts +33 -0
  18. package/dist/packages/env/src/connect-ssh.js +132 -0
  19. package/dist/packages/env/src/index.d.ts +7 -0
  20. package/dist/packages/env/src/index.js +4 -0
  21. package/dist/packages/env/src/local.d.ts +12 -0
  22. package/dist/packages/env/src/local.js +206 -0
  23. package/dist/packages/env/src/remote.d.ts +31 -0
  24. package/dist/packages/env/src/remote.js +170 -0
  25. package/dist/packages/evals/src/bfcl.d.ts +24 -0
  26. package/dist/packages/evals/src/bfcl.js +71 -0
  27. package/dist/packages/evals/src/index.d.ts +3 -0
  28. package/dist/packages/evals/src/index.js +2 -0
  29. package/dist/packages/evals/src/png.d.ts +2 -0
  30. package/dist/packages/evals/src/png.js +30 -0
  31. package/dist/packages/evals/src/pool.d.ts +3 -0
  32. package/dist/packages/evals/src/pool.js +22 -0
  33. package/dist/packages/evals/src/providers.d.ts +13 -0
  34. package/dist/packages/evals/src/providers.js +23 -0
  35. package/dist/packages/kernel/src/channel.d.ts +7 -0
  36. package/dist/packages/kernel/src/channel.js +34 -0
  37. package/dist/packages/kernel/src/compaction.d.ts +44 -0
  38. package/dist/packages/kernel/src/compaction.js +89 -0
  39. package/dist/packages/kernel/src/constants.d.ts +2 -0
  40. package/dist/packages/kernel/src/constants.js +5 -0
  41. package/dist/packages/kernel/src/goal.d.ts +35 -0
  42. package/dist/packages/kernel/src/goal.js +65 -0
  43. package/dist/packages/kernel/src/hooks.d.ts +34 -0
  44. package/dist/packages/kernel/src/hooks.js +153 -0
  45. package/dist/packages/kernel/src/index.d.ts +7 -0
  46. package/dist/packages/kernel/src/index.js +4 -0
  47. package/dist/packages/kernel/src/loop.d.ts +75 -0
  48. package/dist/packages/kernel/src/loop.js +556 -0
  49. package/dist/packages/kernel/src/prompts.d.ts +18 -0
  50. package/dist/packages/kernel/src/prompts.js +99 -0
  51. package/dist/packages/kernel/src/system.d.ts +25 -0
  52. package/dist/packages/kernel/src/system.js +49 -0
  53. package/dist/packages/kernel/src/types.d.ts +398 -0
  54. package/dist/packages/kernel/src/types.js +23 -0
  55. package/dist/packages/kernel/src/usage.d.ts +3 -0
  56. package/dist/packages/kernel/src/usage.js +16 -0
  57. package/dist/packages/llm/src/chat/compat.d.ts +19 -0
  58. package/dist/packages/llm/src/chat/compat.js +16 -0
  59. package/dist/packages/llm/src/chat/errors.d.ts +8 -0
  60. package/dist/packages/llm/src/chat/errors.js +44 -0
  61. package/dist/packages/llm/src/chat/request.d.ts +12 -0
  62. package/dist/packages/llm/src/chat/request.js +109 -0
  63. package/dist/packages/llm/src/chat/retry.d.ts +26 -0
  64. package/dist/packages/llm/src/chat/retry.js +57 -0
  65. package/dist/packages/llm/src/chat/stream.d.ts +17 -0
  66. package/dist/packages/llm/src/chat/stream.js +121 -0
  67. package/dist/packages/llm/src/index.d.ts +45 -0
  68. package/dist/packages/llm/src/index.js +120 -0
  69. package/dist/packages/plugins/agents/src/index.d.ts +33 -0
  70. package/dist/packages/plugins/agents/src/index.js +206 -0
  71. package/dist/packages/plugins/agents/src/view.d.ts +24 -0
  72. package/dist/packages/plugins/agents/src/view.js +60 -0
  73. package/dist/packages/plugins/context/src/index.d.ts +27 -0
  74. package/dist/packages/plugins/context/src/index.js +67 -0
  75. package/dist/packages/plugins/limits/src/cost.d.ts +9 -0
  76. package/dist/packages/plugins/limits/src/cost.js +3 -0
  77. package/dist/packages/plugins/limits/src/index.d.ts +31 -0
  78. package/dist/packages/plugins/limits/src/index.js +31 -0
  79. package/dist/packages/plugins/redaction/src/engine.d.ts +24 -0
  80. package/dist/packages/plugins/redaction/src/engine.js +110 -0
  81. package/dist/packages/plugins/redaction/src/index.d.ts +28 -0
  82. package/dist/packages/plugins/redaction/src/index.js +69 -0
  83. package/dist/packages/plugins/redaction/src/restore.d.ts +6 -0
  84. package/dist/packages/plugins/redaction/src/restore.js +44 -0
  85. package/dist/packages/plugins/redaction/src/rules.gen.d.ts +14 -0
  86. package/dist/packages/plugins/redaction/src/rules.gen.js +2059 -0
  87. package/dist/packages/plugins/resilience/src/index.d.ts +31 -0
  88. package/dist/packages/plugins/resilience/src/index.js +86 -0
  89. package/dist/packages/plugins/review/src/chain.d.ts +33 -0
  90. package/dist/packages/plugins/review/src/chain.js +25 -0
  91. package/dist/packages/plugins/review/src/hard-rules.d.ts +13 -0
  92. package/dist/packages/plugins/review/src/hard-rules.js +156 -0
  93. package/dist/packages/plugins/review/src/index.d.ts +58 -0
  94. package/dist/packages/plugins/review/src/index.js +62 -0
  95. package/dist/packages/plugins/review/src/policy.d.ts +15 -0
  96. package/dist/packages/plugins/review/src/policy.js +65 -0
  97. package/dist/packages/plugins/review/src/reviewer.d.ts +39 -0
  98. package/dist/packages/plugins/review/src/reviewer.js +119 -0
  99. package/dist/packages/plugins/review/src/shell.d.ts +10 -0
  100. package/dist/packages/plugins/review/src/shell.js +124 -0
  101. package/dist/packages/plugins/tools/src/artifacts.d.ts +27 -0
  102. package/dist/packages/plugins/tools/src/artifacts.js +61 -0
  103. package/dist/packages/plugins/tools/src/builtin/args.d.ts +11 -0
  104. package/dist/packages/plugins/tools/src/builtin/args.js +46 -0
  105. package/dist/packages/plugins/tools/src/builtin/bash.d.ts +11 -0
  106. package/dist/packages/plugins/tools/src/builtin/bash.js +110 -0
  107. package/dist/packages/plugins/tools/src/builtin/edit.d.ts +2 -0
  108. package/dist/packages/plugins/tools/src/builtin/edit.js +71 -0
  109. package/dist/packages/plugins/tools/src/builtin/read.d.ts +2 -0
  110. package/dist/packages/plugins/tools/src/builtin/read.js +75 -0
  111. package/dist/packages/plugins/tools/src/builtin/write.d.ts +2 -0
  112. package/dist/packages/plugins/tools/src/builtin/write.js +35 -0
  113. package/dist/packages/plugins/tools/src/edit-match.d.ts +21 -0
  114. package/dist/packages/plugins/tools/src/edit-match.js +159 -0
  115. package/dist/packages/plugins/tools/src/fold.d.ts +16 -0
  116. package/dist/packages/plugins/tools/src/fold.js +65 -0
  117. package/dist/packages/plugins/tools/src/index.d.ts +10 -0
  118. package/dist/packages/plugins/tools/src/index.js +10 -0
  119. package/dist/packages/plugins/toolset/src/bm25.d.ts +8 -0
  120. package/dist/packages/plugins/toolset/src/bm25.js +35 -0
  121. package/dist/packages/plugins/toolset/src/codemode.d.ts +47 -0
  122. package/dist/packages/plugins/toolset/src/codemode.js +226 -0
  123. package/dist/packages/plugins/toolset/src/index.d.ts +6 -0
  124. package/dist/packages/plugins/toolset/src/index.js +3 -0
  125. package/dist/packages/plugins/toolset/src/mcp.d.ts +27 -0
  126. package/dist/packages/plugins/toolset/src/mcp.js +173 -0
  127. package/dist/packages/plugins/toolset/src/search.d.ts +12 -0
  128. package/dist/packages/plugins/toolset/src/search.js +57 -0
  129. package/dist/packages/plugins/trace/src/index.d.ts +27 -0
  130. package/dist/packages/plugins/trace/src/index.js +29 -0
  131. package/dist/packages/plugins/trace/src/modules.d.ts +6 -0
  132. package/dist/packages/plugins/trace/src/modules.js +28 -0
  133. package/dist/packages/session/src/file-log.d.ts +25 -0
  134. package/dist/packages/session/src/file-log.js +144 -0
  135. package/dist/packages/session/src/fork.d.ts +16 -0
  136. package/dist/packages/session/src/fork.js +70 -0
  137. package/dist/packages/session/src/index.d.ts +6 -0
  138. package/dist/packages/session/src/index.js +3 -0
  139. package/dist/packages/session/src/log-core.d.ts +37 -0
  140. package/dist/packages/session/src/log-core.js +47 -0
  141. package/dist/packages/session/src/memory-log.d.ts +7 -0
  142. package/dist/packages/session/src/memory-log.js +7 -0
  143. package/dist/packages/session/src/projection.d.ts +2 -0
  144. package/dist/packages/session/src/projection.js +90 -0
  145. package/dist/packages/session/src/tree.d.ts +13 -0
  146. package/dist/packages/session/src/tree.js +35 -0
  147. package/dist/packages/testing/src/faux.d.ts +36 -0
  148. package/dist/packages/testing/src/faux.js +69 -0
  149. package/dist/packages/testing/src/index.d.ts +4 -0
  150. package/dist/packages/testing/src/index.js +2 -0
  151. package/dist/packages/testing/src/rng.d.ts +7 -0
  152. package/dist/packages/testing/src/rng.js +17 -0
  153. package/dist/sdk/cli.d.ts +2 -0
  154. package/dist/sdk/cli.js +118 -0
  155. package/dist/sdk/index.d.ts +18 -0
  156. package/dist/sdk/index.js +10 -0
  157. package/dist/sdk/testing.d.ts +2 -0
  158. package/dist/sdk/testing.js +2 -0
  159. package/licenses/codex-Apache-2.0.txt +201 -0
  160. package/licenses/gitleaks-MIT.txt +21 -0
  161. package/licenses/pi-mono-MIT.txt +21 -0
  162. package/package.json +78 -4
@@ -0,0 +1,130 @@
1
+ import type { ChatCompat } from "../../llm/src/index.js";
2
+ import type { CompactionConfig, ContextStatus, Environment, GoalRecord, LlmClient, Plugin, Prompts, RunHandle, RunOptions, ThinkingLevel, Tool, UsageTotals, UserInput } from "../../kernel/src/index.js";
3
+ import type { AgentDefinition } from "../../plugins/agents/src/index.js";
4
+ import type { McpServerConfig, McpServerStatus } from "../../plugins/toolset/src/index.js";
5
+ import type { RewindableLog } from "../../session/src/index.js";
6
+ import type { WakeSetting } from "./wake.js";
7
+ type BuiltinToolName = "read" | "write" | "edit" | "bash";
8
+ export interface ModelConfig {
9
+ /** Base URL without /chat/completions, for example https://api.deepseek.com */
10
+ baseUrl: string;
11
+ apiKey: string;
12
+ model: string;
13
+ contextWindow: number;
14
+ compat?: Partial<ChatCompat>;
15
+ headers?: Record<string, string>;
16
+ }
17
+ export interface AgentConfig {
18
+ /** A provider to talk to, or a ready-made client (for tests). */
19
+ model: ModelConfig | LlmClient;
20
+ cwd?: string;
21
+ /**
22
+ * Keep the file tools inside `cwd`: a read, write, edit or list of anything outside it is refused with an error the model sees, however the path is written
23
+ * (absolute, `..`, a link). For an agent with no shell tool this is the whole boundary, and no review is needed. A shell command can still reach anything,
24
+ * so with `bash` use it together with the review plugin. Only for the default local environment: build `createLocalEnvironment(cwd, { confine: true })` yourself
25
+ * for anything else (giving both is an error).
26
+ */
27
+ confine?: boolean;
28
+ /** The tools the model gets: names of the built-in ones (default all four) and tools you wrote. A kind of sub-agent that lists tool names gets only those, whoever registered them. */
29
+ tools?: Array<BuiltinToolName | Tool>;
30
+ /** After this many milliseconds a running shell command is moved to the background (default 120000). */
31
+ bashSoftTimeoutMs?: number;
32
+ /** How long a command the model starts with `background: true` is watched before it is left to run (default 1000), so that one that fails at once is reported as failed. */
33
+ bashBackgroundGraceMs?: number;
34
+ /** Defaults to corePlugins(). Safety plugins (review, redaction, sub-agents) are never in the default set. May contain bundles and plugins that say what they require; see compose(). */
35
+ plugins?: Plugin[];
36
+ /** Replaces the generated system prompt. */
37
+ system?: string;
38
+ /** Rules added to the generated system prompt, after the built-in ones. Sub-agents get them too. Not used when `system` replaces the prompt. */
39
+ rules?: string[];
40
+ /** Write the conversation to this JSONL file; without it the conversation lives in memory only. */
41
+ sessionFile?: string;
42
+ thinking?: ThinkingLevel;
43
+ maxTokens?: number;
44
+ goal?: {
45
+ objective: string;
46
+ verify?: string;
47
+ };
48
+ compaction?: false | Partial<CompactionConfig>;
49
+ prompts?: Partial<Prompts>;
50
+ environment?: Environment;
51
+ now?: () => number;
52
+ /**
53
+ * Wakes the agent by itself. When background work ends (a command moved to the background, a background sub-agent) and no run is going,
54
+ * a run is started so the agent hears about it; `onRun` receives each such run (to show its events). While work is still pending and the agent
55
+ * is quiet, it is woken once after `cacheCheckMs` (default 4 minutes, a little under the prompt cache lifetime) to check, and then left alone until something ends.
56
+ * The agent can also ask for it: `bash` takes `wake: true` or `wake: false` for one command, which wins over this setting for that command. With no `wake` here,
57
+ * a command that asked to wake the agent still does, and its run is not shown to anyone (the log has all of it). The check before the cache expires needs this setting.
58
+ */
59
+ wake?: WakeSetting;
60
+ /**
61
+ * MCP servers, started once when the agent is created (a server that fails is left out and shows in `agent.mcp`). Their tools are named mcp__<server>__<tool>.
62
+ * `expose`: "direct" declares every tool to the model; "search" lists only tool_search and tool_call; "codemode" lists only run_code, a script that can call the
63
+ * tools (each call goes through the review plugin when one is installed). The default is "direct" up to 10 tools and "search" above.
64
+ */
65
+ mcp?: {
66
+ servers: Record<string, McpServerConfig>;
67
+ expose?: "direct" | "search" | "codemode";
68
+ timeoutMs?: number;
69
+ };
70
+ /** Turns on sub-agents (delegate, agent_view, agent_stop). Off unless this is given. */
71
+ subAgents?: {
72
+ agents?: AgentDefinition[];
73
+ maxRegistered?: number;
74
+ maxRunning?: number;
75
+ /**
76
+ * Plugins (by name) that sub-agents do not get. Every other plugin of the agent is shared with its sub-agents, so review and redaction
77
+ * cannot be bypassed by delegating. Default: agents (no nesting), limits (a sub-agent has its own turn limit), context and tools (a sub-agent starts fresh).
78
+ */
79
+ notInherited?: string[];
80
+ /** Models by name, for sub-agent definitions that name one (`AgentDefinition.model`). A definition whose model is not in the table uses the parent's model. */
81
+ models?: Record<string, ModelConfig | LlmClient>;
82
+ };
83
+ }
84
+ export interface Agent {
85
+ /**
86
+ * Starts, resumes or follows up. See Kernel.run. `{ compact: true }` compacts the conversation first, whatever its size: `agent.run(undefined, { compact: true })`
87
+ * is "compact now", and the `compaction` event of the run says it was done.
88
+ */
89
+ run(input?: UserInput, options?: RunOptions): RunHandle;
90
+ queue(text: string): void;
91
+ steer(text: string): void;
92
+ cancel(): void;
93
+ log: RewindableLog;
94
+ /** Goes back to an earlier point of the conversation (an entry id from `log.branch()`, or null for the very start); what is said next starts a new branch. */
95
+ rewindTo(entryId: string | null): void;
96
+ /**
97
+ * Writes the conversation up to an earlier point (an entry id from `log.branch()`; left out, up to now) as a new session file, which is a session of
98
+ * its own: open it with `createAgent({ ...config, sessionFile: path })`. This session is not changed, and both can go on. A turn is not cut in two:
99
+ * the results of the tool calls of the last message copied come with it. Fails if the file exists.
100
+ */
101
+ fork(path: string, upTo?: string): Promise<void>;
102
+ /** What is waiting to be told to the agent and which background work it is waiting for. A host can use it to decide whether to start a run. */
103
+ pending(): {
104
+ signals: string[];
105
+ watching: string[];
106
+ };
107
+ /** The goal of the conversation, as the session file says it is (active, blocked or complete); undefined when there is none. */
108
+ goal(): Promise<GoalRecord | undefined>;
109
+ /** Sets the goal, replacing an earlier one; the next run is checked against it before it may stop. Part of the session, so it survives reopening and fork. Fails while a run is active. */
110
+ setGoal(goal: {
111
+ objective: string;
112
+ verify?: string;
113
+ }): Promise<void>;
114
+ /** Ends the goal (it counts as complete). Fails while a run is active. */
115
+ clearGoal(): Promise<void>;
116
+ /** How full the conversation is now: tokens used, the model's window, what is free, and where compaction starts (undefined when it is off). */
117
+ context(): Promise<ContextStatus>;
118
+ /** What the conversation has spent so far (tokens and replies), counted from the log, so it includes earlier runs of the same session file. */
119
+ usage(): Promise<UsageTotals>;
120
+ /** How each MCP server did when the agent was created (for the person running it; the model is not told). Empty without `mcp`. */
121
+ mcp: Record<string, McpServerStatus>;
122
+ /** Releases the session file lock. */
123
+ close(): Promise<void>;
124
+ }
125
+ /** The plugins every agent gets unless the caller passes its own list: project instructions (AGENTS.md), long tool output saved to files, resilience (replies that were cut off or came back empty are asked again), and limits (unlimited until configured). */
126
+ export declare function corePlugins(): Plugin[];
127
+ export declare function createAgent(config: AgentConfig): Promise<Agent>;
128
+ export { buildSystemPrompt } from "./system-prompt.js";
129
+ export { bundle, compose, CompositionError } from "./compose.js";
130
+ export type { Composable } from "./compose.js";
@@ -0,0 +1,161 @@
1
+ import { createChatClient } from "../../llm/src/index.js";
2
+ import { createLocalEnvironment } from "../../env/src/index.js";
3
+ import { createKernel } from "../../kernel/src/index.js";
4
+ import { agentsPlugin } from "../../plugins/agents/src/index.js";
5
+ import { contextPlugin } from "../../plugins/context/src/index.js";
6
+ import { CODEMODE_TOOL, codemodePlugin, connectMcp, searchPlugin } from "../../plugins/toolset/src/index.js";
7
+ import { limitsPlugin } from "../../plugins/limits/src/index.js";
8
+ import { resiliencePlugin } from "../../plugins/resilience/src/index.js";
9
+ import { builtinTools, toolsPlugin } from "../../plugins/tools/src/index.js";
10
+ import { createMemoryLog, forkLog, openFileLog } from "../../session/src/index.js";
11
+ import { compose } from "./compose.js";
12
+ import { restrictTools } from "./restrict-tools.js";
13
+ import { buildSystemPrompt } from "./system-prompt.js";
14
+ import { createWaker } from "./wake.js";
15
+ /** The plugins every agent gets unless the caller passes its own list: project instructions (AGENTS.md), long tool output saved to files, resilience (replies that were cut off or came back empty are asked again), and limits (unlimited until configured). */
16
+ export function corePlugins() {
17
+ return [contextPlugin(), toolsPlugin(), resiliencePlugin(), limitsPlugin()];
18
+ }
19
+ const SUB_AGENT_NOTE = "You are a sub-agent working for another agent. Do the task you are given completely, then finish with a clear written report of what you did and found. That report is the only thing that goes back to the agent that asked.";
20
+ const isClient = (model) => "stream" in model;
21
+ export async function createAgent(config) {
22
+ const cwd = config.cwd ?? process.cwd();
23
+ if (config.confine && config.environment)
24
+ throw new Error("confine applies to the default local environment; for your own environment build it with createLocalEnvironment(cwd, { confine: true })");
25
+ const env = config.environment ?? createLocalEnvironment(cwd, config.confine ? { confine: true } : {});
26
+ const connection = config.mcp ? await connectMcp(config.mcp.servers, config.mcp.timeoutMs !== undefined ? { timeoutMs: config.mcp.timeoutMs } : {}) : undefined;
27
+ const mcpTools = connection?.tools ?? [];
28
+ const expose = config.mcp?.expose ?? (mcpTools.length > 10 ? "search" : "direct");
29
+ const bashOptions = {
30
+ ...(config.bashSoftTimeoutMs !== undefined ? { softTimeoutMs: config.bashSoftTimeoutMs } : {}),
31
+ ...(config.bashBackgroundGraceMs !== undefined ? { backgroundGraceMs: config.bashBackgroundGraceMs } : {}),
32
+ };
33
+ const tools = (config.tools ?? ["read", "write", "edit", "bash"]).map((entry) => (typeof entry === "string" ? builtinTools([entry], { bash: bashOptions })[0] : entry));
34
+ const declared = expose === "direct" ? [...tools, ...mcpTools] : tools;
35
+ const clientFor = (model) => isClient(model)
36
+ ? model
37
+ : createChatClient({
38
+ baseUrl: model.baseUrl,
39
+ apiKey: model.apiKey,
40
+ model: model.model,
41
+ contextWindow: model.contextWindow,
42
+ ...(model.compat ? { compat: model.compat } : {}),
43
+ ...(model.headers ? { headers: model.headers } : {}),
44
+ // an image path that a tool returned is a path in the environment, which may be another machine
45
+ resolveImage: async (ref, mediaType) => `data:${mediaType};base64,${Buffer.from(await env.readFile(ref)).toString("base64")}`,
46
+ });
47
+ const llm = clientFor(config.model);
48
+ const thinkingModel = !isClient(config.model) && config.model.compat?.thinkingFormat !== undefined && config.model.compat.thinkingFormat !== "none";
49
+ const thinking = config.thinking ?? (thinkingModel ? "medium" : undefined);
50
+ const system = config.system ??
51
+ buildSystemPrompt({
52
+ tools,
53
+ cwd: env.cwd,
54
+ platform: process.platform,
55
+ ...(env.description !== undefined ? { description: env.description } : {}),
56
+ ...(config.rules ? { rules: config.rules } : {}),
57
+ });
58
+ // bundles are opened and a plugin that appears twice is installed once; a missing requirement, a cycle or two plugins with one name fail here, before anything runs
59
+ const plugins = compose(config.plugins ?? corePlugins());
60
+ const notInherited = new Set(config.subAgents?.notInherited ?? ["agents", "limits", "context", "tools"]);
61
+ const inherited = plugins.filter((plugin) => !notInherited.has(plugin.name));
62
+ if (mcpTools.length > 0 && expose === "search")
63
+ plugins.push(searchPlugin({ catalog: mcpTools }));
64
+ // a plugin that offers `authorize` (the review plugin does) is asked before each call a script makes, because those calls do not pass the loop's hooks
65
+ const reviewer = plugins.find((plugin) => typeof plugin.authorize === "function");
66
+ const authorize = reviewer?.authorize;
67
+ if (mcpTools.length > 0 && expose === "codemode") {
68
+ plugins.push(codemodePlugin({ catalog: mcpTools, ...(authorize ? { authorize } : {}) }));
69
+ // every call a script makes is reviewed one by one, so the script as a whole is not reviewed again
70
+ reviewer?.exempt?.(CODEMODE_TOOL);
71
+ }
72
+ const childLogs = [];
73
+ const childModels = new Map(Object.entries(config.subAgents?.models ?? {}).map(([name, model]) => [name, clientFor(model)]));
74
+ if (config.subAgents) {
75
+ const childLogDirectory = config.sessionFile?.replace(/\.jsonl$/, "") ?? undefined;
76
+ plugins.push(agentsPlugin({
77
+ ...(({ models: _models, notInherited: _notInherited, ...rest }) => rest)(config.subAgents),
78
+ createChild: ({ definition, maxTurns, model }) => createKernel({
79
+ llm: (model !== undefined && childModels.get(model)) || llm,
80
+ env,
81
+ system: `${definition.systemPrompt ?? system}\n\n${SUB_AGENT_NOTE}`,
82
+ tools: definition.tools ? tools.filter((tool) => definition.tools?.includes(tool.name)) : tools,
83
+ plugins: [...(definition.tools ? restrictTools(inherited, definition.tools) : inherited), ...(maxTurns !== undefined ? [limitsPlugin({ maxTurns })] : [])],
84
+ ...(config.maxTokens !== undefined ? { maxTokens: config.maxTokens } : {}),
85
+ ...(thinking !== undefined ? { thinking } : {}),
86
+ ...(config.compaction !== undefined ? { compaction: config.compaction } : {}),
87
+ }),
88
+ ...(childLogDirectory
89
+ ? {
90
+ openLog: async (id) => {
91
+ const log = await openFileLog(`${childLogDirectory}/agents/${id}.jsonl`, { cwd: env.cwd });
92
+ childLogs.push(log);
93
+ return log;
94
+ },
95
+ }
96
+ : {}),
97
+ }));
98
+ }
99
+ const kernel = createKernel({
100
+ llm,
101
+ env,
102
+ system,
103
+ tools: declared,
104
+ plugins,
105
+ ...(config.maxTokens !== undefined ? { maxTokens: config.maxTokens } : {}),
106
+ ...(thinking !== undefined ? { thinking } : {}),
107
+ ...(config.now ? { now: config.now } : {}),
108
+ onSignal: (_log, _text, options) => waker.signalled(options),
109
+ ...(config.compaction !== undefined ? { compaction: config.compaction } : {}),
110
+ ...(config.goal ? { goal: config.goal } : {}),
111
+ ...(config.prompts ? { prompts: config.prompts } : {}),
112
+ });
113
+ const fileLog = config.sessionFile ? await openFileLog(config.sessionFile, { cwd: env.cwd }) : undefined;
114
+ const log = fileLog ?? createMemoryLog();
115
+ let active;
116
+ const waker = createWaker({
117
+ setting: config.wake,
118
+ pending: () => kernel.pending(log),
119
+ start: (input) => start(input),
120
+ busy: () => active !== undefined,
121
+ });
122
+ function start(input, options) {
123
+ waker.started();
124
+ const handle = kernel.run(log, input, options);
125
+ active = handle;
126
+ const ended = (cancelled) => {
127
+ if (active === handle)
128
+ active = undefined;
129
+ waker.ended(cancelled);
130
+ };
131
+ void handle.result.then((result) => ended(result.reason === "cancelled"), () => ended(false));
132
+ return handle;
133
+ }
134
+ return {
135
+ log,
136
+ mcp: connection?.status ?? {},
137
+ rewindTo: (entryId) => log.rewindTo(entryId),
138
+ fork: async (path, upTo) => {
139
+ await forkLog(log, path, { cwd: env.cwd, ...(upTo !== undefined ? { upTo } : {}), ...(fileLog ? { sessionId: fileLog.header.id } : {}) });
140
+ },
141
+ pending: () => kernel.pending(log),
142
+ goal: () => kernel.goal(log),
143
+ setGoal: (goal) => kernel.setGoal(log, goal),
144
+ clearGoal: () => kernel.clearGoal(log),
145
+ context: () => kernel.context(log),
146
+ usage: () => kernel.usage(log),
147
+ run: (input, options) => start(input, options),
148
+ queue: (text) => active?.queue(text),
149
+ steer: (text) => active?.steer(text),
150
+ cancel: () => active?.cancel(),
151
+ close: async () => {
152
+ waker.close();
153
+ await connection?.close();
154
+ await env.dispose(); // stops commands that are still running in the background
155
+ await Promise.all(childLogs.map((log) => log.close()));
156
+ await fileLog?.close();
157
+ },
158
+ };
159
+ }
160
+ export { buildSystemPrompt } from "./system-prompt.js";
161
+ export { bundle, compose, CompositionError } from "./compose.js";
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from "../../kernel/src/index.js";
2
+ /**
3
+ * The plugins as a sub-agent with a limited tool list sees them: a tool a plugin registers is kept only if its name is on the list, the same rule
4
+ * that applies to the built-in tools. Everything else about the plugins stays as it is.
5
+ */
6
+ export declare function restrictTools(plugins: readonly Plugin[], allowed: readonly string[]): Plugin[];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The plugins as a sub-agent with a limited tool list sees them: a tool a plugin registers is kept only if its name is on the list, the same rule
3
+ * that applies to the built-in tools. Everything else about the plugins stays as it is.
4
+ */
5
+ export function restrictTools(plugins, allowed) {
6
+ const names = new Set(allowed);
7
+ return plugins.map((plugin) => ({
8
+ ...plugin,
9
+ setup: (api) => plugin.setup({ ...api, registerTool: (tool) => (names.has(tool.name) ? api.registerTool(tool) : undefined) }),
10
+ }));
11
+ }
@@ -0,0 +1,15 @@
1
+ import type { Tool } from "../../kernel/src/index.js";
2
+ export interface SystemPromptOptions {
3
+ tools: readonly Tool[];
4
+ cwd: string;
5
+ platform: string;
6
+ /**
7
+ * What the environment says about where the tools run (Environment.description). When there is one, it takes the place of the platform line,
8
+ * because the platform of this machine is then not where the commands run.
9
+ */
10
+ description?: string;
11
+ /** Extra rules from plugins. */
12
+ rules?: string[];
13
+ }
14
+ /** Builds the system prompt: preamble, tools, rules, environment. Callers can replace the whole thing. */
15
+ export declare function buildSystemPrompt(options: SystemPromptOptions): string;
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Rule texts adapted from the ideas in Pi's system prompt (MIT, Copyright (c) 2025 Mario Zechner) and the
3
+ * task-execution principles of OpenAI Codex's base instructions (Apache-2.0); reworded and shortened.
4
+ * See THIRD_PARTY_NOTICES.md.
5
+ */
6
+ const PREAMBLE = "You are a coding agent. You help the user by reading files, running commands, editing code and writing new files. Be precise, safe and helpful.";
7
+ const RULES = [
8
+ "Keep going until the user's request is completely resolved before you end your turn. Do not stop at a plan or a partial result.",
9
+ "Fix problems at the root cause rather than patching symptoms, and avoid unneeded complexity.",
10
+ "Keep changes minimal and consistent with the style of the existing code. Do not fix unrelated bugs; you may mention them in your final message.",
11
+ "In an existing codebase, do exactly what was asked with precision. In a brand-new project you may be more ambitious.",
12
+ "If the project has tests or a way to build or run, use them to check your work. Start with the most specific check for what you changed, then widen.",
13
+ "Do not re-read a file only to confirm an edit succeeded; a failed edit returns an error.",
14
+ "Do not commit changes or create branches unless the user asks.",
15
+ "Do not add comments or copyright headers to code unless the user asks.",
16
+ "Before a long or risky step, say in one short sentence what you are about to do. Do not narrate trivial reads.",
17
+ "Be concise. Show file paths clearly so the user can open them. Do not paste the full contents of files you have already written.",
18
+ "Instruction files from the project (AGENTS.md) and direct user messages take precedence over these rules.",
19
+ ];
20
+ /** Rules that belong to one tool; they appear only when that tool is available. */
21
+ const TOOL_RULES = {
22
+ read: ["Use read to examine files instead of cat or sed."],
23
+ write: ["Use write only for new files or complete rewrites. Keep each file under about 2000 lines; split larger work across several files, because a very long tool call can be cut off."],
24
+ edit: [
25
+ "Use edit for precise changes (edits[].oldText must match exactly).",
26
+ "When changing multiple separate locations in one file, use one edit call with several entries in edits[].",
27
+ "Each edits[].oldText is matched against the original file, not after earlier edits. Do not emit overlapping or nested edits; merge nearby changes.",
28
+ "Keep edits[].oldText as small as possible while still unique in the file.",
29
+ ],
30
+ bash: [
31
+ "When searching for text or files, prefer rg (rg --files for file names); fall back to grep or find if rg is missing.",
32
+ "Do not use scripts to print large chunks of a file; use read with offset and limit.",
33
+ "Commands run without a terminal and cannot read input. Always pass non-interactive flags (for example --yes, -y, --no-input, CI=1); a command that waits for input will hang.",
34
+ ],
35
+ };
36
+ const firstSentence = (description) => (description.split(/(?<=\.)\s/)[0] ?? description).trim();
37
+ /** Builds the system prompt: preamble, tools, rules, environment. Callers can replace the whole thing. */
38
+ export function buildSystemPrompt(options) {
39
+ const toolLines = options.tools.map((tool) => `- ${tool.name}: ${firstSentence(tool.description)}`);
40
+ const rules = [...new Set([...options.tools.flatMap((tool) => TOOL_RULES[tool.name] ?? []), ...RULES, ...(options.rules ?? [])])];
41
+ return [
42
+ PREAMBLE,
43
+ options.tools.length > 0 ? `<tools>\n${toolLines.join("\n")}\n</tools>` : "",
44
+ `<rules>\n${rules.map((rule) => `- ${rule}`).join("\n")}\n</rules>`,
45
+ `<environment>\nWorking directory: ${options.cwd}\n${options.description !== undefined ? `Where your tools run: ${options.description}` : `Platform: ${options.platform}`}\n</environment>`,
46
+ ]
47
+ .filter((section) => section !== "")
48
+ .join("\n\n");
49
+ }
@@ -0,0 +1,41 @@
1
+ import type { RunHandle } from "../../kernel/src/index.js";
2
+ /** What the application can ask for. */
3
+ export interface WakeSetting {
4
+ /** Receives every run the agent starts by itself, to show its events. */
5
+ onRun(handle: RunHandle): void;
6
+ /** How long a quiet agent with work still under way is left before it is woken once to check on it (default 4 minutes, a little under the prompt cache lifetime). */
7
+ cacheCheckMs?: number;
8
+ }
9
+ export interface WakerOptions {
10
+ /** The application's setting; with none, the agent is woken only for work that asked for it. */
11
+ setting: WakeSetting | undefined;
12
+ /** What the kernel says is waiting: notices the agent has not been told yet, and work that is still under way. */
13
+ pending(): {
14
+ signals: string[];
15
+ watching: string[];
16
+ };
17
+ /** Starts a run on the agent's conversation. With no input it tells the agent what is waiting. */
18
+ start(input?: string): RunHandle;
19
+ /** Whether a run is going now. */
20
+ busy(): boolean;
21
+ }
22
+ /**
23
+ * When the agent starts a run by itself. It decides three things and keeps the state they need in one place:
24
+ * - when background work ends and the agent is quiet, a run starts so that it hears about it, if the application asked for that in general or the work asked
25
+ * for it in particular (the work's own wish wins);
26
+ * - a quiet agent with work still under way is woken once to check on it before the prompt cache expires, then left alone until something ends;
27
+ * - after a run that was cancelled nothing starts by itself: whoever cancelled wanted it to stop, and what is waiting is told in the next run.
28
+ */
29
+ export interface Waker {
30
+ /** Background work ended. `options.wake` is what that work asked for; left out, the application's setting decides. */
31
+ signalled(options?: {
32
+ wake?: boolean;
33
+ }): void;
34
+ /** A run is starting. It tells the agent everything that is waiting. */
35
+ started(): void;
36
+ /** A run ended. */
37
+ ended(cancelled: boolean): void;
38
+ /** No more runs by itself. */
39
+ close(): void;
40
+ }
41
+ export declare function createWaker(options: WakerOptions): Waker;
@@ -0,0 +1,71 @@
1
+ const CACHE_CHECK_MS = 240_000;
2
+ const checkInput = (labels) => `Background work you started is still running: ${labels.join(", ")}. Check on it once now (for example read its output), then stop and wait; you will be told when it ends. Do not poll.`;
3
+ /** The run of an agent that woke itself when the application gave no setting: nobody shows it, the log has all of it. */
4
+ async function drain(handle) {
5
+ for await (const _event of handle.events)
6
+ void _event;
7
+ }
8
+ export function createWaker(options) {
9
+ const { setting } = options;
10
+ let timer;
11
+ let closed = false;
12
+ let checkUsed = false; // the one check per quiet period has been made
13
+ let owed = false; // something that asked to wake the agent ended, and no run has told it yet
14
+ function wakeWith(input) {
15
+ if (closed || options.busy())
16
+ return;
17
+ const handle = options.start(input);
18
+ if (setting)
19
+ setting.onRun(handle);
20
+ else
21
+ void drain(handle);
22
+ }
23
+ return {
24
+ signalled(wish) {
25
+ if (!(wish?.wake ?? setting !== undefined))
26
+ return;
27
+ clearTimeout(timer);
28
+ checkUsed = false;
29
+ owed = true;
30
+ wakeWith(); // while a run is going, it delivers the notice itself or `ended` does
31
+ },
32
+ started() {
33
+ owed = false;
34
+ },
35
+ ended(cancelled) {
36
+ if (closed)
37
+ return;
38
+ if (cancelled) {
39
+ owed = false;
40
+ clearTimeout(timer);
41
+ return;
42
+ }
43
+ const pending = options.pending();
44
+ if (pending.signals.length > 0 && (setting || owed)) {
45
+ wakeWith();
46
+ return;
47
+ }
48
+ if (pending.signals.length === 0)
49
+ owed = false;
50
+ if (!setting)
51
+ return; // the check before the cache expires is the application's to ask for
52
+ if (pending.watching.length === 0 || checkUsed)
53
+ return;
54
+ clearTimeout(timer);
55
+ timer = setTimeout(() => {
56
+ const now = options.pending();
57
+ if (now.signals.length > 0)
58
+ return wakeWith();
59
+ if (now.watching.length === 0)
60
+ return;
61
+ checkUsed = true;
62
+ wakeWith(checkInput(now.watching));
63
+ }, setting.cacheCheckMs ?? CACHE_CHECK_MS);
64
+ timer.unref();
65
+ },
66
+ close() {
67
+ closed = true;
68
+ clearTimeout(timer);
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,13 @@
1
+ /** Thrown when a confined environment is asked for something outside its working directory. */
2
+ export declare class OutsideDirectoryError extends Error {
3
+ readonly path: string;
4
+ readonly root: string;
5
+ constructor(path: string, root: string);
6
+ }
7
+ /** Whether an absolute, normalised path is the root or below it, by text ("/work-evil" is not inside "/work"). */
8
+ export declare const isInside: (root: string, path: string) => boolean;
9
+ /**
10
+ * Where a path really is, following links, for a path that may not exist yet: the real location of its deepest existing ancestor, plus the
11
+ * names below it. A link that points nowhere is not followed blindly (writing through it would create its target): it counts as outside.
12
+ */
13
+ export declare function realLocation(path: string, root: string): Promise<string>;
@@ -0,0 +1,40 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { basename, dirname, resolve, sep } from "node:path";
3
+ /** Thrown when a confined environment is asked for something outside its working directory. */
4
+ export class OutsideDirectoryError extends Error {
5
+ path;
6
+ root;
7
+ constructor(path, root) {
8
+ super(`${path} is outside the working directory ${root}`);
9
+ this.name = "OutsideDirectoryError";
10
+ this.path = path;
11
+ this.root = root;
12
+ }
13
+ }
14
+ /** Whether an absolute, normalised path is the root or below it, by text ("/work-evil" is not inside "/work"). */
15
+ export const isInside = (root, path) => path === root || path.startsWith(root.endsWith(sep) ? root : root + sep);
16
+ /**
17
+ * Where a path really is, following links, for a path that may not exist yet: the real location of its deepest existing ancestor, plus the
18
+ * names below it. A link that points nowhere is not followed blindly (writing through it would create its target): it counts as outside.
19
+ */
20
+ export async function realLocation(path, root) {
21
+ const below = [];
22
+ let current = path;
23
+ while (true) {
24
+ try {
25
+ return resolve(await realpath(current), ...below.reverse());
26
+ }
27
+ catch (error) {
28
+ const code = error.code;
29
+ if (code !== "ENOENT" && code !== "ENOTDIR")
30
+ throw error;
31
+ if (await lstat(current).then(() => true, () => false))
32
+ throw new OutsideDirectoryError(path, root); // a dangling link
33
+ const parent = dirname(current);
34
+ if (parent === current)
35
+ throw error;
36
+ below.push(basename(current));
37
+ current = parent;
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,33 @@
1
+ import type { RemoteRunner } from "./remote.js";
2
+ export interface SshOptions {
3
+ /** What to connect to: a name from ~/.ssh/config, or user@host. Authentication, keys and host keys are whatever ssh is set up to do. */
4
+ host: string;
5
+ port?: number;
6
+ identityFile?: string;
7
+ /** Extra ssh options, written as `-o Name=value` (ProxyJump, StrictHostKeyChecking, ...). */
8
+ options?: Record<string, string>;
9
+ /** Seconds to wait for a connection to be made (default 15). */
10
+ connectTimeoutSec?: number;
11
+ /** The ssh program to run (default "ssh"). */
12
+ ssh?: string;
13
+ }
14
+ export interface SshConnection {
15
+ /** Runs a command on the remote machine over the connection. Rejects with RemoteConnectionError when ssh could not reach it. */
16
+ run: RemoteRunner;
17
+ /** Hangs up and removes what was made on this machine. Safe to call more than once. */
18
+ close(): Promise<void>;
19
+ }
20
+ /**
21
+ * Opens one connection to the machine and runs every command over it. The first ssh logs in (once) and stays in the background; each later ssh finds it
22
+ * through a socket in a directory of ours and only starts a new process on the other side, so a command costs milliseconds and not a login. If the
23
+ * connection dies, the next command makes a new one by itself. Nothing is promised about the connection: when it cannot be reached, the command's
24
+ * result is a RemoteConnectionError and the caller says so.
25
+ */
26
+ export declare function connectSsh(options: SshOptions): Promise<SshConnection>;
27
+ /** What the model is told about where its tools run, over ssh. */
28
+ export declare function describeSsh(host: string, cwd: string): string;
29
+ /** An environment whose tools run on another machine over ssh. Close it (or dispose of the agent) to hang up. */
30
+ export declare function createSshEnvironment(options: SshOptions & {
31
+ cwd: string;
32
+ workDir?: string;
33
+ }): Promise<import("../../kernel/src/index.js").Environment>;