@pikku/skills 0.12.4 → 0.12.8

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 (65) hide show
  1. package/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +74 -33
  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 +45 -10
  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 +75 -10
  14. package/skills/pikku-config/SKILL.md +56 -14
  15. package/skills/pikku-cron/SKILL.md +13 -6
  16. package/skills/pikku-deploy-azure/SKILL.md +83 -28
  17. package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
  18. package/skills/pikku-deploy-express/SKILL.md +40 -4
  19. package/skills/pikku-deploy-fastify/SKILL.md +22 -1
  20. package/skills/pikku-deploy-lambda/SKILL.md +99 -19
  21. package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
  22. package/skills/pikku-deploy-uws/SKILL.md +54 -1
  23. package/skills/pikku-deps/SKILL.md +29 -8
  24. package/skills/pikku-emails/SKILL.md +36 -5
  25. package/skills/pikku-fabric/SKILL.md +27 -2
  26. package/skills/pikku-fabric-debug/SKILL.md +5 -1
  27. package/skills/pikku-feature/SKILL.md +6 -1
  28. package/skills/pikku-gateway-slack/SKILL.md +72 -11
  29. package/skills/pikku-http/SKILL.md +18 -5
  30. package/skills/pikku-http/references/http-options.md +10 -5
  31. package/skills/pikku-i18n/SKILL.md +18 -7
  32. package/skills/pikku-info/SKILL.md +18 -8
  33. package/skills/pikku-jose/SKILL.md +35 -6
  34. package/skills/pikku-knowledge/SKILL.md +50 -7
  35. package/skills/pikku-kysely/SKILL.md +78 -15
  36. package/skills/pikku-machine-auth/SKILL.md +36 -1
  37. package/skills/pikku-mcp/SKILL.md +159 -149
  38. package/skills/pikku-middleware/SKILL.md +17 -5
  39. package/skills/pikku-mongodb/SKILL.md +10 -2
  40. package/skills/pikku-n8n-import/SKILL.md +14 -6
  41. package/skills/pikku-permissions/SKILL.md +102 -22
  42. package/skills/pikku-pino/SKILL.md +12 -4
  43. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  44. package/skills/pikku-queue/SKILL.md +45 -16
  45. package/skills/pikku-react/SKILL.md +41 -14
  46. package/skills/pikku-react-query/SKILL.md +14 -10
  47. package/skills/pikku-realtime/SKILL.md +44 -22
  48. package/skills/pikku-redis/SKILL.md +12 -3
  49. package/skills/pikku-rpc/SKILL.md +23 -12
  50. package/skills/pikku-rtl/SKILL.md +21 -17
  51. package/skills/pikku-scenario/SKILL.md +141 -76
  52. package/skills/pikku-schedule/SKILL.md +39 -6
  53. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  54. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  55. package/skills/pikku-security/SKILL.md +54 -9
  56. package/skills/pikku-services/SKILL.md +49 -9
  57. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  58. package/skills/pikku-template-clone/SKILL.md +10 -5
  59. package/skills/pikku-trigger/SKILL.md +50 -6
  60. package/skills/pikku-versioning/SKILL.md +46 -17
  61. package/skills/pikku-websocket/SKILL.md +72 -44
  62. package/skills/pikku-workflow/SKILL.md +123 -11
  63. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  64. package/skills/pikku-workflows-client/SKILL.md +13 -6
  65. 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.4",
3
+ "version": "0.12.8",
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,66 @@ 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 =
98
+ await secrets.getSecret<GithubCredentials>('GITHUB_CREDENTIALS')
99
+ return { github: new GithubService(creds.reveal()) }
80
100
  }
81
101
  )
82
102
  ```
83
103
 
104
+ `secrets` and `variables` arrive **typed against the addon's own declarations**,
105
+ and a secret is a `SecretValue` — `.reveal()` is the only way to the plaintext
106
+ (see `pikku-config`). `pikkuAddonConfig` is the matching factory for the addon's
107
+ config object.
108
+
84
109
  ### `pikkuAddonWireServices(factory)`
85
110
 
86
111
  Define per-request services for an addon package (created fresh per HTTP request, queue job, etc.):
@@ -104,7 +129,8 @@ export const createWireServices = pikkuAddonWireServices(
104
129
  ### Scaffold
105
130
 
106
131
  ```bash
107
- npx pikku new addon
132
+ npx pikku new addon <name> # name is a required positional
133
+ npx pikku new addon stripe --display-name Stripe --category Payments --dir addons
108
134
  ```
109
135
 
110
136
  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`.
