@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,254 @@
1
+ ---
2
+ title: "Tools and Functions"
3
+ description: "Let your agent take actions: look up data, transfer calls, book appointments."
4
+ ---
5
+
6
+ # Tools and Functions
7
+
8
+ Tools are how your agent moves beyond conversation into action: looking up an order, checking inventory, booking a slot, transferring to a human. In Pinecall, tools are **local functions in your process**, not webhooks.
9
+
10
+ ## Defining tools
11
+
12
+ Use the `tool()` helper with a Zod schema. Tools are auto-executed by the SDK when the LLM calls them — no manual event handler needed.
13
+
14
+ ```typescript
15
+ import { Pinecall, tool } from "@pinecall/sdk";
16
+ import { z } from "zod";
17
+
18
+ const lookupOrder = tool({
19
+ name: "lookupOrder",
20
+ description: "Look up an order by its ID.",
21
+ schema: z.object({
22
+ orderId: z.string().describe("The order ID, like ORD-12345"),
23
+ }),
24
+ execute: async ({ orderId }) => {
25
+ return await db.orders.findOne(orderId);
26
+ },
27
+ });
28
+
29
+ const scheduleCallback = tool({
30
+ name: "scheduleCallback",
31
+ description: "Schedule a callback for a specific date and time.",
32
+ schema: z.object({
33
+ datetime: z.string().describe("ISO 8601 datetime"),
34
+ reason: z.string(),
35
+ }),
36
+ execute: async ({ datetime, reason }, call) => {
37
+ return await scheduler.book({
38
+ phone: call.from,
39
+ datetime,
40
+ reason,
41
+ });
42
+ },
43
+ });
44
+
45
+ const agent = pc.agent("support", {
46
+ prompt: "You are a helpful support agent. Use tools to look up information.",
47
+ llm: "openai/gpt-5-chat-latest",
48
+ voice: "elevenlabs/sarah",
49
+ stt: "deepgram/flux",
50
+ language: "en",
51
+ tools: [lookupOrder, scheduleCallback],
52
+ });
53
+
54
+ agent.on("call.started", (call) => call.say("Hi, how can I help?"));
55
+ ```
56
+
57
+ That's it. When the LLM decides to call `lookupOrder`, the SDK:
58
+
59
+ 1. Parses the arguments through `z.object({ orderId: z.string() })`
60
+ 2. Calls your `execute` function with the validated args + the `Call` object
61
+ 3. Sends the result back to the LLM via `call.toolResult()`
62
+
63
+ ## Tool call lifecycle
64
+
65
+ ![Tool call lifecycle](/assets/diagrams/tool-call-lifecycle.png)
66
+
67
+ ## Ephemeral tools (don't persist the result)
68
+
69
+ By default every tool result is saved to the conversation history — it stays in
70
+ the LLM context for the rest of the call and is written to the persisted
71
+ transcript. That's almost always what you want.
72
+
73
+ Sometimes it isn't. A tool might return a sensitive lookup (a full customer
74
+ record, a one-time code) or a large/noisy payload (a 5 KB JSON blob) that you
75
+ need *for the current reply* but don't want lingering in context or saved to the
76
+ database. Mark such a tool `ephemeral: true`:
77
+
78
+ ```typescript
79
+ const lookupSSN = tool({
80
+ name: "lookupSSN",
81
+ description: "Look up the caller's SSN to verify identity.",
82
+ schema: z.object({ customerId: z.string() }),
83
+ ephemeral: true, // result is used for this reply, then dropped from history
84
+ execute: async ({ customerId }) => ({ ssn: await db.getSSN(customerId) }),
85
+ });
86
+ ```
87
+
88
+ How it works: the result is still sent to the model so it can generate the
89
+ current reply (the API requires every tool call to be followed by its result).
90
+ But once that reply is produced, the server **prunes** the ephemeral result —
91
+ and the originating `tool_calls` entry if all of its calls were ephemeral — from
92
+ the history. It never reaches the next turn's context and is never written to
93
+ the saved transcript. The behavior is identical across voice, chat, and
94
+ WhatsApp.
95
+
96
+ `ephemeral` defaults to `false`, so existing tools are unchanged.
97
+
98
+ ## The `call` parameter
99
+
100
+ Every `execute` function receives the `Call` object as its second argument. Use it to interact with the call mid-tool-execution:
101
+
102
+ ```typescript
103
+ const transferToHuman = tool({
104
+ name: "transferToHuman",
105
+ description: "Escalate to a human agent.",
106
+ schema: z.object({
107
+ department: z.enum(["sales", "support", "billing"]),
108
+ }),
109
+ execute: async ({ department }, call) => {
110
+ const numbers = {
111
+ sales: "+15551110000",
112
+ support: "+15551110001",
113
+ billing: "+15551110002",
114
+ };
115
+ call.say("Of course, let me connect you to a specialist.");
116
+ call.forward(numbers[department]);
117
+ return { transferred: true };
118
+ },
119
+ });
120
+ ```
121
+
122
+ ## Why local functions beat webhooks
123
+
124
+ Other platforms make tools webhook URLs. You define a tool, expose a public endpoint, the platform POSTs to it. The downsides pile up fast:
125
+
126
+ - **You expose a public endpoint** — attack surface, rate limiting, auth headaches
127
+ - **You can't reach internal services** — your DB, your Redis, your hardware
128
+ - **Latency** — every tool call is a network roundtrip across the public internet
129
+ - **Debuggability** — tool call goes out, response comes back, what happened in between?
130
+
131
+ Pinecall tools run in your process. That means:
132
+
133
+ - `await db.query(...)` works directly
134
+ - `await redis.get(...)` works directly
135
+ - `await hardware.openDoor()` works directly (if your process can reach it)
136
+ - Stack traces, breakpoints, and logs work normally
137
+ - No public surface to attack
138
+ - Sub-millisecond "call" overhead — it's a function call, not an HTTP request
139
+
140
+ ## Common patterns
141
+
142
+ ### Database lookups
143
+
144
+ ```typescript
145
+ const findCustomer = tool({
146
+ name: "findCustomer",
147
+ description: "Find a customer by phone number or email.",
148
+ schema: z.object({
149
+ query: z.string().describe("Phone or email"),
150
+ }),
151
+ execute: async ({ query }) => {
152
+ const customer = await db.customers.find({
153
+ or: [{ phone: query }, { email: query }],
154
+ });
155
+ return customer ?? { error: "not_found" };
156
+ },
157
+ });
158
+ ```
159
+
160
+ ### Transfer to human
161
+
162
+ ```typescript
163
+ const transferToHuman = tool({
164
+ name: "transferToHuman",
165
+ description: "Escalate to a human agent when the customer is angry or has a complex issue.",
166
+ schema: z.object({
167
+ department: z.enum(["sales", "support", "billing"]),
168
+ }),
169
+ execute: async ({ department }, call) => {
170
+ const numbers = { sales: "+15551110000", support: "+15551110001", billing: "+15551110002" };
171
+ call.say("Of course, let me connect you to a specialist.");
172
+ call.forward(numbers[department]);
173
+ return { transferred: true };
174
+ },
175
+ });
176
+ ```
177
+
178
+ ### Booking / scheduling
179
+
180
+ ```typescript
181
+ const bookAppointment = tool({
182
+ name: "bookAppointment",
183
+ description: "Book an appointment in the doctor's calendar.",
184
+ schema: z.object({
185
+ datetime: z.string().describe("ISO 8601 datetime"),
186
+ durationMinutes: z.number(),
187
+ patientName: z.string(),
188
+ }),
189
+ execute: async ({ datetime, durationMinutes, patientName }) => {
190
+ const slot = await calendar.book({
191
+ start: new Date(datetime),
192
+ duration: durationMinutes,
193
+ patient: patientName,
194
+ });
195
+ return slot.success
196
+ ? { booked: true, confirmationId: slot.id }
197
+ : { booked: false, error: slot.conflictReason };
198
+ },
199
+ });
200
+ ```
201
+
202
+ ### End the call
203
+
204
+ ```typescript
205
+ const endCall = tool({
206
+ name: "endCall",
207
+ description: "End the call when the customer says goodbye.",
208
+ schema: z.object({}),
209
+ execute: async (_, call) => {
210
+ call.say("Have a great day!");
211
+ call.once("bot.finished", () => call.hangup());
212
+ return { ended: true };
213
+ },
214
+ });
215
+ ```
216
+
217
+ ## Returning errors
218
+
219
+ If a tool call fails, the SDK catches the error and returns `{ error: err.message }` to the LLM automatically. The LLM can then recover (apologize, retry, ask clarifying questions).
220
+
221
+ You can also return errors explicitly:
222
+
223
+ ```typescript
224
+ const lookupOrder = tool({
225
+ name: "lookupOrder",
226
+ description: "Look up an order by ID.",
227
+ schema: z.object({ orderId: z.string() }),
228
+ execute: async ({ orderId }) => {
229
+ const order = await db.orders.findOne(orderId);
230
+ if (!order) return { error: "Order not found" };
231
+ return order;
232
+ },
233
+ });
234
+ ```
235
+
236
+ ## Listening to tool calls (optional)
237
+
238
+ The `llm.toolCall` event still fires for every tool call — useful for logging, metrics, or UI:
239
+
240
+ ```typescript
241
+ agent.on("llm.toolCall", (data, call) => {
242
+ console.log(`Tools called: ${data.toolCalls.map(t => t.name).join(", ")}`);
243
+ });
244
+ ```
245
+
246
+ ## Tools work across all channels
247
+
248
+ The same tools work for phone, WebRTC, chat, and WhatsApp. The `Call` object is your interface regardless of transport.
249
+
250
+ ## What's next
251
+
252
+ - [Hot-reload](/concepts/hot-reload) — change the prompt or tools mid-call
253
+ - [Events reference](/reference/events) — all events including `llm.toolCall`
254
+ - [`Call` API reference](/api/call) — `forward`, `hangup`, etc.
@@ -0,0 +1,200 @@
1
+ ---
2
+ title: "WebRTC in the Browser"
3
+ description: "Embed a Pinecall voice agent in your web app using the React widget."
4
+ ---
5
+
6
+ # WebRTC in the Browser
7
+
8
+ Browser users can talk to your agent directly through WebRTC — no phone number required. This is how voice copilots, in-app assistants, and live demos work.
9
+
10
+ ## Architecture
11
+
12
+ The browser connects **directly** to `voice.pinecall.io` over WebRTC. Your backend's only job is minting short-lived tokens.
13
+
14
+ ![WebRTC browser architecture](/assets/diagrams/webrtc-browser-arch.png)
15
+
16
+ Your backend never proxies audio. The audio path is browser ↔ voice server, peer-to-peer over WebRTC.
17
+
18
+ ## 1. Create the agent
19
+
20
+ WebRTC works automatically for any agent — no channel declaration needed.
21
+
22
+ ```typescript
23
+ import { Pinecall } from "@pinecall/sdk";
24
+
25
+ const pc = new Pinecall({ apiKey: process.env.PINECALL_API_KEY! });
26
+
27
+ const mara = pc.agent("mara", {
28
+ prompt: "You are Mara. Be concise and warm.",
29
+ llm: "openai/gpt-5-chat-latest",
30
+ voice: "elevenlabs/sarah",
31
+ stt: "deepgram/flux",
32
+ language: "es",
33
+ });
34
+
35
+ mara.on("call.started", (call) => call.say("¡Hola!"));
36
+ ```
37
+
38
+ ## 2. Mint tokens from your backend
39
+
40
+ Your token endpoint should be behind your existing auth (session cookie, JWT, OAuth — whatever you use). The endpoint calls `createToken()` and returns the result.
41
+
42
+ ```typescript
43
+ // Express
44
+ app.get("/api/token", authMiddleware, async (req, res) => {
45
+ const token = await mara.createToken("webrtc");
46
+ res.json(token);
47
+ });
48
+ ```
49
+
50
+ ```typescript
51
+ // Next.js App Router
52
+ export async function GET() {
53
+ const session = await getSession();
54
+ if (!session) return new Response("Unauthorized", { status: 401 });
55
+ const token = await mara.createToken("webrtc");
56
+ return Response.json(token);
57
+ }
58
+ ```
59
+
60
+ The response shape:
61
+
62
+ ```json
63
+ {
64
+ "token": "wrtc_abc123...",
65
+ "server": "wss://voice.pinecall.io",
66
+ "expiresIn": 60
67
+ }
68
+ ```
69
+
70
+ Tokens are single-use, scoped to the agent, and expire in 60 seconds. See [Security](/security) for the full security model.
71
+
72
+ ## 3. Drop in the widget
73
+
74
+ ```bash
75
+ npm install @pinecall/web
76
+ ```
77
+
78
+ ```tsx
79
+ import { VoiceWidget } from "@pinecall/web";
80
+
81
+ export default function App() {
82
+ return (
83
+ <VoiceWidget
84
+ agent="mara"
85
+ tokenProvider={async () => {
86
+ const res = await fetch("/api/token", { credentials: "include" });
87
+ return res.json();
88
+ }}
89
+ />
90
+ );
91
+ }
92
+ ```
93
+
94
+ That's the entire frontend. Click the orb, talk, listen.
95
+
96
+ ## Listening for events in the browser
97
+
98
+ Events arrive over the WebRTC DataChannel — you don't need SSE for in-browser UIs. The widget renders its own transcript, and exposes session status plus the full live state via the `useVoice()` hook:
99
+
100
+ ```tsx
101
+ import { VoiceWidget, useVoice } from "@pinecall/web";
102
+
103
+ function Transcript() {
104
+ const { messages, status } = useVoice();
105
+ return messages.map((m) => <p key={m.id}>{m.role}: {m.text}</p>);
106
+ }
107
+
108
+ export default function App() {
109
+ return (
110
+ <VoiceWidget
111
+ agent="mara"
112
+ tokenProvider={getToken}
113
+ onStatusChange={(status) => console.log("Status:", status)}
114
+ >
115
+ <Transcript />
116
+ </VoiceWidget>
117
+ );
118
+ }
119
+ ```
120
+
121
+ For lower-level control, use `@pinecall/web/core` directly — it gives you the raw event stream.
122
+
123
+ ## Custom UI without the widget
124
+
125
+ If the widget doesn't fit your design, build your own UI with `@pinecall/web/core`:
126
+
127
+ ```typescript
128
+ import { VoiceSession } from "@pinecall/web/core";
129
+
130
+ const session = new VoiceSession({
131
+ agent: "mara",
132
+ // Fetch the token from your backend instead of hitting the voice server directly
133
+ tokenProvider: () => fetch("/api/token").then((r) => r.json()),
134
+ });
135
+
136
+ // Re-render whenever the session state changes (messages, status, phase, …)
137
+ session.subscribe(() => {
138
+ const { status, messages } = session.getState();
139
+ console.log("Status:", status, "Last:", messages.at(-1)?.text);
140
+ });
141
+
142
+ // connect() fetches the token (via tokenProvider) and negotiates WebRTC
143
+ await session.connect();
144
+
145
+ // User clicks "End"
146
+ session.disconnect();
147
+ ```
148
+
149
+ ## Skipping the backend for demos
150
+
151
+ For pure demos or prototypes — no backend, no auth — you can opt in to public token access using `allowedOrigins`:
152
+
153
+ ```typescript
154
+ const demo = pc.agent("demo-bot", {
155
+ // ...config
156
+ allowedOrigins: [
157
+ "https://demo.mysite.com",
158
+ "https://*.mysite.com",
159
+ "http://localhost:*",
160
+ ],
161
+ });
162
+ ```
163
+
164
+ Then the widget can fetch tokens directly from the voice server, no backend needed — omit `tokenProvider` and it hits `/webrtc/token` directly:
165
+
166
+ ```tsx
167
+ <VoiceWidget agent="demo-bot" />
168
+ ```
169
+
170
+ > **Warning:** `allowedOrigins` protects against casual embedding but not against a determined attacker (Origin headers can be spoofed from scripts/curl). For production, always use `tokenProvider` with your backend's auth. See [Security](/security).
171
+
172
+ ## Chat channel (text only)
173
+
174
+ Same pattern, different token type. Chat gives you typed conversations without audio:
175
+
176
+ ```typescript
177
+ // Backend
178
+ app.get("/api/chat-token", authMiddleware, async (req, res) => {
179
+ const token = await agent.createToken("chat");
180
+ res.json(token);
181
+ });
182
+ ```
183
+
184
+ Connect from the browser via WebSocket:
185
+
186
+ ```typescript
187
+ const ws = new WebSocket(`${server}/ws?token=${token}`);
188
+ ws.onmessage = (e) => {
189
+ const event = JSON.parse(e.data);
190
+ if (event.event === "chat.token") appendBotToken(event.text); // streaming token
191
+ if (event.event === "chat.done") finishBotMessage(event.text); // final text
192
+ };
193
+ ws.send(JSON.stringify({ event: "message", text: "Hello" }));
194
+ ```
195
+
196
+ ## What's next
197
+
198
+ - [Security](/security) — the full token security model
199
+ - [Multi-tenant](/guides/multi-tenant) — scope tokens per user/tenant
200
+ - [Dev mode](/guides/dev-mode) — slug-based isolation lets every dev have their own agent