@anvia/cli 1.1.1 → 1.3.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 +48 -0
- package/dist/chunk-ERCP4EIZ.js +607 -0
- package/dist/chunk-ERCP4EIZ.js.map +1 -0
- package/dist/cli.js +287 -33
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +81 -1
- package/dist/index.js +27 -3
- package/dist/skills/anvia-agent/SKILL.md +59 -0
- package/dist/skills/anvia-agent/references/agent-options.md +83 -0
- package/dist/skills/anvia-agent/references/providers.md +22 -0
- package/dist/skills/anvia-agent/references/teams.md +84 -0
- package/dist/skills/anvia-agent/references/tools.md +71 -0
- package/dist/skills/anvia-agent/scripts/check-agent.sh +99 -0
- package/dist/skills/anvia-channels/SKILL.md +59 -0
- package/dist/skills/anvia-channels/references/adapters.md +53 -0
- package/dist/skills/anvia-channels/references/channel-agent.md +52 -0
- package/dist/skills/anvia-channels/references/delivery.md +53 -0
- package/dist/skills/anvia-channels/scripts/check-channels.sh +79 -0
- package/dist/skills/anvia-chat/SKILL.md +61 -0
- package/dist/skills/anvia-chat/references/react-ui.md +104 -0
- package/dist/skills/anvia-chat/references/server-protocol.md +53 -0
- package/dist/skills/anvia-chat/references/transports-state.md +71 -0
- package/dist/skills/anvia-chat/scripts/check-chat-boundary.sh +70 -0
- package/dist/skills/anvia-evals/SKILL.md +51 -0
- package/dist/skills/anvia-evals/references/judges.md +61 -0
- package/dist/skills/anvia-evals/references/metrics.md +45 -0
- package/dist/skills/anvia-evals/references/running.md +62 -0
- package/dist/skills/anvia-evals/scripts/check-evals.sh +73 -0
- package/dist/skills/anvia-mcp/SKILL.md +49 -0
- package/dist/skills/anvia-mcp/references/clients.md +73 -0
- package/dist/skills/anvia-mcp/references/safety.md +25 -0
- package/dist/skills/anvia-mcp/scripts/check-mcp.sh +73 -0
- package/dist/skills/anvia-pipeline/SKILL.md +52 -0
- package/dist/skills/anvia-pipeline/references/agents-extract.md +62 -0
- package/dist/skills/anvia-pipeline/references/steps-compose.md +63 -0
- package/dist/skills/anvia-pipeline/scripts/check-pipeline.sh +69 -0
- package/dist/skills/anvia-rag/SKILL.md +50 -0
- package/dist/skills/anvia-rag/references/graph-rag.md +115 -0
- package/dist/skills/anvia-rag/references/pipeline.md +81 -0
- package/dist/skills/anvia-rag/references/rag-tool.md +43 -0
- package/dist/skills/anvia-rag/references/stores.md +68 -0
- package/dist/skills/anvia-rag/scripts/check-rag.sh +75 -0
- package/dist/skills/anvia-studio/SKILL.md +45 -0
- package/dist/skills/anvia-studio/references/inspect.md +33 -0
- package/dist/skills/anvia-studio/references/observe.md +34 -0
- package/dist/skills/anvia-studio/references/serve.md +65 -0
- package/dist/skills/anvia-studio/scripts/check-studio.sh +59 -0
- package/dist/skills/release-notes/SKILL.md +18 -0
- package/dist/skills/release-notes/references/style.md +6 -0
- package/dist/skills/release-notes/scripts/draft.sh +22 -0
- package/package.json +3 -3
- package/dist/chunk-TE2ODJOV.js +0 -135
- package/dist/chunk-TE2ODJOV.js.map +0 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Heuristic checks for Anvia channels code in an app directory.
|
|
3
|
+
# Usage: sh scripts/check-channels.sh [--dir <app-root>]
|
|
4
|
+
# Fails with a list of violations; passes silently with "channels OK".
|
|
5
|
+
|
|
6
|
+
DIR="."
|
|
7
|
+
if [ "$1" = "--dir" ] && [ -n "$2" ]; then
|
|
8
|
+
DIR="$2"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
fail=0
|
|
12
|
+
violation() {
|
|
13
|
+
echo "VIOLATION: $1"
|
|
14
|
+
fail=1
|
|
15
|
+
}
|
|
16
|
+
warning() {
|
|
17
|
+
echo "WARNING: $1"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ROOTS_FOUND=0
|
|
21
|
+
for root in "$DIR/src" "$DIR/app" "$DIR/lib" "$DIR/server"; do
|
|
22
|
+
if [ -d "$root" ]; then
|
|
23
|
+
ROOTS_FOUND=1
|
|
24
|
+
break
|
|
25
|
+
fi
|
|
26
|
+
done
|
|
27
|
+
if [ "$ROOTS_FOUND" -eq 0 ]; then
|
|
28
|
+
echo "ERROR: no src/app/lib/server directory under '$DIR' — nothing was checked."
|
|
29
|
+
echo "Run from the app root or pass --dir <app-root>."
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
scan() {
|
|
34
|
+
# $1 = include pattern; prints matching files under the usual roots.
|
|
35
|
+
grep -rln --include="$1" \
|
|
36
|
+
-e 'createChannelAgent(' -e 'serveChannelAgent(' -e 'sendChannelMessage(' \
|
|
37
|
+
-e 'ChannelAgentService' -e 'ChannelEventHandler' -e 'ChannelInteractionStore' \
|
|
38
|
+
-e 'telegram(' -e 'discord(' -e 'slack(' \
|
|
39
|
+
"$DIR/src" "$DIR/app" "$DIR/lib" "$DIR/server" 2>/dev/null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
FILES=$( { scan '*.ts'; scan '*.tsx'; } | sort -u)
|
|
43
|
+
|
|
44
|
+
if [ -z "$FILES" ]; then
|
|
45
|
+
echo "channels OK (no channel code found)"
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# 1. Platform credentials come from the environment, never literals.
|
|
50
|
+
creds=$(echo "$FILES" | xargs grep -l -E '(appToken|botToken|token): *"' 2>/dev/null)
|
|
51
|
+
if [ -n "$creds" ]; then
|
|
52
|
+
violation "hardcoded channel credential in $(echo "$creds" | tr '\n' ' ') — read tokens from the environment (see references/adapters.md)."
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# 2. Unbounded text must go through sendChannelMessage (it splits), not channel.send.
|
|
56
|
+
if echo "$FILES" | xargs grep -E '\.send\(\{[^}]*\$\{' 2>/dev/null | grep -q .; then
|
|
57
|
+
violation "dynamic text through channel.send() can be truncated — use sendChannelMessage() so long text splits (see references/delivery.md)."
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# 3. The bridge swallows errors unless onError reports them (warning only).
|
|
61
|
+
if echo "$FILES" | xargs grep -l 'createChannelAgent(' 2>/dev/null | grep -q .; then
|
|
62
|
+
echo "$FILES" | xargs grep -l 'onError:' 2>/dev/null | grep -q . || {
|
|
63
|
+
warning "createChannelAgent without onError anywhere — stage failures will be silent (see references/channel-agent.md)."
|
|
64
|
+
}
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# 4. A started service needs a visible shutdown path (warning only).
|
|
68
|
+
if echo "$FILES" | xargs grep -q -E '(service|channelAgent|agent)\.start\(\)' 2>/dev/null; then
|
|
69
|
+
echo "$FILES" | xargs grep -q -E 'stop\(\)|SIGINT|SIGTERM|process\.once|process\.on' 2>/dev/null || {
|
|
70
|
+
warning "service.start() without a visible stop()/signal handler — add a shutdown path (see references/channel-agent.md)."
|
|
71
|
+
}
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
if [ "$fail" -eq 0 ]; then
|
|
75
|
+
echo "channels OK"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
echo "See skills/anvia-channels/references/ for fixes."
|
|
79
|
+
exit 1
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anvia-chat
|
|
3
|
+
description: Wire an Anvia chat UI end to end — server stream route, client transport, React hook, and UI primitives.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Anvia Chat Skill
|
|
7
|
+
|
|
8
|
+
Use this skill when the user wants a chat or completion UI backed by Anvia:
|
|
9
|
+
new chat routes, transports, `useChat`/`useCompletion` wiring, message rendering,
|
|
10
|
+
resumable streams, agent tool-approval interactions, or `anvia add chat` work.
|
|
11
|
+
|
|
12
|
+
## Process
|
|
13
|
+
|
|
14
|
+
1. Build the server route first (`references/server-protocol.md`).
|
|
15
|
+
2. Pick the transport and state conversions (`references/transports-state.md`).
|
|
16
|
+
3. Wire the React controller and render UI primitives (`references/react-ui.md`).
|
|
17
|
+
4. Run `scripts/check-chat-boundary.sh` from the app root before claiming done.
|
|
18
|
+
|
|
19
|
+
## Minimal slice
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// Server route: core events -> client protocol -> framed response.
|
|
23
|
+
import { completionToClientStream, parseClientStreamRequest } from "@anvia/client";
|
|
24
|
+
import { streamCompletion } from "@anvia/core";
|
|
25
|
+
import { createClientStreamResponse } from "@anvia/server";
|
|
26
|
+
|
|
27
|
+
const body = parseClientStreamRequest(await request.json());
|
|
28
|
+
if (body.type !== "messages") throw new Error("This completion endpoint accepts messages only.");
|
|
29
|
+
const events = completionToClientStream({
|
|
30
|
+
events: streamCompletion({ model, messages: body.messages }),
|
|
31
|
+
});
|
|
32
|
+
return createClientStreamResponse({ events }); // JSONL by default
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// App: explicit transport, hook owns state, primitives render it.
|
|
37
|
+
import { createHttpClientTransport } from "@anvia/client";
|
|
38
|
+
import { useChat } from "@anvia/react";
|
|
39
|
+
import { ChatProvider, ComposerPrimitive, ThreadPrimitive } from "@anvia/react-ui";
|
|
40
|
+
|
|
41
|
+
const transport = createHttpClientTransport({ endpoint: "/api/chat" });
|
|
42
|
+
const chat = useChat({ transport });
|
|
43
|
+
|
|
44
|
+
<ChatProvider controller={chat}>
|
|
45
|
+
<ThreadPrimitive.Root>
|
|
46
|
+
<ThreadPrimitive.Viewport>
|
|
47
|
+
<ThreadPrimitive.Messages />
|
|
48
|
+
</ThreadPrimitive.Viewport>
|
|
49
|
+
<ComposerPrimitive.Root>
|
|
50
|
+
<ComposerPrimitive.Input placeholder="Send a message..." />
|
|
51
|
+
<ComposerPrimitive.Submit>Send</ComposerPrimitive.Submit>
|
|
52
|
+
</ComposerPrimitive.Root>
|
|
53
|
+
</ThreadPrimitive.Root>
|
|
54
|
+
</ChatProvider>;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Output
|
|
58
|
+
|
|
59
|
+
Keep the answer to the smallest working vertical slice: one route, one transport,
|
|
60
|
+
one hook, one render tree. Point to the relevant reference file instead of
|
|
61
|
+
pasting its contents into chat.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# React and UI
|
|
2
|
+
|
|
3
|
+
## Controllers (`@anvia/react`)
|
|
4
|
+
|
|
5
|
+
`useChat` and `useCompletion` require an explicit transport boundary:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
useChat({ transport });
|
|
9
|
+
useChat({
|
|
10
|
+
transport: createDirectClientTransport({
|
|
11
|
+
handler: ({ request, abortSignal }) => handleChat({ request, abortSignal }),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`useChat`:
|
|
17
|
+
|
|
18
|
+
- Consumes only framed `ClientStreamFrame` values.
|
|
19
|
+
- Status: `ready | submitted | streaming | waiting | error`.
|
|
20
|
+
- Keeps readonly `UIMessage[]` locally; sends core `Message[]` in
|
|
21
|
+
`ClientStreamRequest`.
|
|
22
|
+
- Exposes canonical events via `onEvent` and the returned `events` array.
|
|
23
|
+
- Exposes aggregate latest-run usage via `runUsage`; each assistant message
|
|
24
|
+
keeps its own provider-generation usage.
|
|
25
|
+
- Actions: `sendMessage` (accepts attachments and metadata), `regenerate`,
|
|
26
|
+
`stop`, `reset`, `setMessages`, `resume()`. State also exposes `suggestions`,
|
|
27
|
+
`contextUsage`, `text`, and `streamId`. Submit through `sendMessage` rather
|
|
28
|
+
than mutating `messages`.
|
|
29
|
+
- Supports resumable streams (`resume: { key }`, paired with the server's
|
|
30
|
+
resumable response) and unified Agent interaction state.
|
|
31
|
+
|
|
32
|
+
When `chat.status === "waiting"`, render `chat.interactions.pending` and resume
|
|
33
|
+
through the same transport boundary:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
await chat.respondToInteraction({
|
|
37
|
+
interactionId,
|
|
38
|
+
response: { type: "tool-approval", approved: true },
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Render those prompts with `HumanInputPrimitive`, or drive them headlessly with
|
|
43
|
+
`useApproval` / `useQuestion` — both from `@anvia/react-ui`.
|
|
44
|
+
|
|
45
|
+
`useCompletion({ transport })` is single-turn: `complete({ prompt })` or manage
|
|
46
|
+
`input` and call `submit()`. Each call replaces the previous completion, events,
|
|
47
|
+
and usage — it never accumulates messages. The UI counterpart is
|
|
48
|
+
`CompletionProvider` + `CompletionPrimitive`.
|
|
49
|
+
|
|
50
|
+
`useSmoothStreamText` / `useSmoothStreamItems` only smooth presentation. They do
|
|
51
|
+
not change protocol events or message state.
|
|
52
|
+
|
|
53
|
+
## Headless primitives (`@anvia/react-ui`)
|
|
54
|
+
|
|
55
|
+
Primitives are headless: no stylesheet, style via `className` or `asChild`.
|
|
56
|
+
The DOM contract is ARIA attributes plus `data-state` / `data-role`.
|
|
57
|
+
|
|
58
|
+
- Keep `useChat` as the owner of transport and `UIMessage[]` state; wrap it with
|
|
59
|
+
`ChatProvider` and render `ThreadPrimitive` / `MessagePrimitive` /
|
|
60
|
+
`ComposerPrimitive`.
|
|
61
|
+
- Control `ComposerPrimitive.Root` with `input` / `attachments` props when
|
|
62
|
+
needed; use `submitMessage` for custom payloads. `keepMounted` keeps empty
|
|
63
|
+
wrappers for layout.
|
|
64
|
+
- `ComposerPrimitive.Input` is Tiptap-backed. Configure `Root` with `triggers`
|
|
65
|
+
for inline `@` / `/` / `$` entity chips; selections submit under
|
|
66
|
+
`metadata.composer.entities`. Entity `data` must be finite strict JSON (no
|
|
67
|
+
class instances, cycles, accessors, symbols, `undefined`, non-finite numbers).
|
|
68
|
+
Use `ComposerPrimitive.TextareaInput` for plain textarea behavior.
|
|
69
|
+
- Streaming reveal is opt-in and display-only. Keep the lifecycle mounted after
|
|
70
|
+
streaming stops so the buffered tail drains; `MessagePrimitive.Parts` holds
|
|
71
|
+
later tool parts behind unrevealed text:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<MessagePrimitive.Parts
|
|
75
|
+
stream={{
|
|
76
|
+
isStreaming:
|
|
77
|
+
chat.status === "streaming" &&
|
|
78
|
+
message.role === "assistant" &&
|
|
79
|
+
chat.messages.at(-1)?.id === message.id,
|
|
80
|
+
resetKey: message.id,
|
|
81
|
+
flushImmediately: chat.status === "error",
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{(part) => (part.type === "text" ? <MessagePrimitive.Markdown /> : <MessagePrimitive.Part />)}
|
|
85
|
+
</MessagePrimitive.Parts>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- For app-owned text state, `StreamMarkdown` from `@anvia/react-ui/stream` is a
|
|
89
|
+
context-free renderer: pass displayed text as `content`, set `live` only for
|
|
90
|
+
the growing tail, and style `[data-state="revealing"]` in the app.
|
|
91
|
+
|
|
92
|
+
## Editable components (`@anvia/cli`)
|
|
93
|
+
|
|
94
|
+
For styled, app-owned components instead of headless composition:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
pnpm dlx @anvia/cli add chat
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Items: `chat`, `thread`, `message`, `composer`, `attachment`, `markdown`,
|
|
101
|
+
`tool-fallback`. `add` writes below the `components` alias (normally
|
|
102
|
+
`src/components/anvia`) and installs the matching `@anvia/react-ui` release.
|
|
103
|
+
`@anvia/cli` installs the `[data-state="revealing"]` animation with the
|
|
104
|
+
`markdown`, `message`, `thread`, and `chat` items.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Server Protocol
|
|
2
|
+
|
|
3
|
+
Server routes adapt native runtime events at the boundary, then frame them.
|
|
4
|
+
`@anvia/core` owns native completion and Agent events; `@anvia/client` owns the
|
|
5
|
+
wire adapters; `@anvia/server` owns the HTTP framing.
|
|
6
|
+
|
|
7
|
+
## Completion route
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { completionToClientStream, parseClientStreamRequest } from "@anvia/client";
|
|
11
|
+
import { streamCompletion } from "@anvia/core";
|
|
12
|
+
import { createClientStreamResponse } from "@anvia/server";
|
|
13
|
+
|
|
14
|
+
const body = parseClientStreamRequest(await request.json());
|
|
15
|
+
if (body.type !== "messages") throw new Error("This completion endpoint accepts messages only.");
|
|
16
|
+
const events = completionToClientStream({
|
|
17
|
+
events: streamCompletion({ model, messages: body.messages }),
|
|
18
|
+
});
|
|
19
|
+
return createClientStreamResponse({ events }); // JSONL by default
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Agent route
|
|
23
|
+
|
|
24
|
+
Use `agentToClientStream` instead of `completionToClientStream` for Agent events
|
|
25
|
+
(it preserves nested-agent scope). Accept both request shapes: `messages` starts
|
|
26
|
+
a run, `interaction_response` resumes one. Validate with `parseClientStreamRequest`.
|
|
27
|
+
|
|
28
|
+
For application-defined stream data, `customAgentEventsToClientStream` wraps an
|
|
29
|
+
agent event stream and `mapCustomEvent` maps app events into `data` events; the
|
|
30
|
+
client validates them against `dataSchemas` (see
|
|
31
|
+
`references/transports-state.md`) and they surface as data parts.
|
|
32
|
+
|
|
33
|
+
## Framing rules
|
|
34
|
+
|
|
35
|
+
- `createClientStreamResponse({ events })` always emits `stream_start`, ordered
|
|
36
|
+
`stream_event` frames, then `stream_end`, with header
|
|
37
|
+
`x-anvia-stream-protocol: anvia.client.v3`. This is the client protocol claim.
|
|
38
|
+
- Pass `format: "sse"` for SSE framing; JSONL is the default.
|
|
39
|
+
- For resumable streams, pass `{ resumable: { streamId, store } }` when creating
|
|
40
|
+
the response and call `resumeClientStreamResponse({ streamId, after, store })`
|
|
41
|
+
for resume requests. The client side pairs this with
|
|
42
|
+
`useChat({ transport, resume: { key } })`.
|
|
43
|
+
- `createEventStreamResponse` / `resumeEventStreamResponse` (and the lower-level
|
|
44
|
+
`createJsonlStream`, `createSseStream`, `createResumableStream`,
|
|
45
|
+
`resumeStreamEvents`, `createMemoryResumableStreamStore`) are generic helpers.
|
|
46
|
+
They serialize the application's own event type and do NOT claim the Anvia
|
|
47
|
+
client protocol. Use them only for endpoints that intentionally expose a
|
|
48
|
+
different event contract.
|
|
49
|
+
|
|
50
|
+
## Error exposure
|
|
51
|
+
|
|
52
|
+
Errors are masked by default. Use `mapError` only at the server adapter boundary
|
|
53
|
+
when the application intentionally exposes a safe error shape.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Transports and State
|
|
2
|
+
|
|
3
|
+
## The boundary rule
|
|
4
|
+
|
|
5
|
+
Client-side `UIMessage[]` never crosses the server boundary. The request carries
|
|
6
|
+
core `Message[]`; the response carries framed `ClientStreamEvent` records.
|
|
7
|
+
Convert explicitly at the edges:
|
|
8
|
+
|
|
9
|
+
- `uiMessagesToMessages()` — client state to request. Rejects partial tool calls
|
|
10
|
+
instead of replaying incomplete JSON or inventing empty arguments.
|
|
11
|
+
- `messagesToUIMessages()` — persisted core messages to UI state.
|
|
12
|
+
- `applyClientStreamEvent(messages, event)` — apply canonical stream events to
|
|
13
|
+
`UIMessage[]`.
|
|
14
|
+
- `parseUIMessage` / `parseUIMessages` — validate externally loaded UI state.
|
|
15
|
+
- `parseClientStreamRequest` / `parseClientStreamEvent` / `parseClientStreamFrame`
|
|
16
|
+
— validate public wire input at runtime.
|
|
17
|
+
|
|
18
|
+
`UIMessage.metadata` is application-owned and round-trips unchanged. Run details
|
|
19
|
+
(run ID, usage, context usage, status, trace correlation) live separately in
|
|
20
|
+
`UIMessage.generation`; converting persisted core messages hydrates usage into
|
|
21
|
+
that field and restores persisted sources as UI parts.
|
|
22
|
+
|
|
23
|
+
## Transports
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// HTTP: framed JSONL or SSE, validates protocol header, frame order,
|
|
27
|
+
// stream identity, and event IDs.
|
|
28
|
+
const transport = createHttpClientTransport({ endpoint: "/api/chat" });
|
|
29
|
+
|
|
30
|
+
// Direct (same process / tests): same framed contract, no HTTP.
|
|
31
|
+
const transport = createDirectClientTransport({
|
|
32
|
+
handler: ({ request, abortSignal }) => handleChat({ request, abortSignal }),
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- Import transports, protocol types, `UIMessage`, and conversion helpers from
|
|
37
|
+
`@anvia/client`. `@anvia/react` deliberately does not re-export them.
|
|
38
|
+
- Low-level JSONL/SSE readers live in `@anvia/client/transport`. They do not
|
|
39
|
+
imply the client protocol — use them only for non-protocol endpoints.
|
|
40
|
+
- `UIToolMessagePart` states are exact: `input-streaming` carries raw partial
|
|
41
|
+
text, `input-available` carries parsed JSON input, terminal `output-available`
|
|
42
|
+
or `error` parts retain that input with their result.
|
|
43
|
+
- Tool-call start/delta/end events are automatic when the provider streams
|
|
44
|
+
arguments.
|
|
45
|
+
|
|
46
|
+
## Agent interactions
|
|
47
|
+
|
|
48
|
+
- The browser never receives an `AgentContinuation`. The server retains it and
|
|
49
|
+
atomically claims it by interaction ID. Agent interaction wire contracts come
|
|
50
|
+
from the browser-safe `@anvia/core/agent/interactions` subpath — importing the
|
|
51
|
+
client never loads the Agent runtime.
|
|
52
|
+
- Name interaction types from `@anvia/core/agent/interactions` when the app
|
|
53
|
+
needs them.
|
|
54
|
+
|
|
55
|
+
## App-specific data
|
|
56
|
+
|
|
57
|
+
Application stream data is explicit and schema-validated:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type AppData = {
|
|
61
|
+
citation_preview: { title: string; url: string };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const transport = createHttpClientTransport<ClientStreamRequest, AppData>({
|
|
65
|
+
endpoint: "/api/chat",
|
|
66
|
+
dataSchemas: { citation_preview: citationPreviewSchema },
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Non-JSON tool outputs require an explicit `mapOutput`: returning `undefined`
|
|
71
|
+
omits the output, returning `null` exposes JSON `null`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Check the Anvia chat boundary rules in the current app directory.
|
|
3
|
+
# Usage: sh scripts/check-chat-boundary.sh [--dir <app-root>]
|
|
4
|
+
# Fails with a list of violations; passes silently with "chat boundary OK".
|
|
5
|
+
|
|
6
|
+
DIR="."
|
|
7
|
+
if [ "$1" = "--dir" ] && [ -n "$2" ]; then
|
|
8
|
+
DIR="$2"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
ROOTS_FOUND=0
|
|
12
|
+
for root in "$DIR/src" "$DIR/app" "$DIR/components"; do
|
|
13
|
+
if [ -d "$root" ]; then
|
|
14
|
+
ROOTS_FOUND=1
|
|
15
|
+
break
|
|
16
|
+
fi
|
|
17
|
+
done
|
|
18
|
+
if [ "$ROOTS_FOUND" -eq 0 ]; then
|
|
19
|
+
echo "ERROR: no src/app/components directory under '$DIR' — nothing was checked."
|
|
20
|
+
echo "Run from the app root or pass --dir <app-root>."
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
fail=0
|
|
25
|
+
violation() {
|
|
26
|
+
echo "VIOLATION: $1"
|
|
27
|
+
fail=1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# 1. Client bundle must not import the Agent runtime or server framing helpers.
|
|
31
|
+
if grep -rn --include='*.ts' --include='*.tsx' \
|
|
32
|
+
-e 'from "@anvia/core/agent"' \
|
|
33
|
+
-e 'from "@anvia/server"' \
|
|
34
|
+
"$DIR/src" "$DIR/app" "$DIR/components" 2>/dev/null | grep -v '^Binary' | grep -q .; then
|
|
35
|
+
violation "client code imports @anvia/core/agent runtime or @anvia/server; keep AgentContinuation and framing server-side (see references/transports-state.md)."
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# 2. useChat/useCompletion require an explicit transport.
|
|
39
|
+
if grep -rln --include='*.tsx' --include='*.ts' \
|
|
40
|
+
-e 'useChat(' -e 'useCompletion(' "$DIR/src" "$DIR/app" "$DIR/components" 2>/dev/null | grep -q .; then
|
|
41
|
+
if ! grep -rn --include='*.tsx' --include='*.ts' \
|
|
42
|
+
-e 'createHttpClientTransport' -e 'createDirectClientTransport' \
|
|
43
|
+
"$DIR/src" "$DIR/app" "$DIR/components" 2>/dev/null | grep -q .; then
|
|
44
|
+
violation "useChat/useCompletion used without createHttpClientTransport or createDirectClientTransport (see references/react-ui.md)."
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# 3. UIMessage must not be sent to the server; requests carry core Message[].
|
|
49
|
+
if grep -rn --include='*.ts' --include='*.tsx' \
|
|
50
|
+
-e 'UIMessage\[\].*fetch' -e 'body:.*UIMessage' -e 'JSON.stringify(.*uiMessages\|.*messages: chat.messages)' \
|
|
51
|
+
"$DIR/src" "$DIR/app" 2>/dev/null | grep -q .; then
|
|
52
|
+
violation "possible UIMessage sent over the wire; convert with uiMessagesToMessages first (see references/transports-state.md)."
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# 4. Server route must validate the request and claim (or not claim) the protocol.
|
|
56
|
+
if grep -rln --include='*.ts' \
|
|
57
|
+
-e 'createClientStreamResponse' -e 'agentToClientStream' -e 'completionToClientStream' \
|
|
58
|
+
"$DIR/src" "$DIR/app" 2>/dev/null | grep -q .; then
|
|
59
|
+
if ! grep -rn --include='*.ts' -e 'parseClientStreamRequest' \
|
|
60
|
+
"$DIR/src" "$DIR/app" 2>/dev/null | grep -q .; then
|
|
61
|
+
violation "stream route does not call parseClientStreamRequest (see references/server-protocol.md)."
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [ "$fail" -eq 0 ]; then
|
|
66
|
+
echo "chat boundary OK"
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
echo "See skills/anvia-chat/references/ for fixes."
|
|
70
|
+
exit 1
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anvia-evals
|
|
3
|
+
description: Evaluate Anvia agents and retrieval — deterministic metrics, semantic similarity, LLM judges, RAG quality, and CLI eval runs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Anvia Evals Skill
|
|
7
|
+
|
|
8
|
+
Use this skill when the user wants to measure output quality: scoring a target
|
|
9
|
+
function or agent over cases, picking metrics, adding an LLM judge, checking RAG
|
|
10
|
+
grounding, or wiring evals into CI.
|
|
11
|
+
|
|
12
|
+
## Process
|
|
13
|
+
|
|
14
|
+
1. Start deterministic (`references/metrics.md`) — exact match, contains, semantic similarity.
|
|
15
|
+
2. Add judges only for what strings cannot check (`references/judges.md`).
|
|
16
|
+
3. Run it right (`references/running.md`) — `runEvalSuite` vs `runEvalCli`, negative controls, expectations.
|
|
17
|
+
4. Run `scripts/check-evals.sh` from the app root before claiming done.
|
|
18
|
+
|
|
19
|
+
## Minimal slice
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { contains, exactMatch, runEvalCli } from "@anvia/core/evals";
|
|
23
|
+
|
|
24
|
+
await runEvalCli({
|
|
25
|
+
name: "support-basic-metrics",
|
|
26
|
+
cases: [
|
|
27
|
+
{
|
|
28
|
+
id: "refund-window",
|
|
29
|
+
input: "When can I request a refund?",
|
|
30
|
+
expected: "Refunds are available for 30 days.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "wrong-refund-window",
|
|
34
|
+
input: "Negative control: when can I request a refund?",
|
|
35
|
+
expected: "Refunds are available for 30 days.",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
target: async (input) => answerSupportQuestion(input),
|
|
39
|
+
metrics: [exactMatch(), contains({ expected: ({ case: testCase }) => "30 days" })],
|
|
40
|
+
expectations: {
|
|
41
|
+
outcomes: { "wrong-refund-window": { exact_match: "fail", contains: "fail" } },
|
|
42
|
+
},
|
|
43
|
+
exitCode: true,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Output
|
|
48
|
+
|
|
49
|
+
Every behavior claim needs a case. Prefer cheap deterministic metrics; spend
|
|
50
|
+
LLM-judge budget where wording varies. Point to the relevant reference file
|
|
51
|
+
instead of pasting its contents into chat.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# LLM Judges
|
|
2
|
+
|
|
3
|
+
Use judges when wording varies but meaning is checkable — policy compliance,
|
|
4
|
+
tone, grading scales. Judges need a model and explicit pass criteria.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { llmJudge, llmScore } from "@anvia/core/evals";
|
|
8
|
+
|
|
9
|
+
llmJudge({
|
|
10
|
+
model,
|
|
11
|
+
schema: z.object({ passed: z.boolean(), reason: z.string() }),
|
|
12
|
+
passes: (judgment) => judgment.passed,
|
|
13
|
+
instructions:
|
|
14
|
+
"Decide whether the output satisfies the expected support policy. Return passed and a short reason.",
|
|
15
|
+
});
|
|
16
|
+
llmScore({
|
|
17
|
+
model,
|
|
18
|
+
threshold: 0.8,
|
|
19
|
+
criteria: [
|
|
20
|
+
"The output answers the user's question directly.",
|
|
21
|
+
"The output matches the expected support policy.",
|
|
22
|
+
"The output does not add unsupported policy details.",
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## RAG quality metrics
|
|
28
|
+
|
|
29
|
+
Cases carry `context` and `retrievalContext`; the metrics check grounding:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { answerRelevancy, faithfulness, hallucination, gEval } from "@anvia/core/evals";
|
|
33
|
+
|
|
34
|
+
answerRelevancy({ model: judgeModel, threshold: 0.8 });
|
|
35
|
+
faithfulness({ model: judgeModel, threshold: 0.8 });
|
|
36
|
+
hallucination({ model: judgeModel, threshold: 0.1 }); // lower is better — note the direction
|
|
37
|
+
gEval({
|
|
38
|
+
name: "correctness",
|
|
39
|
+
model: judgeModel,
|
|
40
|
+
evaluationParams: ["actualOutput", "expectedOutput"],
|
|
41
|
+
evaluationSteps: [
|
|
42
|
+
"Check whether the answer preserves the expected refund window.",
|
|
43
|
+
"Allow different wording when the policy meaning is unchanged.",
|
|
44
|
+
],
|
|
45
|
+
threshold: 0.8,
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Related: `promptAlignment`, `jsonCorrectness`, `abstention`, `summarization` for
|
|
50
|
+
specialized outputs; `turnRelevancy` and `knowledgeRetention` for conversations
|
|
51
|
+
(see the cookbook's conversation-quality example).
|
|
52
|
+
|
|
53
|
+
## Rules
|
|
54
|
+
|
|
55
|
+
- Pin the judge model (`modelId`) — a silently upgraded judge re-grades history.
|
|
56
|
+
- Prefer a different (usually stronger) model for judging than for generating;
|
|
57
|
+
self-judging inflates scores.
|
|
58
|
+
- `threshold` direction matters: most metrics pass at/above it, `hallucination`
|
|
59
|
+
passes at/below it.
|
|
60
|
+
- Keep judge `instructions` / `criteria` / `evaluationSteps` in the eval file
|
|
61
|
+
under version control — they are part of the test, not ambient prompt text.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Deterministic Metrics
|
|
2
|
+
|
|
3
|
+
All metrics come from `@anvia/core/evals`. Metrics compare the target `output`
|
|
4
|
+
against the case `expected` and report `pass | fail | invalid` with scores and
|
|
5
|
+
comments. Start here — they cost nothing and never flake.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { contains, exactMatch, notContains, matches, semanticSimilarity } from "@anvia/core/evals";
|
|
9
|
+
|
|
10
|
+
metrics: [
|
|
11
|
+
exactMatch(), // deep-equal to expected — structurally equal objects/arrays pass
|
|
12
|
+
contains({
|
|
13
|
+
expected: ({ case: testCase }) =>
|
|
14
|
+
testCase.id === "billing-owner" ? "Workspace owners" : "30 days",
|
|
15
|
+
}),
|
|
16
|
+
notContains({ expected: "90 days" }), // forbid known-bad phrases
|
|
17
|
+
matches({ expected: /^\d+ days$/ }), // regex shape checks
|
|
18
|
+
semanticSimilarity({ model: embeddingModel, threshold: 0.8 }), // embedding closeness, no judge LLM needed
|
|
19
|
+
];
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `expected` (and `actual`) accept selector functions over
|
|
23
|
+
`{ suiteName, case, output, signal }` — reach per-case metadata as
|
|
24
|
+
`case.metadata`. Use them to vary expectations per case instead of writing one
|
|
25
|
+
metric per case.
|
|
26
|
+
- `containsAll` / `containsAny` check multi-fragment outputs; `maxLength` and
|
|
27
|
+
`requiredFields` guard shape for structured outputs.
|
|
28
|
+
- `defineMetric` builds custom metrics when nothing fits — keep the metric pure
|
|
29
|
+
(input, output, expected in; outcome out) so results stay reproducible.
|
|
30
|
+
|
|
31
|
+
## Agent outputs
|
|
32
|
+
|
|
33
|
+
Metrics work over any target output. For agents, project the response first:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
contains<string, AgentResponse, string>({ actual: ({ output }) => output.output });
|
|
37
|
+
exactMatch<string, AgentResponse, string>({
|
|
38
|
+
name: "not_blank",
|
|
39
|
+
actual: ({ output }) => output.output.trim().length > 0,
|
|
40
|
+
expected: true,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Always include a not-blank style metric for agent targets — an empty output
|
|
45
|
+
passing every content metric is the classic false green.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Running Evals
|
|
2
|
+
|
|
3
|
+
## Suite vs CLI
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { agentEvalTarget, runEvalCli, runEvalSuite } from "@anvia/core/evals";
|
|
7
|
+
|
|
8
|
+
// Library use: inspect results programmatically.
|
|
9
|
+
const result = await runEvalSuite({ name: "support-agent-target", cases, target, metrics });
|
|
10
|
+
console.log({
|
|
11
|
+
passed: result.metrics.passed,
|
|
12
|
+
failed: result.metrics.failed,
|
|
13
|
+
invalid: result.metrics.invalid,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// CLI/CI use: pretty table, asserted expectations, process exit code.
|
|
17
|
+
await runEvalCli({
|
|
18
|
+
name: "support-basic-metrics",
|
|
19
|
+
cases,
|
|
20
|
+
target,
|
|
21
|
+
metrics,
|
|
22
|
+
expectations,
|
|
23
|
+
exitCode: true,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `runEvalSuite` returns full per-case, per-metric results — use it in scripts
|
|
28
|
+
and notebooks. It also takes execution controls for larger suites:
|
|
29
|
+
`concurrency`, `caseTimeoutMs`, `failFast`, `caseIds`/`caseFilter`, `shard`,
|
|
30
|
+
`signal`, `onProgress`, plus reporters and `cost`/`targetUsage` capture.
|
|
31
|
+
- `runEvalCli` prints tables (`printEvalResult` / `formatEvalResult` under the
|
|
32
|
+
hood), asserts `expectations`, and with `exitCode: true` fails CI on
|
|
33
|
+
regressions. Always set `exitCode: true` in CI. Output is tunable via
|
|
34
|
+
`format: "pretty" | "json" | "quiet"`, `redact`, output writers, and
|
|
35
|
+
`maxValueLength`.
|
|
36
|
+
- `expectations.outcomes` pins known outcomes per case — including intentional
|
|
37
|
+
failures (negative controls), so a "fixed" negative control fails loudly
|
|
38
|
+
instead of silently flipping green. `expectations.totals` pins aggregate
|
|
39
|
+
counts per metric or for the whole run; `defineEvalExpectations`,
|
|
40
|
+
`assertEvalTotals`, `assertEvalOutcomes`, and `evalExitCode` are the
|
|
41
|
+
programmatic helpers behind the CLI.
|
|
42
|
+
|
|
43
|
+
## Targets
|
|
44
|
+
|
|
45
|
+
- Plain functions: `target: async (input) => answer(input)`.
|
|
46
|
+
- Agents: wrap with `agentEvalTarget({ agent, request: ({ input }) => ({ prompt: input }) })`
|
|
47
|
+
and project `output.output` in metrics (see `references/metrics.md`).
|
|
48
|
+
- `defineEvalCases` / `defineEvalSuite` / `createEvalTypes` add type safety to
|
|
49
|
+
large suites — adopt them when cases grow past a handful.
|
|
50
|
+
|
|
51
|
+
## Case design
|
|
52
|
+
|
|
53
|
+
- Every case needs `id`, `input`, `expected`. Keep ids stable — `expectations`
|
|
54
|
+
and history key off them.
|
|
55
|
+
- Include negative controls: inputs the target must get wrong (or must refuse),
|
|
56
|
+
pinned as expected failures. Without them you cannot tell a strict metric
|
|
57
|
+
from a broken target.
|
|
58
|
+
- For RAG cases add `context` (what the generator saw) and `retrievalContext`
|
|
59
|
+
(what retrieval returned) so grounding metrics have something to check.
|
|
60
|
+
- Report evals to observability (Langfuse eval reporting, trace refs via
|
|
61
|
+
`resolveEvalTraceRef`) when runs must be auditable — see the cookbook's
|
|
62
|
+
langfuse eval-reporting example.
|