@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.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +56 -29
- 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 +80 -34
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +82 -10
- package/skills/pikku-concepts/references/concept-mapping.md +2 -2
- package/skills/pikku-config/SKILL.md +134 -52
- 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 +30 -5
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +12 -7
- 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 +3 -3
- 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 +285 -50
- 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-software-archaeology/README.md +16 -6
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
- 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 +35 -1
- 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,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, //
|
|
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
|
-
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,
|
|
223
|
+
import { wireHTTP, ref } from '#pikku'
|
|
199
224
|
|
|
200
225
|
wireHTTP({
|
|
201
226
|
method: 'get',
|
|
202
227
|
route: '/todos',
|
|
203
|
-
func:
|
|
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,
|
|
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:
|
|
218
|
-
add: { method: 'post', route: '/todos', func:
|
|
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 {
|
|
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
|
-
|
|
235
|
-
model: 'openai/gpt-
|
|
259
|
+
goal: 'You help users manage their todos.',
|
|
260
|
+
model: 'openai/gpt-5-mini',
|
|
236
261
|
tools: [
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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,
|
|
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
|
})
|
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
58
|
-
import {
|
|
59
|
-
|
|
60
|
-
const createSingletonServices = pikkuServices(async (config) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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 {
|
|
99
|
+
import { pikkuAIAgent } from '#pikku/agent/pikku-agent-types.gen.js'
|
|
73
100
|
|
|
74
|
-
|
|
101
|
+
export const assistant = pikkuAIAgent({
|
|
75
102
|
name: 'assistant',
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
103
|
+
description: 'Answers questions',
|
|
104
|
+
goal: 'You are a helpful assistant.',
|
|
105
|
+
model: 'openai/gpt-5-mini',
|
|
79
106
|
})
|
|
80
107
|
```
|
|
81
108
|
|
|
82
|
-
|
|
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
|
+
```
|