@springbrand/agent-runtime 0.1.0

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 (75) hide show
  1. package/package.json +28 -0
  2. package/src/db/approval.repo.ts +291 -0
  3. package/src/db/ext-context.repo.ts +34 -0
  4. package/src/db/index.ts +83 -0
  5. package/src/db/message-ui.repo.ts +39 -0
  6. package/src/db/milestone.repo.ts +96 -0
  7. package/src/db/runtime-event-outbox.repo.ts +89 -0
  8. package/src/db/schema.ts +164 -0
  9. package/src/db/settlement.repo.ts +104 -0
  10. package/src/db/steer.repo.ts +73 -0
  11. package/src/db/submission.repo.ts +323 -0
  12. package/src/index.ts +133 -0
  13. package/src/kernel/approval-lifecycle.ts +552 -0
  14. package/src/kernel/bindings.ts +898 -0
  15. package/src/kernel/degradation.ts +15 -0
  16. package/src/kernel/extensions.ts +108 -0
  17. package/src/kernel/profile.ts +116 -0
  18. package/src/kernel/public-contracts.ts +17 -0
  19. package/src/kernel/receipts.ts +124 -0
  20. package/src/kernel/recoverable-chat-agent.ts +899 -0
  21. package/src/kernel/state.ts +76 -0
  22. package/src/kernel/submission-lifecycle.ts +600 -0
  23. package/src/layers/context/budget/gate.ts +88 -0
  24. package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
  25. package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
  26. package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
  27. package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
  28. package/src/layers/orchestration/temporary-agent/core.ts +152 -0
  29. package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
  30. package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
  31. package/src/lib/artifacts.ts +54 -0
  32. package/src/lib/egress.ts +44 -0
  33. package/src/lib/execution-level.ts +27 -0
  34. package/src/lib/extension-name.ts +18 -0
  35. package/src/lib/host-actions.ts +57 -0
  36. package/src/lib/mcp.ts +86 -0
  37. package/src/lib/model-catalog.ts +7 -0
  38. package/src/lib/prompt.ts +139 -0
  39. package/src/lib/telemetry-dev.ts +44 -0
  40. package/src/pi/assembly/context.ts +510 -0
  41. package/src/pi/assembly/extensions.ts +661 -0
  42. package/src/pi/assembly/index.ts +19 -0
  43. package/src/pi/assembly/snapshot.ts +200 -0
  44. package/src/pi/message/contract.ts +8 -0
  45. package/src/pi/message/conversion.ts +73 -0
  46. package/src/pi/message/index.ts +3 -0
  47. package/src/pi/message/projection.ts +604 -0
  48. package/src/pi/runtime-adapter/assembly.ts +552 -0
  49. package/src/pi/runtime-adapter/execution.ts +683 -0
  50. package/src/pi/runtime-adapter/index.ts +232 -0
  51. package/src/pi/runtime-adapter/models.ts +243 -0
  52. package/src/pi/runtime-adapter/recovery.ts +805 -0
  53. package/src/pi/runtime-adapter/transcript.ts +825 -0
  54. package/src/pi/session/index.ts +24 -0
  55. package/src/pi/session/storage.ts +353 -0
  56. package/src/pi/tool/ai-adapter.ts +100 -0
  57. package/src/pi/tool/base.ts +110 -0
  58. package/src/pi/tool/compiler.ts +444 -0
  59. package/src/pi/tool/core-host.ts +48 -0
  60. package/src/pi/tool/core.ts +251 -0
  61. package/src/pi/tool/index.ts +32 -0
  62. package/src/pi/tool/mcp.ts +319 -0
  63. package/src/pi/tool/schedule.ts +198 -0
  64. package/src/pi/tool/skill.ts +455 -0
  65. package/src/pi/tool/subagent.ts +148 -0
  66. package/src/pi/tool/web-search/api.ts +1292 -0
  67. package/src/pi/tool/web-search/index.ts +2 -0
  68. package/src/pi/tool/web-search/web-search.ts +127 -0
  69. package/src/pi/tool/workspace-sandbox.ts +664 -0
  70. package/src/pi/turn/approval.ts +181 -0
  71. package/src/pi/turn/index.ts +62 -0
  72. package/src/pi/turn/tool-recovery.ts +792 -0
  73. package/src/plugins.ts +1024 -0
  74. package/src/runtime-agent.ts +654 -0
  75. package/src/runtime.ts +2880 -0
