niceeval 0.1.1 → 0.2.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 (136) hide show
  1. package/README.md +8 -8
  2. package/README.zh.md +13 -13
  3. package/docs/README.md +119 -0
  4. package/docs/adapters/README.md +60 -0
  5. package/docs/adapters/authoring.md +179 -0
  6. package/docs/adapters/coding-agent-skills-plugins.md +324 -0
  7. package/docs/adapters/collection.md +128 -0
  8. package/docs/adapters/contract.md +263 -0
  9. package/docs/adapters/reference/agent-eval.md +215 -0
  10. package/docs/adapters/reference/agent-loop-apis.md +69 -0
  11. package/docs/adapters/reference/claude-code-otel-telemetry.md +100 -0
  12. package/docs/adapters/reference/eve-protocol.md +127 -0
  13. package/docs/adapters/reference/otel-genai.md +107 -0
  14. package/docs/adapters/reference/otel-instrumentation.md +65 -0
  15. package/docs/adapters/targets.md +99 -0
  16. package/docs/architecture.md +140 -0
  17. package/docs/assertions.md +388 -0
  18. package/docs/capabilities-by-construction.md +48 -0
  19. package/docs/cli.md +171 -0
  20. package/docs/concepts.md +106 -0
  21. package/docs/e2e-ci.md +295 -0
  22. package/docs/eval-authoring.md +319 -0
  23. package/docs/experiments.md +154 -0
  24. package/docs/getting-started.md +224 -0
  25. package/docs/multi-agent.md +145 -0
  26. package/docs/observability.md +333 -0
  27. package/docs/origin-integration.md +195 -0
  28. package/docs/references.md +35 -0
  29. package/docs/results-format.md +228 -0
  30. package/docs/runner.md +104 -0
  31. package/docs/sandbox.md +276 -0
  32. package/docs/scoring.md +119 -0
  33. package/docs/source-map.md +106 -0
  34. package/docs/tier-sync.md +177 -0
  35. package/docs/view.md +157 -0
  36. package/docs/vision.md +88 -0
  37. package/package.json +39 -5
  38. package/src/agents/ai-sdk-otel.test.ts +22 -0
  39. package/src/agents/ai-sdk-otel.ts +62 -0
  40. package/src/agents/ai-sdk.test.ts +462 -0
  41. package/src/agents/ai-sdk.ts +575 -0
  42. package/src/agents/bub.ts +30 -37
  43. package/src/agents/claude-code.ts +7 -4
  44. package/src/agents/codex.ts +15 -10
  45. package/src/agents/index.ts +48 -1
  46. package/src/agents/sdk-streams.test.ts +145 -0
  47. package/src/agents/sdk-streams.ts +457 -0
  48. package/src/agents/shared.ts +77 -39
  49. package/src/agents/streaming.test.ts +152 -0
  50. package/src/agents/streaming.ts +195 -0
  51. package/src/agents/types.ts +218 -0
  52. package/src/agents/ui-message-stream.test.ts +241 -0
  53. package/src/agents/ui-message-stream.ts +368 -0
  54. package/src/cli.ts +124 -77
  55. package/src/context/context.test.ts +203 -7
  56. package/src/context/context.ts +158 -49
  57. package/src/context/session.test.ts +96 -0
  58. package/src/context/session.ts +119 -9
  59. package/src/context/types.ts +205 -0
  60. package/src/define.ts +4 -14
  61. package/src/expect/index.ts +8 -71
  62. package/src/i18n/core.ts +28 -0
  63. package/src/i18n/en.ts +47 -5
  64. package/src/i18n/index.ts +5 -17
  65. package/src/i18n/zh-CN.ts +47 -5
  66. package/src/index.ts +12 -0
  67. package/src/loaders/index.ts +8 -39
  68. package/src/o11y/otlp/canonical.ts +7 -6
  69. package/src/o11y/otlp/mappers/codex.ts +10 -8
  70. package/src/o11y/otlp/mappers/index.ts +9 -21
  71. package/src/o11y/otlp/receiver.ts +25 -7
  72. package/src/o11y/otlp/sandbox-receiver.ts +57 -21
  73. package/src/o11y/otlp/turn-otel.test.ts +110 -0
  74. package/src/o11y/otlp/turn-otel.ts +125 -0
  75. package/src/o11y/parsers/bub.ts +13 -11
  76. package/src/o11y/parsers/claude-code.ts +27 -51
  77. package/src/o11y/parsers/codex.ts +29 -46
  78. package/src/o11y/parsers/index.ts +5 -14
  79. package/src/o11y/tool-names.test.ts +61 -0
  80. package/src/o11y/tool-names.ts +74 -0
  81. package/src/o11y/types.ts +135 -0
  82. package/src/runner/attempt.ts +456 -0
  83. package/src/runner/fingerprint.ts +59 -0
  84. package/src/runner/remote-sandbox.ts +53 -0
  85. package/src/runner/report.ts +53 -0
  86. package/src/runner/reporters/artifacts.ts +25 -4
  87. package/src/runner/reporters/console.ts +2 -8
  88. package/src/runner/reporters/live.ts +2 -8
  89. package/src/runner/reporters/shared.ts +15 -0
  90. package/src/runner/reporters/table.ts +10 -62
  91. package/src/runner/run.ts +114 -619
  92. package/src/runner/types.ts +237 -0
  93. package/src/sandbox/checkpoint.ts +15 -13
  94. package/src/sandbox/docker-stream.ts +87 -0
  95. package/src/sandbox/docker.ts +42 -120
  96. package/src/sandbox/e2b.ts +24 -75
  97. package/src/sandbox/local-files.ts +33 -0
  98. package/src/sandbox/paths.test.ts +85 -0
  99. package/src/sandbox/paths.ts +45 -0
  100. package/src/sandbox/resolve.ts +10 -34
  101. package/src/sandbox/shell.ts +17 -0
  102. package/src/sandbox/source-files.ts +42 -2
  103. package/src/sandbox/types.ts +158 -0
  104. package/src/sandbox/vercel.ts +55 -71
  105. package/src/scoring/collector.ts +3 -2
  106. package/src/scoring/judge.ts +72 -146
  107. package/src/scoring/match.ts +79 -0
  108. package/src/scoring/scoped.ts +66 -18
  109. package/src/scoring/types.ts +76 -0
  110. package/src/shared/aggregate.ts +48 -0
  111. package/src/shared/format.ts +20 -0
  112. package/src/shared/outcome.ts +48 -0
  113. package/src/shared/types.ts +37 -0
  114. package/src/types.ts +20 -911
  115. package/src/view/aggregate.ts +103 -0
  116. package/src/view/app/App.tsx +26 -5
  117. package/src/view/app/components/AttemptModal.tsx +12 -3
  118. package/src/view/app/components/CodeView.tsx +16 -19
  119. package/src/view/app/components/CopyControls.tsx +4 -4
  120. package/src/view/app/components/ExperimentTable.tsx +5 -5
  121. package/src/view/app/i18n.ts +31 -7
  122. package/src/view/app/lib/format.ts +17 -20
  123. package/src/view/app/lib/outcome.ts +4 -16
  124. package/src/view/app/lib/transcript-data.tsx +20 -4
  125. package/src/view/app/main.tsx +3 -5
  126. package/src/view/app/pages/RunsPage.tsx +2 -2
  127. package/src/view/app/pages/TracesPage.tsx +2 -2
  128. package/src/view/app/shared.ts +2 -1
  129. package/src/view/app/types.ts +9 -44
  130. package/src/view/client-dist/app.css +1 -1
  131. package/src/view/client-dist/app.js +18 -18
  132. package/src/view/index.ts +24 -401
  133. package/src/view/loader.ts +234 -0
  134. package/src/view/server.ts +136 -0
  135. package/src/view/shared/types.ts +64 -0
  136. package/src/view/styles.css +60 -9
