niceeval 0.4.4 → 0.4.5
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 +21 -12
- package/README.zh.md +8 -9
- package/docs-site/.mintignore +13 -0
- package/docs-site/AGENTS.md +46 -0
- package/docs-site/concepts/adapter.mdx +287 -0
- package/docs-site/concepts/assert.mdx +243 -0
- package/docs-site/concepts/drive.mdx +118 -0
- package/docs-site/concepts/evals.mdx +108 -0
- package/docs-site/concepts/experiment.mdx +37 -0
- package/docs-site/concepts/hitl.mdx +83 -0
- package/docs-site/concepts/judge.mdx +129 -0
- package/docs-site/concepts/overview.mdx +98 -0
- package/docs-site/concepts/tier.mdx +42 -0
- package/docs-site/docs-ref/00-index.md +23 -0
- package/docs-site/docs-ref/01-page-types.md +43 -0
- package/docs-site/docs-ref/03-style-rules.md +45 -0
- package/docs-site/docs-ref/04-code-examples.md +37 -0
- package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
- package/docs-site/docs-ref/06-checklists.md +53 -0
- package/docs-site/docs.json +256 -0
- package/docs-site/example/ai-agent-application.mdx +148 -0
- package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
- package/docs-site/example/claude-code-codex-skill.mdx +147 -0
- package/docs-site/example/showcase.mdx +39 -0
- package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
- package/docs-site/example/tier1-claude-sdk.mdx +119 -0
- package/docs-site/example/tier1-codex-sdk.mdx +111 -0
- package/docs-site/example/tier1-langgraph.mdx +119 -0
- package/docs-site/example/tier1-pi-sdk.mdx +119 -0
- package/docs-site/favicon.svg +5 -0
- package/docs-site/github-diff.css +135 -0
- package/docs-site/github-diff.js +11 -0
- package/docs-site/guides/authoring.mdx +129 -0
- package/docs-site/guides/ci-integration.mdx +97 -0
- package/docs-site/guides/connect-otel.mdx +209 -0
- package/docs-site/guides/connect-your-agent.mdx +221 -0
- package/docs-site/guides/dataset-fanout.mdx +98 -0
- package/docs-site/guides/experiments.mdx +71 -0
- package/docs-site/guides/fixtures.mdx +147 -0
- package/docs-site/guides/reporters.mdx +113 -0
- package/docs-site/guides/runner.mdx +79 -0
- package/docs-site/guides/sandbox-agent.mdx +138 -0
- package/docs-site/guides/sandbox-backends.mdx +64 -0
- package/docs-site/guides/scoring-guide.mdx +92 -0
- package/docs-site/guides/viewing-results.mdx +195 -0
- package/docs-site/guides/write-experiment.mdx +81 -0
- package/docs-site/guides/write-send.mdx +347 -0
- package/docs-site/images/adapter-tiers-zh.svg +60 -0
- package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
- package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
- package/docs-site/images/hitl-handshake-zh.svg +89 -0
- package/docs-site/images/logo.svg +6 -0
- package/docs-site/index.mdx +181 -0
- package/docs-site/introduction.mdx +141 -0
- package/docs-site/quickstart.mdx +136 -0
- package/docs-site/reference/builtin-agents.mdx +161 -0
- package/docs-site/reference/capabilities.mdx +76 -0
- package/docs-site/reference/cli.mdx +120 -0
- package/docs-site/reference/define-agent.mdx +168 -0
- package/docs-site/reference/define-config.mdx +41 -0
- package/docs-site/reference/define-eval.mdx +160 -0
- package/docs-site/reference/events.mdx +131 -0
- package/docs-site/reference/expect.mdx +112 -0
- package/docs-site/tracker.js +8 -0
- package/docs-site/zh/concepts/adapter.mdx +286 -0
- package/docs-site/zh/concepts/assert.mdx +243 -0
- package/docs-site/zh/concepts/drive.mdx +118 -0
- package/docs-site/zh/concepts/evals.mdx +124 -0
- package/docs-site/zh/concepts/experiment.mdx +37 -0
- package/docs-site/zh/concepts/hitl.mdx +83 -0
- package/docs-site/zh/concepts/judge.mdx +130 -0
- package/docs-site/zh/concepts/overview.mdx +109 -0
- package/docs-site/zh/concepts/tier.mdx +44 -0
- package/docs-site/zh/example/ai-agent-application.mdx +152 -0
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
- package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
- package/docs-site/zh/example/showcase.mdx +39 -0
- package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
- package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
- package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
- package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
- package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
- package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
- package/docs-site/zh/guides/authoring.mdx +129 -0
- package/docs-site/zh/guides/ci-integration.mdx +97 -0
- package/docs-site/zh/guides/connect-otel.mdx +208 -0
- package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
- package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
- package/docs-site/zh/guides/experiments.mdx +72 -0
- package/docs-site/zh/guides/fixtures.mdx +147 -0
- package/docs-site/zh/guides/official-adapters.mdx +132 -0
- package/docs-site/zh/guides/reporters.mdx +113 -0
- package/docs-site/zh/guides/runner.mdx +82 -0
- package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
- package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
- package/docs-site/zh/guides/scoring-guide.mdx +92 -0
- package/docs-site/zh/guides/viewing-results.mdx +195 -0
- package/docs-site/zh/guides/write-experiment.mdx +101 -0
- package/docs-site/zh/guides/write-send.mdx +353 -0
- package/docs-site/zh/index.mdx +180 -0
- package/docs-site/zh/introduction.mdx +140 -0
- package/docs-site/zh/quickstart.mdx +137 -0
- package/docs-site/zh/reference/builtin-agents.mdx +364 -0
- package/docs-site/zh/reference/capabilities.mdx +76 -0
- package/docs-site/zh/reference/cli.mdx +140 -0
- package/docs-site/zh/reference/define-agent.mdx +461 -0
- package/docs-site/zh/reference/define-config.mdx +107 -0
- package/docs-site/zh/reference/define-eval.mdx +674 -0
- package/docs-site/zh/reference/events.mdx +252 -0
- package/docs-site/zh/reference/expect.mdx +208 -0
- package/package.json +3 -2
- package/src/agents/bub.ts +1 -1
- package/src/agents/claude-code.ts +8 -5
- package/src/agents/codex.ts +9 -5
- package/src/agents/sdk-streams.test.ts +4 -4
- package/src/agents/sdk-streams.ts +14 -9
- package/src/agents/types.ts +40 -1
- package/src/agents/ui-message-stream.ts +1 -0
- package/src/cli.ts +36 -7
- package/src/context/types.ts +140 -0
- package/src/expect/index.ts +15 -8
- package/src/i18n/en.ts +19 -6
- package/src/i18n/zh-CN.ts +19 -6
- package/src/o11y/parsers/bub.test.ts +71 -0
- package/src/o11y/parsers/bub.ts +5 -0
- package/src/o11y/types.ts +27 -2
- package/src/runner/reporters/artifacts.ts +11 -3
- package/src/runner/reporters/live.ts +45 -8
- package/src/runner/run.ts +71 -21
- package/src/runner/types.ts +45 -2
- package/src/sandbox/types.ts +18 -0
- package/src/scoring/types.ts +5 -0
- package/src/shared/types.ts +3 -0
- package/src/util.test.ts +26 -1
- package/src/util.ts +16 -0
- package/src/view/app/App.tsx +2 -2
- package/src/view/app/components/AttemptModal.tsx +2 -0
- package/src/view/app/components/CopyControls.tsx +87 -28
- package/src/view/app/i18n.ts +3 -3
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/docs/README.md +0 -120
- package/docs/adapters/README.md +0 -60
- package/docs/adapters/authoring.md +0 -179
- package/docs/adapters/coding-agent-skills-plugins.md +0 -324
- package/docs/adapters/collection.md +0 -128
- package/docs/adapters/contract.md +0 -264
- package/docs/adapters/reference/agent-eval.md +0 -215
- package/docs/adapters/reference/agent-loop-apis.md +0 -69
- package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
- package/docs/adapters/reference/eve-protocol.md +0 -127
- package/docs/adapters/reference/otel-genai.md +0 -107
- package/docs/adapters/reference/otel-instrumentation.md +0 -65
- package/docs/adapters/targets.md +0 -99
- package/docs/architecture.md +0 -140
- package/docs/assertions.md +0 -387
- package/docs/capabilities-by-construction.md +0 -48
- package/docs/cli.md +0 -173
- package/docs/concepts.md +0 -106
- package/docs/e2e-ci.md +0 -332
- package/docs/eval-authoring.md +0 -319
- package/docs/experiments.md +0 -157
- package/docs/getting-started.md +0 -224
- package/docs/multi-agent.md +0 -145
- package/docs/observability.md +0 -337
- package/docs/origin-integration.md +0 -195
- package/docs/references.md +0 -35
- package/docs/reports.md +0 -551
- package/docs/results-format.md +0 -228
- package/docs/results-lib.md +0 -191
- package/docs/runner.md +0 -104
- package/docs/sandbox.md +0 -276
- package/docs/scoring.md +0 -119
- package/docs/source-map.md +0 -126
- package/docs/tier-sync.md +0 -193
- package/docs/view.md +0 -194
- package/docs/vision.md +0 -88
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "How to write send"
|
|
3
|
+
sidebarTitle: "Write Send"
|
|
4
|
+
description: "Hand-write the Adapter's send function along one storyline in seven steps: send a message and get the reply, continue from earlier messages, record usage, parse tools into events, human-in-the-loop (HITL), hook up OTel traces, and pass through experiment flags. Each step adds only a few lines to the previous step's code, and each step unlocks a family of assertions."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
[Adapter](/concepts/adapter) covers the contract: `send` receives `TurnInput` and `AgentContext` and returns a `Turn`. This page covers how to write it — starting from the minimal "send one message, get the reply" form and building step by step to a complete integration. Each step adds only a few lines to the previous step's code; parts already shown are marked with `// …omitted` in the code, and each step ends by noting which assertions those lines unlock. Every step is immediately verifiable: write the newly unlocked assertions into an eval, rerun `npx niceeval exp`, and `niceeval view` shows the data this step added — multi-turn trajectories, usage, tool events, pending requests.
|
|
8
|
+
|
|
9
|
+
Two principles run through the whole page:
|
|
10
|
+
|
|
11
|
+
- **Always connect at the interface your user-facing frontend already uses.** Whatever endpoint the page calls and whatever format it receives, the Adapter calls that endpoint and receives that format — do not open a new endpoint for evals, and do not import app internals to call functions directly (for why, see [Connect your agent](/guides/connect-your-agent)).
|
|
12
|
+
- **The only thing you truly hand-write is transport** — URL, auth, and request body differ per app anyway. Parsing (raw response → standard event stream) has official converters, exported from `niceeval/adapter`; the state slots for orchestration (session continuation, HITL pause/resume) hang off `ctx.session`, ready to use, with nothing extra to declare.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## Starting point: what shape is your app's interface?
|
|
17
|
+
|
|
18
|
+
NiceEval does not invent protocols. Your app is almost certainly built on one of the industry protocols below (or a variant of one), and the official pieces cover exactly these shapes:
|
|
19
|
+
|
|
20
|
+
| Protocol | Who defines it | In one sentence | Reference |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| **Chat Completions** | OpenAI; the industry's de facto standard — nearly every model provider offers a compatible endpoint | Stateless question-and-answer: the client sends the full `messages` list every turn and one JSON comes back | [API reference](https://platform.openai.com/docs/api-reference/chat) |
|
|
23
|
+
| **Responses / Open Responses** | OpenAI (the Responses API); Open Responses is an open specification built on it, initiated by OpenAI and co-developed with Hugging Face and the community for multi-provider interoperability | Stateful and agent-oriented: requests carry `previous_response_id` so the server continues the history, and the `output` array promises a record of the full process | [API reference](https://platform.openai.com/docs/api-reference/responses) · [Open Responses spec](https://www.openresponses.org/specification) |
|
|
24
|
+
| **AI SDK (Vercel)** | Vercel; the de facto standard of the TypeScript ecosystem | Not a wire protocol but an SDK: `generateText` returns a complete result, and `useChat` speaks the UI Message Stream protocol it defines | [generateText reference](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text) · [UI Message Stream protocol](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol) |
|
|
25
|
+
|
|
26
|
+
The storyline below writes an app with a Chat Completions-shaped interface (the most common shape) from zero to complete; if your interface is Responses-shaped or streaming, the replacement pieces are given in steps two and four respectively.
|
|
27
|
+
|
|
28
|
+
## Step 1: send one message, get the reply
|
|
29
|
+
|
|
30
|
+
The minimal `send` does exactly three things: send `input.text` to the app's interface, put the reply into one `message` event, and report this turn's `status`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// agents/chat-app.ts
|
|
34
|
+
import { defineAgent } from "niceeval/adapter";
|
|
35
|
+
|
|
36
|
+
const BASE_URL = "http://localhost:8080"; // to switch the URL per experiment, make it a factory option: see the integration guide
|
|
37
|
+
|
|
38
|
+
export default defineAgent({
|
|
39
|
+
name: "chat-app",
|
|
40
|
+
async send(input, ctx) {
|
|
41
|
+
const res = await fetch(`${BASE_URL}/v1/chat/completions`, { // ← the interface the frontend already uses
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "content-type": "application/json" },
|
|
44
|
+
body: JSON.stringify({ messages: [{ role: "user", content: input.text }] }),
|
|
45
|
+
signal: ctx.signal, // ← runner timeout and cancellation
|
|
46
|
+
}).then((r) => r.json());
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
status: "completed",
|
|
50
|
+
events: [{ type: "message", role: "assistant", text: res.choices[0].message.content }],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**This step unlocks**: `t.reply`, `t.messageIncludes()`, and all the conversation material for judges.
|
|
57
|
+
|
|
58
|
+
It has two obvious limitations: every turn is a brand-new conversation (the second `t.send` cannot continue from the first), and tool calls are completely invisible. The next two steps solve one each.
|
|
59
|
+
|
|
60
|
+
## Step 2: continue from earlier messages
|
|
61
|
+
|
|
62
|
+
The runner promises exactly one thing about sessions: **every `send` on the same session line gets the same `ctx.session`; a new session line (the eval's first turn, or after `t.newSession()`) gets a brand-new one.**
|
|
63
|
+
|
|
64
|
+
How "earlier messages" get continued depends on the shape of the app's interface. The industry has exactly two patterns, and `ctx.session` provides a pair of accessors for each:
|
|
65
|
+
|
|
66
|
+
- **Client carries the full history** (stateless server; the complete message list is sent every turn: the Chat Completions shape is the typical case) → `ctx.session.history<TMsg>()`
|
|
67
|
+
- **Server keeps the history** (the interface takes a session id: the Responses shape's `previous_response_id`, the native sessions / threads of various SDKs) → `ctx.session.id` + `ctx.session.capture(id)`
|
|
68
|
+
|
|
69
|
+
The storyline's interface is the former:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// agents/chat-app.ts
|
|
73
|
+
import { defineAgent } from "niceeval/adapter";
|
|
74
|
+
|
|
75
|
+
const BASE_URL = "http://localhost:8080";
|
|
76
|
+
|
|
77
|
+
interface Msg { role: "user" | "assistant"; content: string | null }
|
|
78
|
+
|
|
79
|
+
export default defineAgent({
|
|
80
|
+
name: "chat-app",
|
|
81
|
+
async send(input, ctx) {
|
|
82
|
+
const history = ctx.session.history<Msg>(); // ← this session line's history slot; empty on a new session line
|
|
83
|
+
const messages = [...history.get(), { role: "user" as const, content: input.text }];
|
|
84
|
+
|
|
85
|
+
const res = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
86
|
+
// …method, headers, signal same as step one, omitted
|
|
87
|
+
body: JSON.stringify({ messages }), // ← the change: send the full history
|
|
88
|
+
}).then((r) => r.json());
|
|
89
|
+
|
|
90
|
+
const reply = res.choices[0].message;
|
|
91
|
+
history.commit([...messages, reply]); // ← write back; the next turn on the same line picks it up via get()
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
status: "completed",
|
|
95
|
+
events: [{ type: "message", role: "assistant", text: reply.content }],
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Notice there is **no "first turn" branch**: `history.get()` on a new session line naturally returns an empty array — the "first send" form is the natural consequence of a new session line, not a condition for you to check. Nothing needs declaring on `defineAgent` either: connect `ctx.session` and multi-turn continues; skip it and every turn is a new conversation.
|
|
102
|
+
|
|
103
|
+
If the app's interface takes a session id, the history lives on the server and the Adapter only records the id — just two changes inside `send`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// Inside send:
|
|
107
|
+
// the request carries the previous turn's id … previous_response_id: ctx.session.id ← naturally undefined on a new session line
|
|
108
|
+
// write back whatever id the response returns … ctx.session.capture(res.id) ← the next turn continues from it
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`capture` only lands when no id has been recorded yet; a backend re-sending the id (or even changing it due to a fork) will not overwrite the line being continued.
|
|
112
|
+
|
|
113
|
+
**This step unlocks**: multi-turn conversations, and `t.newSession()` session isolation.
|
|
114
|
+
|
|
115
|
+
## Step 3: record usage
|
|
116
|
+
|
|
117
|
+
An agent that answers correctly but burns ten times the tokens should not get the same score as one that is frugal. Usage is `usage`, the fourth field on `Turn` alongside `events` and `status`: if the app's interface returns usage, report it truthfully, and the runner accumulates it turn by turn into the session line and the whole run. Chat Completions-shaped responses come with `usage` — copy it over; the rest of `send` is exactly the same as step two:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// …transport and session same as step two, omitted
|
|
121
|
+
return {
|
|
122
|
+
status: "completed",
|
|
123
|
+
events: [{ type: "message", role: "assistant", text: reply.content }],
|
|
124
|
+
usage: { // ← the change: report the usage the interface returned, as-is
|
|
125
|
+
inputTokens: res.usage.prompt_tokens,
|
|
126
|
+
outputTokens: res.usage.completion_tokens,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The full `Usage` fields also include the optional `cacheReadTokens` / `cacheWriteTokens`, the request count `requests`, and `costUSD` — if a gateway returns measured cost, fill that in; it takes precedence over price-table estimation. If the interface returns no usage, leave `usage` out entirely; other assertions are unaffected. This hand-copying is also transitional: the official converter in the next step fills in `usage` along with everything else.
|
|
132
|
+
|
|
133
|
+
**This step unlocks**: the `t.maxTokens()` / `t.maxCost()` scorers (`maxCost` uses `costUSD` or the price table in the config), plus usage in reports and `niceeval view`.
|
|
134
|
+
|
|
135
|
+
## Step 4: parse tools into events
|
|
136
|
+
|
|
137
|
+
The app's response contains more than reply text — the `tool_calls` in a Chat Completions-shaped response record which tools this turn called. The Adapter's most important job is **normalizing the interface's response into the standard event stream**: one object per thing that happened this turn, ordered by actual occurrence in `Turn.events`, each object one of the nine types below (for the actual field values, [the contract page has a complete one-turn example](/concepts/adapter)):
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
type StreamEvent =
|
|
141
|
+
| { type: "message"; role: "assistant" | "user"; text: string }
|
|
142
|
+
| { type: "action.called"; callId: string; name: string; input: JsonValue; tool?: ToolName }
|
|
143
|
+
| { type: "action.result"; callId: string; output?: JsonValue;
|
|
144
|
+
status: "completed" | "failed" | "rejected" }
|
|
145
|
+
| { type: "subagent.called"; callId: string; name: string; remoteUrl?: string }
|
|
146
|
+
| { type: "subagent.completed"; callId: string; output?: JsonValue;
|
|
147
|
+
status: "completed" | "failed" }
|
|
148
|
+
| { type: "input.requested"; request: InputRequest }
|
|
149
|
+
| { type: "thinking"; text: string }
|
|
150
|
+
| { type: "compaction"; reason?: string }
|
|
151
|
+
| { type: "error"; message: string };
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Parsing is one "response field → event" mapping. Hand-written it looks like this — the rest of `send` is exactly the same as step two:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// …transport and session same as step two, usage same as step three, omitted
|
|
158
|
+
const reply = res.choices[0].message;
|
|
159
|
+
|
|
160
|
+
const events: StreamEvent[] = [];
|
|
161
|
+
for (const call of reply.tool_calls ?? []) {
|
|
162
|
+
events.push({ type: "action.called", callId: call.id,
|
|
163
|
+
name: call.function.name, input: JSON.parse(call.function.arguments) });
|
|
164
|
+
}
|
|
165
|
+
if (reply.content) events.push({ type: "message", role: "assistant", text: reply.content });
|
|
166
|
+
|
|
167
|
+
return { events, status: "completed", usage };
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
But you usually **do not have to write this loop**. When the response is a standard shape, one line of official converter replaces all of the above — `events`, `status`, even step three's hand-copied `usage` are all in the return value; just `return` it:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// …transport and session same as step two, omitted
|
|
174
|
+
return fromChatCompletion(res); // ← the mapping above plus step three's usage, official edition
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
If the interface is not this shape, swap in the matching piece:
|
|
178
|
+
|
|
179
|
+
| How the interface's data arrives | Official piece | What you write |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| Chat Completions-shaped response | `fromChatCompletion(res)` | Zero mapping |
|
|
182
|
+
| Responses-shaped response | `fromResponses(res)` | Zero mapping |
|
|
183
|
+
| AI SDK `generateText` complete result | `fromAiSdk(result)` | Zero mapping |
|
|
184
|
+
| Streaming: SDK-native events passed through (one complete unit per frame) | `fromClaudeSdkMessages()` / `fromPiAgentEvents()` / `fromCodexThreadEvents()` | Zero mapping |
|
|
185
|
+
| Streaming: the AI SDK's UI Message Stream (the `useChat` protocol) | `uiMessageStreamAgent()` | Zero mapping (the whole Agent is prebuilt — you do not even write send) |
|
|
186
|
+
| Streaming: token-by-token / argument-by-argument deltas | The protocol's official reducer; only if there is none, `deltaStream({ toOps })` | One "frame type → operation" mapping table |
|
|
187
|
+
| Final answer only | A few hand-written events | Start with one `message` (step one is this tier) |
|
|
188
|
+
|
|
189
|
+
The prebuilt pieces go as far as "shape", not "protocol" — interfaces arrive in only these few shapes, one official piece each; only "delta stream with no official reducer from the protocol side" requires you to write a mapping, and what you write is a declaration (which operation this frame corresponds to) — concatenation, pairing, and landing timing are handled uniformly by `deltaStream`.
|
|
190
|
+
|
|
191
|
+
Once normalized, whichever events you emit determine which assertion families eval authors can write:
|
|
192
|
+
|
|
193
|
+
| Events you emit | Assertions unlocked |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `message` | `t.reply`, `t.messageIncludes()`, judge conversation material |
|
|
196
|
+
| `action.called` / `action.result` (paired by `callId`) | `t.calledTool()` / `t.toolOrder()` / `t.maxToolCalls()` / `t.noFailedActions()`… |
|
|
197
|
+
| `input.requested` (together with status `"waiting"`) | `t.parked()`, `t.requireInputRequest()`, `t.respond()` (step five) |
|
|
198
|
+
| `thinking` / `compaction` / `error` | The matching assertions and o11y counts |
|
|
199
|
+
|
|
200
|
+
One honest disclosure: the Chat Completions shape **does not promise** that "response = full process record" (the app may well run the tool loop server-side and hand you only the final answer), so `fromChatCompletion`'s return carries no completeness proof — positive assertions like `calledTool` work as usual, while negative assertions like `notCalledTool` warn that they cannot be trusted. The Responses shape is the exact opposite: the protocol contract promises the `output` array records the full process, so `fromResponses`'s return carries a completeness proof and negative assertions are trustworthy. Same zero mapping — the difference in trustworthiness comes entirely from the interface shape itself.
|
|
201
|
+
|
|
202
|
+
**This step unlocks**: the whole family of tool assertions — `t.calledTool()`, `t.toolOrder()`, `t.maxToolCalls()`, `t.noFailedActions()`, and more.
|
|
203
|
+
|
|
204
|
+
## Step 5: HITL
|
|
205
|
+
|
|
206
|
+
When the app stops mid-turn to wait for a human (tool approval, missing information), `send` has obligations on both sides:
|
|
207
|
+
|
|
208
|
+
- **The pausing turn**: return `status: "waiting"`, and emit one `input.requested` event with a stable `id` per pending question — `t.parked()` and `t.requireInputRequest()` read them, and answers are matched by this `id`.
|
|
209
|
+
- **The answer turn**: `t.respond(...)` in the eval reaches the Adapter as **just another ordinary `send`** (still the same session line, the same state); the human verdict arrives in structured form via `input.responses`, one entry per answer carrying `requestId` and `optionId` or `text` (shapes in [the answer input shapes](/concepts/adapter#input-turninput)). The Adapter hands the verdict back to the app first, then continues fetching the result. For calls a human rejected, set the `action.result` `status` to `"rejected"` rather than `"failed"` — a rejection is a human decision, not a tool failure, so `noFailedActions()` does not misfire.
|
|
210
|
+
|
|
211
|
+
The "scene read halfway when the turn paused" (say, an SSE stream read halfway through) also lives on `ctx.session`: `ctx.session.hold(scene)` when pausing, `ctx.session.take()` at the start of the answer turn — taking clears it, single consumption.
|
|
212
|
+
|
|
213
|
+
HITL almost always happens on streaming interfaces (pausing mid-stream), so this step's example switches to an app that passes native events through over SSE — it also exercises everything from the earlier steps (complete runnable version in the [tier1 examples](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1)):
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// agents/my-app.ts
|
|
217
|
+
import { defineAgent, sseJsonFrames, fromPiAgentEvents, driveFrameStream } from "niceeval/adapter";
|
|
218
|
+
import type { AgentContext, PiAgentStream, SseFrameCursor } from "niceeval/adapter";
|
|
219
|
+
import type { Turn, TurnInput } from "niceeval";
|
|
220
|
+
|
|
221
|
+
const BASE_URL = "http://localhost:5299"; // the app's own port; evals do not manage the process
|
|
222
|
+
|
|
223
|
+
interface Pending { cursor: SseFrameCursor<Frame>; stream: PiAgentStream; callId: string }
|
|
224
|
+
|
|
225
|
+
function readStream(cursor: SseFrameCursor<Frame>, ctx: AgentContext, stream: PiAgentStream): Promise<Turn> {
|
|
226
|
+
return driveFrameStream(cursor, stream, ctx, (frame) => {
|
|
227
|
+
if (frame.type === "session") {
|
|
228
|
+
ctx.session.capture(frame.sessionId); // ← step two: write back the returned id; the next turn continues from it
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (frame.type === "approval_request") { // ← this step: the app stops for approval; hold the scene
|
|
232
|
+
ctx.session.hold<Pending>({ cursor, stream, callId: frame.toolCallId });
|
|
233
|
+
return { pause: { id: frame.toolCallId, action: frame.toolName,
|
|
234
|
+
options: [{ id: "approve" }, { id: "deny" }] } };
|
|
235
|
+
// driveFrameStream on pause: emits an input.requested, sets status to "waiting", stops reading the stream
|
|
236
|
+
}
|
|
237
|
+
if (frame.type === "server_error") return { fail: frame.message };
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default defineAgent({
|
|
242
|
+
name: "my-app",
|
|
243
|
+
async send(input: TurnInput, ctx: AgentContext): Promise<Turn> {
|
|
244
|
+
const held = ctx.session.take<Pending>();
|
|
245
|
+
if (held) { // ← t.respond("approve"): an ordinary send; deliver the verdict first
|
|
246
|
+
const approved = input.responses?.[0]?.optionId === "approve"; // with multiple pending requests, match by requestId
|
|
247
|
+
if (!approved) held.stream.markRejected(held.callId);
|
|
248
|
+
await fetch(`${BASE_URL}/api/chat/approve`, { method: "POST", signal: ctx.signal,
|
|
249
|
+
body: JSON.stringify({ toolUseId: held.callId, approved }) });
|
|
250
|
+
return readStream(held.cursor, ctx, held.stream); // then keep reading the same stream; no new request
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const res = await fetch(`${BASE_URL}/api/chat`, { // ← transport: the only part you truly hand-write
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
message: input.text, // ← step one: send the message
|
|
257
|
+
sessionId: ctx.session.id, // ← step two: absent on the first send, present automatically afterward
|
|
258
|
+
}),
|
|
259
|
+
signal: ctx.signal,
|
|
260
|
+
});
|
|
261
|
+
return readStream(sseJsonFrames<Frame>(res.body!), ctx, fromPiAgentEvents()); // ← step four: official converter, zero mapping
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
For interfaces without HITL, delete the three held-scene parts (`Pending`, `hold`, and the opening `take` branch) and keep the rest unchanged. The complete mental model of pause / answer / resume is in [HITL](/concepts/hitl).
|
|
267
|
+
|
|
268
|
+
**This step unlocks**: `t.parked()`, `t.requireInputRequest()`, `t.respond()` / `t.respondAll()`, and the precise assertion `calledTool(..., { status: "rejected" })`.
|
|
269
|
+
|
|
270
|
+
## Step 6: hook up OTel traces
|
|
271
|
+
|
|
272
|
+
If the app is already instrumented (standard OTel HTTP server instrumentation is enough), the integration comes in two halves — one is startup-time configuration, the other lives in `send`. Keeping them apart means separating "what never changes" from "what changes every turn".
|
|
273
|
+
|
|
274
|
+
**The endpoint is startup-time configuration; it is not passed from send.** NiceEval's OTLP receiver address is the same on every run, so it does not go through `ctx`: pin the receiver port in `niceeval.config.ts`, point the app's OTel exporter at that fixed URL on startup, and never touch it again no matter how many eval runs follow:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// niceeval.config.ts
|
|
278
|
+
import { defineConfig } from "niceeval";
|
|
279
|
+
|
|
280
|
+
export default defineConfig({
|
|
281
|
+
telemetry: { port: 4318 }, // the receiver always listens on http://localhost:4318/v1/traces
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces node server.js
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**What send passes is this turn's trace context, not the endpoint.** `ctx.telemetry.headers` is a W3C `traceparent` header the runner generates fresh every turn — spread it into the request, and the spans your app produces this turn attach precisely to this turn's trace, with no misattribution when multiple evals run concurrently. Back on the storyline's chat-app, `send` gains exactly one line:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const res = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
293
|
+
// …rest same as step four, omitted
|
|
294
|
+
headers: { "content-type": "application/json", ...ctx.telemetry?.headers }, // ← the only added line
|
|
295
|
+
}).then((r) => r.json());
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`ctx.telemetry` only exists when OTel integration is configured; spreading an `undefined` is safe when it is not, so the line can stay permanently. Spans still arrive without this header, but attribution degrades to time windows and that agent's turns fall back to serial execution — carrying it is what makes attribution accurate under concurrency.
|
|
299
|
+
|
|
300
|
+
**This step unlocks**: the call waterfall in `niceeval view` — every model call and every tool execution inside the app, with duration and tokens, laid out as a per-turn timeline. Assertions are unchanged: the verdict evidence was already complete in the event mapping of the earlier steps; spans go only into the waterfall and never feed assertions. For how the receiver starts and how spans attribute to turns, see [OTel integration](/guides/connect-otel).
|
|
301
|
+
|
|
302
|
+
## Step 7: pass through the experiment's flags (A/B comparison)
|
|
303
|
+
|
|
304
|
+
Once the app exposes variants as switchable configuration, the experiment declares `flags`, and the runner hands them verbatim to `send` via `ctx.flags` every turn; the Adapter does not interpret their meaning, it only forwards them with the request — the app switches variants on the flag:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// …rest of send same as before, omitted
|
|
308
|
+
body: JSON.stringify({
|
|
309
|
+
messages,
|
|
310
|
+
flags: ctx.flags, // ← experiment.flags passed through as-is; {} when not configured
|
|
311
|
+
}),
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
// experiments/concise.ts
|
|
316
|
+
import { defineExperiment } from "niceeval";
|
|
317
|
+
import chatApp from "../agents/chat-app.ts";
|
|
318
|
+
|
|
319
|
+
export default defineExperiment({
|
|
320
|
+
agent: chatApp,
|
|
321
|
+
flags: { promptVariant: "concise" }, // ← reaches send via ctx.flags every turn
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Two experiment files, each declaring its own `flags`, running the same set of evals via `npx niceeval exp` — that is an A/B comparison. This is Tier 3 of the three integration tiers (it requires the app to cooperate by exposing switches); cost and payoff in [Tier](/concepts/tier); `flags` alongside `model`, `runs`, and the other experiment fields in [Write experiments](/guides/write-experiment).
|
|
326
|
+
|
|
327
|
+
**This step unlocks**: score comparison across variants over the same set of evals.
|
|
328
|
+
|
|
329
|
+
## Reference: the five t APIs as send sees them
|
|
330
|
+
|
|
331
|
+
With all seven steps written, look back at the five driving APIs on the eval side — by the time they reach `send` they are just the same function receiving different fields; there is no second method to implement:
|
|
332
|
+
|
|
333
|
+
| Eval side | Reaching the Adapter | What `send` receives |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| `t.send(text)` | One `send` | `input.text`; `ctx.session` is this session line's own state |
|
|
336
|
+
| `t.sendFile(path, text?)` | One `send` | Same as above, plus `input.files` (base64 `InputFile[]`); put them into the request however the app's interface expects, or ignore them if it is not multimodal |
|
|
337
|
+
| `t.newSession()` | Does not trigger a `send` by itself | Opens a second session line; that line's next first `send` gets **completely empty** state — exactly the same form as the eval's very first `send` |
|
|
338
|
+
| `t.respond(...answers)` | One **ordinary** `send` | `input.text` = the answer text; `input.responses` has one `{ requestId, optionId }` per request (`{ requestId, text }` for free-text answers); still the same line, the same state |
|
|
339
|
+
| `t.respondAll(optionId)` | One **ordinary** `send` | Same as above, with one answer per pending request; `optionId` is validated eval-side against every request — a typo throws instead of silently reaching the app |
|
|
340
|
+
|
|
341
|
+
## Related reading
|
|
342
|
+
|
|
343
|
+
- [Adapter](/concepts/adapter) — the contract itself: what `send` receives and returns, the three integration tiers, where capabilities come from.
|
|
344
|
+
- [Connect your agent](/guides/connect-your-agent) — the integration picture: minimal integration, parameter channels, and the increment map.
|
|
345
|
+
- [HITL](/concepts/hitl) — the complete concept of pausing for a human: handshake timing and both sides' obligations.
|
|
346
|
+
- [Drive](/concepts/drive) — the eval-side view: how to use `t.send()`, `t.newSession()`, and HITL.
|
|
347
|
+
- [Assert](/concepts/assert) — the complete assertion vocabulary driven by the standard event stream.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1240 380" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">接入分三个 Tier</title>
|
|
3
|
+
<desc id="desc">Tier 1 只接 send,应用零改动;Tier 2 加 OTel,事件免映射并带完整性证明;Tier 3 侵入改造,把变体暴露成 experiment flags 做 A/B。三档递进不互斥。</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<marker id="tierHead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
6
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#8ab4f8" />
|
|
7
|
+
</marker>
|
|
8
|
+
<linearGradient id="card" x1="0" x2="1" y1="0" y2="1">
|
|
9
|
+
<stop offset="0" stop-color="#0d1117" />
|
|
10
|
+
<stop offset="1" stop-color="#111820" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<style>
|
|
13
|
+
.tierTitle { fill: #e6edf3; font: 600 24px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
14
|
+
.give { fill: #f0b429; font: 600 18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
15
|
+
.get { fill: #7ee787; font: 600 18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
16
|
+
.line { fill: #c9d1d9; font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
17
|
+
.muted { fill: #8b949e; font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
18
|
+
.box { fill: #161b22; stroke: #30363d; stroke-width: 1.5; }
|
|
19
|
+
.arrow { stroke: #8ab4f8; stroke-width: 3; fill: none; marker-end: url(#tierHead); }
|
|
20
|
+
</style>
|
|
21
|
+
</defs>
|
|
22
|
+
|
|
23
|
+
<rect x="1" y="1" width="1238" height="378" rx="24" fill="url(#card)" stroke="#30363d" stroke-width="2" />
|
|
24
|
+
|
|
25
|
+
<!-- Tier 1 -->
|
|
26
|
+
<rect x="40" y="44" width="352" height="252" rx="16" class="box" />
|
|
27
|
+
<text x="64" y="86" class="tierTitle">Tier 1 · 只接 send</text>
|
|
28
|
+
<text x="64" y="126" class="give">投入</text>
|
|
29
|
+
<text x="64" y="152" class="line">一个 Adapter 文件</text>
|
|
30
|
+
<text x="64" y="178" class="line">应用代码一行不改</text>
|
|
31
|
+
<text x="64" y="218" class="get">买到</text>
|
|
32
|
+
<text x="64" y="244" class="line">文本断言、judge、多轮、HITL</text>
|
|
33
|
+
<text x="64" y="270" class="line">手写映射的工具断言、模型对比</text>
|
|
34
|
+
|
|
35
|
+
<path d="M 402 170 H 434" class="arrow" />
|
|
36
|
+
|
|
37
|
+
<!-- Tier 2 -->
|
|
38
|
+
<rect x="444" y="44" width="352" height="252" rx="16" class="box" />
|
|
39
|
+
<text x="468" y="86" class="tierTitle">Tier 2 · send + OTel</text>
|
|
40
|
+
<text x="468" y="126" class="give">投入</text>
|
|
41
|
+
<text x="468" y="152" class="line">应用把 OTel span 发给 NiceEval</text>
|
|
42
|
+
<text x="468" y="178" class="line">已埋点的应用零改动</text>
|
|
43
|
+
<text x="468" y="218" class="get">买到</text>
|
|
44
|
+
<text x="468" y="244" class="line">niceeval view 的调用瀑布图</text>
|
|
45
|
+
<text x="468" y="270" class="line">每次模型 / 工具调用的耗时与 token</text>
|
|
46
|
+
|
|
47
|
+
<path d="M 806 170 H 838" class="arrow" />
|
|
48
|
+
|
|
49
|
+
<!-- Tier 3 -->
|
|
50
|
+
<rect x="848" y="44" width="352" height="252" rx="16" class="box" />
|
|
51
|
+
<text x="872" y="86" class="tierTitle">Tier 3 · 侵入改造</text>
|
|
52
|
+
<text x="872" y="126" class="give">投入</text>
|
|
53
|
+
<text x="872" y="152" class="line">应用内部把变体暴露成</text>
|
|
54
|
+
<text x="872" y="178" class="line">experiment 可传的 flags</text>
|
|
55
|
+
<text x="872" y="218" class="get">买到</text>
|
|
56
|
+
<text x="872" y="244" class="line">prompt / 工具集 / feature</text>
|
|
57
|
+
<text x="872" y="270" class="line">的 A/B 对比</text>
|
|
58
|
+
|
|
59
|
+
<text x="620" y="340" class="muted" text-anchor="middle">前两档无侵入:只对着前端本来就在用的接口收发。三档递进不互斥,已写的 eval 全程复用。</text>
|
|
60
|
+
</svg>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1160 320" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">One complete t.send round trip</title>
|
|
3
|
+
<desc id="desc">An eval calls t.send, the niceeval runner builds TurnInput and ctx, the adapter calls the application interface, and maps the raw response into a standard event-stream Turn.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
6
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#8ab4f8" />
|
|
7
|
+
</marker>
|
|
8
|
+
<linearGradient id="card" x1="0" x2="1" y1="0" y2="1">
|
|
9
|
+
<stop offset="0" stop-color="#0d1117" />
|
|
10
|
+
<stop offset="1" stop-color="#111820" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<style>
|
|
13
|
+
.label { fill: #e6edf3; font: 600 24px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
14
|
+
.small { fill: #c9d1d9; font: 22px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
15
|
+
.muted { fill: #8b949e; font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
16
|
+
.pill { fill: #1f2937; stroke: #374151; stroke-width: 1; }
|
|
17
|
+
.pillText { fill: #e5e7eb; font: 20px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
18
|
+
.arrow { stroke: #8ab4f8; stroke-width: 3; fill: none; marker-end: url(#arrow); }
|
|
19
|
+
.returnArrow { stroke: #a78bfa; stroke-width: 3; fill: none; marker-end: url(#arrow); }
|
|
20
|
+
.lane { stroke: #30363d; stroke-width: 1; stroke-dasharray: 5 8; }
|
|
21
|
+
</style>
|
|
22
|
+
</defs>
|
|
23
|
+
|
|
24
|
+
<rect x="1" y="1" width="1158" height="318" rx="24" fill="url(#card)" stroke="#30363d" stroke-width="2" />
|
|
25
|
+
|
|
26
|
+
<text x="54" y="54" class="label">evals/*.eval.ts</text>
|
|
27
|
+
<text x="410" y="54" class="label">niceeval runner</text>
|
|
28
|
+
<text x="775" y="54" class="label">Adapter (you write)</text>
|
|
29
|
+
<text x="1060" y="42" class="muted">your app</text>
|
|
30
|
+
<text x="1060" y="66" class="muted">unchanged</text>
|
|
31
|
+
|
|
32
|
+
<line x1="342" y1="84" x2="342" y2="268" class="lane" />
|
|
33
|
+
<line x1="735" y1="84" x2="735" y2="268" class="lane" />
|
|
34
|
+
<line x1="965" y1="84" x2="965" y2="268" class="lane" />
|
|
35
|
+
|
|
36
|
+
<text x="54" y="126" class="small">await t.send("...")</text>
|
|
37
|
+
<path d="M 278 119 H 397" class="arrow" />
|
|
38
|
+
<text x="410" y="126" class="small">build TurnInput + ctx</text>
|
|
39
|
+
<path d="M 692 119 H 778" class="arrow" />
|
|
40
|
+
<text x="775" y="126" class="small">send(input, ctx)</text>
|
|
41
|
+
<path d="M 1008 119 H 1100" class="arrow" />
|
|
42
|
+
|
|
43
|
+
<text x="54" y="182" class="small">t.reply</text>
|
|
44
|
+
<path d="M 397 175 H 278" class="returnArrow" />
|
|
45
|
+
<text x="410" y="182" class="small">derive facts</text>
|
|
46
|
+
<path d="M 778 175 H 692" class="returnArrow" />
|
|
47
|
+
<text x="775" y="182" class="small">map to events</text>
|
|
48
|
+
<text x="1012" y="203" class="muted">response / SSE</text>
|
|
49
|
+
<path d="M 1100 175 H 1008" class="returnArrow" />
|
|
50
|
+
|
|
51
|
+
<text x="54" y="238" class="small">t.calledTool()...</text>
|
|
52
|
+
<text x="410" y="238" class="small">update reply and usage</text>
|
|
53
|
+
<text x="775" y="238" class="small">return Turn</text>
|
|
54
|
+
|
|
55
|
+
<rect x="49" y="278" width="118" height="29" rx="8" class="pill" />
|
|
56
|
+
<text x="63" y="299" class="pillText">t.send</text>
|
|
57
|
+
<rect x="175" y="278" width="136" height="29" rx="8" class="pill" />
|
|
58
|
+
<text x="189" y="299" class="pillText">t.sendFile</text>
|
|
59
|
+
<rect x="319" y="278" width="130" height="29" rx="8" class="pill" />
|
|
60
|
+
<text x="333" y="299" class="pillText">t.respond</text>
|
|
61
|
+
<text x="468" y="299" class="muted">share one runner event path; the adapter implements send once.</text>
|
|
62
|
+
</svg>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1240 340" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">一次 t.send 的完整往返</title>
|
|
3
|
+
<desc id="desc">eval 调用 t.send,NiceEval 组装 TurnInput 和 ctx,adapter 调用应用接口,并把原始返回翻译成标准事件流 Turn。</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<marker id="rtHeadBlue" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
6
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#8ab4f8" />
|
|
7
|
+
</marker>
|
|
8
|
+
<marker id="rtHeadPurple" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
9
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#a78bfa" />
|
|
10
|
+
</marker>
|
|
11
|
+
<linearGradient id="card" x1="0" x2="1" y1="0" y2="1">
|
|
12
|
+
<stop offset="0" stop-color="#0d1117" />
|
|
13
|
+
<stop offset="1" stop-color="#111820" />
|
|
14
|
+
</linearGradient>
|
|
15
|
+
<style>
|
|
16
|
+
.label { fill: #e6edf3; font: 600 24px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
17
|
+
.small { fill: #c9d1d9; font: 22px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
18
|
+
.muted { fill: #8b949e; font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
19
|
+
.pill { fill: #1f2937; stroke: #374151; stroke-width: 1; }
|
|
20
|
+
.pillText { fill: #e5e7eb; font: 20px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
21
|
+
.box { fill: #161b22; stroke: #30363d; stroke-width: 1.5; }
|
|
22
|
+
.arrow { stroke: #8ab4f8; stroke-width: 3; fill: none; marker-end: url(#rtHeadBlue); }
|
|
23
|
+
.returnArrow { stroke: #a78bfa; stroke-width: 3; fill: none; marker-end: url(#rtHeadPurple); }
|
|
24
|
+
.lane { stroke: #30363d; stroke-width: 1; stroke-dasharray: 5 8; }
|
|
25
|
+
</style>
|
|
26
|
+
</defs>
|
|
27
|
+
|
|
28
|
+
<rect x="1" y="1" width="1238" height="338" rx="24" fill="url(#card)" stroke="#30363d" stroke-width="2" />
|
|
29
|
+
|
|
30
|
+
<text x="54" y="54" class="label">evals/*.eval.ts</text>
|
|
31
|
+
<text x="380" y="54" class="label">NiceEval</text>
|
|
32
|
+
<text x="730" y="54" class="label">Adapter(你写)</text>
|
|
33
|
+
|
|
34
|
+
<!-- 图例:去程 / 回程 -->
|
|
35
|
+
<path d="M 990 46 H 1020" class="arrow" />
|
|
36
|
+
<text x="1030" y="53" class="muted">去程</text>
|
|
37
|
+
<path d="M 1128 46 H 1098" class="returnArrow" />
|
|
38
|
+
<text x="1138" y="53" class="muted">回程</text>
|
|
39
|
+
|
|
40
|
+
<line x1="350" y1="84" x2="350" y2="268" class="lane" />
|
|
41
|
+
<line x1="700" y1="84" x2="700" y2="268" class="lane" />
|
|
42
|
+
|
|
43
|
+
<!-- 你的应用:箭头的真实落点 -->
|
|
44
|
+
<rect x="1030" y="90" width="170" height="178" rx="14" class="box" />
|
|
45
|
+
<text x="1115" y="130" class="label" text-anchor="middle">你的应用</text>
|
|
46
|
+
<text x="1115" y="164" class="muted" text-anchor="middle">前端在用的接口</text>
|
|
47
|
+
<text x="1115" y="192" class="muted" text-anchor="middle">响应 / SSE 流</text>
|
|
48
|
+
<text x="1115" y="238" class="muted" text-anchor="middle">一行不改</text>
|
|
49
|
+
|
|
50
|
+
<!-- 去程 -->
|
|
51
|
+
<text x="54" y="126" class="small">await t.send("...")</text>
|
|
52
|
+
<path d="M 312 119 H 372" class="arrow" />
|
|
53
|
+
<text x="380" y="126" class="small">组装 TurnInput + ctx</text>
|
|
54
|
+
<path d="M 650 119 H 722" class="arrow" />
|
|
55
|
+
<text x="730" y="126" class="small">send(input, ctx)</text>
|
|
56
|
+
<path d="M 950 119 H 1024" class="arrow" />
|
|
57
|
+
|
|
58
|
+
<!-- 回程 -->
|
|
59
|
+
<text x="54" y="182" class="small">t.reply</text>
|
|
60
|
+
<path d="M 372 175 H 312" class="returnArrow" />
|
|
61
|
+
<text x="380" y="182" class="small">折叠 events 成事实</text>
|
|
62
|
+
<path d="M 722 175 H 650" class="returnArrow" />
|
|
63
|
+
<text x="730" y="182" class="small">翻译成 events</text>
|
|
64
|
+
<path d="M 1024 175 H 950" class="returnArrow" />
|
|
65
|
+
|
|
66
|
+
<text x="54" y="238" class="small">t.calledTool()...</text>
|
|
67
|
+
<text x="380" y="238" class="small">更新 t.reply 与用量</text>
|
|
68
|
+
<text x="730" y="238" class="small">return Turn</text>
|
|
69
|
+
|
|
70
|
+
<rect x="49" y="284" width="118" height="29" rx="8" class="pill" />
|
|
71
|
+
<text x="63" y="305" class="pillText">t.send</text>
|
|
72
|
+
<rect x="175" y="284" width="136" height="29" rx="8" class="pill" />
|
|
73
|
+
<text x="189" y="305" class="pillText">t.sendFile</text>
|
|
74
|
+
<rect x="319" y="284" width="130" height="29" rx="8" class="pill" />
|
|
75
|
+
<text x="333" y="305" class="pillText">t.respond</text>
|
|
76
|
+
<text x="468" y="305" class="muted">都是运行器侧的统一事件入口;adapter 只实现 send。</text>
|
|
77
|
+
</svg>
|