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
@@ -0,0 +1,462 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { aiSdkAgent, fromAiSdk } from "./ai-sdk.ts";
4
+ import type { AiSdkGenerateContext, AiSdkResultLike, AiSdkTracing } from "./ai-sdk.ts";
5
+ import type { AgentContext } from "../types.ts";
6
+ import { createAgentSession } from "../context/session.ts";
7
+
8
+ describe("fromAiSdk", () => {
9
+ it("v5+ content parts:保留真实顺序,tool-error 映射成 failed", () => {
10
+ const { events } = fromAiSdk({
11
+ steps: [
12
+ {
13
+ content: [
14
+ { type: "reasoning", text: "先查天气" },
15
+ { type: "tool-call", toolCallId: "call_1", toolName: "get_weather", input: { city: "Brooklyn" } },
16
+ { type: "tool-result", toolCallId: "call_1", output: { temp: 21 } },
17
+ { type: "tool-call", toolCallId: "call_2", toolName: "web_search", input: { query: "穿衣" } },
18
+ { type: "tool-error", toolCallId: "call_2", error: new Error("rate limited") },
19
+ ],
20
+ },
21
+ { content: [{ type: "text", text: "布鲁克林 21 度。" }] },
22
+ ],
23
+ });
24
+
25
+ expect(events.map((e) => e.type)).toEqual([
26
+ "thinking",
27
+ "action.called",
28
+ "action.result",
29
+ "action.called",
30
+ "action.result",
31
+ "message",
32
+ ]);
33
+ expect(events[1]).toMatchObject({ callId: "call_1", name: "get_weather", tool: "unknown" });
34
+ expect(events[3]).toMatchObject({ callId: "call_2", name: "web_search", tool: "web_search" });
35
+ expect(events[4]).toMatchObject({ status: "failed", output: { error: "rate limited" } });
36
+ expect(events[5]).toMatchObject({ role: "assistant", text: "布鲁克林 21 度。" });
37
+ });
38
+
39
+ it("v4 退路:认 args / result / promptTokens 旧命名", () => {
40
+ const { events, usage } = fromAiSdk({
41
+ steps: [
42
+ {
43
+ text: "算好了,是 42。",
44
+ toolCalls: [{ toolCallId: "c1", toolName: "calculate", args: { expression: "6*7" } }],
45
+ toolResults: [{ toolCallId: "c1", result: { value: 42 } }],
46
+ },
47
+ ],
48
+ usage: { promptTokens: 100, completionTokens: 20 },
49
+ });
50
+
51
+ expect(events).toEqual([
52
+ { type: "action.called", callId: "c1", name: "calculate", input: { expression: "6*7" }, tool: "unknown" },
53
+ { type: "action.result", callId: "c1", output: { value: 42 }, status: "completed" },
54
+ { type: "message", role: "assistant", text: "算好了,是 42。" },
55
+ ]);
56
+ expect(usage).toEqual({ inputTokens: 100, outputTokens: 20, requests: 1 });
57
+ });
58
+
59
+ it("没有 steps:退回顶层 text / toolCalls / toolResults", () => {
60
+ const { events } = fromAiSdk({
61
+ text: "你好!",
62
+ toolCalls: [{ toolCallId: "c1", toolName: "read_file", input: { path: "a.ts" } }],
63
+ toolResults: [{ toolCallId: "c1", output: "content" }],
64
+ });
65
+
66
+ expect(events.map((e) => e.type)).toEqual(["action.called", "action.result", "message"]);
67
+ expect(events[0]).toMatchObject({ tool: "file_read" });
68
+ });
69
+
70
+ it("usage:totalUsage 优先于 usage,cachedInputTokens 进 cacheReadTokens,requests = step 数", () => {
71
+ const { usage } = fromAiSdk({
72
+ steps: [{ text: "a" }, { text: "b" }],
73
+ totalUsage: { inputTokens: 300, outputTokens: 50, cachedInputTokens: 120 },
74
+ usage: { inputTokens: 1, outputTokens: 1 },
75
+ });
76
+
77
+ expect(usage).toEqual({ inputTokens: 300, outputTokens: 50, requests: 2, cacheReadTokens: 120 });
78
+ });
79
+
80
+ it("全零 usage 视为缺失(别让 maxTokens 拿 0 假通过时看起来像有数据)", () => {
81
+ const { usage } = fromAiSdk({ steps: [{ text: "hi" }], usage: {} });
82
+ expect(usage).toBeUndefined();
83
+ });
84
+
85
+ it("空文本 / 空白 step 不产 message 事件", () => {
86
+ const { events } = fromAiSdk({ steps: [{ text: " " }, { content: [{ type: "text", text: "" }] }] });
87
+ expect(events).toEqual([]);
88
+ });
89
+
90
+ it("无 approval 请求时 status 是 completed", () => {
91
+ const { status } = fromAiSdk({ steps: [{ content: [{ type: "text", text: "你好" }] }] });
92
+ expect(status).toBe("completed");
93
+ });
94
+
95
+ it("v7 tool-approval-request:input.requested + waiting,与 tool-call part 按 callId 去重", () => {
96
+ const { events, status } = fromAiSdk({
97
+ steps: [
98
+ {
99
+ content: [
100
+ { type: "tool-call", toolCallId: "call_1", toolName: "send_email", input: { to: "a@b.c" } },
101
+ {
102
+ type: "tool-approval-request",
103
+ approvalId: "appr_1",
104
+ toolCall: { toolCallId: "call_1", toolName: "send_email", input: { to: "a@b.c" } },
105
+ },
106
+ ],
107
+ },
108
+ ],
109
+ });
110
+
111
+ expect(status).toBe("waiting");
112
+ expect(events.map((e) => e.type)).toEqual(["action.called", "input.requested"]);
113
+ expect(events[0]).toMatchObject({ callId: "call_1", name: "send_email" });
114
+ expect(events[1]).toMatchObject({
115
+ request: {
116
+ id: "appr_1",
117
+ action: "send_email",
118
+ input: { to: "a@b.c" },
119
+ options: [{ id: "approve" }, { id: "deny" }],
120
+ },
121
+ });
122
+ });
123
+
124
+ it("automatic 的 approval 请求不吐 input.requested(SDK 自动裁决,不等人)", () => {
125
+ const { events, status } = fromAiSdk({
126
+ steps: [
127
+ {
128
+ content: [
129
+ {
130
+ type: "tool-approval-request",
131
+ approvalId: "appr_1",
132
+ isAutomatic: true,
133
+ toolCall: { toolCallId: "call_1", toolName: "send_email", input: {} },
134
+ },
135
+ ],
136
+ },
137
+ ],
138
+ });
139
+
140
+ expect(status).toBe("completed");
141
+ expect(events.map((e) => e.type)).toEqual(["action.called"]);
142
+ });
143
+
144
+ it("approval resume:responseMessages 里的执行结果补成 action.result,排在本轮事件之前", () => {
145
+ const { events, status } = fromAiSdk({
146
+ steps: [{ content: [{ type: "text", text: "邮件已发送。" }] }],
147
+ responseMessages: [
148
+ {
149
+ role: "tool",
150
+ content: [
151
+ {
152
+ type: "tool-result",
153
+ toolCallId: "call_1",
154
+ toolName: "send_email",
155
+ output: { type: "json", value: { delivered: true } },
156
+ },
157
+ ],
158
+ },
159
+ { role: "assistant", content: [{ type: "text", text: "邮件已发送。" }] },
160
+ ],
161
+ });
162
+
163
+ expect(status).toBe("completed");
164
+ expect(events).toEqual([
165
+ { type: "action.result", callId: "call_1", output: { delivered: true }, status: "completed" },
166
+ { type: "message", role: "assistant", text: "邮件已发送。" },
167
+ ]);
168
+ });
169
+
170
+ it("approval 拒绝:execution-denied 映射成 rejected", () => {
171
+ const { events } = fromAiSdk({
172
+ steps: [{ content: [{ type: "text", text: "好的,不发了。" }] }],
173
+ responseMessages: [
174
+ {
175
+ role: "tool",
176
+ content: [
177
+ {
178
+ type: "tool-result",
179
+ toolCallId: "call_1",
180
+ toolName: "send_email",
181
+ output: { type: "execution-denied", reason: "用户拒绝" },
182
+ },
183
+ ],
184
+ },
185
+ ],
186
+ });
187
+
188
+ expect(events[0]).toEqual({
189
+ type: "action.result",
190
+ callId: "call_1",
191
+ output: { reason: "用户拒绝" },
192
+ status: "rejected",
193
+ });
194
+ });
195
+
196
+ it("steps 里已有的 tool-result 不因 responseMessages 重复(普通工具循环两边都有)", () => {
197
+ const { events } = fromAiSdk({
198
+ steps: [
199
+ {
200
+ content: [
201
+ { type: "tool-call", toolCallId: "c1", toolName: "get_weather", input: { city: "北京" } },
202
+ { type: "tool-result", toolCallId: "c1", output: { temp: 26 } },
203
+ { type: "text", text: "北京 26 度。" },
204
+ ],
205
+ },
206
+ ],
207
+ responseMessages: [
208
+ {
209
+ role: "tool",
210
+ content: [
211
+ { type: "tool-result", toolCallId: "c1", toolName: "get_weather", output: { type: "json", value: { temp: 26 } } },
212
+ ],
213
+ },
214
+ ],
215
+ });
216
+
217
+ expect(events.filter((e) => e.type === "action.result")).toHaveLength(1);
218
+ });
219
+
220
+ it("aiSdkAgent:ctx.session.id 未记录时开新会话线,同 id 续接同一份历史", async () => {
221
+ const seen: unknown[][] = [];
222
+ const agent = aiSdkAgent({
223
+ generate: async ({ messages }: AiSdkGenerateContext) => {
224
+ seen.push([...messages]);
225
+ const result: AiSdkResultLike = {
226
+ steps: [{ content: [{ type: "text", text: `回复#${seen.length}` }] }],
227
+ responseMessages: [{ role: "assistant", content: `回复#${seen.length}` }],
228
+ };
229
+ return result;
230
+ },
231
+ });
232
+
233
+ const ctx = fakeCtx();
234
+ await agent.send({ text: "第一轮" }, ctx);
235
+ expect(ctx.session.id).toBeDefined();
236
+ const turn2 = await agent.send({ text: "第二轮" }, fakeCtx({ id: ctx.session.id }));
237
+
238
+ expect(turn2.status).toBe("completed");
239
+ // 第二轮的历史 = 用户#1 + 助手#1 + 用户#2
240
+ expect(seen[1]).toEqual([
241
+ { role: "user", content: "第一轮" },
242
+ { role: "assistant", content: "回复#1" },
243
+ { role: "user", content: "第二轮" },
244
+ ]);
245
+
246
+ // 新会话线:历史必须是干净的
247
+ const ctx3 = fakeCtx();
248
+ await agent.send({ text: "新会话" }, ctx3);
249
+ expect(ctx3.session.id).not.toBe(ctx.session.id);
250
+ expect(seen[2]).toEqual([{ role: "user", content: "新会话" }]);
251
+ });
252
+
253
+ it("aiSdkAgent:approval 停轮 → waiting;下一轮 responses 翻译成 tool-approval-response", async () => {
254
+ const seen: unknown[][] = [];
255
+ let call = 0;
256
+ const agent = aiSdkAgent({
257
+ generate: async ({ messages }: AiSdkGenerateContext) => {
258
+ seen.push([...messages]);
259
+ call++;
260
+ if (call === 1) {
261
+ return {
262
+ steps: [
263
+ {
264
+ content: [
265
+ { type: "tool-call", toolCallId: "c1", toolName: "send_email", input: { to: "a@b.c" } },
266
+ { type: "tool-approval-request", approvalId: "appr_1", toolCall: { toolCallId: "c1", toolName: "send_email", input: { to: "a@b.c" } } },
267
+ ],
268
+ },
269
+ ],
270
+ responseMessages: [{ role: "assistant", content: [] }],
271
+ } satisfies AiSdkResultLike;
272
+ }
273
+ return {
274
+ steps: [{ content: [{ type: "text", text: "邮件已发送。" }] }],
275
+ responseMessages: [],
276
+ } satisfies AiSdkResultLike;
277
+ },
278
+ });
279
+
280
+ const ctx = fakeCtx();
281
+ const first = await agent.send({ text: "发邮件" }, ctx);
282
+ expect(first.status).toBe("waiting");
283
+ expect(first.events.some((e) => e.type === "input.requested")).toBe(true);
284
+
285
+ const second = await agent.send(
286
+ { text: "approve", responses: [{ requestId: "appr_1", optionId: "approve" }] },
287
+ fakeCtx({ id: ctx.session.id }),
288
+ );
289
+ expect(second.status).toBe("completed");
290
+ // 第二轮历史的最后一条是翻译好的裁决(tool 消息),不是用户文本
291
+ const last = (seen[1] as { role: string; content: unknown }[]).at(-1);
292
+ expect(last).toEqual({
293
+ role: "tool",
294
+ content: [{ type: "tool-approval-response", approvalId: "appr_1", approved: true }],
295
+ });
296
+ });
297
+
298
+ it("aiSdkAgent:approval resume 优先按 requestId 从 input.responses 读裁决,而不是猜 input.text", async () => {
299
+ const seen: unknown[][] = [];
300
+ let call = 0;
301
+ const agent = aiSdkAgent({
302
+ generate: async ({ messages }: AiSdkGenerateContext) => {
303
+ seen.push([...messages]);
304
+ call++;
305
+ if (call === 1) {
306
+ return {
307
+ steps: [
308
+ {
309
+ content: [
310
+ { type: "tool-call", toolCallId: "c1", toolName: "send_email", input: { to: "a@b.c" } },
311
+ { type: "tool-approval-request", approvalId: "appr_1", toolCall: { toolCallId: "c1", toolName: "send_email", input: { to: "a@b.c" } } },
312
+ ],
313
+ },
314
+ ],
315
+ responseMessages: [{ role: "assistant", content: [] }],
316
+ } satisfies AiSdkResultLike;
317
+ }
318
+ return {
319
+ steps: [{ content: [{ type: "text", text: "好的,没有发送。" }] }],
320
+ responseMessages: [],
321
+ } satisfies AiSdkResultLike;
322
+ },
323
+ });
324
+
325
+ const ctx = fakeCtx();
326
+ await agent.send({ text: "发邮件" }, ctx);
327
+
328
+ // input.text 读起来像批准,但 responses 结构化裁决说 deny——必须以 responses 为准。
329
+ const second = await agent.send(
330
+ { text: "approve", responses: [{ requestId: "appr_1", optionId: "deny" }] },
331
+ fakeCtx({ id: ctx.session.id }),
332
+ );
333
+ expect(second.status).toBe("completed");
334
+ const last = (seen[1] as { role: string; content: unknown }[]).at(-1);
335
+ expect(last).toEqual({
336
+ role: "tool",
337
+ content: [{ type: "tool-approval-response", approvalId: "appr_1", approved: false }],
338
+ });
339
+ });
340
+
341
+ it("aiSdkAgent:approval 停轮期间的 send 没带对位 responses → 直接报错(不从文本猜)", async () => {
342
+ const agent = aiSdkAgent({
343
+ generate: async () => ({
344
+ steps: [
345
+ {
346
+ content: [
347
+ { type: "tool-call", toolCallId: "c1", toolName: "send_email", input: {} },
348
+ { type: "tool-approval-request", approvalId: "appr_1", toolCall: { toolCallId: "c1", toolName: "send_email", input: {} } },
349
+ ],
350
+ },
351
+ ],
352
+ responseMessages: [{ role: "assistant", content: [] }],
353
+ } satisfies AiSdkResultLike),
354
+ });
355
+
356
+ const ctx = fakeCtx();
357
+ const first = await agent.send({ text: "发邮件" }, ctx);
358
+ expect(first.status).toBe("waiting");
359
+ await expect(agent.send({ text: "approve" }, fakeCtx({ id: ctx.session.id }))).rejects.toThrow(/t\.respond/);
360
+ });
361
+
362
+ it("aiSdkAgent:tracing 管线——每轮拿 per-attempt 端点建集成经 ctx.telemetry 交给 generate,轮末 flush", async () => {
363
+ const endpoints: string[] = [];
364
+ const flushed: string[] = [];
365
+ const tracing: AiSdkTracing = {
366
+ telemetryForEndpoint(endpoint) {
367
+ endpoints.push(endpoint);
368
+ return {
369
+ settings: { integrations: ["fake-integration"] },
370
+ flush: async () => { flushed.push(endpoint); },
371
+ };
372
+ },
373
+ };
374
+ let seenTelemetry: unknown;
375
+ const agent = aiSdkAgent({
376
+ tracing,
377
+ generate: async ({ telemetry }) => {
378
+ seenTelemetry = telemetry;
379
+ return { text: "ok" };
380
+ },
381
+ });
382
+
383
+ const turn = await agent.send(
384
+ { text: "hi" },
385
+ { ...fakeCtx(), telemetry: { endpoint: "http://127.0.0.1:4318/v1/traces" } },
386
+ );
387
+ expect(turn.status).toBe("completed");
388
+ expect(seenTelemetry).toEqual({ integrations: ["fake-integration"] });
389
+ expect(endpoints).toEqual(["http://127.0.0.1:4318/v1/traces"]);
390
+ expect(flushed).toEqual(["http://127.0.0.1:4318/v1/traces"]);
391
+ });
392
+
393
+ it("aiSdkAgent:generate 抛错也要 flush(本轮已产生的 span 不能丢)", async () => {
394
+ let flushes = 0;
395
+ const tracing: AiSdkTracing = {
396
+ telemetryForEndpoint: () => ({
397
+ settings: { integrations: [] },
398
+ flush: async () => { flushes++; },
399
+ }),
400
+ };
401
+ const agent = aiSdkAgent({
402
+ tracing,
403
+ generate: async () => { throw new Error("upstream timeout"); },
404
+ });
405
+
406
+ const turn = await agent.send({ text: "hi" }, { ...fakeCtx(), telemetry: { endpoint: "http://e/v1/traces" } });
407
+ expect(turn.status).toBe("failed");
408
+ expect(flushes).toBe(1);
409
+ });
410
+
411
+ it("aiSdkAgent:没配 tracing 时 generate 的 telemetry 恒为 undefined", async () => {
412
+ let seenTelemetry: unknown = "sentinel";
413
+ const agent = aiSdkAgent({
414
+ generate: async ({ telemetry }) => {
415
+ seenTelemetry = telemetry;
416
+ return { text: "ok" };
417
+ },
418
+ });
419
+ await agent.send({ text: "hi" }, fakeCtx());
420
+ expect(seenTelemetry).toBeUndefined();
421
+ });
422
+
423
+ it("aiSdkAgent:generate 抛错 / 空结果 → failed + error 事件", async () => {
424
+ const boom = aiSdkAgent({ generate: async () => { throw new Error("上游超时"); } });
425
+ const failed = await boom.send({ text: "hi" }, fakeCtx());
426
+ expect(failed.status).toBe("failed");
427
+ expect(failed.events[0]).toMatchObject({ type: "error", message: "上游超时" });
428
+
429
+ const empty = aiSdkAgent({ generate: async () => ({ steps: [{ text: "" }] }) });
430
+ const emptyTurn = await empty.send({ text: "hi" }, fakeCtx());
431
+ expect(emptyTurn.status).toBe("failed");
432
+ });
433
+
434
+ it("v7 usage:cache tokens 从 inputTokenDetails 读", () => {
435
+ const { usage } = fromAiSdk({
436
+ steps: [{ text: "hi" }],
437
+ usage: { inputTokens: 100, outputTokens: 20, inputTokenDetails: { cacheReadTokens: 40, cacheWriteTokens: 8 } },
438
+ });
439
+
440
+ expect(usage).toEqual({
441
+ inputTokens: 100,
442
+ outputTokens: 20,
443
+ requests: 1,
444
+ cacheReadTokens: 40,
445
+ cacheWriteTokens: 8,
446
+ });
447
+ });
448
+ });
449
+
450
+ /** 省略 `id`(或不给参数)= 全新会话线;传 `id` = 这条线已经 capture 过这个 id(续接轮)。 */
451
+ function fakeCtx(opts: { id?: string } = {}): AgentContext {
452
+ const session = createAgentSession();
453
+ if (opts.id) session.capture(opts.id);
454
+ return {
455
+ signal: new AbortController().signal,
456
+ model: undefined,
457
+ flags: {},
458
+ sandbox: undefined as unknown as AgentContext["sandbox"],
459
+ session,
460
+ log: () => {},
461
+ };
462
+ }