package/src/agents/bub.ts CHANGED
@@ -2,8 +2,9 @@ import { defineSandboxAgent } from "../define.ts";
2
2
  import { requireEnv, getEnv } from "../util.ts";
3
3
  import { shared } from "./shared.ts";
4
4
  import { createCheckpoint, restoreCheckpoint } from "../sandbox/checkpoint.ts";
5
- import type { Agent, Sandbox, StreamEvent } from "../types.ts";
6
- import { createHash } from "node:crypto";
5
+ import { mapBubSpans } from "../o11y/otlp/mappers/bub.ts";
6
+ import type { Agent, Sandbox } from "../types.ts";
7
+ import { createHash, randomUUID } from "node:crypto";
7
8
  import { readFile, writeFile, mkdir } from "node:fs/promises";
8
9
  import { homedir } from "node:os";
9
10
  import { join, dirname } from "node:path";
@@ -37,15 +38,19 @@ const UV = "$HOME/.local/bin/uv";
37
38
  // 预制模板把 bub 装到 /usr/local/bin(见 sandbox/docker/Dockerfile),command -v 命中即用 → 跳过安装。
38
39
  const BUB = "$(command -v bub || echo $HOME/.local/bin/bub)";
39
40
 
40
- const BUB_OVERRIDE = "bub @ git+https://github.com/CorrectRoadH/bub.git@fix/streaming-usage-include-usage";
41
+ // TODO(upstream): 这两个默认值钉在个人 fork 的修复分支上,等上游合并后改回发布版并删掉本注释。
42
+ // 可用 NICEEVAL_BUB_OVERRIDE / NICEEVAL_BUB_OTEL_PLUGIN 覆盖,不必改源码。
43
+ const BUB_OVERRIDE =
44
+ getEnv("NICEEVAL_BUB_OVERRIDE") ??
45
+ "bub @ git+https://github.com/CorrectRoadH/bub.git@fix/streaming-usage-include-usage";
41
46
  const BUB_OVERRIDE_FILE = "/tmp/bub-override.txt";
