rome agentic OS
Rome 把可安装 app、agent、action、skill、hook、workflow 和聊天运行时放进同一个 TypeScript monorepo。它不是一个单点 coding assistant,而是把用户自定义应用、运行时权限、密钥注入、子代理 trace 和 workflow 生成器组织成一个 agentic OS。
guardian request ├─ app manifest: agents/actions/skills/hooks ├─ runtime loader: cache-busted module graph ├─ app keys: write-only env injection ├─ workflow studio: natural language → design-workflow chat └─ trace DTO: tool/subagent/result accounting
它解决什么,凭什么值得拆
Rome 的核心不是“再做一个聊天框”,而是把 app 作为一等运行单元管理。packages/api-types/src/apps.ts:57 到 packages/api-types/src/apps.ts:69 定义 agents、actions、skills、hooks 四类 artifact;packages/api-types/src/apps.ts:83 到 packages/api-types/src/apps.ts:119 又把安装状态、权限、前端入口、公开访问、来源和 channel binding 一次性投影到 dashboard。这个契约让用户看到的是一个可管理 app,而不是散落的 prompt 和脚本。
最新提交里最值得拆的是 app keys 和模块重载,因为它们把 agentic OS 的安全边界写进运行时:packages/core/src/api/routes/app-keys.ts:9 说明密钥值永不返回;packages/core/src/app-keys/injector.ts:16 管理 process.env 注入状态;packages/core/src/actions/module-loader.ts:121 到 packages/core/src/actions/module-loader.ts:138 用 env epoch 解决模块级缓存读旧环境的问题。加上 rome_apps/workflow-studio/src/web/App.tsx:19 启动 workflow chat、packages/api-types/src/trace-segments.ts:92 记录子代理生命周期,Rome 已经接近“可安装自动化系统”的形态。
▸核心数据
App 契约把能力面结构化
App 契约从 source、bundle、appstore 三种安装来源开始,packages/api-types/src/apps.ts:9、packages/api-types/src/apps.ts:18 和 packages/api-types/src/apps.ts:23 分别约束编辑区、已打包 bundle 与商店版本。这个分层重要,因为本地开发、离线安装、云商店分发需要完全不同的信任边界,但 dashboard 仍要以统一卡片显示。
能力面集中在 packages/api-types/src/apps.ts:57 到 packages/api-types/src/apps.ts:69:agents、actions、skills、hooks 都是带 description 和 loadError 的 artifact summary。前端在 packages/web/src/pages/AppDetailPage.tsx:419 到 packages/web/src/pages/AppDetailPage.tsx:428 对 agents 额外展示“发起聊天”按钮,说明 agent 被当作可直接对话的能力,而 action/skill/hook 更像被运行时调用的资产。
▸系统拓扑
App source/bundle/store → AppSpec → InstalledAppCard
│ │
├─ capabilityDetails.agents/actions/skills/hooks
├─ dashboard management rows
└─ startChatWithAgent(app, artifactName)▸模块清单
packages/api-types/src/apps.ts:34 对应这条实现证据;packages/api-types/src/apps.ts:45 对应这条实现证据;packages/api-types/src/apps.ts:51 对应这条实现证据;packages/api-types/src/apps.ts:57 对应这条实现证据;packages/api-types/src/apps.ts:71 对应这条实现证据;packages/api-types/src/apps.ts:83 对应这条实现证据;packages/api-types/src/apps.ts:94 对应这条实现证据;packages/api-types/src/apps.ts:107 对应这条实现证据,这些行共同说明 Rome 的 app 不是单个前端页面,而是携带来源、状态、权限和能力清单的运行实体。
▸关键数据结构
57export interface AppArtifactSummary { 58 name: string; 59 description: string; 60 /** Diagnostic for a declared artifact that failed to load. Consumers must surface it. */ 61 loadError?: string; 62} 63 64export interface AppArtifactDetails { 65 agents: AppArtifactSummary[]; 66 actions: AppArtifactSummary[]; 67 skills: AppArtifactSummary[]; 68 hooks: AppArtifactSummary[]; 69} 70 71/** Non-binding routing hint from an app manifest. Channel contract: docs/architecture/channels.md. */ 72export interface SuggestedChannelBinding { 73 channel: "discord" | "telegram" | "whatsapp" | "wechat" | "webchat" | "feishu"; 74 agent: string; 75 reason?: string; 76} 77 78export interface AppSpec { 79 source: SpecSource; 80 enabled: boolean; 81} 82 83export interface InstalledAppCard { 84 id: string; 85 version: string; 86 description: string; 87 displayName: string; 88 status: RomeAppRuntimeStatus; 89 phase: AppViewState; 90 error?: string; 91 hasFrontend: boolean; 92 href: string | null; 93 fullHref: string | null; 94 capabilities: string[]; 95 capabilityDetails: AppArtifactDetails; 96 isEnabled: boolean; 97 canToggle: boolean; 98 canUninstall: boolean; 99 /** Projection of the installed app.yaml flag. Missing means false. */ 100 includeSource?: boolean; 101 /** UI capability hint only; the publish endpoint still enforces store policy. */ 102 canPublish: boolean; 103 accessMode: AppAccessMode; 104 isPublic: boolean; 105 cloudAllowedEmails: string[]; 106 canManagePublicAccess: boolean; 107 /** Authoritative installed source, suitable for a re-install or upgrade request. */ 108 source: SpecSource; 109 /** 110 * Project-root-relative path of the app's editable source workspace. This is 111 * present only when the installed source is a workspace inside Rome's 112 * projects tree, and can be passed directly to the new-chat flow. 113 */ 114 projectPath: string | null; 115 origin: AppOrigin; 116 /** Root-relative icon URL, or `null` when the manifest declares no icon. */ 117 iconUrl: string | null; 118 /** Optional on the wire; consumers must default to `[]`. */ 119 suggestedChannelBindings?: SuggestedChannelBinding[]; 120}
运行时装载在缓存与安全之间取舍
▸ESM cache buster
Node ESM 的模块缓存会让 app 代码在 module scope 捕获旧环境。Rome 在 packages/core/src/actions/module-loader.ts:27 定义 salt query,在 packages/core/src/actions/module-loader.ts:34 注册 resolve/load hooks,并在 packages/core/src/actions/module-loader.ts:48 把父模块盐传播给相对导入;否则入口文件刷新了,helper 仍会读旧配置。
27const SALT_QUERY = /\?v=[^&]*$/; 28 29function isSaltableUrl(url: string): boolean { 30 const path = url.split("?")[0]; 31 return path.endsWith(".js") || path.endsWith(".mjs"); 32} 33 34registerHooks({ 35 resolve(specifier, context, nextResolve) { 36 if (specifier.startsWith("file:") && SALT_QUERY.test(specifier) && isSaltableUrl(specifier)) { 37 return { url: specifier, format: "module", shortCircuit: true }; 38 } 39 const result = nextResolve(specifier, context); 40 if ( 41 (specifier.startsWith("./") || specifier.startsWith("../")) && 42 context.parentURL !== undefined && 43 SALT_QUERY.test(context.parentURL) && 44 result.url.startsWith("file:") && 45 !result.url.includes("?") && 46 isSaltableUrl(result.url) 47 ) { 48 const salt = new URL(context.parentURL).searchParams.get("v"); 49 if (salt) return { url: `${result.url}?v=${salt}`, format: "module", shortCircuit: true }; 50 } 51 return result; 52 }, 53 load(url, context, nextLoad) { 54 if (url.startsWith("file:") && SALT_QUERY.test(url) && isSaltableUrl(url)) { 55 const cleanUrl = new URL(url); 56 cleanUrl.search = ""; 57 return { 58 format: "module", 59 source: readFileSync(fileURLToPath(cleanUrl)), 60 shortCircuit: true, 61 }; 62 } 63 return nextLoad(url, context); 64 }, 65});
▸Action factory 解析
Action 装载不是靠约定字符串硬猜。packages/core/src/actions/module-loader.ts:101 会先解析 entry 并拒绝逃逸 baseDir 的路径;packages/core/src/actions/module-loader.ts:140 到 packages/core/src/actions/module-loader.ts:168 按 createAction、具名 createXAction、default export、唯一工厂的顺序解析。这让用户 app 有自由度,但错误也能集中报出。
101export async function resolveModuleEntryPath(baseDir: string, entry?: string): Promise<string> { 102 const absoluteBaseDir = resolve(baseDir); 103 const specifier = entry ?? "./index"; 104 const result = await sharedResolver.async(absoluteBaseDir, specifier); 105 if (!result.path) { 106 throw new Error( 107 `No entry module found for "${baseDir}" (resolving "${specifier}"): ${result.error ?? "no path returned"}`, 108 ); 109 } 110 const [realBaseDir, realEntryPath] = await Promise.all([ 111 realpath(absoluteBaseDir), 112 realpath(result.path), 113 ]); 114 const relativeEntryPath = relative(realBaseDir, realEntryPath); 115 if (relativeEntryPath.startsWith("..") || isAbsolute(relativeEntryPath)) { 116 throw new Error(`Invalid entry path "${specifier}" for ${baseDir}: escapes base dir`); 117 } 118 return result.path; 119} 120 121// App modules can read process.env at module scope, and the file-identity 122// buster below cannot see an environment change. Bumping this epoch salts the 123// cache key so every subsequent import re-evaluates module scope against the 124// current environment (app keys do this on save/delete). Each bump strands the 125// previously imported instances in the ESM cache — the same cost as an app 126// upgrade changing mtime — bounded by how often a guardian edits keys. 127let envEpoch = 0; 128 129export function bumpModuleEnvEpoch(): void { 130 envEpoch++; 131} 132 133export async function importModuleWithCacheBuster(path: string): Promise<Record<string, unknown>> { 134 const fileStat = await stat(path); 135 const cacheBuster = `${Math.trunc(fileStat.mtimeMs)}-${fileStat.size}-e${envEpoch}`; 136 const url = `${pathToFileURL(path).href}?v=${cacheBuster}`; 137 return await import(url); 138}
▸App action 注册
packages/core/src/actions/app-actions-wiring.ts:127 到 packages/core/src/actions/app-actions-wiring.ts:159 展示懒装载 wrapper,packages/core/src/actions/app-actions-wiring.ts:162 到 packages/core/src/actions/app-actions-wiring.ts:197 展示 eager 注册路径,packages/core/src/actions/app-actions-wiring.ts:229 到 packages/core/src/actions/app-actions-wiring.ts:251 则在 catalog 变化后重注册对应 app。这里能看到 Rome 的关键设计:catalog 是运行时真相源,action registry 是投影。
127function createLazyAppAction( 128 record: AppActionRecord, 129 catalog: AppCatalog, 130 deps: Record<string, unknown>, 131 services: AppActionServices, 132): Action { 133 let actionPromise: Promise<Action> | null = null; 134 135 const load = async (): Promise<Action> => { 136 if (!actionPromise) { 137 actionPromise = instantiateActionFromDirectory( 138 record.config, 139 record.directory, 140 createAppActionRuntimeDeps(record, catalog, deps, services), 141 ).catch((err) => { 142 actionPromise = null; 143 throw err; 144 }); 145 } 146 return actionPromise; 147 }; 148 149 return { 150 config: record.config, 151 async execute(args: Record<string, unknown>, context?: ActionExecutionContext) { 152 const action = await load(); 153 return action.execute(args, context); 154 }, 155 async preview(args: Record<string, unknown>) { 156 const action = await load(); 157 return action.preview?.(args); 158 }, 159 };
App keys 让密钥进入 app,但不回读
密钥路线很清楚:类型层在 packages/api-types/src/app-keys.ts:6 声明 value-free DTO;API 层在 packages/core/src/api/routes/app-keys.ts:16 列表只返回 name、label、updatedAt 和 overridden;写入层在 packages/core/src/api/routes/app-keys.ts:54 upsert 后调用 injector;删除层在 packages/core/src/api/routes/app-keys.ts:68 删除存储后按需刷新运行时。
▸写入与刷新流程
PUT /api/app-keys/:name ├─ validate appKeyNameError ├─ reject missing/oversized value ├─ repo.upsert(name,label,value) ├─ injector.apply(name,value) └─ refreshAppRuntime() only if live env changed
▸环境变量优先级
最关键的安全点在 packages/core/src/app-keys/injector.ts:5 到 packages/core/src/app-keys/injector.ts:15:真实 process.env 永远优先,dashboard 存储值不能覆盖 operator 预置环境。packages/core/src/app-keys/injector.ts:25 检测预置变量,packages/core/src/app-keys/injector.ts:32 只在安全时写入,packages/core/src/app-keys/injector.ts:39 到 packages/core/src/app-keys/injector.ts:44 只删除 injector 自己写过的键。
5/** 6 * Owns the stored-key slice of `process.env`. Two invariants: 7 * 8 * - The real process environment always wins. A name that already had a value 9 * before injection is never overwritten — operator config (env files, CI, 10 * container env) cannot be shadowed from the dashboard. Such keys are 11 * reported as overridden instead of silently going live. 12 * - Only names this injector set are ever deleted or scrubbed. It tracks its 13 * own writes, so removing a stored key can never unset an operator-set var 14 * that happens to share the name. 15 */ 16export class AppKeyInjector { 17 private injected = new Set<string>(); 18 private overridden = new Set<string>(); 19 20 constructor(private env: NodeJS.ProcessEnv = process.env) {} 21 22 /** Applies a stored key. Returns true when the value went live, false when a 23 * pre-existing environment value kept precedence. */ 24 apply(name: string, value: string): boolean { 25 if (!this.injected.has(name) && this.env[name] !== undefined) { 26 this.overridden.add(name); 27 logger.warn( 28 `App key ${name} is overridden by the process environment; the stored value is not live`, 29 ); 30 return false; 31 } 32 this.env[name] = value; 33 this.injected.add(name); 34 return true; 35 } 36 37 /** Removes a stored key from the environment, only if this injector set it. 38 * Returns true when the environment actually changed. */ 39 remove(name: string): boolean { 40 this.overridden.delete(name); 41 if (!this.injected.has(name)) return false; 42 delete this.env[name]; 43 this.injected.delete(name); 44 return true; 45 } 46 47 isOverridden(name: string): boolean { 48 return this.overridden.has(name); 49 } 50}
▸名称保留
packages/api-types/src/app-keys.ts:21 对应这条实现证据;packages/api-types/src/app-keys.ts:25 对应这条实现证据;packages/api-types/src/app-keys.ts:33 对应这条实现证据;packages/api-types/src/app-keys.ts:58 对应这条实现证据;packages/api-types/src/app-keys.ts:80 对应这条实现证据;packages/api-types/src/app-keys.ts:85 对应这条实现证据;packages/api-types/src/app-keys.ts:88 对应这条实现证据;packages/api-types/src/app-keys.ts:91 对应这条实现证据,这些规则防止用户把 PATH、DATABASE、ANTHROPIC、ROME 等平台级变量伪装成 app key。它是密钥功能能上线的前提,不是表单校验细节。
21export const APP_KEY_MAX_NAME_LENGTH = 64; 22export const APP_KEY_MAX_LABEL_LENGTH = 120; 23export const APP_KEY_MAX_VALUE_LENGTH = 32768; 24 25const NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/; 26 27// An app key lands in `process.env`, so a name that collides with platform 28// configuration could reroute the database, the API ports, or an upstream 29// credential. Config env names are only expressed as inline `env.X` reads in 30// core's `config.ts` (`envToRawConfig`) — there is no enumerable schema to 31// check against — so this list carries every prefix family the platform 32// consumes plus the ambient OS names a child process relies on. 33const RESERVED_PREFIXES = [ 34 "ROME_", 35 "NODE_", 36 "NPM_", 37 "PNPM_", 38 "PANTHEON_", 39 "INTERNAL_API_", 40 "RELAY_", 41 "STATSIG_", 42 "FEATURE_GATE_", 43 "OTEL_", 44 "SQLITE_", 45 "POSTGRES_", 46 "DATABASE_", 47 "WEB_", 48 "LINKEDIN_", 49 "SENTINEL_", 50 "ANTHROPIC_", 51 "DOCKERHUB_", 52 "CLICKHOUSE_", 53 "AWS_", 54 "SSH_", 55 "NEXT_", 56]; 57 58const RESERVED_NAMES = new Set([ 59 "PATH", 60 "HOME", 61 "USER", 62 "LOGNAME", 63 "SHELL", 64 "TMPDIR", 65 "TMP", 66 "TEMP", 67 "PWD", 68 "LANG", 69 "LC_ALL", 70 "TZ", 71 "TERM", 72 "HOSTNAME", 73 "EDITOR", 74 "CI", 75 "LOG_LEVEL", 76 "UPDATES_DOMAIN", 77]); 78 79/** Returns a human-readable rejection reason, or null when the name is usable. */ 80export function appKeyNameError(name: string): string | null { 81 if (name.length === 0) return "Name is required."; 82 if (name.length > APP_KEY_MAX_NAME_LENGTH) { 83 return `Name must be at most ${APP_KEY_MAX_NAME_LENGTH} characters.`; 84 } 85 if (!NAME_PATTERN.test(name)) { 86 return "Name must use only uppercase letters, digits, and underscores, and start with a letter."; 87 } 88 if (RESERVED_NAMES.has(name)) { 89 return `${name} is reserved by Rome. Pick a different name.`; 90 } 91 const prefix = RESERVED_PREFIXES.find((p) => name.startsWith(p)); 92 if (prefix) { 93 return `Names starting with ${prefix} are reserved by Rome. Pick a different name.`; 94 } 95 return null; 96}
Workflow Studio 是技能入口,不是表单生成器
Workflow Studio 的前端只有一个核心动作:rome_apps/workflow-studio/src/web/App.tsx:19 读取用户自然语言,rome_apps/workflow-studio/src/web/App.tsx:25 用 startChat 开启带 design-workflow skill 的新聊天。对应 skill 在 rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:14 到 rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:68 要求先塑形规格、获取批准,再构建并安装 workflow app。
▸前门组件
1import "./styles.css"; 2import { useCallback, useState } from "react"; 3import { startChat, type RomeAppBootstrap } from "@rome-os/app-web-sdk"; 4 5const EXAMPLE_PROMPTS = [ 6 "Every morning, summarize my unread emails and post the digest to my chat.", 7 "When I save a link to my reading list, fetch the article and add a short summary.", 8]; 9 10// The app is a front door: describing an automation opens a fresh guardian chat 11// with that message under the `design-workflow` skill, which shapes it into a 12// short spec, gets the guardian's approval, and builds the workflow — all in that 13// one conversation. There is no panel to mirror, so the page is just the launcher. 14export default function App({ bootstrap: _bootstrap }: { bootstrap: RomeAppBootstrap }) { 15 const [draft, setDraft] = useState(""); 16 const [starting, setStarting] = useState(false); 17 const [error, setError] = useState<string | null>(null); 18 19 const start = useCallback(async () => { 20 const message = draft.trim(); 21 if (!message || starting) return; 22 setStarting(true); 23 setError(null); 24 try { 25 await startChat({ message, skillName: "design-workflow" }); 26 // startChat navigates to the new chat on success; no further work here. 27 } catch { 28 setStarting(false); 29 setError("Couldn't start a chat just now — try again."); 30 } 31 }, [draft, starting]); 32 33 return ( 34 <div className="ws-root"> 35 <div className="ws-frontdoor"> 36 <h1>Build a workflow</h1> 37 <p className="ws-lede"> 38 Describe an automation in plain language. Your guardian shapes it into a short spec you 39 approve, then builds it into a runnable workflow — all in one conversation. 40 </p> 41 <textarea 42 className="ws-input" 43 rows={3} 44 placeholder="e.g. Every morning, summarize my unread emails and post the digest to my chat." 45 value={draft} 46 onChange={(e) => setDraft(e.target.value)} 47 disabled={starting} 48 /> 49 <div className="ws-cta-row"> 50 <button 51 className="ws-cta" 52 onClick={() => void start()} 53 disabled={!draft.trim() || starting} 54 > 55 {starting ? "Starting…" : "Design this workflow"} 56 </button> 57 </div> 58 {error && <p className="ws-error-line">{error}</p>} 59 <div className="ws-examples"> 60 <span className="ws-examples-label">Try one of these</span> 61 {EXAMPLE_PROMPTS.map((p) => ( 62 <button 63 key={p} 64 type="button" 65 className="ws-example" 66 onClick={() => setDraft(p)} 67 disabled={starting} 68 > 69 {p} 70 </button> 71 ))} 72 </div> 73 </div> 74 </div> 75 ); 76}
▸技能约束
rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:20 对应这条实现证据;rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:31 对应这条实现证据;rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:38 对应这条实现证据;rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:45 对应这条实现证据;rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:52 对应这条实现证据;rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:61 对应这条实现证据;rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:73 对应这条实现证据;rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:78 对应这条实现证据,这些行说明 Rome 把“自动化生成”约束为静态结构、明确交付物和批准后构建,而不是让 agent 无限规划。
14## Phase 1 — Shape the idea into a spec 15 16Turn what the guardian said into a short, plain-language spec they can read and 17sign off on. The spec is the thing they approve, so write it for a non-technical 18reader — what the automation *does*, not how it's coded. Use this shape: 19 20> **Trigger** — what kicks it off (a schedule, an event, a "run now"). 21> **Steps** — the ordered things it does, one short verb phrase each 22> ("fetch my unread emails", "summarize them", "post the digest to my chat"). 23> **Decisions** — any branch ("if the total is over $5k, escalate; otherwise file 24> it"), stated as plain conditions. 25> **Per-item work** — anything done "for each" of a list ("for each ticket, rate 26> its severity"). 27> **Delivers** — the final step that sends, posts, saves, or shows the result. 28 29Hold these disciplines while you shape it — they decide whether the spec is real: 30 31- **Ask the guardian for facts only they can supply; never invent them.** A 32 threshold amount, a recipient, a channel, which account, which event fires the 33 trigger — a made-up "$5,000" looks authoritative and gets approved by accident. 34 When a fact like this is missing, ask for it in your reply rather than guessing. 35- **Resolve design questions in conversation, not in the spec.** Anything that 36 changes the *shape* — whether to branch, what to do per item — gets settled with 37 the guardian before you present the spec, not parked as an open question in it. 38- **Every workflow must deliver.** A spec that only fetches and computes, with no 39 step that sends/posts/saves/shows a result, has no destination and is never 40 finished. If the guardian didn't say where the result goes, that's a fact to ask 41 for (see above). 42- **Don't let vague quantifiers pass.** "the top labs", "the latest news", "key 43 themes", "top outlets" each hide a fact — how many, which sources, what time 44 window. If it changes what the automation does, pin it down with the guardian. 45- **Keep it statically structured.** The full space of what it does must be 46 knowable before it runs: ordered steps, parallel work, a decision, or bounded 47 per-item fan-out. No "keep polling until…", no unbounded loops. 48 49Keep the spec itself conversational and free of technical artifacts — no JSON, no 50flow chart, no node/combinator vocabulary. Talk about what it will *do*. 51 52## Phase 2 — Get approval 53 54Present the spec and ask the guardian to approve it or tell you what to change. 55Make the ask explicit ("Want me to build this, or change anything first?"). If 56they ask for a change, revise the spec and ask again. Build **only** after a clear 57yes — don't start scaffolding on a maybe. 58 59## Phase 3 — Build it 60 61On approval, build the workflow into a new, runnable Rome app. **Load the 62`coding:workflow_creation` skill and follow it end to end** — it owns the real work: 63deriving the control flow, scaffolding the `workflow` template, authoring 64`src/workflow/definition.ts` in plain TypeScript (`await`/`if`/`for`/`Promise.all`), 65the `system:summon` discipline for generative steps, the `connector:connector_proxy` discipline for 66SaaS calls, the `ctx.dryRun` guard on every write, the display envelope, the web 67surface, and the install. Translate the approved spec into its control flow 68faithfully — don't redesign the automation while coding it.
Trace DTO 让多代理执行可回放
▸块级事件流
Trace 不是日志字符串。packages/api-types/src/trace-segments.ts:27 到 packages/api-types/src/trace-segments.ts:67 定义 session、turn 和 text block;packages/api-types/src/trace-segments.ts:74 到 packages/api-types/src/trace-segments.ts:90 给 tool use/result 配对;packages/api-types/src/trace-segments.ts:92 到 packages/api-types/src/trace-segments.ts:122 明确子代理 start/result 的状态和错误形状。这个结构让 UI 可以按 turn、tool、subagent 重建执行过程。
92export interface SubagentStartBlock extends TraceBlockBase { 93 type: "subagent_start"; 94 toolUseId: string; 95 agentName: string; 96 input: unknown; 97 sessionId: string; 98 turnId: string; 99 startedAt?: string; 100} 101 102export type SubagentResultBlock = 103 | (TraceBlockBase & { 104 type: "subagent_result"; 105 toolUseId: string; 106 agentName: string; 107 sessionId: string; 108 turnId: string; 109 status: "completed"; 110 output: unknown; 111 endedAt?: string; 112 }) 113 | (TraceBlockBase & { 114 type: "subagent_result"; 115 toolUseId: string; 116 agentName: string; 117 sessionId: string; 118 turnId: string; 119 status: "failed" | "cancelled"; 120 error: { message: string; code?: string }; 121 endedAt?: string; 122 });
▸使用量与子代理汇总
packages/api-types/src/trace-segments.ts:124 到 packages/api-types/src/trace-segments.ts:162 定义 token、上下文、模型和 descendant subagent 汇总;packages/api-types/src/trace-segments.ts:235 到 packages/api-types/src/trace-segments.ts:253 又把直接子代理和 trace summary 连接起来。对 agentic OS 来说,这决定了成本、耗时和工具调用能不能被解释给用户。
124export interface TraceTokenUsage { 125 cacheReadTokens: number; 126 cacheWriteTokens: number; 127 inputTokens: number; 128 outputTokens: number; 129} 130 131export interface TraceContextUsage { 132 usedTokens: number; 133 windowTokens?: number; 134 remainingTokens?: number; 135} 136 137/** Read-time usage grouped by the provider/model that produced it. */ 138export interface TraceModelUsage { 139 provider: string; 140 model: string; 141 usage: TraceTokenUsage; 142 costUsd?: number; 143 runCount: number; 144} 145 146export interface TraceAccounting { 147 provider: string; 148 model: string; 149 usage: TraceTokenUsage; 150 context?: TraceContextUsage; 151 costUsd?: number; 152 numTurns?: number; 153 stopReason?: string; 154 durationMs?: number; 155 rawUsage?: Record<string, unknown>; 156 /** Number of descendant subagent runs included in `usage` and `costUsd`. 157 * Derived only for trace reads; never persisted in canonical trace blocks. */ 158 includedSubagentCount?: number; 159 /** Main + descendant usage grouped by provider/model. Like 160 * `includedSubagentCount`, this is derived only on trace reads. */ 161 usageByModel?: TraceModelUsage[]; 162}
▸测试证据
模块 loader 测试在 packages/core/src/actions/module-loader.test.ts:19 到 packages/core/src/actions/module-loader.test.ts:75 用真实 Node 进程验证 epoch bump;app keys API 测试在 packages/core/src/api/routes/app-keys.test.ts:86 到 packages/core/src/api/routes/app-keys.test.ts:157 验证 worker pool 会因密钥变化重启。这些测试覆盖的是运行时边界,而不是简单 snapshot。
19 it("re-evaluates the relative-import graph only after an epoch bump", async () => { 20 const dir = await mkdtemp(join(tmpdir(), "module-loader-epoch-")); 21 try { 22 // The env read lives in an imported helper, not the entry: the resolve 23 // hook must propagate the entry's cache salt onto the relative import 24 // or the helper stays cached across epochs. 25 await writeFile( 26 join(dir, "config.js"), 27 `export const captured = process.env.${TEST_KEY} ?? "unset";\n`, 28 "utf-8", 29 ); 30 await writeFile(join(dir, "entry.js"), `export { captured } from "./config.js";\n`, "utf-8"); 31 32 const loaderUrl = pathToFileURL( 33 resolve(dirname(fileURLToPath(import.meta.url)), "module-loader.ts"), 34 ).href; 35 const probePath = join(dir, "probe.mjs"); 36 await writeFile( 37 probePath, 38 ` 39import { bumpModuleEnvEpoch, importModuleWithCacheBuster } from ${JSON.stringify(loaderUrl)}; 40 41const entry = process.argv[2]; 42const before = await importModuleWithCacheBuster(entry); 43process.env.${TEST_KEY} = "v1"; 44const withoutBump = await importModuleWithCacheBuster(entry); 45bumpModuleEnvEpoch(); 46const after = await importModuleWithCacheBuster(entry); 47console.log(JSON.stringify({ 48 before: before.captured, 49 withoutBump: withoutBump.captured, 50 after: after.captured, 51})); 52`, 53 "utf-8", 54 ); 55 56 // Run under tsx — both the `source` container mode and the forked action 57 // workers in dev use it, and it is the loader that normalizes queries 58 // away without the short-circuit hook. (Plain node, the `compiled` mode, 59 // exercises the same hooks minus the tsx chain.) 60 const coreRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); 61 const { stdout } = await execFileAsync( 62 process.execPath, 63 ["--import", "tsx", probePath, join(dir, "entry.js")], 64 { cwd: coreRoot }, 65 ); 66 const lines = stdout.trim().split("\n"); 67 expect(JSON.parse(lines[lines.length - 1])).toEqual({ 68 before: "unset", 69 withoutBump: "unset", 70 after: "v1", 71 }); 72 } finally { 73 await rm(dir, { recursive: true, force: true }); 74 } 75 }, 30_000);
技术亮点之外,别忽略这些
▸密钥仍是全 app 可见
packages/web/src/components/connections/app-keys-section.tsx:224 的 UI consent 明确说所有安装 app 都能读取该值;packages/core/src/api/routes/app-keys.ts:55 的 injector.apply 会把值放进进程环境。这个设计简单可用,但不是细粒度 per-app secret permission,适合早期 OS,但未来需要更强隔离。
▸模块缓存换来可控热刷新
packages/core/src/actions/module-loader.ts:121 到 packages/core/src/actions/module-loader.ts:127 承认每次 bump 会把旧实例留在 ESM cache,这和 app upgrade 的 mtime 策略成本类似。频繁编辑密钥或高 churn app 可能累积缓存压力,需要后续通过 worker 边界或进程重启治理。
▸Workflow 生成仍依赖技能纪律
rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:88 到 rome_apps/workflow-studio/src/skills/design-workflow/SKILL.md:91 要求安装完成才算 done;这很好,但说明可靠性来自 skill discipline 与 runtime 共同约束。若 skill 过期或调用者绕开批准流程,系统仍可能生成不完整自动化。