howone 0.1.11 → 0.1.12

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.
@@ -0,0 +1,489 @@
1
+ # AI Actions
2
+
3
+ ## Core Concepts
4
+
5
+ - `defineAiAction(id, config)` declares a typed AI action from a workflow ID.
6
+ - `defineAiActions({ ... })` groups multiple action definitions.
7
+ - `withAiActions(client, actions)` binds them onto the composed client as `howone.ai.*`.
8
+ - Each bound action exposes `.run()`, `.stream()`, and `.events()`.
9
+ - Input/output are validated at runtime using zod schemas.
10
+
11
+ ---
12
+
13
+ ## Type System
14
+
15
+ ### AiActionConfig
16
+
17
+ ```ts
18
+ type AiActionConfig<TInput, TOutput> = {
19
+ inputSchema?: z.ZodType<TInput> // validates input before calling the workflow
20
+ outputSchema?: z.ZodType<TOutput> // validates the full ExecutionResult envelope (see caution below)
21
+ mode?: 'run' | 'stream' | 'events' // default: supports all three modes
22
+ }
23
+ ```
24
+
25
+ ### AiResult (ExecutionResult)
26
+
27
+ ```ts
28
+ type AiResult = {
29
+ success: boolean
30
+ finalResult: Record<string, unknown> | null // the workflow output payload
31
+ nodeExecutions: Array<{
32
+ nodeName: string
33
+ content: string
34
+ timestamp: number
35
+ }>
36
+ costUpdates: Array<{
37
+ token: number
38
+ totalToken: number
39
+ cost: number
40
+ totalCost: number
41
+ timestamp: number
42
+ }>
43
+ totalDuration: number
44
+ errors: string[]
45
+ }
46
+ ```
47
+
48
+ ### AiSession (for stream)
49
+
50
+ ```ts
51
+ type AiSession = {
52
+ result: Promise<AiResult> // resolves when the stream completes
53
+ cancel: () => void // abort the request (safe to call multiple times)
54
+ signal: AbortSignal
55
+ }
56
+ ```
57
+
58
+ ### AiEvent (SSE events)
59
+
60
+ ```ts
61
+ type AiEvent = {
62
+ type: string
63
+ data?: Record<string, unknown>
64
+ [key: string]: unknown
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Defining AI Actions
71
+
72
+ ### Basic action — input only, output unwrapped manually
73
+
74
+ ```ts
75
+ import { defineAiAction, defineAiActions } from '@howone/sdk'
76
+ import { z } from 'zod'
77
+
78
+ export const generateStoryInputSchema = z.object({
79
+ topic: z.string().min(1),
80
+ ageRange: z.enum(['3-5', '6-8', '9-12']),
81
+ language: z.string().default('en'),
82
+ })
83
+ export type GenerateStoryInput = z.infer<typeof generateStoryInputSchema>
84
+
85
+ export const ai = defineAiActions({
86
+ generateStory: defineAiAction('generateStory', {
87
+ inputSchema: generateStoryInputSchema,
88
+ }),
89
+ })
90
+ ```
91
+
92
+ ### Action with typed output (unwrap finalResult manually)
93
+
94
+ ```ts
95
+ export const generateStoryOutputSchema = z.object({
96
+ title: z.string(),
97
+ content: z.string(),
98
+ summary: z.string(),
99
+ })
100
+ export type GenerateStoryOutput = z.infer<typeof generateStoryOutputSchema>
101
+
102
+ // Do NOT put outputSchema in defineAiAction unless it matches the full
103
+ // AiResult envelope. Instead, cast finalResult after run():
104
+ export const ai = defineAiActions({
105
+ generateStory: defineAiAction('generateStory', {
106
+ inputSchema: generateStoryInputSchema,
107
+ // outputSchema: omit — cast manually
108
+ }),
109
+ })
110
+ ```
111
+
112
+ ### Multiple actions
113
+
114
+ ```ts
115
+ export const ai = defineAiActions({
116
+ generateStory: defineAiAction('generateStory', {
117
+ inputSchema: z.object({ topic: z.string(), language: z.string() }),
118
+ }),
119
+ translateText: defineAiAction('translateText', {
120
+ inputSchema: z.object({ text: z.string(), targetLang: z.string() }),
121
+ }),
122
+ summarizeArticle: defineAiAction('summarizeArticle', {
123
+ inputSchema: z.object({ url: z.string().url(), maxWords: z.number().int().optional() }),
124
+ }),
125
+ analyzeImage: defineAiAction('analyzeImage', {
126
+ inputSchema: z.object({ imageUrl: z.string().url(), prompt: z.string().optional() }),
127
+ }),
128
+ })
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Calling AI Actions
134
+
135
+ ### run() — await the full result
136
+
137
+ ```ts
138
+ import howone, { type GenerateStoryInput, type GenerateStoryOutput } from '@/lib/sdk'
139
+
140
+ async function generateStory(input: GenerateStoryInput) {
141
+ const result = await howone.ai.generateStory.run(input)
142
+ // result is AiResult
143
+
144
+ if (!result.success) {
145
+ throw new Error(result.errors.join(', '))
146
+ }
147
+
148
+ // Cast finalResult to your output type
149
+ const output = result.finalResult as GenerateStoryOutput
150
+ return output
151
+ }
152
+ ```
153
+
154
+ ### run() — with SSE callbacks
155
+
156
+ ```ts
157
+ const result = await howone.ai.generateStory.run(input, {
158
+ onStreamChunk: (chunk) => {
159
+ console.log('chunk:', chunk)
160
+ },
161
+ onNodeStart: (nodeName, content) => {
162
+ console.log(`Node ${nodeName} started`)
163
+ },
164
+ onCostUpdate: (cost) => {
165
+ console.log(`Tokens used: ${cost.totalToken}`)
166
+ },
167
+ onProgress: (progress) => {
168
+ setProgress(progress)
169
+ },
170
+ onError: (error) => {
171
+ console.error('SSE error:', error)
172
+ },
173
+ })
174
+ ```
175
+
176
+ ### stream() — start and control a session
177
+
178
+ ```ts
179
+ function startStream(input: GenerateStoryInput) {
180
+ const session = howone.ai.generateStory.stream(input, {
181
+ onStreamChunk: (chunk) => {
182
+ setOutput(prev => prev + chunk)
183
+ },
184
+ onComplete: (result) => {
185
+ console.log('Done:', result.finalResult)
186
+ },
187
+ onError: (error) => {
188
+ console.error('Error:', error)
189
+ },
190
+ })
191
+
192
+ // session.result is a Promise<AiResult>
193
+ // session.cancel() aborts the stream
194
+ return session
195
+ }
196
+
197
+ // Cancel mid-stream
198
+ const session = startStream(myInput)
199
+ setTimeout(() => session.cancel(), 5000)
200
+
201
+ // Or await the full result via the session
202
+ const result = await session.result
203
+ ```
204
+
205
+ ### events() — async iterable SSE events
206
+
207
+ ```ts
208
+ async function consumeEvents(input: GenerateStoryInput) {
209
+ for await (const event of howone.ai.generateStory.events(input)) {
210
+ switch (event.type) {
211
+ case 'stream_content':
212
+ setOutput(prev => prev + (event.data?.delta ?? ''))
213
+ break
214
+ case 'node_start':
215
+ console.log('Node started:', event.data?.nodeName)
216
+ break
217
+ case 'cost_update':
218
+ console.log('Cost:', event.data?.totalCost)
219
+ break
220
+ case 'complete':
221
+ console.log('Final result:', event.data)
222
+ break
223
+ }
224
+ }
225
+ }
226
+ ```
227
+
228
+ ---
229
+
230
+ ## zod Schema Patterns
231
+
232
+ ### JSON Schema → zod mapping
233
+
234
+ Generate zod from `.howone/ai/manifest.json` inputSchema / outputSchema fields:
235
+
236
+ | JSON Schema type | zod |
237
+ |---|---|
238
+ | `string` | `z.string()` |
239
+ | `number` | `z.number()` |
240
+ | `integer` | `z.number().int()` |
241
+ | `boolean` | `z.boolean()` |
242
+ | `array of string` | `z.array(z.string())` |
243
+ | `array of object` | `z.array(z.object({ ... }))` |
244
+ | `object` | `z.object({ ... })` |
245
+ | `enum` (string) | `z.enum(['a', 'b', 'c'])` |
246
+ | optional field (not in `required[]`) | `.optional()` on the field |
247
+ | field with default | `.default(value)` |
248
+ | nullable field | `.nullable()` |
249
+
250
+ ### Real examples
251
+
252
+ ```ts
253
+ // Simple text generation
254
+ export const summarizeInputSchema = z.object({
255
+ text: z.string().min(1).max(10000),
256
+ maxWords: z.number().int().min(10).max(500).optional(),
257
+ language: z.string().default('en'),
258
+ })
259
+
260
+ // Image analysis
261
+ export const analyzeImageInputSchema = z.object({
262
+ imageUrl: z.string().url(),
263
+ prompt: z.string().optional(),
264
+ outputFormat: z.enum(['json', 'text', 'markdown']).default('json'),
265
+ })
266
+
267
+ // Multi-step generation with options
268
+ export const generatePostInputSchema = z.object({
269
+ topic: z.string().min(1),
270
+ tone: z.enum(['professional', 'casual', 'humorous']),
271
+ platform: z.enum(['twitter', 'linkedin', 'blog']),
272
+ keywords: z.array(z.string()).min(1).max(10),
273
+ includeHashtags: z.boolean().default(true),
274
+ })
275
+
276
+ // Nested object
277
+ export const analyzeDataInputSchema = z.object({
278
+ dataset: z.array(
279
+ z.object({
280
+ id: z.string(),
281
+ value: z.number(),
282
+ label: z.string().optional(),
283
+ })
284
+ ),
285
+ aggregationType: z.enum(['sum', 'average', 'median', 'max', 'min']),
286
+ groupBy: z.string().optional(),
287
+ })
288
+ ```
289
+
290
+ ---
291
+
292
+ ## SSEExecutionOptions — All Callbacks
293
+
294
+ ```ts
295
+ type SSEExecutionOptions = {
296
+ // Called for every raw SSE event
297
+ onEvent?: (event: { type: string; data?: Record<string, unknown> }) => void
298
+
299
+ // Called when a workflow node starts executing
300
+ onNodeStart?: (nodeName: string, content: string) => void
301
+
302
+ // Called with streaming text delta (for LLM text generation nodes)
303
+ onStreamContent?: (delta: string) => void
304
+
305
+ // Called with each raw stream chunk
306
+ onStreamChunk?: (chunk: string) => void
307
+
308
+ // Called when token/cost counters update
309
+ onCostUpdate?: (cost: {
310
+ token: number
311
+ totalToken: number
312
+ cost: number
313
+ totalCost: number
314
+ timestamp: number
315
+ }) => void
316
+
317
+ // Called with an estimated progress value (0–100)
318
+ onProgress?: (progress: number) => void
319
+
320
+ // Called with log messages from workflow nodes
321
+ onLog?: (message: string) => void
322
+
323
+ // Called if an error occurs
324
+ onError?: (error: Error) => void
325
+
326
+ // Called when the workflow completes (same as awaiting run())
327
+ onComplete?: (result: AiResult) => void
328
+
329
+ // Abort signal — connect to an AbortController for cancellation
330
+ signal?: AbortSignal
331
+
332
+ // Limit-exceeded handler (overrides client-level config)
333
+ limitExceeded?: {
334
+ onLimitExceeded?: (context: LimitExceededContext) => void
335
+ showUpgradeToast?: boolean
336
+ upgradeUrl?: string
337
+ }
338
+ }
339
+ ```
340
+
341
+ ---
342
+
343
+ ## AiSchemaValidationError
344
+
345
+ Thrown by `run()` when input or output schema validation fails.
346
+
347
+ ```ts
348
+ import { AiSchemaValidationError } from '@howone/sdk'
349
+
350
+ try {
351
+ const result = await howone.ai.generateStory.run(input)
352
+ } catch (err) {
353
+ if (err instanceof AiSchemaValidationError) {
354
+ console.error('Validation failed:')
355
+ console.error(' Action:', err.actionId) // 'generateStory'
356
+ console.error(' Direction:', err.direction) // 'input' | 'output'
357
+ console.error(' Issues:', err.issues) // [{ path, message, code }]
358
+ }
359
+ }
360
+ ```
361
+
362
+ ---
363
+
364
+ ## AI Result Persistence
365
+
366
+ When AI-generated content should be saved to an entity:
367
+
368
+ ```ts
369
+ // 1. Run the AI action
370
+ const result = await howone.ai.generateStory.run({
371
+ topic: 'Dragons and magic',
372
+ ageRange: '6-8',
373
+ })
374
+
375
+ if (!result.success) throw new Error(result.errors.join(', '))
376
+
377
+ const output = result.finalResult as GenerateStoryOutput
378
+
379
+ // 2. Save to entity
380
+ const saved = await howone.entities.Story.create({
381
+ title: output.title,
382
+ content: output.content,
383
+ authorId: currentUser.id,
384
+ status: 'draft',
385
+ wordCount: output.content.split(' ').length,
386
+ // Track generation metadata
387
+ promptTopic: 'Dragons and magic',
388
+ generatedAt: new Date().toISOString(),
389
+ })
390
+ ```
391
+
392
+ ---
393
+
394
+ ## React Patterns
395
+
396
+ ### One-shot run with loading state
397
+
398
+ ```tsx
399
+ import { useState } from 'react'
400
+ import { AiSchemaValidationError } from '@howone/sdk'
401
+ import howone, { type GenerateStoryInput, type GenerateStoryOutput } from '@/lib/sdk'
402
+
403
+ function GenerateStoryButton({ input }: { input: GenerateStoryInput }) {
404
+ const [loading, setLoading] = useState(false)
405
+ const [result, setResult] = useState<GenerateStoryOutput | null>(null)
406
+ const [error, setError] = useState<string | null>(null)
407
+
408
+ async function handleGenerate() {
409
+ setLoading(true)
410
+ setError(null)
411
+ try {
412
+ const res = await howone.ai.generateStory.run(input)
413
+ if (!res.success) throw new Error(res.errors.join(', '))
414
+ setResult(res.finalResult as GenerateStoryOutput)
415
+ } catch (err) {
416
+ if (err instanceof AiSchemaValidationError) {
417
+ setError(`Validation error: ${err.issues.map(i => i.message).join(', ')}`)
418
+ } else {
419
+ setError(err instanceof Error ? err.message : 'Unknown error')
420
+ }
421
+ } finally {
422
+ setLoading(false)
423
+ }
424
+ }
425
+
426
+ return (
427
+ <>
428
+ <button onClick={handleGenerate} disabled={loading}>
429
+ {loading ? 'Generating...' : 'Generate Story'}
430
+ </button>
431
+ {result && <div>{result.title}</div>}
432
+ {error && <div className="error">{error}</div>}
433
+ </>
434
+ )
435
+ }
436
+ ```
437
+
438
+ ### Streaming with live text output
439
+
440
+ ```tsx
441
+ import { useRef, useState } from 'react'
442
+ import howone, { type GenerateStoryInput } from '@/lib/sdk'
443
+ import type { AiSession } from '@howone/sdk'
444
+
445
+ function StreamingStoryGenerator({ input }: { input: GenerateStoryInput }) {
446
+ const [text, setText] = useState('')
447
+ const [streaming, setStreaming] = useState(false)
448
+ const sessionRef = useRef<AiSession | null>(null)
449
+
450
+ function startGeneration() {
451
+ setText('')
452
+ setStreaming(true)
453
+
454
+ sessionRef.current = howone.ai.generateStory.stream(input, {
455
+ onStreamChunk: (chunk) => setText(prev => prev + chunk),
456
+ onComplete: () => setStreaming(false),
457
+ onError: (err) => {
458
+ console.error(err)
459
+ setStreaming(false)
460
+ },
461
+ })
462
+ }
463
+
464
+ function cancelGeneration() {
465
+ sessionRef.current?.cancel()
466
+ setStreaming(false)
467
+ }
468
+
469
+ return (
470
+ <>
471
+ <button onClick={startGeneration} disabled={streaming}>Start</button>
472
+ <button onClick={cancelGeneration} disabled={!streaming}>Cancel</button>
473
+ <pre>{text}</pre>
474
+ </>
475
+ )
476
+ }
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Common Mistakes
482
+
483
+ | Mistake | Correct Pattern |
484
+ |---|---|
485
+ | `howone.ai.run.generateStory(input)` | `howone.ai.generateStory.run(input)` |
486
+ | Action named `run`, `stream`, or `events` | Rename to e.g. `executeWorkflow`, `streamContent` |
487
+ | `outputSchema: z.object({ title: z.string() })` expecting `finalResult` shape | Omit `outputSchema`; cast `result.finalResult` manually |
488
+ | Not checking `result.success` before using `finalResult` | Always check `if (!result.success) throw new Error(...)` |
489
+ | Calling `howone.ai.generateStory.run(input)` inside JSX render | Move to event handler or useEffect |