@pikku/skills 0.12.2 → 0.12.6

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/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +56 -29
  4. package/skills/pikku-ai-agent/SKILL.md +197 -105
  5. package/skills/pikku-ai-vercel/SKILL.md +57 -18
  6. package/skills/pikku-ai-voice/SKILL.md +126 -52
  7. package/skills/pikku-audit/SKILL.md +35 -13
  8. package/skills/pikku-aws/SKILL.md +66 -16
  9. package/skills/pikku-backblaze/SKILL.md +44 -11
  10. package/skills/pikku-better-auth/SKILL.md +80 -34
  11. package/skills/pikku-cli/SKILL.md +67 -18
  12. package/skills/pikku-cli/references/complete-example.md +2 -0
  13. package/skills/pikku-concepts/SKILL.md +82 -10
  14. package/skills/pikku-concepts/references/concept-mapping.md +2 -2
  15. package/skills/pikku-config/SKILL.md +134 -52
  16. package/skills/pikku-cron/SKILL.md +13 -6
  17. package/skills/pikku-deploy-azure/SKILL.md +83 -28
  18. package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
  19. package/skills/pikku-deploy-express/SKILL.md +40 -4
  20. package/skills/pikku-deploy-fastify/SKILL.md +22 -1
  21. package/skills/pikku-deploy-lambda/SKILL.md +99 -19
  22. package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
  23. package/skills/pikku-deploy-uws/SKILL.md +54 -1
  24. package/skills/pikku-deps/SKILL.md +29 -8
  25. package/skills/pikku-emails/SKILL.md +36 -5
  26. package/skills/pikku-fabric/SKILL.md +30 -5
  27. package/skills/pikku-fabric-debug/SKILL.md +5 -1
  28. package/skills/pikku-feature/SKILL.md +12 -7
  29. package/skills/pikku-gateway-slack/SKILL.md +72 -11
  30. package/skills/pikku-http/SKILL.md +18 -5
  31. package/skills/pikku-http/references/http-options.md +10 -5
  32. package/skills/pikku-i18n/SKILL.md +18 -7
  33. package/skills/pikku-info/SKILL.md +18 -8
  34. package/skills/pikku-jose/SKILL.md +35 -6
  35. package/skills/pikku-knowledge/SKILL.md +3 -3
  36. package/skills/pikku-kysely/SKILL.md +78 -15
  37. package/skills/pikku-machine-auth/SKILL.md +36 -1
  38. package/skills/pikku-mcp/SKILL.md +159 -149
  39. package/skills/pikku-middleware/SKILL.md +17 -5
  40. package/skills/pikku-mongodb/SKILL.md +10 -2
  41. package/skills/pikku-n8n-import/SKILL.md +14 -6
  42. package/skills/pikku-permissions/SKILL.md +102 -22
  43. package/skills/pikku-pino/SKILL.md +12 -4
  44. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  45. package/skills/pikku-queue/SKILL.md +45 -16
  46. package/skills/pikku-react/SKILL.md +41 -14
  47. package/skills/pikku-react-query/SKILL.md +14 -10
  48. package/skills/pikku-realtime/SKILL.md +44 -22
  49. package/skills/pikku-redis/SKILL.md +12 -3
  50. package/skills/pikku-rpc/SKILL.md +23 -12
  51. package/skills/pikku-rtl/SKILL.md +21 -17
  52. package/skills/pikku-scenario/SKILL.md +285 -50
  53. package/skills/pikku-schedule/SKILL.md +39 -6
  54. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  55. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  56. package/skills/pikku-security/SKILL.md +54 -9
  57. package/skills/pikku-services/SKILL.md +49 -9
  58. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  59. package/skills/pikku-software-archaeology/README.md +16 -6
  60. package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
  61. package/skills/pikku-template-clone/SKILL.md +10 -5
  62. package/skills/pikku-trigger/SKILL.md +50 -6
  63. package/skills/pikku-versioning/SKILL.md +46 -17
  64. package/skills/pikku-websocket/SKILL.md +72 -44
  65. package/skills/pikku-workflow/SKILL.md +35 -1
  66. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  67. package/skills/pikku-workflows-client/SKILL.md +13 -6
  68. package/skills/pikku-ws/SKILL.md +44 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.2",
3
+ "version": "0.12.6",
4
4
  "description": "The Pikku agent skills — the instruction set coding agents read to build, wire and deploy Pikku projects",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -2,8 +2,8 @@
2
2
  name: pikku-addon
3
3
  description: >-