@@ -159,11 +185,24 @@ approvalDescription: async (_services, { title }) => `Add a todo called "${title
159
185
  ### Build
160
186
 
161
187
  ```bash
162
- npx pikku all # Generate types
163
- yarn tsc # Compile TypeScript
164
- cp -r .pikku dist/ # Include generated files in dist
188
+ yarn pikku all # Generate types
189
+ yarn tsc # Compile TypeScript
190
+ cp -r .pikku types dist/ # Ship the generated files and the types they import
191
+ yarn pikku validate # Check the published file set holds together
165
192
  ```
166
193
 
194
+ `yarn pikku`, not `npx pikku`: a scaffolded addon carries `@pikku/cli` as a
195
+ devDependency, and building it against a different CLI than it declares is how
196
+ generated output ends up disagreeing with the packaged one. `npx pikku new
197
+ addon` above is the exception — it runs before the addon, and its CLI, exist.
198
+
199
+ `types/` has to be copied alongside `.pikku`: the generated files import
200
+ `SingletonServices`, `Services`, `Config` and `UserSession` from
201
+ `../../types/application-types.d.js`, and `tsc` never emits a hand-written
202
+ `.d.ts` to `outDir`, so nothing else puts it in `dist`. Leave it out and the
203
+ addon installs fine and fails to typecheck in every app that depends on it —
204
+ which is what `pikku validate` is there to catch before you publish.
205
+
167
206
  ## Consuming an Addon
168
207
 
169
208
  ### Install & Register
@@ -179,7 +218,7 @@ import { wireAddon } from '#pikku'
179
218
  wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
180
219
  ```
181
220
 
182
- After registration, run `npx pikku all` to generate types for the addon's functions.
221
+ After registration, run `yarn pikku all` to generate types for the addon's functions.
183
222
 
184
223
  ### Call via RPC
185
224
 
@@ -195,12 +234,12 @@ export const myFunc = pikkuFunc({
195
234
  ### Wire to HTTP
196
235
 
197
236
  ```typescript
198
- import { wireHTTP, addon } from '#pikku'
237
+ import { wireHTTP, ref } from '#pikku'
199
238
 
200
239
  wireHTTP({
201
240
  method: 'get',
202
241
  route: '/todos',
203
- func: addon('todos:listTodos'),
242
+ func: ref('todos:listTodos'),
204
243
  auth: false,
205
244
  })
206
245
  ```
@@ -208,14 +247,14 @@ wireHTTP({
208
247
  Or batch multiple addon routes with `defineHTTPRoutes` + `wireHTTPRoutes`:
209
248
 
210
249
  ```typescript
211
- import { wireHTTPRoutes, defineHTTPRoutes, addon } from '#pikku'
250
+ import { wireHTTPRoutes, defineHTTPRoutes, ref } from '#pikku'
212
251
 
213
252
  const todoRoutes = defineHTTPRoutes({
214
253
  tags: ['todos'],
215
254
  auth: false,
216
255
  routes: {
217
- list: { method: 'get', route: '/todos', func: addon('todos:listTodos') },
218
- add: { method: 'post', route: '/todos', func: addon('todos:addTodo') },
256
+ list: { method: 'get', route: '/todos', func: ref('todos:listTodos') },
257
+ add: { method: 'post', route: '/todos', func: ref('todos:addTodo') },
219
258
  },
220
259
  })
221
260
 
@@ -225,19 +264,21 @@ wireHTTPRoutes({ basePath: '/api', routes: { todos: todoRoutes } })
225
264
  ### Use in AI Agents
226
265
 
227
266
  ```typescript
228
- import { pikkuAIAgent } from '#pikku'
229
- import { addon } from '#pikku'
267
+ import { pikkuAIAgent } from '#pikku/agent/pikku-agent-types.gen.js'
268
+ import { ref } from '#pikku'
230
269
 
231
270
  export const todoAgent = pikkuAIAgent({
232
271
  name: 'todo-agent',
233
272
  description: 'Manages a todo list',
234
- instructions: 'You help users manage their todos.',
235
- model: 'openai/gpt-4o',
273
+ goal: 'You help users manage their todos.',
274
+ model: 'openai/gpt-5-mini',
236
275
  tools: [
237
- addon('todos:listTodos'),
238
- addon('todos:addTodo'),
239
- addon('todos:deleteTodo'),
276
+ ref('todos:listTodos'),
277
+ ref('todos:addTodo'),
278
+ ref('todos:deleteTodo'),
240
279
  ],
241
280
  maxSteps: 5,
242
281
  })
243
282
  ```
283
+
284
+ 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
  })