42
47
  const OTEL_PLUGIN =
48
+ getEnv("NICEEVAL_BUB_OTEL_PLUGIN") ??
43
49
  "git+https://github.com/CorrectRoadH/bub-contrib.git@fix/tapestore-otel-tape-entry-validation" +
44
- "#subdirectory=packages/bub-tapestore-otel";
50
+ "#subdirectory=packages/bub-tapestore-otel";
45
51
 
46
52
  const INSTALL_SPEC = `bub --override(${BUB_OVERRIDE}) --with ${OTEL_PLUGIN}`;
47
53
  const INSTALL_HASH = createHash("md5").update(INSTALL_SPEC).digest("hex").slice(0, 12);
48
- const DEFAULT_WORKSPACE = "/home/sandbox/workspace";
49
54
 
50
55
  function diskCachePath(home: string): string {
51
56
  const homeKey = createHash("md5").update(home).digest("hex").slice(0, 8);
@@ -108,8 +113,10 @@ async function ensureBub(sb: Sandbox, home: string): Promise<void> {
108
113
  resolveInstall();
109
114
  } catch (e) {
110
115
  rejectInstall(e);
111
- installsInProgress.delete(home);
112
116
  throw e;
117
+ } finally {
118
+ // 成功/失败都清锁:锁只表达「正在装」,装完后 memCheckpoints 是唯一缓存事实源。
119
+ installsInProgress.delete(home);
113
120
  }
114
121
  }
115
122
 
@@ -119,25 +126,6 @@ function tapePath(workspace: string, sessionId: string, bubHome: string): string
119
126
  return `${bubHome}/tapes/${w}__${s}.jsonl`;
120
127
  }
121
128
 