4
4
  Use when creating or consuming reusable function packages (addons) in Pikku. Covers wireAddon,
5
- addon(), pikkuAddonServices, pikkuAddonWireServices, addon package structure, and cross-project
6
- function sharing. TRIGGER when: code uses wireAddon/addon()/pikkuAddonServices, user asks about
5
+ ref(), pikkuAddonServices, pikkuAddonWireServices, addon package structure, and cross-project
6
+ function sharing. TRIGGER when: code uses wireAddon/ref()/pikkuAddonServices, user asks about
7
7
  addons, reusable function packages, cross-project sharing, or addon package structure. DO NOT
8
8
  TRIGGER when: user asks about internal function composition (use pikku-rpc) or general function
9
9
  definitions (use pikku-concepts).
@@ -46,41 +46,65 @@ wireAddon({
46
46
  name: string, // Namespace for addon functions (e.g. 'todos')
47
47
  package: string, // NPM package name (e.g. '@pikku/addon-todos')
48
48
  rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution
49
- auth?: boolean, // Whether addon functions require authentication
49
+ auth?: boolean, // Require a session for every function in the addon
50
+ mcp?: boolean,
50
51
  tags?: string[], // Tags applied to all addon functions
51
- secretOverrides?: Record<string, string>, // Remap secret names
52
- variableOverrides?: Record<string, string>, // Remap variable names
52
+ scopes?: string[], // Required of every function, on top of its own
53
+ secretOverrides?: Record<string, string>, // Remap secret names
54
+ variableOverrides?: Record<string, string>, // Remap variable names
55
+ credentialOverrides?: Record<string, string>, // Remap credential names
53
56
  })
54
57
  ```
55
58
 
56
- ### `addon(name)`
59
+ **`auth`, `tags` and `scopes` only ever tighten.** `auth: false` is not honoured —
60
+ it would weaken the wiring's own gate — so the addon-level setting can require a
61
+ session but never waive one. The same package wired twice under two namespaces is
62
+ governed by the union of both instances' scopes and tags.
57
63
 
58
- Type-safe reference to an addon function — use when wiring to HTTP, agents, etc.:
64
+ ### `ref(name)`
65
+
66
+ Type-safe reference to a function — local or addon — for use in any wiring. It
67
+ returns a function config that proxies the call via RPC at runtime:
68
+
69
+ ```typescript
70
+ import { ref } from '#pikku'
71
+
72
+ ref('todos:addTodo') // namespace:functionName for an addon function
73
+ ref('myLocalFunc') // a local function by name
74
+ ```
75
+
76
+ There is no `addon()` helper; `ref()` covers both. For an addon that publishes
77
+ **wiring contracts** rather than bare functions, codegen also emits `refHTTP`,
78
+ `refChannel` and `refCLI`, which carry the addon's own route/config metadata:
59
79
 
60
80
  ```typescript
61
- import { addon } from '#pikku'
81
+ import { refHTTP } from '#pikku'
62
82
 
63
- addon('todos:addTodo') // Returns a typed function config
64
- addon('emails:sendEmail') // Namespace:functionName format
83
+ wireHTTP(refHTTP('todos:listTodos', { basePath: '/api' }))
65
84
  ```
66
85
 
67
86
  ### `pikkuAddonServices(factory)`
68
87
 
69
- Define singleton services for an addon package (created once at startup):
88
+ Define singleton services for an addon package (created once at startup). The
89
+ second argument is always present — an addon never falls back to its own logger,
90
+ variables or secrets; the consuming app supplies them:
70
91
 
71
92
  ```typescript
72
93
  import { pikkuAddonServices } from '#pikku'
73
94
 
