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.
- package/README.md +8 -8
- package/README.zh.md +13 -13
- package/docs/README.md +119 -0
- package/docs/adapters/README.md +60 -0
- package/docs/adapters/authoring.md +179 -0
- package/docs/adapters/coding-agent-skills-plugins.md +324 -0
- package/docs/adapters/collection.md +128 -0
- package/docs/adapters/contract.md +263 -0
- package/docs/adapters/reference/agent-eval.md +215 -0
- package/docs/adapters/reference/agent-loop-apis.md +69 -0
- package/docs/adapters/reference/claude-code-otel-telemetry.md +100 -0
- package/docs/adapters/reference/eve-protocol.md +127 -0
- package/docs/adapters/reference/otel-genai.md +107 -0
- package/docs/adapters/reference/otel-instrumentation.md +65 -0
- package/docs/adapters/targets.md +99 -0
- package/docs/architecture.md +140 -0
- package/docs/assertions.md +388 -0
- package/docs/capabilities-by-construction.md +48 -0
- package/docs/cli.md +171 -0
- package/docs/concepts.md +106 -0
- package/docs/e2e-ci.md +295 -0
- package/docs/eval-authoring.md +319 -0
- package/docs/experiments.md +154 -0
- package/docs/getting-started.md +224 -0
- package/docs/multi-agent.md +145 -0
- package/docs/observability.md +333 -0
- package/docs/origin-integration.md +195 -0
- package/docs/references.md +35 -0
- package/docs/results-format.md +228 -0
- package/docs/runner.md +104 -0
- package/docs/sandbox.md +276 -0
- package/docs/scoring.md +119 -0
- package/docs/source-map.md +106 -0
- package/docs/tier-sync.md +177 -0
- package/docs/view.md +157 -0
- package/docs/vision.md +88 -0
- package/package.json +39 -5
- package/src/agents/ai-sdk-otel.test.ts +22 -0
- package/src/agents/ai-sdk-otel.ts +62 -0
- package/src/agents/ai-sdk.test.ts +462 -0
- package/src/agents/ai-sdk.ts +575 -0
- package/src/agents/bub.ts +30 -37
- package/src/agents/claude-code.ts +7 -4
- package/src/agents/codex.ts +15 -10
- package/src/agents/index.ts +48 -1
- package/src/agents/sdk-streams.test.ts +145 -0
- package/src/agents/sdk-streams.ts +457 -0
- package/src/agents/shared.ts +77 -39
- package/src/agents/streaming.test.ts +152 -0
- package/src/agents/streaming.ts +195 -0
- package/src/agents/types.ts +218 -0
- package/src/agents/ui-message-stream.test.ts +241 -0
- package/src/agents/ui-message-stream.ts +368 -0
- package/src/cli.ts +124 -77
- package/src/context/context.test.ts +203 -7
- package/src/context/context.ts +158 -49
- package/src/context/session.test.ts +96 -0
- package/src/context/session.ts +119 -9
- package/src/context/types.ts +205 -0
- package/src/define.ts +4 -14
- package/src/expect/index.ts +8 -71
- package/src/i18n/core.ts +28 -0
- package/src/i18n/en.ts +47 -5
- package/src/i18n/index.ts +5 -17
- package/src/i18n/zh-CN.ts +47 -5
- package/src/index.ts +12 -0
- package/src/loaders/index.ts +8 -39
- package/src/o11y/otlp/canonical.ts +7 -6
- package/src/o11y/otlp/mappers/codex.ts +10 -8
- package/src/o11y/otlp/mappers/index.ts +9 -21
- package/src/o11y/otlp/receiver.ts +25 -7
- package/src/o11y/otlp/sandbox-receiver.ts +57 -21
- package/src/o11y/otlp/turn-otel.test.ts +110 -0
- package/src/o11y/otlp/turn-otel.ts +125 -0
- package/src/o11y/parsers/bub.ts +13 -11
- package/src/o11y/parsers/claude-code.ts +27 -51
- package/src/o11y/parsers/codex.ts +29 -46
- package/src/o11y/parsers/index.ts +5 -14
- package/src/o11y/tool-names.test.ts +61 -0
- package/src/o11y/tool-names.ts +74 -0
- package/src/o11y/types.ts +135 -0
- package/src/runner/attempt.ts +456 -0
- package/src/runner/fingerprint.ts +59 -0
- package/src/runner/remote-sandbox.ts +53 -0
- package/src/runner/report.ts +53 -0
- package/src/runner/reporters/artifacts.ts +25 -4
- package/src/runner/reporters/console.ts +2 -8
- package/src/runner/reporters/live.ts +2 -8
- package/src/runner/reporters/shared.ts +15 -0
- package/src/runner/reporters/table.ts +10 -62
- package/src/runner/run.ts +114 -619
- package/src/runner/types.ts +237 -0
- package/src/sandbox/checkpoint.ts +15 -13
- package/src/sandbox/docker-stream.ts +87 -0
- package/src/sandbox/docker.ts +42 -120
- package/src/sandbox/e2b.ts +24 -75
- package/src/sandbox/local-files.ts +33 -0
- package/src/sandbox/paths.test.ts +85 -0
- package/src/sandbox/paths.ts +45 -0
- package/src/sandbox/resolve.ts +10 -34
- package/src/sandbox/shell.ts +17 -0
- package/src/sandbox/source-files.ts +42 -2
- package/src/sandbox/types.ts +158 -0
- package/src/sandbox/vercel.ts +55 -71
- package/src/scoring/collector.ts +3 -2
- package/src/scoring/judge.ts +72 -146
- package/src/scoring/match.ts +79 -0
- package/src/scoring/scoped.ts +66 -18
- package/src/scoring/types.ts +76 -0
- package/src/shared/aggregate.ts +48 -0
- package/src/shared/format.ts +20 -0
- package/src/shared/outcome.ts +48 -0
- package/src/shared/types.ts +37 -0
- package/src/types.ts +20 -911
- package/src/view/aggregate.ts +103 -0
- package/src/view/app/App.tsx +26 -5
- package/src/view/app/components/AttemptModal.tsx +12 -3
- package/src/view/app/components/CodeView.tsx +16 -19
- package/src/view/app/components/CopyControls.tsx +4 -4
- package/src/view/app/components/ExperimentTable.tsx +5 -5
- package/src/view/app/i18n.ts +31 -7
- package/src/view/app/lib/format.ts +17 -20
- package/src/view/app/lib/outcome.ts +4 -16
- package/src/view/app/lib/transcript-data.tsx +20 -4
- package/src/view/app/main.tsx +3 -5
- package/src/view/app/pages/RunsPage.tsx +2 -2
- package/src/view/app/pages/TracesPage.tsx +2 -2
- package/src/view/app/shared.ts +2 -1
- package/src/view/app/types.ts +9 -44
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/src/view/index.ts +24 -401
- package/src/view/loader.ts +234 -0
- package/src/view/server.ts +136 -0
- package/src/view/shared/types.ts +64 -0
- 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
|
|
6
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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)
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
ctx.session.id
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
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.
|
|
90
|
+
ctx.session.capture(shared.sessionIdFromClaudeTranscript(raw));
|
|
90
91
|
const parsed = shared.parseClaudeCode(raw);
|
|
91
|
-
|
|
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
|
}
|
package/src/agents/codex.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
|
107
|
-
const resuming =
|
|
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}
|
|
110
|
-
: `codex exec ${flags}
|
|
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.
|
|
118
|
+
ctx.session.capture(shared.codexThreadId(res.stdout));
|
|
116
119
|
const parsed = shared.parseCodex(raw);
|
|
117
|
-
|
|
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
|
}
|
package/src/agents/index.ts
CHANGED
|
@@ -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
|
+
});
|