@anvia/cli 1.2.0 → 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.
Files changed (53) hide show
  1. package/README.md +32 -0
  2. package/dist/chunk-ERCP4EIZ.js +607 -0
  3. package/dist/chunk-ERCP4EIZ.js.map +1 -0
  4. package/dist/cli.js +278 -74
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +54 -1
  7. package/dist/index.js +21 -3
  8. package/dist/skills/anvia-agent/SKILL.md +59 -0
  9. package/dist/skills/anvia-agent/references/agent-options.md +83 -0
  10. package/dist/skills/anvia-agent/references/providers.md +22 -0
  11. package/dist/skills/anvia-agent/references/teams.md +84 -0
  12. package/dist/skills/anvia-agent/references/tools.md +71 -0
  13. package/dist/skills/anvia-agent/scripts/check-agent.sh +99 -0
  14. package/dist/skills/anvia-channels/SKILL.md +59 -0
  15. package/dist/skills/anvia-channels/references/adapters.md +53 -0
  16. package/dist/skills/anvia-channels/references/channel-agent.md +52 -0
  17. package/dist/skills/anvia-channels/references/delivery.md +53 -0
  18. package/dist/skills/anvia-channels/scripts/check-channels.sh +79 -0
  19. package/dist/skills/anvia-chat/SKILL.md +61 -0
  20. package/dist/skills/anvia-chat/references/react-ui.md +104 -0
  21. package/dist/skills/anvia-chat/references/server-protocol.md +53 -0
  22. package/dist/skills/anvia-chat/references/transports-state.md +71 -0
  23. package/dist/skills/anvia-chat/scripts/check-chat-boundary.sh +70 -0
  24. package/dist/skills/anvia-evals/SKILL.md +51 -0
  25. package/dist/skills/anvia-evals/references/judges.md +61 -0
  26. package/dist/skills/anvia-evals/references/metrics.md +45 -0
  27. package/dist/skills/anvia-evals/references/running.md +62 -0
  28. package/dist/skills/anvia-evals/scripts/check-evals.sh +73 -0
  29. package/dist/skills/anvia-mcp/SKILL.md +49 -0
  30. package/dist/skills/anvia-mcp/references/clients.md +73 -0
  31. package/dist/skills/anvia-mcp/references/safety.md +25 -0
  32. package/dist/skills/anvia-mcp/scripts/check-mcp.sh +73 -0
  33. package/dist/skills/anvia-pipeline/SKILL.md +52 -0
  34. package/dist/skills/anvia-pipeline/references/agents-extract.md +62 -0
  35. package/dist/skills/anvia-pipeline/references/steps-compose.md +63 -0
  36. package/dist/skills/anvia-pipeline/scripts/check-pipeline.sh +69 -0
  37. package/dist/skills/anvia-rag/SKILL.md +50 -0
  38. package/dist/skills/anvia-rag/references/graph-rag.md +115 -0
  39. package/dist/skills/anvia-rag/references/pipeline.md +81 -0
  40. package/dist/skills/anvia-rag/references/rag-tool.md +43 -0
  41. package/dist/skills/anvia-rag/references/stores.md +68 -0
  42. package/dist/skills/anvia-rag/scripts/check-rag.sh +75 -0
  43. package/dist/skills/anvia-studio/SKILL.md +45 -0
  44. package/dist/skills/anvia-studio/references/inspect.md +33 -0
  45. package/dist/skills/anvia-studio/references/observe.md +34 -0
  46. package/dist/skills/anvia-studio/references/serve.md +65 -0
  47. package/dist/skills/anvia-studio/scripts/check-studio.sh +59 -0
  48. package/dist/skills/release-notes/SKILL.md +18 -0
  49. package/dist/skills/release-notes/references/style.md +6 -0
  50. package/dist/skills/release-notes/scripts/draft.sh +22 -0
  51. package/package.json +2 -2
  52. package/dist/chunk-IH4ZNWHB.js +0 -285
  53. package/dist/chunk-IH4ZNWHB.js.map +0 -1
@@ -0,0 +1,53 @@
1
+ # Delivery and raw events
2
+
3
+ Not every channel consumer needs an agent. An alerting worker only needs an
4
+ adapter and `sendChannelMessage(channel, address, message)` from
5
+ `@anvia/channel` — receiving never has to be started:
6
+
7
+ ```ts
8
+ import { sendChannelMessage } from "@anvia/channel";
9
+ import { discord } from "@anvia/discord";
10
+
11
+ const channel = discord({ token: process.env.DISCORD_BOT_TOKEN ?? "" });
12
+
13
+ await sendChannelMessage(
14
+ channel,
15
+ { platform: "discord", conversationId: process.env.DISCORD_CHANNEL_ID ?? "" },
16
+ {
17
+ text: monitoringReport,
18
+ actions: [{ id: "incident:ack", label: "Acknowledge", style: "primary" }],
19
+ },
20
+ );
21
+ ```
22
+
23
+ ## sendChannelMessage vs channel.send
24
+
25
+ Use `sendChannelMessage()` at application boundaries: it validates the message
26
+ and splits long text to fit the platform before delivering each part. Use
27
+ `channel.send()` only for one already-bounded logical message — piping dynamic
28
+ or generated text through it is how messages silently get truncated.
29
+
30
+ Related helpers, all from `@anvia/channel`:
31
+
32
+ - `splitChannelText` / `splitChannelMessage` — the splitter, if you need the
33
+ parts yourself.
34
+ - `validateChannelMessage` / `validateChannelActions` /
35
+ `validateChannelAttachments` — the validators.
36
+ - Limits are exported constants: `MAX_CHANNEL_ACTIONS`,
37
+ `MAX_CHANNEL_ACTION_ID_BYTES`, `MAX_CHANNEL_ACTION_LABEL_LENGTH`,
38
+ `MAX_CHANNEL_ATTACHMENTS`.
39
+ - `isChannelActionId` — action IDs must be CLI-safe strings.
40
+ - `PartialDeliveryError` — thrown when a split message only partially
41
+ delivered; catch and reconcile rather than resend blind.
42
+
43
+ Messages are `{ text, actions?, attachments?, replyToMessageId? }` and resolve
44
+ to `SentChannelMessage` `{ id, address }` for later edits/deletes/reactions.
45
+
46
+ ## Direct event handling
47
+
48
+ To consume events without the agent bridge, subscribe with a
49
+ `ChannelEventHandler` and switch on the `ChannelEvent` union: message,
50
+ message-edited, message-deleted, reaction, action, or command events — each
51
+ carries the normalized content plus the raw payload as `RawEvent`. Action and
52
+ command events are how button taps and slash commands reach application code
53
+ when no agent is attached.
@@ -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.