howone 0.1.11 → 0.1.13
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/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/SKILL.md +53 -63
- package/templates/vite/.howone/skills/howone-sdk/references/01-client-setup.md +278 -0
- package/templates/vite/.howone/skills/howone-sdk/references/02-entity-operations.md +379 -0
- package/templates/vite/.howone/skills/howone-sdk/references/03-ai-actions.md +489 -0
- package/templates/vite/.howone/skills/howone-sdk/references/04-auth.md +484 -0
- package/templates/vite/.howone/skills/howone-sdk/references/05-file-upload.md +319 -0
- package/templates/vite/.howone/skills/howone-sdk/references/06-react-integration.md +394 -0
- package/templates/vite/.howone/skills/howone-sdk/references/07-raw-http.md +299 -0
- package/templates/vite/.howone/skills/howone-sdk/references/08-manifest-codegen.md +400 -0
- package/templates/vite/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/references/usage-patterns.md +0 -215
|
@@ -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 |
|