@pinecall/skills 0.1.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 (68) hide show
  1. package/README.md +65 -0
  2. package/build.mjs +204 -0
  3. package/package.json +29 -0
  4. package/skills/pinecall-concepts/SKILL.md +41 -0
  5. package/skills/pinecall-concepts/references/concepts/agents-and-channels.md +155 -0
  6. package/skills/pinecall-concepts/references/concepts/deployment-topologies.md +120 -0
  7. package/skills/pinecall-concepts/references/concepts/hot-reload.md +119 -0
  8. package/skills/pinecall-concepts/references/concepts/philosophy.md +100 -0
  9. package/skills/pinecall-concepts/references/concepts/server-vs-client-llm.md +119 -0
  10. package/skills/pinecall-examples/SKILL.md +59 -0
  11. package/skills/pinecall-examples/references/examples/browser-widget.md +206 -0
  12. package/skills/pinecall-examples/references/examples/chat-bot.md +184 -0
  13. package/skills/pinecall-examples/references/examples/headless-agent.md +121 -0
  14. package/skills/pinecall-examples/references/examples/index.md +183 -0
  15. package/skills/pinecall-examples/references/examples/multi-channel-bot.md +173 -0
  16. package/skills/pinecall-examples/references/examples/outbound-dispatch.md +109 -0
  17. package/skills/pinecall-examples/references/examples/turn-detection.md +150 -0
  18. package/skills/pinecall-guides/SKILL.md +68 -0
  19. package/skills/pinecall-guides/references/guides/call-ringing.md +149 -0
  20. package/skills/pinecall-guides/references/guides/conversation-history.md +377 -0
  21. package/skills/pinecall-guides/references/guides/dev-mode.md +130 -0
  22. package/skills/pinecall-guides/references/guides/events.md +677 -0
  23. package/skills/pinecall-guides/references/guides/human-takeover.md +184 -0
  24. package/skills/pinecall-guides/references/guides/inbound-voice.md +201 -0
  25. package/skills/pinecall-guides/references/guides/knowledge-bases.md +166 -0
  26. package/skills/pinecall-guides/references/guides/live-listening.md +199 -0
  27. package/skills/pinecall-guides/references/guides/multi-tenant.md +158 -0
  28. package/skills/pinecall-guides/references/guides/outbound-calls.md +279 -0
  29. package/skills/pinecall-guides/references/guides/sse-streaming.md +207 -0
  30. package/skills/pinecall-guides/references/guides/testing-agents.md +272 -0
  31. package/skills/pinecall-guides/references/guides/tools-and-functions.md +254 -0
  32. package/skills/pinecall-guides/references/guides/webrtc-browser.md +200 -0
  33. package/skills/pinecall-guides/references/guides/whatsapp.md +370 -0
  34. package/skills/pinecall-guides/references/guides/ws-streaming.md +235 -0
  35. package/skills/pinecall-quickstart/SKILL.md +54 -0
  36. package/skills/pinecall-quickstart/references/index.md +123 -0
  37. package/skills/pinecall-quickstart/references/quickstart.md +185 -0
  38. package/skills/pinecall-reference/SKILL.md +43 -0
  39. package/skills/pinecall-reference/references/reference/cli.md +578 -0
  40. package/skills/pinecall-reference/references/reference/events.md +366 -0
  41. package/skills/pinecall-reference/references/reference/llm-providers.md +263 -0
  42. package/skills/pinecall-reference/references/reference/rest-api.md +122 -0
  43. package/skills/pinecall-reference/references/reference/session-limits.md +119 -0
  44. package/skills/pinecall-reference/references/reference/stt-providers.md +174 -0
  45. package/skills/pinecall-reference/references/reference/tts-providers.md +149 -0
  46. package/skills/pinecall-sdk-api/SKILL.md +56 -0
  47. package/skills/pinecall-sdk-api/references/api/agent.md +328 -0
  48. package/skills/pinecall-sdk-api/references/api/call.md +324 -0
  49. package/skills/pinecall-sdk-api/references/api/pinecall.md +186 -0
  50. package/skills/pinecall-sdk-api/references/api/reply-stream.md +148 -0
  51. package/skills/pinecall-security/SKILL.md +37 -0
  52. package/skills/pinecall-security/references/security.md +138 -0
  53. package/skills/pinecall-web-chat/SKILL.md +38 -0
  54. package/skills/pinecall-web-chat/references/web/chat/chat-session.md +178 -0
  55. package/skills/pinecall-web-chat/references/web/chat/overview.md +98 -0
  56. package/skills/pinecall-web-components/SKILL.md +37 -0
  57. package/skills/pinecall-web-components/references/web/components/overview.md +128 -0
  58. package/skills/pinecall-web-voice/SKILL.md +40 -0
  59. package/skills/pinecall-web-voice/references/web/core/datachannel-protocol.md +149 -0
  60. package/skills/pinecall-web-voice/references/web/core/overview.md +70 -0
  61. package/skills/pinecall-web-voice/references/web/core/state-and-phases.md +153 -0
  62. package/skills/pinecall-web-voice/references/web/core/voice-session.md +279 -0
  63. package/skills/pinecall-web-widget/SKILL.md +41 -0
  64. package/skills/pinecall-web-widget/references/web/widget/overview.md +67 -0
  65. package/skills/pinecall-web-widget/references/web/widget/props.md +291 -0
  66. package/skills/pinecall-web-widget/references/web/widget/theming.md +131 -0
  67. package/skills/pinecall-web-widget/references/web/widget/tools-api.md +381 -0
  68. package/skills/pinecall-web-widget/references/web/widget/use-voice-session-hook.md +130 -0