74
95
  export const createSingletonServices = pikkuAddonServices(
75
- async (config, parentServices?) => {
76
- // parentServices: logger, variables, secrets from the consuming app
77
- return {
78
- myStore: new MyStore(),
79
- }
96
+ async (config, { secrets, logger }) => {
97
+ const creds = await secrets.getSecret<GithubCredentials>('GITHUB_CREDENTIALS')
98
+ return { github: new GithubService(creds.reveal()) }
80
99
  }
81
100
  )
82
101
  ```
83
102
 
103
+ `secrets` and `variables` arrive **typed against the addon's own declarations**,
104
+ and a secret is a `SecretValue` — `.reveal()` is the only way to the plaintext
105
+ (see `pikku-config`). `pikkuAddonConfig` is the matching factory for the addon's
106
+ config object.
107
+
84
108
  ### `pikkuAddonWireServices(factory)`
85
109
 
86
110
  Define per-request services for an addon package (created fresh per HTTP request, queue job, etc.):
@@ -104,7 +128,8 @@ export const createWireServices = pikkuAddonWireServices(
104
128
  ### Scaffold
105
129
 
106
130
  ```bash
107
- npx pikku new addon
131
+ npx pikku new addon <name> # name is a required positional
132
+ npx pikku new addon stripe --display-name Stripe --category Payments --dir addons
108
133
  ```
109
134
 
110
135
  This generates `package.json` (exports `.pikku/*` + `dist/`), `pikku.config.json` (`addon: true`), `tsconfig.json` (`#pikku` path mapping), `src/services.ts`, `src/functions/`, and `types/application-types.d.ts`. For the full file contents/exports you rarely hand-edit, read `references/addon-package-manifest.md`.
@@ -195,12 +220,12 @@ export const myFunc = pikkuFunc({
195
220
  ### Wire to HTTP
196
221
 
197
222
  ```typescript
198
- import { wireHTTP, addon } from '#pikku'
223
+ import { wireHTTP, ref } from '#pikku'
199
224
 
200
225
  wireHTTP({
201
226
  method: 'get',
202
227
  route: '/todos',
203
- func: addon('todos:listTodos'),
228
+ func: ref('todos:listTodos'),
204
229
  auth: false,
205
230
  })
206
231
  ```
@@ -208,14 +233,14 @@ wireHTTP({
208
233
  Or batch multiple addon routes with `defineHTTPRoutes` + `wireHTTPRoutes`:
209
234
 
210
235
  ```typescript
211
- import { wireHTTPRoutes, defineHTTPRoutes, addon } from '#pikku'
236
+ import { wireHTTPRoutes, defineHTTPRoutes, ref } from '#pikku'
212
237
 
213
238
  const todoRoutes = defineHTTPRoutes({
214
239
  tags: ['todos'],
215
240
  auth: false,
216
241
  routes: {
217
- list: { method: 'get', route: '/todos', func: addon('todos:listTodos') },
218
- add: { method: 'post', route: '/todos', func: addon('todos:addTodo') },
242
+ list: { method: 'get', route: '/todos', func: ref('todos:listTodos') },
243
+ add: { method: 'post', route: '/todos', func: ref('todos:addTodo') },
219
244
  },
220
245
  })
221
246
 
@@ -225,19 +250,21 @@ wireHTTPRoutes({ basePath: '/api', routes: { todos: todoRoutes } })
225
250
  ### Use in AI Agents
226
251
 
227
252
  ```typescript
228
- import { pikkuAIAgent } from '#pikku'
229
- import { addon } from '#pikku'
253
+ import { pikkuAIAgent } from '#pikku/agent/pikku-agent-types.gen.js'
254
+ import { ref } from '#pikku'
230
255
 
231
256
  export const todoAgent = pikkuAIAgent({
232
257
  name: 'todo-agent',
233
258
  description: 'Manages a todo list',
234
- instructions: 'You help users manage their todos.',
235
- model: 'openai/gpt-4o',
259
+ goal: 'You help users manage their todos.',
260
+ model: 'openai/gpt-5-mini',
236
261
  tools: [
237
- addon('todos:listTodos'),
238
- addon('todos:addTodo'),
239
- addon('todos:deleteTodo'),
262
+ ref('todos:listTodos'),
263
+ ref('todos:addTodo'),
264
+ ref('todos:deleteTodo'),
240
265
  ],
241
266
  maxSteps: 5,
242
267
  })
243
268
  ```
269
+
270
+ See `pikku-ai-agent` — an addon function is just another `ref()` in `tools`.
@@ -2,9 +2,10 @@
2
2
  name: pikku-ai-agent
3
3
  description: >-
4
4
  Use when building AI agents, chatbots, or LLM-powered assistants with Pikku. Covers
5
- pikkuAIAgent, tool registration, memory, streaming, and agent invocation. TRIGGER when: code
6
- uses pikkuAIAgent/runAIAgent/streamAIAgent, user asks about AI agents, chatbots, LLM assistants,
7
- tool-calling agents, or agent memory/streaming. DO NOT TRIGGER when: user asks about MCP tool
5
+ pikkuAIAgent, ref() tool registration, memory, streaming, tool approval, thread ownership, and
6
+ invocation via rpc.agent. TRIGGER when: code uses pikkuAIAgent/rpc.agent/runAIAgent/
7
+ streamAIAgent, user asks about AI agents, chatbots, LLM assistants, tool-calling agents, agent
8
+ memory/streaming, or `pikku enable agent`. DO NOT TRIGGER when: user asks about MCP tool
8
9
  exposure (use pikku-mcp) or general function definitions (use pikku-concepts).
9
10
  installGroups: [core]
10
11
  ---
@@ -36,16 +37,35 @@ See `pikku-concepts` for the core mental model.
36
37
 
37
38
  ### `pikkuAIAgent(config)`
38
39
 
40
+ Import it from the generated agent types file — `#pikku` does not re-export it:
41
+
39
42
  ```typescript
40
- import { pikkuAIAgent } from '#pikku'
43
+ import { pikkuAIAgent } from '#pikku/agent/pikku-agent-types.gen.js'
44
+ import { ref } from '#pikku/pikku-types.gen.js'
41
45
 
42
46
  pikkuAIAgent({
43
47
  name: string, // Unique agent identifier
44
- description: string, // What the agent does
45
- instructions: string | string[], // System prompt / behavior instructions
46
- model: string, // LLM model (e.g. 'openai/gpt-5-mini')
47
- tools?: PikkuFunc[], // Pikku functions the agent can call
48
- agents?: AIAgentConfig[], // Sub-agents this agent can delegate to
48
+ description: string, // What the agent does (shown in agent listings)
49
+ summary?: string,
50
+ errors?: string[],
51
+
52
+ // --- system prompt: three fields, joined role → personality → goal ---
53
+ role?: string, // Who it is: 'You are a support engineer triaging bugs.'
54
+ personality?: string, // How it sounds: tone, verbosity
55
+ goal: string, // REQUIRED — what it is for
56
+
57
+ model: string, // e.g. 'openai/gpt-5-mini'
58
+ temperature?: number,
59
+ providerOptions?: { // passed through untouched, keyed by provider
60
+ openai?: { reasoningEffort?: 'minimal' | ... },
61
+ },
62
+
63
+ // --- capabilities: all three take ref() handles, not imported values ---
64
+ tools?: unknown[], // ref('todos:addTodo'), ref('graph:sleep'), …
65
+ agents?: unknown[], // sub-agents to delegate to
66
+ workflows?: unknown[], // workflows callable as a tool
67
+ agentMode?: 'delegate' | 'supervise',
68
+
49
69
  memory?: {
50
70
  storage?: string, // Service name for persistence (e.g. 'aiStorage')
51
71
  vector?: string, // Vector store service name
@@ -53,118 +73,189 @@ pikkuAIAgent({
53
73
  lastMessages?: number, // How many messages to retain in context
54
74
  workingMemory?: ZodSchema, // Schema for structured working memory
55
75
  },
76
+
56
77
  maxSteps?: number, // Max tool-call rounds per invocation
57
- temperature?: number, // LLM temperature (0-1)
58
78
  toolChoice?: 'auto' | 'required' | 'none',
59
- input?: ZodSchema, // Input validation schema
60
- output?: ZodSchema, // Output validation schema
61
- tags?: string[], // For grouping and middleware targeting
62
- aiMiddleware?: PikkuAIMiddlewareHooks[], // AI-specific middleware
63
- middleware?: PikkuMiddleware[],
79
+ prepareStep?: (ctx) => void, // See "Narrowing tools per step"
80
+ input?: ZodSchema,
81
+ output?: ZodSchema, // Structured output — only honoured with NO tools
82
+ tags?: string[],
83
+
84
+ sessionScope?: 'user' | 'org', // Who owns this agent's threads. Default 'user'
85
+ auth?: boolean, // Default false — see below
86
+ scopes?: ScopeId[], // AND gate, checked before permissions
64
87
  permissions?: PermissionGroup,
88
+
89
+ middleware?: PikkuMiddleware[],
90
+ channelMiddleware?: PikkuChannelMiddleware[],
91
+ aiMiddleware?: PikkuAIMiddlewareHooks[],
65
92
  })
66
93
  ```
67
94
 
68
- ### `runAIAgent(name, input, options)` — Non-streaming
95
+ **`goal` is the required prompt field, not `instructions`** — there is no
96
+ `instructions` key. `role`/`personality`/`goal` are concatenated in that order,
97
+ and nothing validates which text lands in which, so the split is purely for
98
+ legibility: prose in the "wrong" one still reaches the model.
99
+
100
+ **Tools are `ref('domain:funcName')` handles, not imported function values.** The
101
+ inspector resolves the ref against the generated function map, which is what lets
102
+ an agent reach a function in another package (or a `graph:*` builtin) without an
103
+ import cycle.
104
+
105
+ `auth` defaults to `false` because agents are usually invoked from an
106
+ already-authenticated `pikkuFunc`. `scopes` and `permissions` are enforced either
107
+ way — see `pikku-permissions`.
108
+
109
+ ### Invoking an agent
110
+
111
+ From inside a Pikku function, go through `wire.rpc.agent` — it carries the
112
+ session, credentials, and RPC depth for you:
69
113
 
70
114
  ```typescript
71
- const result = await runAIAgent(
72
- agentName,
73
- {
74
- message: string, // User message
75
- threadId: string, // Conversation thread ID
76
- resourceId: string, // User/resource identifier
77
- },
78
- { singletonServices }
79
- )
115
+ const result = await rpc.agent.run('todo-agent', {
116
+ message, threadId, resourceId, // required
117
+ attachments?, model?, temperature?, context?,
118
+ })
80
119
 
81
- result.text // Agent's text response
82
- result.steps // Array of tool calls made
83
- result.usage // Token usage { inputTokens, outputTokens }
120
+ await rpc.agent.stream('todo-agent', input) // writes to the wire's channel
121
+ await rpc.agent.approve(runId, [{ toolCallId, approved }], expectedAgentName?)
122
+ await rpc.agent.resume(runId, { toolCallId, approved })
123
+ await rpc.agent.interrupt(runId, 'user' | 'speech' | 'timeout')
84
124
  ```
85
125
 
86
- ### `streamAIAgent(name, input, channel, options)` — Streaming
126
+ `context` is a string injected into the system prompt for this request only —
127
+ use it for upfront state (current org, project, deployment) so the agent stops
128
+ asking the user for identifiers it could have been handed.
129
+
130
+ `run` resolves to:
87
131
 
88
132
  ```typescript
89
- await streamAIAgent(
90
- agentName,
91
- {
92
- message: string,
93
- threadId: string,
94
- resourceId: string,
95
- },
96
- channel,
97
- { singletonServices }
98
- )
99
-
100
- // Channel receives events:
101
- // { type: 'step-start', stepNumber: 1 }
102
- // { type: 'text-delta', text: '...' }
103
- // { type: 'reasoning-delta', text: '...' }
133
+ {
134
+ runId, threadId, text,
135
+ object?, // set when the agent has an `output` schema
136
+ steps, // tool calls made
137
+ usage: { inputTokens, outputTokens },
138
+ status?: 'completed' | 'suspended',
139
+ pendingApprovals?: [{ toolCallId, toolName, args, reason?, runId }],
140
+ }
141
+ ```
142
+
143
+ `runAIAgent` / `streamAIAgent` from `@pikku/core/ai-agent` are the layer beneath
144
+ this. Their third argument is `RunAIAgentParams` (`{ sessionService?,
145
+ getCredential?, anonymousOwnerResourceId? }`) — **not** `{ singletonServices }`.
146
+ Reach for them only outside a wired function; inside one, `rpc.agent` is the
147
+ supported path.
148
+
149
+ ### Stream events
150
+
151
+ `rpc.agent.stream` pushes `AIStreamEvent`s onto the channel:
152
+
153
+ ```typescript
154
+ // { type: 'step-start', stepNumber }
155
+ // { type: 'text-delta' | 'reasoning-delta', text }
104
156
  // { type: 'tool-call', toolCallId, toolName, args }
105
157
  // { type: 'tool-result', toolCallId, toolName, result }
106
- // { type: 'agent-call', agentName, session, input }
107
- // { type: 'agent-result', agentName, session, result }
108
- // { type: 'approval-request', toolCallId, toolName, args, reason? }
158
+ // { type: 'agent-call' | 'agent-result', agentName, session, input | result }
159
+ // { type: 'approval-request', toolCallId, toolName, args, reason?, runId? }
160
+ // { type: 'credential-request', toolCallId, toolName, credentialName,
161
+ // credentialType: 'oauth2' | 'apikey', connectUrl?, runId }
109
162
  // { type: 'usage', tokens: { input, output }, model }
163
+ // { type: 'transcript', text } // what the user was heard to say
164
+ // { type: 'audio-delta', data, format, text? } | { type: 'audio-done' }
165
+ // { type: 'data', name, data } | { type: 'generative-ui', spec }
166
+ // { type: 'suspended', reason: 'rpc-missing', missingRpcs }
167
+ // { type: 'interrupted', runId, text, reason }
110
168
  // { type: 'error', message }
111
169
  // { type: 'done' }
112
170
  ```
113
171
 
172
+ Every event except `agent-call`/`agent-result`/`suspended` also carries optional
173
+ `agent` and `session` fields, so a UI can attribute output to a sub-agent rather
174
+ than folding it into the parent's transcript.
175
+
114
176
  ## Usage Patterns
115
177
 
116
178
  ### Define an Agent
117
179
 
118
180
  ```typescript
119
- const todoAssistant = pikkuAIAgent({
120
- name: 'todo-assistant',
121
- description: 'A helpful assistant that manages todos',
122
- instructions:
123
- 'You help users manage their todo lists. Be concise and helpful.',
181
+ import { pikkuAIAgent } from '#pikku/agent/pikku-agent-types.gen.js'
182
+ import { ref } from '#pikku/pikku-types.gen.js'
183
+
184
+ export const todoAgent = pikkuAIAgent({
185
+ name: 'todo-agent',
186
+ description: 'Manages a todo list',
187
+ goal: 'You help users manage their todos. You can list, add, complete and delete them.',
124
188
  model: 'openai/gpt-5-mini',
125
- tools: [listTodos, createTodo, completeTodo],
126
- memory: {
127
- storage: 'aiStorage',
128
- lastMessages: 20,
129
- },
130
- maxSteps: 5,
131
- temperature: 0.7,
189
+ tools: [
190
+ ref('todos:listTodos'),
191
+ ref('todos:addTodo'),
192
+ ref('todos:completeTodo'),
193
+ ref('graph:sleep'),
194
+ ],
195
+ memory: { storage: 'aiStorage', lastMessages: 20 },
196
+ maxSteps: 10,
197
+ toolChoice: 'auto',
132
198
  })
133
199
  ```
134
200
 
135
- ### Invoke Non-Streaming
201
+ ### Scaffold the HTTP surface
136
202
 
137
- ```typescript
138
- const result = await runAIAgent(
139
- 'todo-assistant',
140
- {
141
- message: 'Create a task for tomorrow: buy groceries',
142
- threadId: 'thread-123',
143
- resourceId: 'user-456',
144
- },
145
- { singletonServices }
146
- )
203
+ ```bash
204
+ pikku enable agent # session required
205
+ pikku enable agent --noAuth # public
206
+ ```
207
+
208
+ The next `pikku all` generates `agent.gen.ts` — run/stream/approve/resume
209
+ callers plus thread listing endpoints, with thread ownership already enforced
210
+ against the session. Don't hand-write these routes.
211
+
212
+ ### Structured output
213
+
214
+ An `output` schema fills `result.object`, but **only when the agent exposes no
215
+ tools** — with a tool present the runner falls back to free text, silently. If
216
+ you need both, split the classification into its own tool-free agent.
147
217
 
148
- console.log(result.text) // "I've created a task 'buy groceries' for tomorrow."
149
- console.log(result.steps) // [{ tool: 'createTodo', args: {...}, result: {...} }]
150
- console.log(result.usage) // { inputTokens: 150, outputTokens: 42 }
218
+ ```typescript
219
+ export const structuredAgent = pikkuAIAgent({
220
+ name: 'structured-agent',
221
+ description: 'Classifies a message and returns a structured verdict',
222
+ goal: 'You classify the sentiment of the user message.',
223
+ model: 'openai/gpt-5-mini',
224
+ output: z.object({ sentiment: z.string(), score: z.number() }),
225
+ })
151
226
  ```
152
227
 
153
- ### Stream Responses
228
+ ### Narrowing tools per step
229
+
230
+ `prepareStep` runs before each step with the live tool array for that step, so
231
+ mutating it in place changes what the model is offered from there on. `stop()`
232
+ ends the loop — called before step 0 the run completes with an empty result
233
+ rather than signalling that it was short-circuited.
154
234
 
155
235
  ```typescript
156
- await streamAIAgent(
157
- 'todo-assistant',
158
- {
159
- message: 'Create a task for tomorrow',
160
- threadId: 'thread-123',
161
- resourceId: 'user-456',
162
- },
163
- channel,
164
- { singletonServices }
165
- )
236
+ prepareStep: ({ stepNumber, tools }) => {
237
+ if (stepNumber >= 1) tools.length = 0 // withdraw tools after the first step
238
+ }
166
239
  ```
167
240
 
241
+ ### Tool approval
242
+
243
+ A tool that should pause for a human sets `approvalRequired: true` (with an
244
+ optional `approvalDescription`) on the *function*, not on the agent. The run then
245
+ resolves with `status: 'suspended'` and `pendingApprovals`, and streaming emits
246
+ `approval-request`. Answer with `rpc.agent.approve(runId, approvals)`.
247
+
248
+ Authorization around tools is two-layer: an agent only sees tools its session can
249
+ reach, and the function's own `permissions` still guard the call when the model
250
+ picks one.
251
+
252
+ ### Thread ownership
253
+
254
+ `resourceId` is caller-supplied but never trusted as an owner. The session's
255
+ principal (`userId`, or `orgId` when `sessionScope: 'org'`) is prefixed onto it,
256
+ so a client can sub-partition inside its own boundary and cannot read across one.
257
+ A sessionless run gets an ephemeral anonymous owner instead.
258
+
168
259
  ## Complete Example
169
260
 
170
261
  ```typescript
@@ -190,41 +281,42 @@ export const completeTodo = pikkuFunc({
190
281
  },
191
282
  })
192
283
 
193
- // agents/todo-assistant.ts
194
- const todoAssistant = pikkuAIAgent({
284
+ // agents/todo-assistant.agent.ts
285
+ import { pikkuAIAgent } from '#pikku/agent/pikku-agent-types.gen.js'
286
+ import { ref } from '#pikku/pikku-types.gen.js'
287
+
288
+ export const todoAssistant = pikkuAIAgent({
195
289
  name: 'todo-assistant',
196
290
  description: 'A helpful assistant that manages todos',
197
- instructions: `You help users manage their todo lists.
198
- - Be concise and helpful
291
+ role: 'You are an assistant that manages a user’s todo list.',
292
+ personality: 'Concise. One short paragraph unless asked for detail.',
293
+ goal: `Keep the user's todos accurate.
199
294
  - When creating todos, infer priority if not specified
200
295
  - When listing todos, summarize the results`,
201
296
  model: 'openai/gpt-5-mini',
202
- tools: [listTodos, createTodo, completeTodo],
203
- memory: {
204
- storage: 'aiStorage',
205
- lastMessages: 20,
206
- },
297
+ tools: [
298
+ ref('todos:listTodos'),
299
+ ref('todos:createTodo'),
300
+ ref('todos:completeTodo'),
301
+ ],
302
+ memory: { storage: 'aiStorage', lastMessages: 20 },
207
303
  maxSteps: 5,
208
304
  temperature: 0.7,
209
305
  })
210
306
 
211
- // Wire to HTTP for chat endpoint
307
+ // Wire to HTTP for a chat endpoint — or skip this entirely and run
308
+ // `pikku enable agent`, which scaffolds run/stream/approve/resume for you.
212
309
  wireHTTP({
213
310
  method: 'post',
214
311
  route: '/chat',
215
312
  func: pikkuFunc({
216
313
  title: 'Chat',
217
- func: async (services, { message, threadId }, wire) => {
218
- const { session } = wire
219
- return await runAIAgent(
220
- 'todo-assistant',
221
- {
222
- message,
223
- threadId,
224
- resourceId: session.userId,
225
- },
226
- { singletonServices: services }
227
- )
314
+ func: async (_services, { message, threadId }, { session, rpc }) => {
315
+ return await rpc.agent.run('todo-assistant', {
316
+ message,
317
+ threadId,
318
+ resourceId: session.userId,
319
+ })
228
320
  },
229
321
  }),
230
322
  })
@@ -37,7 +37,9 @@ yarn add @pikku/ai-vercel ai @ai-sdk/openai # or any AI SDK provider
37
37
  import { VercelAIAgentRunner } from '@pikku/ai-vercel'
38
38
 
39
39
  const runner = new VercelAIAgentRunner(
40
- providers: Record<string, any> // Map of provider name → Vercel AI SDK provider instance
40
+ providers: Record<string, any>, // provider name → AI SDK provider
41
+ providerFactory?: (apiKey: string) => Record<string, any>,
42
+ allowedAttachmentHosts?: string[]
41
43
  )
42
44
  ```
43
45
 
@@ -45,8 +47,28 @@ const runner = new VercelAIAgentRunner(
45
47
 
46
48
  - `stream(params: AIAgentRunnerParams, channel: AIStreamChannel): Promise<AIAgentStepResult>` — Stream AI responses with tool calls
47
49
  - `run(params: AIAgentRunnerParams): Promise<AIAgentStepResult>` — Execute a single AI step (non-streaming)
50
+ - `transcribe({ model, audio, … })` / `generateSpeech({ model, text, voice, … })` — what `voiceInput`/`voiceOutput` call; see `pikku-ai-voice`
51
+ - `generateImage`, `embed`, `embedMany`, `rerank` — the remaining AI SDK surfaces
52
+ - `withApiKey(apiKey)` — returns a **new** runner built from `providerFactory`; returns `this` unchanged when no factory was supplied or the key is blank. This is the per-user-credential path
48
53
 
49
- The `providers` map lets you register multiple AI providers. Model strings use `provider:model` format (e.g., `"openai:gpt-4o"`).
54
+ ### Model strings are `provider/model`
55
+
56
+ Slash, not colon: `'openai/gpt-5-mini'`, `'deepinfra/hexgrad/Kokoro-82M'`,
57
+ `'ollama/qwen2.5:7b'`. Only the **first** slash splits, so the model name may
58
+ contain its own. A string with no slash at all throws rather than defaulting to
59
+ a provider.
60
+
61
+ ### The `'*'` catch-all
62
+
63
+ `providers['*']` resolves any provider name with no exact entry, and exact
64
+ entries win — which makes "everything through the gateway except this one"
65
+ expressible as `{ deepinfra: direct, '*': gateway }`. Point it only at something
66
+ that genuinely accepts arbitrary model names (a gateway, or a scripted test
67
+ provider); aimed at a single vendor, an `anthropic/...` string silently reaching
68
+ OpenAI is a bug, not a fallback.
69
+
70
+ `providers` is public and mutable so deploy-time contributors can swap in
71
+ gateway-routed providers after construction.
50
72
 
51
73
  ## Usage Patterns
52
74
 
@@ -54,29 +76,46 @@ The `providers` map lets you register multiple AI providers. Model strings use `
54
76
 
55
77
  ```typescript
56
78
  import { VercelAIAgentRunner } from '@pikku/ai-vercel'
57
- import { openai } from '@ai-sdk/openai'
58
- import { anthropic } from '@ai-sdk/anthropic'
59
-
60
- const createSingletonServices = pikkuServices(async (config) => {
61
- const aiRunner = new VercelAIAgentRunner({
62
- openai: openai,
63
- anthropic: anthropic,
64
- })
65
- return { config, aiRunner }
79
+ import { createOpenAI } from '@ai-sdk/openai'
80
+ import { createAnthropic } from '@ai-sdk/anthropic'
81
+
82
+ const createSingletonServices = pikkuServices(async (config, { secrets }) => {
83
+ const providers: Record<string, any> = {}
84
+ if (await secrets.hasSecret('OPENAI_API_KEY')) {
85
+ providers.openai = createOpenAI({
86
+ apiKey: (await secrets.getSecret('OPENAI_API_KEY')).reveal(),
87
+ })
88
+ }
89
+ return { config, aiAgentRunner: new VercelAIAgentRunner(providers) }
66
90
  })
67
91
  ```
68
92
 
69
- ### With AI Agent Wiring
93
+ The service key is **`aiAgentRunner`** — that is the name the agent wiring looks
94
+ up. Registering it as `aiRunner` leaves every agent unable to call a model.
95
+
96
+ ### With an agent
70
97
 
71
98
  ```typescript
72
- import { wireAIAgent } from '@pikku/core/ai-agent'
99
+ import { pikkuAIAgent } from '#pikku/agent/pikku-agent-types.gen.js'
73
100
 
74
- wireAIAgent({
101
+ export const assistant = pikkuAIAgent({
75
102
  name: 'assistant',
76
- model: 'openai:gpt-4o',
77
- systemPrompt: 'You are a helpful assistant.',
78
- func: myAgentFunc,
103
+ description: 'Answers questions',
104
+ goal: 'You are a helpful assistant.',
105
+ model: 'openai/gpt-5-mini',
79
106
  })
80
107
  ```
81
108
 
82
- The `VercelAIAgentRunner` is used internally by Pikku's AI agent wiring to execute model calls. See `pikku-ai-agent` for wiring details.
109
+ There is no `wireAIAgent` — agents are declared with `pikkuAIAgent` from the
110
+ generated agent types. See `pikku-ai-agent` for the full config.
111
+
112
+ ### Testing without a real provider
113
+
114
+ Replacing the *provider* rather than the runner keeps every code path under test
115
+ real — tool loop, streaming, memory, approvals — and only scripts the replies.
116
+ Sealing it with `'*'` means no model string, including ones added later, can
117
+ reach a live endpoint:
118
+
119
+ ```typescript
120
+ new VercelAIAgentRunner({ '*': createMockLlmProvider() })
121
+ ```