122
- function diagnose(
123
- res: { exitCode: number; stdout: string; stderr: string },
124
- events: StreamEvent[],
125
- rawTape: string | undefined,
126
- ): string {
127
- const parts: string[] = [t("bub.diagnose.exitCode", { code: res.exitCode })];
128
- if (rawTape === undefined) parts.push(t("bub.diagnose.noTape"));
129
- else if (events.length === 0) parts.push(t("bub.diagnose.zeroEvents"));
130
- const lastErr = [...events].reverse().find((e) => e.type === "error") as { type: "error"; message: string } | undefined;
131
- if (lastErr) parts.push(t("bub.diagnose.lastError", { message: lastErr.message }));
132
- const errTail = tail(res.stderr) || tail(res.stdout);
133
- if (errTail) parts.push(t("bub.diagnose.outputTail", { tail: errTail }));
134
- return parts.join(" · ");
135
- }
136
-
137
- function tail(s: string, n = 6): string {
138
- return s.trim().split("\n").filter(Boolean).slice(-n).join(" ⏎ ").slice(0, 600);
139
- }
140
-
141
129
  export function bubAgent(config?: BubConfig): Agent {
142
130
  const getApiKey = () => config?.apiKey ?? requireEnv("BUB_API_KEY");
143
131
  const getApiBase = () => config?.apiBase ?? requireEnv("BUB_API_BASE");
@@ -146,7 +134,7 @@ export function bubAgent(config?: BubConfig): Agent {
146
134
 
147
135
  return defineSandboxAgent({
148
136
  name: "bub",
149
- capabilities: { conversation: true, toolObservability: true, workspace: true, compactionObservability: true, tracing: true },
137
+ spanMapper: mapBubSpans,
150
138
 
151
139
  tracing: {
152
140
  protocol: "http/protobuf",
@@ -158,8 +146,11 @@ export function bubAgent(config?: BubConfig): Agent {
158
146
  },
159
147
 
160
148
  async setup(sb) {
161
- const home = (await sb.runShell("printf '%s' $HOME")).stdout.trim() || "/home/node";
162
- const workspace = DEFAULT_WORKSPACE;
149
+ // home 必须来自运行时探测:各 sandbox 后端不同(/home/node、/home/vercel-sandbox…),
150
+ // 兜一个后端专属常量会静默走错路径(tape 读不到 → 空事件流 → 负断言假通过)。
151
+ const home = (await sb.runShell("printf '%s' $HOME")).stdout.trim();
152
+ if (!home) throw new Error(t("bub.homeDetectFailed"));
153
+ const workspace = sb.workdir;
163
154
  sessionInfo.set(sb.sandboxId, { home, workspace });
164
155
  await ensureBub(sb, home);
165
156
 
@@ -191,30 +182,32 @@ export function bubAgent(config?: BubConfig): Agent {
191
182
 
192
183
  async send(input, ctx) {
193
184
  const sb = ctx.sandbox;
194
- const info = sessionInfo.get(sb.sandboxId) ?? { home: "/home/node", workspace: DEFAULT_WORKSPACE };
185
+ const info = sessionInfo.get(sb.sandboxId);
186
+ if (!info) throw new Error(t("bub.setupNotRun"));
195
187
  const { home, workspace } = info;
196
188
  const bubHome = `${home}/.bub`;
197
- const model = ctx.model ?? "gpt-5.4";
198
- const sessionId = `fe-${sb.sandboxId}`;
199
- ctx.session.id = sessionId;
189
+ // 会话契约:ctx.session.id 未记录时开新 tape(新 sessionId),否则 resume 传入的 id。
190
+ // tape 路径由 md5(workspace)+md5(sessionId) 决定,同沙箱多会话靠 sessionId 区分。
191
+ const sessionId = ctx.session.id ?? `fe-${sb.sandboxId}-${randomUUID().slice(0, 8)}`;
192
+ ctx.session.capture(sessionId);
200
193
 
201
- const env = {
194
+ const env: Record<string, string> = {
202
195
  BUB_API_KEY: getApiKey(),
203
196
  BUB_API_BASE: getApiBase(),
204
- BUB_MODEL: `openai:${model}`,
205
197
  BUB_HOME: bubHome,
206
198
  ...ctx.telemetry?.env,
207
199
  };
208
- const escaped = input.text.replace(/'/g, "'\\''");
200
+ // model 归属:实验决定(ctx.model),省略时交给 bub 原生默认 / 用户环境,不硬编码。
201
+ if (ctx.model) env.BUB_MODEL = `openai:${ctx.model}`;
209
202
  const res = await sb.runShell(
210
- `${BUB} --workspace ${workspace} run '${escaped}' --session-id ${sessionId}`,
203
+ `${BUB} --workspace ${workspace} run ${shared.shellQuote(input.text)} --session-id ${sessionId}`,
211
204
  { env, stream: true },
212
205
  );
213
206
 
214
207
  const raw = await sb.readFile(tapePath(workspace, sessionId, bubHome)).catch(() => undefined);
215
208
  const parsed = shared.parseBub(raw);
216
209
  const events = [...parsed.events];
217
- if (res.exitCode !== 0) events.push({ type: "error", message: diagnose(res, parsed.events, raw) });
210
+ if (res.exitCode !== 0) events.push({ type: "error", message: shared.diagnoseFailure(res, parsed.events, raw) });
218
211
  return { events, usage: parsed.usage, status: res.exitCode === 0 ? "completed" : "failed" };
219
212
  },
220
213
  });
@@ -43,7 +43,6 @@ export function claudeCodeAgent(config?: ClaudeCodeConfig): Agent {
43
43
 
44
44
  return defineSandboxAgent({
45
45
  name: "claude-code",
46
- capabilities: { conversation: true, toolObservability: true, workspace: true, compactionObservability: true },
47
46
 
48
47
  async setup(sb) {
49
48
  // 预制模板已把 claude 烘焙进镜像(PATH 上)就跳过安装;否则 npm 全局装。
@@ -76,7 +75,7 @@ export function claudeCodeAgent(config?: ClaudeCodeConfig): Agent {
76
75
  if (ctx.model) args.push("--model", ctx.model);
77
76
  if (config?.maxTurns != null) args.push("--max-turns", String(config.maxTurns));
78
77
  if (ctx.flags.webResearch) args.push("--allowedTools", "WebSearch,WebFetch");
79
- if (!ctx.session.isNew && ctx.session.id) args.push("--resume", ctx.session.id);
78
+ if (ctx.session.id) args.push("--resume", ctx.session.id);
80
79
  args.push(input.text);
81
80
 
82
81
  const env: Record<string, string> = { ANTHROPIC_API_KEY: getApiKey() };
@@ -85,10 +84,14 @@ export function claudeCodeAgent(config?: ClaudeCodeConfig): Agent {
85
84
 
86
85
  const res = await sb.runCommand("claude", args, { env, stream: true });
87
86
 
87
+ // 「最新 jsonl」而非按 session id 精确定位:--resume 会 fork 新 session id 的新文件,
88
+ // 精确匹配旧 id 会读到过期 transcript。send 串行,最新的一定是刚跑完的这次。
88
89
  const raw = await shared.captureLatestJsonl(sb, "~/.claude/projects");
89
- ctx.session.id = shared.sessionIdFromClaudeTranscript(raw) ?? ctx.session.id;
90
+ ctx.session.capture(shared.sessionIdFromClaudeTranscript(raw));
90
91
  const parsed = shared.parseClaudeCode(raw);
91
- return { events: parsed.events, usage: parsed.usage, status: res.exitCode === 0 ? "completed" : "failed" };
92
+ const events = [...parsed.events];
93
+ if (res.exitCode !== 0) events.push({ type: "error", message: shared.diagnoseFailure(res, parsed.events, raw) });
94
+ return { events, usage: parsed.usage, status: res.exitCode === 0 ? "completed" : "failed" };
92
95
  },
93
96
  });
94
97
  }
@@ -1,6 +1,7 @@
1
1
  import { defineSandboxAgent } from "../define.ts";
2
2
  import { requireEnv, getEnv } from "../util.ts";
3
3
  import { shared } from "./shared.ts";
4
+ import { mapCodexSpans } from "../o11y/otlp/mappers/codex.ts";
4
5
  import type { Agent, McpServer } from "../types.ts";
5
6
 
6
7
  // ───────────────────────────────────────────────────────────────────────────
@@ -35,13 +36,15 @@ export function codexAgent(config?: CodexConfig): Agent {
35
36
 
36
37
  return defineSandboxAgent({
37
38
  name: "codex",
38
- capabilities: { conversation: true, toolObservability: true, workspace: true, compactionObservability: true, tracing: true },
39
+ spanMapper: mapCodexSpans,
39
40
 
40
41
  async setup(sb, ctx) {
41
42
  // 预制模板已把 codex 烘焙进镜像(PATH 上)就跳过安装;否则 npm 全局装。
42
43
  await sb.runShell("command -v codex >/dev/null 2>&1 || npm install -g @openai/codex");
43
44
 
44
- const model = ctx.model ?? "gpt-5.4";
45
+ // model 归属:实验决定(ctx.model);省略时不写 model 行,交给 codex CLI 原生默认,
46
+ // 不在 adapter 里硬编码一个会过期的模型名。
47
+ const modelLine = ctx.model ? `model = "${ctx.model}"\n` : "";
45
48
  const effort = (ctx.flags.effort as string | undefined) ?? "medium";
46
49
  const base = getBaseUrl();
47
50
 
@@ -49,7 +52,7 @@ export function codexAgent(config?: CodexConfig): Agent {
49
52
  await shared.writeFile(
50
53
  sb,
51
54
  "~/.codex/config.toml",
52
- `model = "${model}"\n` +
55
+ modelLine +
53
56
  `model_provider = "s2a"\n` +
54
57
  `model_reasoning_effort = "${effort}"\n\n` +
55
58
  `[model_providers.s2a]\n` +
@@ -59,7 +62,7 @@ export function codexAgent(config?: CodexConfig): Agent {
59
62
  `wire_api = "responses"\n`,
60
63
  );
61
64
  } else {
62
- await shared.writeFile(sb, "~/.codex/config.toml", `model = "${model}"\nmodel_reasoning_effort = "${effort}"\n`);
65
+ await shared.writeFile(sb, "~/.codex/config.toml", `${modelLine}model_reasoning_effort = "${effort}"\n`);
63
66
  }
64
67
 
65
68
  if (config?.mcpServers?.length) {
@@ -103,18 +106,20 @@ export function codexAgent(config?: CodexConfig): Agent {
103
106
  async send(input, ctx) {
104
107
  const sb = ctx.sandbox;
105
108
  const flags = "--json --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check";
106
- const escaped = input.text.replace(/'/g, "'\\''");
107
- const resuming = !ctx.session.isNew && ctx.session.id;
109
+ const prompt = shared.shellQuote(input.text);
110
+ const resuming = ctx.session.id;
108
111
  const cmd = resuming
109
- ? `codex exec resume ${ctx.session.id} ${flags} '${escaped}'`
110
- : `codex exec ${flags} '${escaped}'`;
112
+ ? `codex exec resume ${ctx.session.id} ${flags} ${prompt}`
113
+ : `codex exec ${flags} ${prompt}`;
111
114
 
112
115
  const res = await sb.runShell(cmd, { env: { CODEX_API_KEY: getApiKey() }, stream: true });
113
116
 
114
117
  const raw = shared.extractJsonlFromStdout(res.stdout);
115
- ctx.session.id = shared.codexThreadId(res.stdout) ?? ctx.session.id;
118
+ ctx.session.capture(shared.codexThreadId(res.stdout));
116
119
  const parsed = shared.parseCodex(raw);
117
- return { events: parsed.events, usage: parsed.usage, status: res.exitCode === 0 ? "completed" : "failed" };
120
+ const events = [...parsed.events];
121
+ if (res.exitCode !== 0) events.push({ type: "error", message: shared.diagnoseFailure(res, parsed.events, raw) });
122
+ return { events, usage: parsed.usage, status: res.exitCode === 0 ? "completed" : "failed" };
118
123
  },
119
124
  });
120
125
  }
@@ -4,6 +4,53 @@ export { defineAgent, defineSandboxAgent } from "../define.ts";
4
4
  export { shared } from "./shared.ts";
5
5
  export type { Shared } from "./shared.ts";
6
6
 
7
+ // span → canonical GenAI 归一(只服务瀑布图,不喂断言)。私有埋点写自己的 spanMapper 时用:
8
+ // tagSpan 把判定写回 span(原属性只增不改),heuristicTag 是通用兜底判定;mapCodexSpans 是
9
+ // 现成的参考实现(无侵入接 codex 后端时直接声明 `spanMapper: mapCodexSpans`)。
10
+ // 映射目标(什么属性亮起瀑布图的什么)见 docs-site/zh/guides/connect-otel.mdx「瀑布图画得准不准」。
11
+ export { tagSpan, heuristicTag } from "../o11y/otlp/canonical.ts";
12
+ export type { SpanTag } from "../o11y/otlp/canonical.ts";
13
+ export { mapCodexSpans } from "../o11y/otlp/mappers/codex.ts";
14
+
15
+ export { uiMessageStreamAgent } from "./ui-message-stream.ts";
16
+ export type { UiMessageStreamAgentOptions, UIMessageLike, UIMessagePartLike } from "./ui-message-stream.ts";
17
+
18
+ // SDK 原生事件流 → 标准事件的官方转换器(无侵入 adapter 只剩传输粘合)+ 通用 SSE 读帧器。
19
+ export { sseJsonFrames, fromClaudeSdkMessages, fromPiAgentEvents, fromCodexThreadEvents } from "./sdk-streams.ts";
20
+ export type {
21
+ SseFrameCursor,
22
+ ClaudeSdkMessageLike,
23
+ ClaudeSdkStream,
24
+ PiAgentEventLike,
25
+ PiAgentStream,
26
+ CodexThreadEventLike,
27
+ CodexThreadStream,
28
+ } from "./sdk-streams.ts";
29
+
30
+ // 通用「拼装方式」件:逐帧驱动循环、逐 token/参数增量累加器。见 docs-site/zh/guides/write-send.mdx——
31
+ // 这些和任何具体协议无关,自己写 adapter 时优先拿这些拼,只有 transport(怎么发)与
32
+ // 「帧类型 → 操作」这张映射表才是真正要手写的。会话续接与 HITL 停轮现场不再是可选件,
33
+ // 而是 ctx.session(AgentSession)本身自带的存取器(history()/id+capture()、hold()/take())。
34
+ export { driveFrameStream, deltaStream } from "./streaming.ts";
35
+ export type { FrameReducer, FrameHook, DeltaOp, DeltaStreamSpec } from "./streaming.ts";
36
+
37
+ // tracing 管线的内置实现 aiSdkOtel() 在 `niceeval/adapter/otel`(独立子路径,不从这里
38
+ // re-export):OTel 三件套是可选 peer 依赖,只有 import 那个入口的项目才需要安装。
39
+ export { fromAiSdk, aiSdkAgent } from "./ai-sdk.ts";
40
+ export type {
41
+ AiSdkAgentOptions,
42
+ AiSdkGenerateContext,
43
+ AiSdkResultLike,
44
+ AiSdkStepLike,
45
+ AiSdkTelemetrySettings,
46
+ AiSdkToolCallLike,
47
+ AiSdkToolResultLike,
48
+ AiSdkTracing,
49
+ AiSdkTurn,
50
+ AiSdkTurnTelemetry,
51
+ AiSdkUsageLike,
52
+ } from "./ai-sdk.ts";
53
+
7
54
  export { BUILTIN_AGENTS } from "./builtin.ts";
8
55
  export { claudeCodeAgent } from "./claude-code.ts";
9
56
  export { codexAgent } from "./codex.ts";
@@ -15,9 +62,9 @@ export type { BubConfig } from "./bub.ts";
15
62
  export type {
16
63
  Agent,
17
64
  AgentContext,
18
- AgentCapabilities,
19
65
  AgentSession,
20
66
  AgentTracing,
67
+ SpanMapper,
21
68
  Telemetry,
22
69
  SandboxAgentDef,
23
70
  RemoteAgentDef,
@@ -0,0 +1,145 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { fromClaudeSdkMessages, fromCodexThreadEvents, fromPiAgentEvents, sseJsonFrames } from "./sdk-streams.ts";
4
+
5
+ function sseBody(frames: unknown[]): ReadableStream<Uint8Array> {
6
+ const text = frames.map((f) => `data: ${JSON.stringify(f)}\n\n`).join("") + "data: [DONE]\n\n";
7
+ return new Response(text).body!;
8
+ }
9
+
10
+ describe("sseJsonFrames", () => {
11
+ it("逐帧解析 data: JSON,跳过 [DONE],流结束返回 null", async () => {
12
+ const cursor = sseJsonFrames<{ n: number }>(sseBody([{ n: 1 }, { n: 2 }]));
13
+ expect(await cursor.next()).toEqual({ n: 1 });
14
+ expect(await cursor.next()).toEqual({ n: 2 });
15
+ expect(await cursor.next()).toBeNull();
16
+ });
17
+ });
18
+
19
+ describe("fromClaudeSdkMessages", () => {
20
+ it("system/init → sessionId;assistant/user → 事件对;result → usage", () => {
21
+ const s = fromClaudeSdkMessages();
22
+ expect(s.add({ type: "system", subtype: "init", session_id: "sess-1" })).toEqual([]);
23
+ expect(s.sessionId).toBe("sess-1");
24
+
25
+ const called = s.add({
26
+ type: "assistant",
27
+ message: { content: [{ type: "text", text: "查一下" }, { type: "tool_use", id: "tu1", name: "get_weather", input: { city: "北京" } }] },
28
+ });
29
+ expect(called).toEqual([
30
+ { type: "message", role: "assistant", text: "查一下" },
31
+ { type: "action.called", callId: "tu1", name: "get_weather", input: { city: "北京" } },
32
+ ]);
33
+
34
+ expect(s.add({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tu1", content: "晴 21 度" }] } })).toEqual([
35
+ { type: "action.result", callId: "tu1", output: "晴 21 度", status: "completed" },
36
+ ]);
37
+
38
+ s.add({ type: "result", is_error: false, num_turns: 2, total_cost_usd: 0.01, usage: { input_tokens: 100, output_tokens: 20 } });
39
+ expect(s.usage).toMatchObject({ inputTokens: 100, outputTokens: 20, requests: 2, costUSD: 0.01 });
40
+ expect(s.failed).toBe(false);
41
+ });
42
+
43
+ it("markRejected:tool_result 落成 rejected;permission_denied 与 tool_result 只产一条", () => {
44
+ const s = fromClaudeSdkMessages();
45
+ s.markRejected("tu1");
46
+ expect(s.add({ type: "system", subtype: "permission_denied", tool_use_id: "tu1" })).toEqual([
47
+ { type: "action.result", callId: "tu1", status: "rejected" },
48
+ ]);
49
+ // SDK 若随后也发 tool_result,不重复
50
+ expect(s.add({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tu1", content: "denied" }] } })).toEqual([]);
51
+ });
52
+
53
+ it("stream_event 等未知帧返回 []", () => {
54
+ expect(fromClaudeSdkMessages().add({ type: "stream_event" })).toEqual([]);
55
+ });
56
+ });
57
+
58
+ describe("fromPiAgentEvents", () => {
59
+ it("message_end → message/thinking + usage 累加;tool_execution_* → 事件对", () => {
60
+ const s = fromPiAgentEvents();
61
+ expect(s.add({ type: "tool_execution_start", toolCallId: "c1", toolName: "get_weather", args: { city: "上海" } })).toEqual([
62
+ { type: "action.called", callId: "c1", name: "get_weather", input: { city: "上海" } },
63
+ ]);
64
+ expect(s.add({ type: "tool_execution_end", toolCallId: "c1", result: { temp: 18 }, isError: false })).toEqual([
65
+ { type: "action.result", callId: "c1", output: { temp: 18 }, status: "completed" },
66
+ ]);
67
+ const msgEvents = s.add({
68
+ type: "message_end",
69
+ message: {
70
+ role: "assistant",
71
+ content: [{ type: "thinking", thinking: "想想" }, { type: "text", text: "18 度" }],
72
+ usage: { input: 10, output: 5, cacheRead: 1, cacheWrite: 2, cost: { total: 0.001 } },
73
+ },
74
+ });
75
+ expect(msgEvents).toEqual([
76
+ { type: "message", role: "assistant", text: "18 度" },
77
+ { type: "thinking", text: "想想" },
78
+ ]);
79
+ expect(s.usage).toMatchObject({ inputTokens: 10, outputTokens: 5, requests: 1 });
80
+ });
81
+
82
+ it("markRejected:isError 的 tool_execution_end 落成 rejected 而非 failed", () => {
83
+ const s = fromPiAgentEvents();
84
+ s.markRejected("c9");
85
+ expect(s.add({ type: "tool_execution_end", toolCallId: "c9", isError: true })).toEqual([
86
+ { type: "action.result", callId: "c9", output: undefined, status: "rejected" },
87
+ ]);
88
+ });
89
+ });
90
+
91
+ describe("fromCodexThreadEvents", () => {
92
+ it("thread.started → threadId;agent_message/reasoning → 消息;turn.failed → error", () => {
93
+ const s = fromCodexThreadEvents();
94
+ s.add({ type: "thread.started", thread_id: "t-1" });
95
+ expect(s.threadId).toBe("t-1");
96
+ expect(s.add({ type: "item.completed", item: { type: "agent_message", text: "改好了" } })).toEqual([
97
+ { type: "message", role: "assistant", text: "改好了" },
98
+ ]);
99
+ expect(s.add({ type: "item.completed", item: { type: "reasoning", text: "先看文件" } })).toEqual([
100
+ { type: "thinking", text: "先看文件" },
101
+ ]);
102
+ expect(s.failed).toBe(false);
103
+ expect(s.add({ type: "turn.failed", error: { message: "boom" } })).toEqual([{ type: "error", message: "boom" }]);
104
+ expect(s.failed).toBe(true);
105
+ });
106
+
107
+ it("command_execution:started 发 called,completed 只补 result(按 exit_code 判状态)", () => {
108
+ const s = fromCodexThreadEvents();
109
+ expect(s.add({ type: "item.started", item: { type: "command_execution", id: "c1", command: "ls" } })).toEqual([
110
+ { type: "action.called", callId: "c1", name: "command_execution", input: { command: "ls" } },
111
+ ]);
112
+ expect(
113
+ s.add({ type: "item.completed", item: { type: "command_execution", id: "c1", command: "ls", exit_code: 0, aggregated_output: "a.txt" } }),
114
+ ).toEqual([{ type: "action.result", callId: "c1", output: { output: "a.txt", exit_code: 0 }, status: "completed" }]);
115
+ });
116
+
117
+ it("只有 completed 的工具项也成对;失败 exit_code → failed", () => {
118
+ const s = fromCodexThreadEvents();
119
+ expect(s.add({ type: "item.completed", item: { type: "command_execution", id: "c2", command: "false", exit_code: 1 } })).toEqual([
120
+ { type: "action.called", callId: "c2", name: "command_execution", input: { command: "false" } },
121
+ { type: "action.result", callId: "c2", output: { output: null, exit_code: 1 }, status: "failed" },
122
+ ]);
123
+ });
124
+
125
+ it("mcp_tool_call 带 server 前缀;turn.completed 聚合 usage", () => {
126
+ const s = fromCodexThreadEvents();
127
+ expect(s.add({ type: "item.completed", item: { type: "mcp_tool_call", id: "m1", server: "kb", tool: "search", arguments: { q: "x" } } })).toEqual([
128
+ { type: "action.called", callId: "m1", name: "kb.search", input: { q: "x" } },
129
+ { type: "action.result", callId: "m1", output: null, status: "completed" },
130
+ ]);
131
+ expect(s.usage).toBeUndefined();
132
+ s.add({ type: "turn.completed", usage: { input_tokens: 100, cached_input_tokens: 40, output_tokens: 7 } });
133
+ expect(s.usage).toEqual({ inputTokens: 100, outputTokens: 7, cacheReadTokens: 40, requests: 1 });
134
+ });
135
+
136
+ it("file_change 每个文件一对 called/result", () => {
137
+ const s = fromCodexThreadEvents();
138
+ expect(
139
+ s.add({ type: "item.completed", item: { type: "file_change", id: "p1", changes: [{ path: "a.ts", kind: "update" }] } }),
140
+ ).toEqual([
141
+ { type: "action.called", callId: "p1#0", name: "file_change", input: { path: "a.ts", kind: "update" } },
142
+ { type: "action.result", callId: "p1#0", output: { path: "a.ts", kind: "update" }, status: "completed" },
143
+ ]);
144
+ });
145
+ });