@@ -0,0 +1,381 @@
1
+ ---
2
+ title: "Tools API"
3
+ description: "Render interactive UI in response to LLM tool calls. Buttons, forms, pickers — all synced to the conversation."
4
+ ---
5
+
6
+ # Tools API
7
+
8
+ The widget can render interactive UI in response to server-side LLM tool calls. The agent decides when to surface a UI (slot picker, form, confirmation), the widget renders your component, the user interacts, and the agent sees the result through the same conversation.
9
+
10
+ ## The flow
11
+
12
+ ![Tool call lifecycle flow](/assets/diagrams/tool-call-flow.png)
13
+
14
+ ## Enabling tool tracking
15
+
16
+ Tell the widget which tool names to track via the `trackedTools` prop. Untracked tools are handled silently by the agent — only tracked ones expose their state to the UI.
17
+
18
+ ```tsx
19
+ <VoiceWidget
20
+ agent="booking-demo"
21
+ trackedTools={["getAvailableSlots", "showContactForm", "fillField"]}
22
+ >
23
+ <ToolPanel />
24
+ </VoiceWidget>
25
+ ```
26
+
27
+ Children of `<VoiceWidget>` (like `<ToolPanel />`) can read the tool state through the `useVoice()` context hook.
28
+
29
+ ## `ToolUI` shape
30
+
31
+ Each tracked tool call is stored in `state.toolCalls` as:
32
+
33
+ ```typescript
34
+ interface ToolUI {
35
+ toolCallId: string; // correlation ID — pass to dismissTool()
36
+ name: string; // tool function name
37
+ arguments: Record<string, unknown>; // parsed LLM arguments
38
+ result?: unknown; // populated when the tool result arrives
39
+ timestamp: number;
40
+ }
41
+ ```
42
+
43
+ `result` is `undefined` between the call and the result — render a loading state if needed.
44
+
45
+ ## `useVoice()` — the context hook
46
+
47
+ Reads from `<VoiceWidget>` context. Use it in any component that lives inside the widget.
48
+
49
+ ```tsx
50
+ import { useVoice } from "@pinecall/web";
51
+
52
+ function SlotPicker() {
53
+ const { toolCalls, sendText, dismissTool } = useVoice();
54
+
55
+ const tool = toolCalls.find(
56
+ (tc) => tc.name === "getAvailableSlots" && tc.result !== undefined,
57
+ );
58
+ if (!tool) return null;
59
+
60
+ return (
61
+ <div className="slot-picker">
62
+ {tool.result.slots.map((slot: string) => (
63
+ <button
64
+ key={slot}
65
+ onClick={() => {
66
+ sendText(`I'd like the ${slot} slot`);
67
+ dismissTool(tool.toolCallId);
68
+ }}
69
+ >
70
+ {slot}
71
+ </button>
72
+ ))}
73
+ </div>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ### What `useVoice()` exposes
79
+
80
+ | Field | Type | What it does |
81
+ |---|---|---|
82
+ | `toolCalls` | `ToolUI[]` | Active tracked tool calls |
83
+ | `messages` | `TranscriptMessage[]` | Full transcript |
84
+ | `status` | `SessionStatus` | Connection status |
85
+ | `phase` | `CallPhase` | Current phase |
86
+ | `sendText` | `(text: string) => void` | Inject text as if the user spoke it |
87
+ | `setContext` | `(key: string, value: string \| null) => void` | Inject keyed context into the LLM system prompt |
88
+ | `dismissTool` | `(toolCallId: string) => void` | Remove a tool from state (hides the UI) |
89
+
90
+ > **`useVoice()` vs `useVoiceSession()`.** `useVoice()` reads from the widget's context — only works inside `<VoiceWidget>` children. `useVoiceSession()` creates its own standalone session. Use `useVoice()` when building tool renderers.
91
+
92
+ ## The three primitives
93
+
94
+ ### `sendText(text)`
95
+
96
+ Inject text into the conversation as if the user spoke it. It routes through the server's LLM pipeline so the agent processes it normally.
97
+
98
+ ```tsx
99
+ // User clicks a slot button
100
+ sendText("I'd like to book the 10:00 AM slot");
101
+
102
+ // User submits a form
103
+ sendText("Form submitted: name=John, email=john@example.com, phone=+1555000");
104
+ ```
105
+
106
+ Use this for click-based interactions where you want the agent to react conversationally.
107
+
108
+ ### `setContext(key, value)`
109
+
110
+ Inject dynamic context into the agent's LLM system prompt. Keyed — setting the same key replaces its value. Pass `null` to clear.
111
+
112
+ This is the magic for syncing UI state (form inputs, selections, page content) into the agent's awareness:
113
+
114
+ ```tsx
115
+ // Sync form state on every keystroke
116
+ useEffect(() => {
117
+ setContext("contact_form", JSON.stringify({
118
+ name: formData.name || "(empty)",
119
+ email: formData.email || "(empty)",
120
+ phone: formData.phone || "(empty)",
121
+ }));
122
+ }, [formData, setContext]);
123
+
124
+ // Clear when done
125
+ setContext("contact_form", null);
126
+ ```
127
+
128
+ On the server, this appears in the LLM's system prompt as:
129
+
130
+ ```
131
+ ## UI Context
132
+ ### contact_form
133
+ {"name":"John","email":"john@example.com","phone":"(empty)"}
134
+ ```
135
+
136
+ The agent can now reason about UI state without you having to explicitly tell it.
137
+
138
+ ### `dismissTool(toolCallId)`
139
+
140
+ Remove a tool from `state.toolCalls`. Hides the rendered UI. Call this after the user interacts (selects a slot, submits a form, etc.).
141
+
142
+ ```tsx
143
+ dismissTool(tool.toolCallId);
144
+ ```
145
+
146
+ ## Full example — booking with auto-fill
147
+
148
+ This shows the full pattern: slot picker, contact form, agent-driven auto-fill, and live context sync.
149
+
150
+ ### Server-side agent
151
+
152
+ ```typescript
153
+ import { tool } from "@pinecall/sdk";
154
+ import { z } from "zod";
155
+
156
+ const getAvailableSlots = tool({
157
+ name: "getAvailableSlots",
158
+ description: "Get available slots for a date.",
159
+ schema: z.object({ date: z.string() }),
160
+ execute: async ({ date }) => ({
161
+ slots: ["9:00 AM", "10:00 AM", "2:00 PM", "4:00 PM"],
162
+ }),
163
+ });
164
+
165
+ const showContactForm = tool({
166
+ name: "showContactForm",
167
+ description: "Show a contact form on screen.",
168
+ schema: z.object({}),
169
+ execute: async () => ({ shown: true }),
170
+ });
171
+
172
+ const fillField = tool({
173
+ name: "fillField",
174
+ description: "Auto-fill a form field with a value extracted from the conversation.",
175
+ schema: z.object({
176
+ field: z.enum(["name", "email", "phone"]),
177
+ value: z.string(),
178
+ }),
179
+ execute: async ({ field, value }) => ({ field, value }),
180
+ });
181
+
182
+ const confirmBooking = tool({
183
+ name: "confirmBooking",
184
+ description: "Confirm the booking.",
185
+ schema: z.object({ date: z.string(), time: z.string(), clientName: z.string() }),
186
+ execute: async ({ date, time, clientName }) => ({
187
+ confirmationId: "BK-" + Math.random().toString(36).slice(2, 8),
188
+ }),
189
+ });
190
+
191
+ const agent = pc.agent("booking-demo", {
192
+ prompt: `You are a booking assistant.
193
+ - Call getAvailableSlots when the user wants to book.
194
+ - After they pick a slot, call showContactForm.
195
+ - If they say their name/email/phone, call fillField to auto-fill.
196
+ - The form state is in "## UI Context" — you can see what they've typed.
197
+ - When the form is submitted, call confirmBooking.`,
198
+ llm: "openai/gpt-5-chat-latest",
199
+ voice: "elevenlabs/sarah",
200
+ tools: [getAvailableSlots, showContactForm, fillField, confirmBooking],
201
+ greeting: "Hi! Want to book an appointment?",
202
+ });
203
+ ```
204
+
205
+ ### Browser — contact form with auto-fill
206
+
207
+ ```tsx
208
+ import { useState, useEffect } from "react";
209
+ import { VoiceWidget, useVoice } from "@pinecall/web";
210
+
211
+ function ContactForm({ tool }) {
212
+ const { sendText, dismissTool, setContext, toolCalls } = useVoice();
213
+ const [form, setForm] = useState({ name: "", email: "", phone: "" });
214
+
215
+ // Agent calls fillField → auto-fill the form
216
+ const fillTool = toolCalls.find((tc) => tc.name === "fillField" && tc.result);
217
+ useEffect(() => {
218
+ if (fillTool?.result) {
219
+ const { field, value } = fillTool.result as { field: string; value: string };
220
+ setForm((prev) => ({ ...prev, [field]: value }));
221
+ dismissTool(fillTool.toolCallId);
222
+ }
223
+ }, [fillTool, dismissTool]);
224
+
225
+ // Sync form state → LLM system prompt
226
+ useEffect(() => {
227
+ setContext("contact_form", JSON.stringify(form));
228
+ }, [form, setContext]);
229
+
230
+ const submit = (e: React.FormEvent) => {
231
+ e.preventDefault();
232
+ sendText(`Form submitted: ${JSON.stringify(form)}`);
233
+ setContext("contact_form", null);
234
+ dismissTool(tool.toolCallId);
235
+ };
236
+
237
+ return (
238
+ <form onSubmit={submit}>
239
+ <input
240
+ placeholder="Name"
241
+ value={form.name}
242
+ onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
243
+ />
244
+ <input
245
+ placeholder="Email"
246
+ value={form.email}
247
+ onChange={(e) => setForm((p) => ({ ...p, email: e.target.value }))}
248
+ />
249
+ <input
250
+ placeholder="Phone"
251
+ value={form.phone}
252
+ onChange={(e) => setForm((p) => ({ ...p, phone: e.target.value }))}
253
+ />
254
+ <button type="submit">Confirm</button>
255
+ </form>
256
+ );
257
+ }
258
+
259
+ function SlotPicker({ tool }) {
260
+ const { sendText, dismissTool } = useVoice();
261
+ return (
262
+ <div>
263
+ {(tool.result as any).slots.map((slot: string) => (
264
+ <button
265
+ key={slot}
266
+ onClick={() => {
267
+ sendText(`I'd like the ${slot} slot`);
268
+ dismissTool(tool.toolCallId);
269
+ }}
270
+ >
271
+ {slot}
272
+ </button>
273
+ ))}
274
+ </div>
275
+ );
276
+ }
277
+
278
+ function ToolPanel() {
279
+ const { toolCalls } = useVoice();
280
+ return (
281
+ <>
282
+ {toolCalls.map((tool) => {
283
+ if (tool.name === "getAvailableSlots" && tool.result) {
284
+ return <SlotPicker key={tool.toolCallId} tool={tool} />;
285
+ }
286
+ if (tool.name === "showContactForm" && tool.result) {
287
+ return <ContactForm key={tool.toolCallId} tool={tool} />;
288
+ }
289
+ return null;
290
+ })}
291
+ </>
292
+ );
293
+ }
294
+
295
+ export default function App() {
296
+ return (
297
+ <VoiceWidget
298
+ agent="booking-demo"
299
+ trackedTools={["getAvailableSlots", "showContactForm", "fillField", "confirmBooking"]}
300
+ >
301
+ <ToolPanel />
302
+ </VoiceWidget>
303
+ );
304
+ }
305
+ ```
306
+
307
+ ## Alternative: `tools` prop (render functions)
308
+
309
+ If you don't need the flexibility of `trackedTools` + `useVoice()`, use the `tools` prop for a simpler inline approach. Each tool name maps to a render function that receives the result:
310
+
311
+ ```tsx
312
+ <VoiceWidget
313
+ agent="booking-demo"
314
+ tools={{
315
+ getAvailableSlots: (result, { respond, dismiss }) => (
316
+ <div className="slot-picker">
317
+ {result.slots.map((slot: string) => (
318
+ <button
319
+ key={slot}
320
+ onClick={() => {
321
+ respond(`I'd like the ${slot} slot`);
322
+ dismiss();
323
+ }}
324
+ >
325
+ {slot}
326
+ </button>
327
+ ))}
328
+ </div>
329
+ ),
330
+ confirmBooking: (result, { dismiss }) => (
331
+ <div className="confirmation">
332
+ <p>✅ Booked for {result.time}</p>
333
+ <button onClick={dismiss}>Done</button>
334
+ </div>
335
+ ),
336
+ }}
337
+ />
338
+ ```
339
+
340
+ ### Render function signature
341
+
342
+ ```typescript
343
+ type ToolRenderer = (
344
+ result: any, // parsed tool result
345
+ context: ToolRenderContext, // { respond, dismiss }
346
+ toolCall: ToolUI, // full tool call metadata
347
+ ) => React.ReactNode;
348
+ ```
349
+
350
+ | Parameter | Type | Description |
351
+ |---|---|---|
352
+ | `result` | `any` | The parsed JSON result from your backend tool |
353
+ | `context.respond` | `(text: string) => void` | Inject text as if the user spoke it |
354
+ | `context.dismiss` | `() => void` | Remove the tool UI from the transcript |
355
+ | `toolCall` | `ToolUI` | Full tool call metadata (name, arguments, ID) |
356
+
357
+ ### `tools` vs `trackedTools` — when to use which
358
+
359
+ | Scenario | Use |
360
+ |---|---|
361
+ | Simple inline renderers | `tools` prop |
362
+ | Complex components needing React state | `trackedTools` + `useVoice()` |
363
+ | Multiple components sharing tool state | `trackedTools` + `useVoice()` |
364
+ | Context injection via `setContext` | `trackedTools` + `useVoice()` |
365
+
366
+ ## Why this pattern is powerful
367
+
368
+ What this enables is multimodal interaction:
369
+
370
+ - **Voice for natural language** — "I want to book an appointment for next Tuesday morning"
371
+ - **UI for precise input** — pick the exact slot, type your email, submit a form
372
+ - **Sync via setContext** — the agent always knows what's on screen
373
+ - **sendText for confirmation** — the user's UI action becomes part of the conversation
374
+
375
+ The agent doesn't need different code paths for "voice user" vs "GUI user" — it just sees a conversation with rich context.
376
+
377
+ ## What's next
378
+
379
+ - [Props reference](/web/widget/props) — `tools`, `trackedTools`, `tokenProvider`, and more
380
+ - [`useVoiceSession` hook](/web/widget/use-voice-session-hook) — for non-tool custom UIs
381
+ - [Tools and functions guide](/guides/tools-and-functions) — server-side tool definition
@@ -0,0 +1,130 @@
1
+ ---
2
+ title: "useVoiceSession hook"
3
+ description: "Build a fully custom voice UI without giving up the widget's session management."
4
+ ---
5
+
6
+ # `useVoiceSession` hook
7
+
8
+ If the orb UI doesn't fit your design, use the `useVoiceSession()` hook directly. It gives you the same reactive state and actions, with no orb, no transcript bubbles, no styling — you bring the UI.
9
+
10
+ ## When to use this vs `<VoiceWidget />`
11
+
12
+ | Use `<VoiceWidget />` | Use `useVoiceSession()` |
13
+ |---|---|
14
+ | You want a floating orb in the corner | You want voice as part of your app's existing UI |
15
+ | You need theming via presets | You're styling everything yourself anyway |
16
+ | You want the Tools API context (`useVoice()`) | You're building a transcript-first interface |
17
+
18
+ The hook wraps `VoiceSession` from `@pinecall/web/core` with `useSyncExternalStore` for efficient React rendering. The session is created once on mount and destroyed on unmount.
19
+
20
+ ## Quick start
21
+
22
+ ```tsx
23
+ import { useVoiceSession } from "@pinecall/web";
24
+
25
+ function CustomVoice() {
26
+ const {
27
+ status, error, isMuted, phase,
28
+ userSpeaking, agentSpeaking, duration,
29
+ messages, idleWarning,
30
+ connect, disconnect, toggleMute, setMuted,
31
+ } = useVoiceSession({ agent: "mara" });
32
+
33
+ return (
34
+ <div>
35
+ <p>Status: {status} · Phase: {phase} · {duration}s</p>
36
+
37
+ {status === "idle" && <button onClick={connect}>Start call</button>}
38
+ {status === "connected" && (
39
+ <>
40
+ <button onClick={disconnect}>End call</button>
41
+ <button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>
42
+ </>
43
+ )}
44
+
45
+ <div>
46
+ {messages.map((m) => (
47
+ <div key={m.id} className={m.role}>
48
+ <strong>{m.role}:</strong> {m.text}
49
+ {m.isInterim && " (typing...)"}
50
+ {m.speaking && " 🔊"}
51
+ {m.interrupted && " ⚡ interrupted"}
52
+ </div>
53
+ ))}
54
+ </div>
55
+ </div>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## Return shape
61
+
62
+ The hook returns the full session state **plus** action methods:
63
+
64
+ | Field | Type | What it is |
65
+ |---|---|---|
66
+ | `status` | `SessionStatus` | `"idle" \| "connecting" \| "connected" \| "error"` |
67
+ | `error` | `string \| null` | Error message when `status === "error"` |
68
+ | `isMuted` | `boolean` | Mic state |
69
+ | `phase` | `CallPhase` | `"idle" \| "listening" \| "speaking" \| "pause" \| "thinking"` |
70
+ | `userSpeaking` | `boolean` | User is physically talking (VAD-level) |
71
+ | `agentSpeaking` | `boolean` | TTS is currently playing |
72
+ | `duration` | `number` | Seconds since connected, updates every second |
73
+ | `messages` | `TranscriptMessage[]` | Full transcript — see [State and Phases](/web/core/state-and-phases) |
74
+ | `idleWarning` | `number \| null` | Seconds until idle timeout (null = no warning) |
75
+ | `connect` | `() => Promise<void>` | Start the call |
76
+ | `disconnect` | `() => void` | End the call |
77
+ | `toggleMute` | `() => void` | Toggle mic |
78
+ | `setMuted` | `(muted: boolean) => void` | Explicit mute control |
79
+
80
+ ## Accessing raw events
81
+
82
+ For tool calls or other low-level events the state machine doesn't expose, drop down to `@pinecall/web/core` directly and listen to the `event` listener:
83
+
84
+ ```tsx
85
+ import { useState, useEffect } from "react";
86
+ import { VoiceSession } from "@pinecall/web/core";
87
+
88
+ function AdvancedVoice() {
89
+ const [session] = useState(() => new VoiceSession({ agent: "mara" }));
90
+
91
+ useEffect(() => {
92
+ const onEvent = (e: CustomEvent) => {
93
+ const { event, tool_calls } = e.detail;
94
+
95
+ if (event === "llm.tool_call" && tool_calls) {
96
+ for (const tc of tool_calls) {
97
+ console.log(`Tool call: ${tc.name}`, tc.arguments);
98
+ }
99
+ }
100
+ };
101
+
102
+ session.addEventListener("event", onEvent);
103
+ return () => {
104
+ session.removeEventListener("event", onEvent);
105
+ session.destroy();
106
+ };
107
+ }, [session]);
108
+
109
+ // ... render UI using session.getState()
110
+ }
111
+ ```
112
+
113
+ If you specifically want to render interactive UI for tool calls, stick with `<VoiceWidget>` and use the [Tools API](/web/widget/tools-api) — it handles the correlation between calls and results for you.
114
+
115
+ ## `useVoice()` vs `useVoiceSession()`
116
+
117
+ There are two hooks in this package and the names are easy to confuse:
118
+
119
+ | Hook | Purpose | Where to use |
120
+ |---|---|---|
121
+ | `useVoiceSession()` | Creates its own session | Anywhere — standalone |
122
+ | `useVoice()` | Reads from `<VoiceWidget>` context | Inside `<VoiceWidget>` children only |
123
+
124
+ Use `useVoiceSession()` for fully custom UIs that replace the widget entirely. Use `useVoice()` when you're building tool renderers as children of `<VoiceWidget>` — see [Tools API](/web/widget/tools-api).
125
+
126
+ ## What's next
127
+
128
+ - [Props reference](/web/widget/props) — if you want the orb after all
129
+ - [Tools API](/web/widget/tools-api) — interactive UI for tool calls
130
+ - [`@pinecall/web/core`](/web/core/overview) — for non-React frameworks