@@ -0,0 +1,154 @@
1
+ import type {
2
+ WorkspaceFileInfo,
3
+ WorkspacePort,
4
+ } from "../../../kernel/bindings";
5
+
6
+ const MEMORY_ROOT = "/shared/memories";
7
+
8
+ // 作用:把 Workspace 路径统一成以 `/` 开头的正斜杠形式。
9
+ // 调用:所有临时 Agent 路径在检查 Memory 边界前都经过这里。
10
+ // 原因:先收敛相对路径、反斜杠和重复分隔符,可避免同一目标用不同写法绕过后续判断。
11
+ function normalize(path: string): string {
12
+ const absolute = path.trim().replaceAll("\\", "/");
13
+ const parts = (absolute.startsWith("/") ? absolute : `/${absolute}`)
14
+ .split("/")
15
+ .filter(Boolean);
16
+ if (parts.some((part) => part === "." || part === "..")) {
17
+ throw new Error("workspace path traversal is not allowed");
18
+ }
19
+ return `/${parts.join("/")}`;
20
+ }
21
+
22
+ // 作用:判断一条路径是否指向共享 Memory 根目录或其子项。
23
+ // 调用:`allowed` 拦截直接访问,`visible` 过滤目录和 glob 结果。
24
+ // 原因:同时比较根路径和带 `/` 的子路径前缀,避免误伤 `/shared/memories-old`。
25
+ function isMemoryPath(path: string): boolean {
26
+ const normalized = normalize(path);
27
+ return (
28
+ normalized === MEMORY_ROOT ||
29
+ normalized.startsWith(`${MEMORY_ROOT}/`)
30
+ );
31
+ }
32
+
33
+ // 作用:返回可交给底层 Workspace 的归一化路径。
34
+ // 调用:临时 Agent Workspace 外观的每个路径参数都必须先经过它。
35
+ // 原因:在一个共享边界拒绝 `/shared/memories`,比在每个上层 Tool 里分别检查更不容易漏掉新调用方。
36
+ function allowed(path: string): string {
37
+ const normalized = normalize(path);
38
+ if (isMemoryPath(normalized)) {
39
+ throw new Error(
40
+ "/shared/memories is not accessible to temporary agents",
41
+ );
42
+ }
43
+ return normalized;
44
+ }
45
+
46
+ // 作用:判断一个 Workspace 列表项是否可以向临时 Agent 展示。
47
+ // 调用:`readDir` 和 `glob` 在底层查询返回后用它过滤结果。
48
+ // 原因:只拦截输入路径不够,列出父目录仍可能暴露 Memory 挂载项。
49
+ const visible = (entry: WorkspaceFileInfo): boolean =>
50
+ !isMemoryPath(entry.path);
51
+
52
+ /**
53
+ * 共享当前 Session 的 Workspace,但隐藏其中的长期 Memory 挂载。
54
+ *
55
+ * @remarks
56
+ * 宿主为临时 Agent 装配 Workspace Plugin 时调用;后续只应把返回的 `WorkspacePort` 交给子运行。
57
+ *
58
+ * 外观复用已限定 Session 范围的底层 Workspace,只在每个路径边界增加 Memory 禁止;这样不会引入第二套文件系统或复制状态。
59
+ *
60
+ * 返回值被冻结,防止子运行替换已审核的代理方法;新增 `WorkspacePort` 方法时必须同步在此处加入同样的路径检查。
61
+ *
62
+ * Session、Workspace、Memory、Port 和 Plugin 的术语见 `src/index.ts`。
63
+ */
64
+ export function createTemporaryAgentWorkspace(
65
+ workspace: WorkspacePort,
66
+ ): WorkspacePort {
67
+ const facade: WorkspacePort = {
68
+ // 作用:读取一个允许的文本文件。
69
+ // 调用:临时 Agent 的 Workspace Tool 通过外观调用,使用方式与底层 Port 相同。
70
+ // 原因:先走 `allowed` 才委托,保证文本读取不会绕过 Memory 边界。
71
+ readFile: async (path) => workspace.readFile(allowed(path)),
72
+ // 作用:读取一个允许的二进制文件。
73
+ // 调用:临时 Agent 需要原始字节时通过 Workspace Tool 调用。
74
+ // 原因:二进制读取与文本读取共用同一路径禁止,避免另一个读取入口泄露 Memory。
75
+ readFileBytes: async (path) => workspace.readFileBytes(allowed(path)),
76
+ // 作用:向一个允许的路径写入文本。
77
+ // 调用:临时 Agent 获得 Workspace 写权限后通过 Tool 调用。
78
+ // 原因:路径检查放在真实写入前,保证不会新建或覆盖 Memory 文件。
79
+ writeFile: async (path, content, mimeType) =>
80
+ workspace.writeFile(allowed(path), content, mimeType),
81
+ // 作用:向一个允许的路径写入原始字节。
82
+ // 调用:临时 Agent 的二进制写入 Tool 通过外观调用。
83
+ // 原因:二进制写入必须与文本写入使用同一 Memory 禁止,不能留下第二条写入路径。
84
+ writeFileBytes: async (path, data, mimeType) =>
85
+ workspace.writeFileBytes(allowed(path), data, mimeType),
86
+ // 作用:在一个允许的文件末尾追加文本。
87
+ // 调用:临时 Agent 需要增量写入时通过 Workspace Tool 调用。
88
+ // 原因:追加也是写操作,必须在委托前拦截 Memory 路径。
89
+ appendFile: async (path, content, mimeType) =>
90
+ workspace.appendFile(allowed(path), content, mimeType),
91
+ // 作用:检查一个允许的路径是否存在。
92
+ // 调用:临时 Agent 的 Workspace Tool 在读写前探测路径时调用。
93
+ // 原因:连「是否存在」也不向子运行泄露,所以不能绕过 `allowed`。
94
+ exists: async (path) => workspace.exists(allowed(path)),
95
+ // 作用:读取一个允许路径指向项的元数据。
96
+ // 调用:临时 Agent 的 Workspace Tool 需要跟随符号链接的文件信息时调用。
97
+ // 原因:元数据同样可暴露 Memory 内容存在与否,因此共用路径拦截。
98
+ stat: async (path) => workspace.stat(allowed(path)),
99
+ // 作用:读取一个允许路径自身的元数据。
100
+ // 调用:临时 Agent 需要检查符号链接本身时通过 Workspace Tool 调用。
101
+ // 原因:`lstat` 是另一条元数据入口,必须与 `stat` 使用同一 Memory 边界。
102
+ lstat: async (path) => workspace.lstat(allowed(path)),
103
+ // 作用:在一个允许的路径创建目录。
104
+ // 调用:临时 Agent 的 Workspace Tool 在写文件前准备目录时调用。
105
+ // 原因:目录创建会改变持久状态,必须先拒绝 Memory 目标。
106
+ mkdir: async (path, options) =>
107
+ workspace.mkdir(allowed(path), options),
108
+ // 作用:列出一个允许目录中可向临时 Agent 展示的项。
109
+ // 调用:临时 Agent 浏览 Workspace 时调用,未传目录时从根目录列出。
110
+ // 原因:输入路径和返回项要分别检查,否则列根目录仍会暴露 Memory 挂载。
111
+ readDir: async (path, options) =>
112
+ (await workspace.readDir(allowed(path ?? "/"), options))
113
+ .filter(visible),
114
+ // 作用:删除一个允许的文件或目录。
115
+ // 调用:临时 Agent 拥有 Workspace 写权限并调用删除 Tool 时进入。
116
+ // 原因:删除不可通过另一个方法触及 Memory,因此在副作用发生前强制路径检查。
117
+ rm: async (path, options) => workspace.rm(allowed(path), options),
118
+ // 作用:在两个允许路径之间复制文件或目录。
119
+ // 调用:临时 Agent 的复制 Tool 传入源和目标时调用。
120
+ // 原因:源和目标任意一端落入 Memory 都会越界,因此两端分别检查。
121
+ cp: async (source, destination, options) =>
122
+ workspace.cp(
123
+ allowed(source),
124
+ allowed(destination),
125
+ options,
126
+ ),
127
+ // 作用:在两个允许路径之间移动文件或目录。
128
+ // 调用:临时 Agent 的移动 Tool 传入源和目标时调用。
129
+ // 原因:同时检查源和目标,防止从 Memory 移出或向 Memory 移入。
130
+ mv: async (source, destination, options) =>
131
+ workspace.mv(
132
+ allowed(source),
133
+ allowed(destination),
134
+ options,
135
+ ),
136
+ // 作用:在允许的目标和链接路径之间创建符号链接。
137
+ // 调用:临时 Agent 的符号链接 Tool 通过外观调用。
138
+ // 原因:只检查链接路径会留下指向 Memory 的绕路,所以目标也必须检查。
139
+ symlink: async (target, linkPath) =>
140
+ workspace.symlink(allowed(target), allowed(linkPath)),
141
+ // 作用:读取一个允许符号链接的允许目标。
142
+ // 调用:临时 Agent 的链接检查 Tool 通过外观调用。
143
+ // 原因:输入链接和底层返回的目标都需检查,否则返回值会暴露 Memory 路径。
144
+ readlink: async (path) => allowed(
145
+ await workspace.readlink(allowed(path)),
146
+ ),
147
+ // 作用:查找匹配模式且可向临时 Agent 展示的 Workspace 项。
148
+ // 调用:临时 Agent 的 glob Tool 通过外观调用。
149
+ // 原因:先检查模式、再过滤返回项,同时防住直接匹配和间接枚举 Memory。
150
+ glob: async (pattern) =>
151
+ (await workspace.glob(allowed(pattern))).filter(visible),
152
+ };
153
+ return Object.freeze(facade);
154
+ }
@@ -0,0 +1,54 @@
1
+ // Artifact identity and persistence primitives. Budget policy lives in
2
+ // context/budget so storage mechanics do not decide when an output is spilled.
3
+
4
+ /** Stable narrow reference shared by messages, RPC responses and the UI. */
5
+ export interface ArtifactRef {
6
+ kind: "artifact_ref";
7
+ path: string;
8
+ bytes: number;
9
+ hash: string;
10
+ preview: string;
11
+ note: string;
12
+ }
13
+
14
+ export interface SpillWorkspace {
15
+ /** 把溢出的 Artifact 内容写入指定 Workspace 路径。 */
16
+ writeFile(path: string, content: string, mimeType?: string): Promise<void>;
17
+ }
18
+
19
+ /**
20
+ * 把一段文本计算成小写十六进制的 SHA-256 摘要。
21
+ *
22
+ * @remarks
23
+ * Artifact 持久化路径在需要稳定内容标识时调用;传入的文本按 UTF-8 编码。
24
+ *
25
+ * 实现直接使用 Workers 提供的 Web Crypto `crypto.subtle.digest`,不需要 Node.js 兼容层或额外依赖。
26
+ *
27
+ * Artifact 和 Workspace 的术语见 `src/index.ts`。
28
+ *
29
+ * @see https://developers.cloudflare.com/workers/runtime-apis/web-crypto/
30
+ */
31
+ export async function sha256Hex(text: string): Promise<string> {
32
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
33
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
34
+ }
35
+
36
+ /**
37
+ * 把 Tool 结果转成可存储的文本,并选择 `.txt` 或 `.json` 扩展名。
38
+ *
39
+ * @remarks
40
+ * Pi Tool 适配器在生成模型可读内容和 Artifact 内容时调用;原始字符串保持为纯文本,其他值尽量格式化为 JSON。
41
+ *
42
+ * 循环引用或 `BigInt` 会让 `JSON.stringify` 失败,因此降级为 `String(output)`;这里要保证 Tool 错误和大小估算仍有可用文本,不应因序列化再丢掉原结果。
43
+ *
44
+ * Artifact 和 Tool 的术语见 `src/index.ts`。
45
+ */
46
+ export function serializeOutput(output: unknown): { text: string; ext: "txt" | "json" } {
47
+ if (typeof output === "string") return { text: output, ext: "txt" };
48
+ try {
49
+ return { text: JSON.stringify(output, null, 2) ?? "", ext: "json" };
50
+ } catch {
51
+ // 循环引用和 BigInt 仍需要一份可用文本,供 Tool 展示和 Artifact 大小估算。
52
+ return { text: String(output), ext: "txt" };
53
+ }
54
+ }
@@ -0,0 +1,44 @@
1
+ import { WorkerEntrypoint } from "cloudflare:workers";
2
+
3
+ // 作用:为没有显式客户端标识的沙箱请求提供一个兼容性 User-Agent。
4
+ // 调用:`HttpGateway.fetch` 只在请求没有 UA,或 UA 含 Workers/Cloudflare 标识时使用。
5
+ // 原因:已显式设置的其他 UA 必须保留;此常量只是默认兼容策略,不是安全身份。
6
+ const DEFAULT_UA =
7
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
8
+ "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
9
+
10
+ /**
11
+ * 把 Code Mode Dynamic Worker 的 HTTP 出站请求转发到公网。
12
+ *
13
+ * @remarks
14
+ * 宿主把导出的 `HttpGateway` 实例作为 `globalOutbound` 交给执行沙箱,Cloudflare 再调用其 `fetch` 方法处理出站 HTTP 请求。
15
+ *
16
+ * 这个类当前只做 UA 兼容处理和转发,不实施 allowlist、凭据注入、限速或审计;不能把它视为完整的安全网关。
17
+ *
18
+ * Cloudflare 官方文档要求可调用的类继承 `WorkerEntrypoint`,而入站 `Request` 需通过新建 `Request` 才能修改;这两个约束决定了当前类和克隆写法。
19
+ *
20
+ * 待确认:旧注释称 `connect()` 也会经过这个只实现 `fetch` 的入口,当前代码和 Cloudflare Outbound Worker 文档未能证明该说法。
21
+ *
22
+ * Runtime 的术语见 `src/index.ts`。
23
+ *
24
+ * @see https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/
25
+ * @see https://developers.cloudflare.com/workers/runtime-apis/request/#immutability
26
+ * @see https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/
27
+ */
28
+ export class HttpGateway extends WorkerEntrypoint {
29
+ /**
30
+ * 保留显式的非 Workers UA,否则补上默认 UA 后转发请求。
31
+ *
32
+ * @remarks
33
+ * Cloudflare 作为出站 Worker 入口调用;直接调用方只需传入完整 `Request`。
34
+ *
35
+ * 修改 header 时创建新 `Request`,因为 Cloudflare Workers 的入站 Request 不可变;已明确设置的其他 UA 则直接转发,不改变调用方意图。
36
+ */
37
+ override async fetch(request: Request): Promise<Response> {
38
+ const ua = request.headers.get("user-agent");
39
+ if (ua && !/workers|cloudflare/i.test(ua)) return fetch(request);
40
+ const req = new Request(request);
41
+ req.headers.set("user-agent", DEFAULT_UA);
42
+ return fetch(req);
43
+ }
44
+ }
@@ -0,0 +1,27 @@
1
+ export const EXECUTION_LEVELS = [
2
+ "safe",
3
+ "low",
4
+ "medium",
5
+ "high",
6
+ ] as const;
7
+
8
+ export type ExecutionLevel = (typeof EXECUTION_LEVELS)[number];
9
+
10
+ export function requiresExecutionApproval(
11
+ granted: ExecutionLevel,
12
+ required: ExecutionLevel,
13
+ ): boolean {
14
+ return EXECUTION_LEVELS.indexOf(required) > EXECUTION_LEVELS.indexOf(granted);
15
+ }
16
+
17
+ export function maxExecutionLevel(
18
+ left: ExecutionLevel,
19
+ right: ExecutionLevel,
20
+ ): ExecutionLevel {
21
+ return EXECUTION_LEVELS[
22
+ Math.max(
23
+ EXECUTION_LEVELS.indexOf(left),
24
+ EXECUTION_LEVELS.indexOf(right),
25
+ )
26
+ ]!;
27
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 把 Extension 名字整理成 Tool 名和 Context label 可用的前缀。
3
+ *
4
+ * @remarks
5
+ * Pi Extension 在发现 Tool、执行 Tool 和生成 Context label 时调用;同一个 manifest 名字必须在这些入口得到同一前缀。
6
+ *
7
+ * 实现只保留 ASCII 字母和数字,把其他字符折叠成单个下划线,避免注册和调用使用不同规则而找不到 Tool。
8
+ *
9
+ * 这个规则已用于发布的 Extension 工具名和 Context label;修改会同时改变旧 Extension 的可见名称,需先确认兼容策略。
10
+ *
11
+ * Extension、Tool 和 Runtime 术语见 `src/index.ts`。
12
+ */
13
+ export function sanitizeExtensionName(name: string): string {
14
+ return name
15
+ .replace(/[^a-zA-Z0-9]/g, "_")
16
+ .replace(/_+/g, "_")
17
+ .replace(/^_|_$/g, "");
18
+ }
@@ -0,0 +1,57 @@
1
+ export interface HostActionPolicy {
2
+ /** 显式开启的 Action 名。未列出的平台 Action 默认不给。 */
3
+ allow?: readonly string[];
4
+ /** 逃生口:无论来自哪里都不给。对全部工具生效,不只是 Action。 */
5
+ deny?: readonly string[];
6
+ }
7
+
8
+ export interface HostActionSelection {
9
+ /** 本次装配应当注入的 Action 名,保持候选顺序。 */
10
+ inject: string[];
11
+ /** 配置里点名了、但本次部署根本没有的名字。用于降级上报。 */
12
+ missing: string[];
13
+ }
14
+
15
+ /**
16
+ * 挑出本次 Runtime 装配应注入的 Host Action。
17
+ *
18
+ * @param candidates 本次部署实际存在的全部 Host Action 名。
19
+ * @param policy 该 Agent 的 allow/deny 配置。
20
+ * @param bound 通过 Tool Resource 绑定授权的 Action 名。
21
+ *
22
+ * @remarks
23
+ * Host Action Plugin 在候选项进入 Runtime 前调用;调用方注入 `inject` 中的项,并把 `missing` 作为降级信息上报。
24
+ *
25
+ * `allow` 和 `bound` 都可授权,`deny` 始终优先;筛选在注入前完成,避免把未授权的 Action 先暴露给 Turn 再二次计算白名单。
26
+ *
27
+ * 实现保持纯函数和候选顺序,因为配置判断不应修改运行状态,稳定顺序也便于重现装配结果。
28
+ *
29
+ * Runtime、Plugin 和 Tool 的术语见 `src/index.ts`。
30
+ */
31
+ export function decideHostActions(
32
+ candidates: readonly string[],
33
+ policy: HostActionPolicy | undefined,
34
+ bound: readonly string[] = [],
35
+ ): HostActionSelection {
36
+ const deny = new Set(compact(policy?.deny));
37
+ const wanted = new Set([
38
+ ...compact(policy?.allow),
39
+ ...compact(bound),
40
+ ]);
41
+ const available = new Set(candidates);
42
+
43
+ return {
44
+ inject: candidates.filter(
45
+ (name) => wanted.has(name) && !deny.has(name),
46
+ ),
47
+ // 点名了却不存在,通常是配置抄错或能力已下线,必须显性可见。
48
+ missing: [...wanted].filter((name) => !available.has(name)),
49
+ };
50
+ }
51
+
52
+ // 作用:去掉空名并保持顺序。
53
+ // 调用:读取任何一份外部名单时调用。
54
+ // 原因:配置来自 JSON,空字符串会污染集合判断。
55
+ function compact(names: readonly string[] | undefined): string[] {
56
+ return (names ?? []).filter((name) => name.trim().length > 0);
57
+ }
package/src/lib/mcp.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type { RetryOptions } from "agents";
2
+
3
+ const MCP_RETRY: RetryOptions = {
4
+ maxAttempts: 3,
5
+ baseDelayMs: 500,
6
+ maxDelayMs: 5_000,
7
+ };
8
+
9
+ interface McpHost {
10
+ // 作用:返回宿主当前已连接的 MCP Server 表。
11
+ // 调用:启动对账在新建连接前调用,仅用来读取已有 URL。
12
+ // 原因:先读现状可避免 Runtime 每次启动都重复连接同一端点。
13
+ getMcpServers(): {
14
+ servers: Record<string, { server_url: string }>;
15
+ };
16
+ // 作用:按名字和 URL 向宿主添加一个 MCP Server 连接。
17
+ // 调用:启动对账只对未连接且配置完整的 URL 调用,并传入统一重试策略。
18
+ // 原因:真实连接生命周期归宿主所有,Runtime 只依赖这个最小边界。
19
+ addMcpServer(
20
+ name: string,
21
+ url: string,
22
+ options?: { retry?: RetryOptions },
23
+ ): Promise<unknown>;
24
+ }
25
+
26
+ /**
27
+ * 把本次 Runtime 启动配置的 MCP Server 连接到宿主。
28
+ *
29
+ * @remarks
30
+ * Runtime `initConfig` 在冻结 Pi 工具目录前调用;调用方只需提供本次解析出的名字与 URL。
31
+ *
32
+ * 连接以 URL 作为身份,已连接和本批重复项都跳过,同一 URL 保留第一份声明,使重复配置得到确定结果。
33
+ *
34
+ * 这里故意只做增量连接,不删除正在运行的 Server;精确集合对账需要独立的生命周期规则,不能在启动补齐中顺带执行。
35
+ *
36
+ * 历史修复把 `Promise.allSettled` 中的失败显式上报为 connector 降级;请保留「单个连接失败不阻断 Runtime 启动」的边界。
37
+ *
38
+ * Runtime 和 degradation 的术语见 `src/index.ts`。
39
+ */
40
+ export async function connectConfiguredMcpServers(
41
+ host: McpHost,
42
+ configured: readonly { name: string; url: string }[],
43
+ ): Promise<void> {
44
+ const connectedUrls = new Set(
45
+ Object.values(host.getMcpServers().servers).map(
46
+ (server) => server.server_url,
47
+ ),
48
+ );
49
+ const pending = new Map<string, { name: string; url: string }>();
50
+ for (const server of configured) {
51
+ if (
52
+ server.name.length === 0 ||
53
+ server.url.length === 0 ||
54
+ connectedUrls.has(server.url) ||
55
+ pending.has(server.url)
56
+ ) {
57
+ continue;
58
+ }
59
+ // URL is the connection identity. Keeping the first declaration makes
60
+ // duplicate configuration deterministic without mutating active servers.
61
+ pending.set(server.url, server);
62
+ }
63
+ const servers = [...pending.values()];
64
+ const settled = await Promise.allSettled(
65
+ servers.map((server) =>
66
+ host.addMcpServer(server.name, server.url, {
67
+ retry: MCP_RETRY,
68
+ }),
69
+ ),
70
+ );
71
+ const failed = settled.flatMap((result, index) =>
72
+ result.status === "rejected" ? [servers[index]!.name] : [],
73
+ );
74
+ if (failed.length > 0) {
75
+ console.warn(
76
+ "[runtime-load:degraded]",
77
+ JSON.stringify({
78
+ degradations: failed.map((detail) => ({
79
+ capability: "connector",
80
+ reason: "unavailable",
81
+ detail,
82
+ })),
83
+ }),
84
+ );
85
+ }
86
+ }
@@ -0,0 +1,7 @@
1
+ /** Browser-safe option returned by the deployment model catalog. */
2
+ export interface ModelOption {
3
+ /** Provider model ID written to the current Session-bound User Agent snapshot. */
4
+ readonly value: string;
5
+ readonly label: string;
6
+ readonly hint?: string;
7
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Stable system-prompt sections shared by every Runtime profile.
3
+ *
4
+ * Only `PERSONA` is replaceable by `RuntimeProfile.systemPrompt`. The global
5
+ * `SYSTEM_PROMPT`, environment, tool and safety facts remain because a custom
6
+ * Agent persona must not redefine capabilities that the running kernel does
7
+ * not actually provide.
8
+ * Dynamic skills, memory and context blocks are injected by their own modules
9
+ * so this static prefix remains cacheable.
10
+ */
11
+
12
+ // Describes the default role and the capabilities the kernel truly exposes.
13
+ export const PERSONA =
14
+ "You are a personal assistant agent running on the universal-agent runtime. " +
15
+ "You can recall user-managed cold memory across sessions and keep working memory within the current Session, " +
16
+ "manage files in your workspace, and use execute Code Mode for network requests and tool composition.";
17
+
18
+ // Base constraint injected alongside every Agent-specific persona.
19
+ export const SYSTEM_PROMPT =
20
+ "Web development: when writing a website or web page, use browser-ready mode: emit browser-ready static files " +
21
+ "that run directly in Workspace preview. Use ./ or ../ for every Workspace asset, never a root-relative path; " +
22
+ "load CSS from HTML instead of importing it from JavaScript. Resolve third-party ESM with a full pinned HTTPS URL " +
23
+ "or import map to a pinned CORS-enabled CDN. Do not require Vite, Node.js, bundling, or Sandbox at preview time.";
24
+
25
+ // Explains how to reach capabilities instead of listing unsupported substitutes.
26
+ export const RUNTIME =
27
+ "Runtime: this agent runs on Cloudflare Workers. The execute Code Mode Dynamic Worker is your " +
28
+ "instrument — you write code there and it runs with outbound network access (fetch), your " +
29
+ "workspace filesystem (state.*), and your other tools (tools.*). Use it for raw or customized HTTP " +
30
+ "requests, parsing a payload, hitting several known endpoints, or computing over a file. Write plain " +
31
+ "JavaScript — the sandbox evaluates " +
32
+ "it directly, so TypeScript type annotations (`: number`, `as Type`) are a syntax error, and there " +
33
+ "is no Python interpreter or package manager to invoke. Use the JS/Workers equivalent of what you " +
34
+ "would reach for in another ecosystem.";
35
+
36
+ // Shared behavioral contract for the main Agent and bounded sub-agents.
37
+ export const BEHAVIOR =
38
+ "Behavior: Prioritize technical accuracy over agreeing with the user. State facts and trade-offs " +
39
+ "directly; disagree and correct when the user is wrong, even if it isn't what they want to hear. " +
40
+ "When uncertain, investigate before answering rather than guess or confirm a belief. Match response " +
41
+ "length to the task — a short question gets a short answer; skip filler preamble and don't restate " +
42
+ "what you just did. Be proactive when asked to *do* something (take the needed follow-up actions too), " +
43
+ "but when the user only asks *how* to approach something, answer first and don't jump into changes.";
44
+
45
+ // The main Agent alone owns user-visible planning.
46
+ export const PLANNING =
47
+ "Planning: For work with 3 or more distinct steps, or any non-trivial / multi-file change, call " +
48
+ "update_plan with the complete plan (it fully replaces the previous one) so the user sees live " +
49
+ "progress. Mark a step in_progress before starting it and completed the moment it's done; keep only " +
50
+ "one step in_progress at a time and don't batch completions. Do not make a plan for a single trivial " +
51
+ "step or a purely conversational reply — just do it.";
52
+
53
+ // Tool-selection guidance mirrors the actual approval and network boundaries.
54
+ export const TOOLS =
55
+ "Tools: Prefer the dedicated workspace file tools (read/write/edit/list/find/grep) for file work — " +
56
+ "they run without approval. For web tasks, use web_search for web discovery, current facts, cited research, " +
57
+ "and public URL analysis. Use execute Code Mode only for raw or customized network requests, structured " +
58
+ "API calls, or when web_search cannot retrieve the required content; do not use execute for ordinary web " +
59
+ "searches. When sandbox_* tools are present, use that isolated Linux environment for Python/Node, " +
60
+ "package managers, system commands, builds, tests and background processes; its filesystem is temporary, " +
61
+ "persistent inputs are copied in automatically on first use, /shared is read-only, and only explicitly " +
62
+ "published /workspace outputs survive. Use Code Mode for computation, multi-step data work, and the " +
63
+ "raw or customized network cases described above; every response and failure it sees is visible to you. " +
64
+ "bash is a shell over the workspace filesystem only " +
65
+ "(no network, no system utilities) and is approval-gated — don't reach for it to read files or fetch. " +
66
+ "When tool calls are independent, issue them in one turn so they run in parallel. When you reference " +
67
+ "code, cite it as file_path:line_number.";
68
+
69
+ // Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
70
+ export const FILES =
71
+ "Uploaded files live under /uploads/ in your workspace; convertible formats have a companion " +
72
+ "'<file>.md' (structured markdown). Policy: to summarize/quote/search, read or grep the .md; " +
73
+ "to compute/aggregate/transform (especially csv/xlsx), write code in execute Code Mode or the Linux Sandbox that reads " +
74
+ "the ORIGINAL file — converted markdown tables are not for computation, and large spreadsheets may " +
75
+ "have no .md at all. For formats without a companion .md (e.g. pptx: unzip and read ppt/slides/*.xml; " +
76
+ "zip archives; unknown types), parse the original in the sandbox with JS.";
77
+
78
+ // User-interaction actions are called only when their UI semantics are useful.
79
+ export const INTERACTION =
80
+ "Interaction: When the user must pick among a small set of options, call ask_user (2-6 options) " +
81
+ "instead of writing 'please choose A/B/C' as text, then end your turn and wait. Don't use it for " +
82
+ "open-ended questions or when a sensible default lets you proceed. " +
83
+ "After completing a substantive task (report, analysis, multi-step job): write your full answer " +
84
+ "FIRST, then call suggest_followups (2-4 directions) as the very last action and end the turn — " +
85
+ "write no text after it (the tool renders its own closing block). Never for small talk or while a " +
86
+ "task is still in progress; it is non-blocking, unlike ask_user.";
87
+
88
+ // Separates short always-on memory from indexed long-tail workspace files.
89
+ export const MEMORY =
90
+ "Memory conventions: memory/preferences context blocks are writable working memory " +
91
+ "for this Session only. Cold memory is managed explicitly by the user and is shared " +
92
+ "across Sessions under /shared/memories/. Runtime access to that directory is read-only: " +
93
+ "never create, edit, move, or delete its files. To recall long-tail information, check " +
94
+ "the memory_index block first, then read the listed entry under /shared/memories/ when relevant.";
95
+
96
+ /**
97
+ * 把 Agent 个性和 Runtime 的固定行为说明组成主 Agent 系统提示词。
98
+ *
99
+ * @param base Agent 专用的角色文本;为 `null` 或 `undefined` 时使用 `PERSONA`。
100
+ *
101
+ * @remarks
102
+ * Pi 快照装配在生成每次 Runtime Snapshot 时调用;调用方只替换角色文本,不要重复拼接其他段落。
103
+ *
104
+ * 全局 SYSTEM_PROMPT 固定前置;Runtime、行为、规划、Tool、文件、交互和 Memory 按固定顺序追加,
105
+ * 因为自定义角色不应重新定义实际 Kernel 没有的能力。
106
+ *
107
+ * 各段保持静态,动态 Skill、Context 和 Memory 由各自模块注入,避免这个可缓存前缀每回合改变。
108
+ *
109
+ * Agent、Runtime 和 Snapshot 的术语见 `src/index.ts`。
110
+ */
111
+ export function assembleSystemPrompt(base?: string | null): string {
112
+ return [
113
+ SYSTEM_PROMPT,
114
+ base ?? PERSONA,
115
+ RUNTIME,
116
+ BEHAVIOR,
117
+ PLANNING,
118
+ TOOLS,
119
+ FILES,
120
+ INTERACTION,
121
+ MEMORY,
122
+ ].join("\n\n");
123
+ }
124
+
125
+ /**
126
+ * 把子代理角色和共用 Runtime 行为说明组成系统提示词。
127
+ *
128
+ * @remarks
129
+ * Pi SubAgent 在启动有界子任务前调用;调用方应传入该子代理类型自己的 persona。
130
+ *
131
+ * 子代理共享真实执行环境和基本行为,但不获得主 Agent 专属的规划、Memory 和用户交互指令,因为这些责任留在主循环。
132
+ *
133
+ * 不要直接改成复用 `assembleSystemPrompt`,否则子代理会收到它没有的 Session 记忆和对话交互责任。
134
+ *
135
+ * Agent、Runtime 和 Memory 的术语见 `src/index.ts`。
136
+ */
137
+ export function assembleSubagentPrompt(persona: string): string {
138
+ return [persona, RUNTIME, BEHAVIOR].join("\n\n");
139
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * dev-only 观测降级:把 observability 事件打到 console,让 `wrangler dev` /
3
+ * `wrangler tail` 里能实时看到埋点(否则 publish 到零订阅 channel = 静默 no-op)。
4
+ *
5
+ * 仅在 env `TELEMETRY_CONSOLE==="1"` 时由 onStart 装载(`.dev.vars` 里开,prod 不设 → 自动关)。
6
+ * 生产不靠它:prod 所有 channel 事件自动转发 Tail Worker。
7
+ *
8
+ * 用 `agents/observability` 的类型化 `subscribe`(按 channel key 订阅),避免引 `node:diagnostics_channel`
9
+ * 与 `@types/node`(tsconfig 只带 workers-types)。
10
+ */
11
+ import { subscribe } from "agents/observability";
12
+
13
+ // Covers chat events, Runtime lifecycle events and tool messages.
14
+ const CHANNEL_KEYS = ["chat", "lifecycle", "message", "schedule", "mcp"] as const;
15
+
16
+ // Module state is isolate-wide, so multiple DO instances share one subscription.
17
+ let installed = false;
18
+
19
+ /**
20
+ * 在当前 Worker isolate 中安装一次把观测事件打到 console 的订阅。
21
+ *
22
+ * @remarks
23
+ * Runtime `onStart` 只在 `telemetryConsole` 开启时调用,用于 `wrangler dev` 和日志调试。
24
+ *
25
+ * `installed` 是 isolate 级模块状态,因此多个 Durable Object 实例共享一组订阅;不做幂等检查会让每个事件重复输出。
26
+ *
27
+ * 使用 `agents/observability` 的 channel 订阅而不是 Node.js `diagnostics_channel`,保持当前 Workers TypeScript 环境不需要 Node 类型。
28
+ *
29
+ * Runtime 的术语见 `src/index.ts`。
30
+ */
31
+ export function installConsoleSink(): void {
32
+ if (installed) return;
33
+ installed = true;
34
+ for (const key of CHANNEL_KEYS) {
35
+ subscribe(key, (event) => {
36
+ const e = event as { type?: string; payload?: unknown };
37
+ try {
38
+ console.log(`[obs] ${e.type}`, JSON.stringify(e.payload ?? {}));
39
+ } catch {
40
+ console.log(`[obs] ${e?.type} (payload 不可序列化)`);
41
+ }
42
+ });
43
+ }
44
+ }