@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.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +74 -33
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +45 -10
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +75 -10
- package/skills/pikku-config/SKILL.md +56 -14
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +27 -2
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +6 -1
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-knowledge/SKILL.md +50 -7
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +141 -76
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +123 -11
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- package/skills/pikku-ws/SKILL.md +44 -8
package/package.json
CHANGED
|
@@ -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
|
-
|
|
6
|
-
function sharing. TRIGGER when: code uses wireAddon/
|
|
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, //
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
81
|
+
import { refHTTP } from '#pikku'
|
|
62
82
|
|
|
63
|
-
|
|
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,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
163
|
-
yarn tsc
|
|
164
|
-
cp -r .pikku 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 `
|
|
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,
|
|
237
|
+
import { wireHTTP, ref } from '#pikku'
|
|
199
238
|
|
|
200
239
|
wireHTTP({
|
|
201
240
|
method: 'get',
|
|
202
241
|
route: '/todos',
|
|
203
|
-
func:
|
|
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,
|
|
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:
|
|
218
|
-
add: { method: 'post', route: '/todos', func:
|
|
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 {
|
|
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
|
-
|
|
235
|
-
model: 'openai/gpt-
|
|
273
|
+
goal: 'You help users manage their todos.',
|
|
274
|
+
model: 'openai/gpt-5-mini',
|
|
236
275
|
tools: [
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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,
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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: '
|
|
108
|
-
// { type: '
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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: [
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
###
|
|
201
|
+
### Scaffold the HTTP surface
|
|
136
202
|
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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: [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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 (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
})
|