howone 0.2.3 → 0.2.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/package.json +1 -1
- package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +8 -7
- package/templates/vite/.howone/skills/howone/01-architect/02-manifest-codegen.md +121 -436
- package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +13 -4
- package/templates/vite/.howone/skills/howone/04-app-sdk/01-client-setup.md +94 -261
- package/templates/vite/.howone/skills/howone/04-app-sdk/02-entity-operations.md +85 -465
- package/templates/vite/.howone/skills/howone/04-app-sdk/03-auth.md +11 -7
- package/templates/vite/.howone/skills/howone/04-app-sdk/04-react-integration.md +84 -137
- package/templates/vite/.howone/skills/howone/04-app-sdk/05-file-upload.md +66 -273
- package/templates/vite/.howone/skills/howone/04-app-sdk/06-raw-http.md +72 -249
- package/templates/vite/.howone/skills/howone/04-app-sdk/07-ai-action-calls.md +135 -499
- package/templates/vite/.howone/skills/howone/04-app-sdk/08-ai-manifest-handoff.md +49 -196
- package/templates/vite/.howone/skills/howone/04-app-sdk/09-extension-boundaries.md +4 -4
- package/templates/vite/.howone/skills/howone/04-app-sdk/10-workflow-execute-sse.md +94 -61
- package/templates/vite/.howone/skills/howone/04-app-sdk/11-entity-data-access-patterns.md +4 -3
- package/templates/vite/.howone/skills/howone/SKILL.md +48 -8
- package/templates/vite/.howone/skills/howone/references/common-errors.md +27 -0
- package/templates/vite/.howone/skills/howone/references/version-evidence.md +47 -0
- package/templates/vite/.howone/skills/howone/scripts/verify-project.mjs +151 -0
- package/templates/vite/package.json +1 -1
- package/templates/vite/src/App.tsx +9 -5
- package/templates/vite/src/lib/sdk.ts +7 -5
- package/templates/vite/bun.lock +0 -1478
|
@@ -1,574 +1,210 @@
|
|
|
1
|
-
# AI
|
|
1
|
+
# AI Action Calls
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use this track only after the AI capability has been applied, synced, submitted to EAX, and synced
|
|
4
|
+
again. `.howone/ai/manifest.json` is the source for action IDs, JSON schemas, and workflow config
|
|
5
|
+
UUIDs.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Binding order
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
2. Generate a zod schema from `inputSchema.properties`
|
|
13
|
-
3. Generate a zod schema from `outputSchema.properties` when an output schema exists
|
|
14
|
-
4. Call `defineAiAction(name, { workflowId, inputSchema, outputSchema })` — **`workflowId` is mandatory**
|
|
15
|
-
|
|
16
|
-
Without `workflowId`, the SDK falls back to using the action name as the URL segment. Action names are not UUIDs — the EAX server will reject the call with "invalid input syntax for type uuid".
|
|
17
|
-
|
|
18
|
-
Do not mark required manifest output fields as `.optional()` to silence validation. Do not add
|
|
19
|
-
`.passthrough()` as a workaround for execution envelopes. A typed `run()` validates and returns the
|
|
20
|
-
workflow `finalResult` payload, not the raw execution envelope.
|
|
21
|
-
|
|
22
|
-
## When to Write SDK Bindings
|
|
23
|
-
|
|
24
|
-
**Do NOT write `defineAiAction` until `.howone/ai/manifest.json` contains the workflowId for the capability and the external workflow implementation has been submitted/confirmed by the workflow layer.**
|
|
25
|
-
|
|
26
|
-
Correct sequence:
|
|
27
|
-
1. `ai-capability-design` — design the capability contract
|
|
28
|
-
2. `sync_ai_artifacts` — sync manifest to disk
|
|
29
|
-
3. `external-ai-capability` — submit workflow create/update to EAX from the synced manifest
|
|
30
|
-
4. Re-read `.howone/ai/manifest.json`; update may have rotated `workflowId`
|
|
31
|
-
5. Write `src/lib/sdk.ts` with the current manifest `workflowId`
|
|
32
|
-
|
|
33
|
-
Building without errors does **not** mean the AI workflow binding is correct. A missing `workflowId` causes a runtime UUID error at the EAX execution call.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Core Concepts
|
|
38
|
-
|
|
39
|
-
- `defineAiAction(id, config)` declares a typed AI action from a workflow ID.
|
|
40
|
-
- `defineAiActions({ ... })` groups multiple action definitions.
|
|
41
|
-
- `withAiActions(client, actions)` binds them onto the composed client as `howone.ai.*`.
|
|
42
|
-
- Each bound action exposes `.run()`, `.stream()`, and `.events()`.
|
|
43
|
-
- Input/output are validated at runtime using zod schemas.
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## Type System
|
|
48
|
-
|
|
49
|
-
### AiActionConfig
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
type AiActionConfig<TInput, TOutput> = {
|
|
53
|
-
workflowId: string // REQUIRED — UUID from manifest.json. Without this, SDK uses action name as URL segment (not a UUID → EAX rejects).
|
|
54
|
-
inputSchema?: z.ZodType<TInput> // validates input before calling the workflow
|
|
55
|
-
outputSchema?: z.ZodType<TOutput> // validates the workflow finalResult payload for run()
|
|
56
|
-
mode?: 'run' | 'stream' | 'events' // default: supports all three modes
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### AiResult (ExecutionResult)
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
type AiResult = {
|
|
64
|
-
success: boolean
|
|
65
|
-
runId?: string
|
|
66
|
-
/** Terminal outcome of the run */
|
|
67
|
-
outcome: 'success' | 'credit_insufficient' | 'run_error' | null
|
|
68
|
-
finalResult: Record<string, unknown> | null // run_complete.message
|
|
69
|
-
progressLogs: string[] // progress.message lines
|
|
70
|
-
totalDuration: number
|
|
71
|
-
errors: string[]
|
|
72
|
-
events: AiEvent[]
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### AiSession (for stream)
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
type AiSession = {
|
|
80
|
-
result: Promise<AiResult> // resolves when the stream completes
|
|
81
|
-
cancel: () => void // abort the request (safe to call multiple times)
|
|
82
|
-
signal: AbortSignal
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### AiEvent (SSE events)
|
|
87
|
-
|
|
88
|
-
All workflow execute SSE events use the current envelope shape:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
type AiEvent = {
|
|
92
|
-
id: string
|
|
93
|
-
type: 'run_start' | 'progress' | 'run_complete' | 'run_error' | 'credit_insufficient'
|
|
94
|
-
event: AiEvent['type']
|
|
95
|
-
message: string | Record<string, unknown>
|
|
96
|
-
payload?: Record<string, unknown> // present for run_complete as a compatibility alias of message
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Stream terminates after exactly one of: `run_complete`, `credit_insufficient`, or `run_error`.
|
|
101
|
-
For the full wire protocol, read `04-app-sdk/10-workflow-execute-sse.md`.
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## Defining AI Actions
|
|
106
|
-
|
|
107
|
-
### Basic action — always include workflowId from manifest.json
|
|
9
|
+
1. Read the current `.howone/ai/manifest.json`.
|
|
10
|
+
2. Convert its input/output JSON Schemas to Zod without weakening required fields.
|
|
11
|
+
3. Copy the exact manifest `workflowId` UUID.
|
|
12
|
+
4. Define and compose the action in `src/lib/sdk.ts`.
|
|
13
|
+
5. Run the project verifier, typecheck, and build.
|
|
108
14
|
|
|
109
15
|
```ts
|
|
110
16
|
import { defineAiAction, defineAiActions } from '@howone/sdk'
|
|
111
17
|
import { z } from 'zod'
|
|
112
18
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
ageRange: z.enum(['3-5', '6-8', '9-12']),
|
|
117
|
-
language: z.string().default('en'),
|
|
118
|
-
})
|
|
119
|
-
export type GenerateStoryInput = z.infer<typeof generateStoryInputSchema>
|
|
120
|
-
|
|
121
|
-
// workflowId from .howone/ai/manifest.json → capabilities[0].workflowId
|
|
122
|
-
export const ai = defineAiActions({
|
|
123
|
-
generateStory: defineAiAction('generateStory', {
|
|
124
|
-
workflowId: 'd69ab648-2c00-4d94-928e-01bd7b2a5bb2', // ← from manifest.json
|
|
125
|
-
inputSchema: generateStoryInputSchema,
|
|
126
|
-
}),
|
|
127
|
-
})
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Action with typed output
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
export const generateStoryOutputSchema = z.object({
|
|
134
|
-
title: z.string(),
|
|
135
|
-
content: z.string(),
|
|
136
|
-
summary: z.string(),
|
|
19
|
+
export const generateImageInputSchema = z.object({
|
|
20
|
+
prompt: z.string().min(1),
|
|
21
|
+
aspectRatio: z.enum(['1:1', '16:9']).optional(),
|
|
137
22
|
})
|
|
138
|
-
export type GenerateStoryOutput = z.infer<typeof generateStoryOutputSchema>
|
|
139
23
|
|
|
140
|
-
export const
|
|
141
|
-
|
|
142
|
-
workflowId: 'd69ab648-2c00-4d94-928e-01bd7b2a5bb2', // ← from manifest.json
|
|
143
|
-
inputSchema: generateStoryInputSchema,
|
|
144
|
-
outputSchema: generateStoryOutputSchema,
|
|
145
|
-
}),
|
|
24
|
+
export const generateImageOutputSchema = z.object({
|
|
25
|
+
imageUrl: z.string().url(),
|
|
146
26
|
})
|
|
147
|
-
```
|
|
148
27
|
|
|
149
|
-
### Multiple actions — each must have its own workflowId from manifest.json
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
// Each workflowId is the UUID from .howone/ai/manifest.json for that capability
|
|
153
28
|
export const ai = defineAiActions({
|
|
154
|
-
|
|
29
|
+
generateImage: defineAiAction('generateImage', {
|
|
155
30
|
workflowId: 'd69ab648-2c00-4d94-928e-01bd7b2a5bb2',
|
|
156
|
-
inputSchema:
|
|
157
|
-
|
|
158
|
-
translateText: defineAiAction('translateText', {
|
|
159
|
-
workflowId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
160
|
-
inputSchema: z.object({ text: z.string(), targetLang: z.string() }),
|
|
161
|
-
}),
|
|
162
|
-
summarizeArticle: defineAiAction('summarizeArticle', {
|
|
163
|
-
workflowId: 'f9e8d7c6-b5a4-3210-fedc-ba9876543210',
|
|
164
|
-
inputSchema: z.object({ url: z.string().url(), maxWords: z.number().int().optional() }),
|
|
165
|
-
}),
|
|
166
|
-
analyzeImage: defineAiAction('analyzeImage', {
|
|
167
|
-
workflowId: '11223344-5566-7788-99aa-bbccddeeff00',
|
|
168
|
-
inputSchema: z.object({ imageUrl: z.string().url(), prompt: z.string().optional() }),
|
|
31
|
+
inputSchema: generateImageInputSchema,
|
|
32
|
+
outputSchema: generateImageOutputSchema,
|
|
169
33
|
}),
|
|
170
34
|
})
|
|
171
35
|
```
|
|
172
36
|
|
|
173
|
-
|
|
37
|
+
`workflowId` is required and validated as a UUID when the module loads. There is no action-name
|
|
38
|
+
fallback. Never guess, reuse an ID from another project, or continue with a placeholder.
|
|
174
39
|
|
|
175
|
-
|
|
40
|
+
Pass Zod schemas, not manifest JSON objects. With an `outputSchema`, `.run()` validates and returns
|
|
41
|
+
`run_complete.message` directly. Without one, `.run()` returns the raw `ExecutionResult`.
|
|
176
42
|
|
|
177
|
-
##
|
|
43
|
+
## Four execution surfaces
|
|
178
44
|
|
|
179
|
-
### run
|
|
45
|
+
### `run`: exceptions are control flow
|
|
180
46
|
|
|
181
47
|
```ts
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
48
|
+
try {
|
|
49
|
+
const output = await howone.ai.generateImage.run({ prompt })
|
|
50
|
+
setImageUrl(output.imageUrl)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof WorkflowExecutionError) {
|
|
53
|
+
console.error(error.outcome, error.runId, error.result.errors)
|
|
54
|
+
} else if (error instanceof AiSchemaValidationError) {
|
|
55
|
+
console.error(error.direction, error.issues)
|
|
56
|
+
}
|
|
57
|
+
throw error
|
|
188
58
|
}
|
|
189
59
|
```
|
|
190
60
|
|
|
191
|
-
|
|
192
|
-
When an action omits `outputSchema`, `run()` returns the raw `AiResult` execution envelope.
|
|
61
|
+
`run()` rejects for:
|
|
193
62
|
|
|
194
|
-
|
|
63
|
+
- input or output schema validation errors;
|
|
64
|
+
- HTTP/network/abort errors;
|
|
65
|
+
- a `run_error` terminal event;
|
|
66
|
+
- a `credit_insufficient` terminal event;
|
|
67
|
+
- a stream that closes without a terminal event.
|
|
195
68
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
onMessageChunk: (line) => {
|
|
199
|
-
appendLog(line)
|
|
200
|
-
},
|
|
201
|
-
onProgress: (percent, line) => {
|
|
202
|
-
if (line?.startsWith('[DISPLAY]')) setStatus(line.replace('[DISPLAY]', '').trim())
|
|
203
|
-
if (percent === 100) setProgress(100)
|
|
204
|
-
},
|
|
205
|
-
onError: (error) => {
|
|
206
|
-
console.error('SSE error:', error)
|
|
207
|
-
},
|
|
208
|
-
})
|
|
209
|
-
```
|
|
69
|
+
Do not check `result.success` after a failed run; failure does not resolve. A successful typed action
|
|
70
|
+
returns the typed output, not the execution envelope.
|
|
210
71
|
|
|
211
|
-
|
|
212
|
-
promises and callbacks to update app-owned state:
|
|
72
|
+
### `runSafe`: explicit result union
|
|
213
73
|
|
|
214
|
-
|
|
215
|
-
setStatus({ type: 'loading', message: 'Generating story...' })
|
|
74
|
+
Use this when UI code prefers data-flow branching:
|
|
216
75
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
} catch (error) {
|
|
223
|
-
setStatus({
|
|
224
|
-
type: 'error',
|
|
225
|
-
message: error instanceof Error ? error.message : 'Story generation failed',
|
|
226
|
-
})
|
|
76
|
+
```ts
|
|
77
|
+
const result = await howone.ai.generateImage.runSafe({ prompt })
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
setError(result.error.message)
|
|
80
|
+
return
|
|
227
81
|
}
|
|
82
|
+
setImageUrl(result.value.imageUrl)
|
|
228
83
|
```
|
|
229
84
|
|
|
230
|
-
### stream() — start and control a session
|
|
231
|
-
|
|
232
85
|
```ts
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
setOutput(prev => prev + text)
|
|
237
|
-
},
|
|
238
|
-
onComplete: (result) => {
|
|
239
|
-
console.log('Done:', result.finalResult)
|
|
240
|
-
},
|
|
241
|
-
onError: (error) => {
|
|
242
|
-
console.error('Error:', error)
|
|
243
|
-
},
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
// session.result is a Promise<AiResult>
|
|
247
|
-
// session.cancel() aborts the stream
|
|
248
|
-
return session
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Cancel mid-stream
|
|
252
|
-
const session = startStream(myInput)
|
|
253
|
-
setTimeout(() => session.cancel(), 5000)
|
|
254
|
-
|
|
255
|
-
// Or await the full result via the session
|
|
256
|
-
const result = await session.result
|
|
86
|
+
type AiSafeResult<T> =
|
|
87
|
+
| { ok: true; value: T }
|
|
88
|
+
| { ok: false; error: Error }
|
|
257
89
|
```
|
|
258
90
|
|
|
259
|
-
|
|
91
|
+
`runSafe()` catches the same errors that `run()` throws. Do not use it to silently ignore failures.
|
|
92
|
+
|
|
93
|
+
### `stream`: callbacks plus a rejecting result promise
|
|
260
94
|
|
|
261
95
|
```ts
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
}
|
|
96
|
+
const session = howone.ai.generateImage.stream(
|
|
97
|
+
{ prompt },
|
|
98
|
+
{
|
|
99
|
+
onRunStart: () => setPhase('running'),
|
|
100
|
+
onMessageChunk: (line) => appendLog(line),
|
|
101
|
+
onRunComplete: (_event, result) => console.log(result.finalResult),
|
|
102
|
+
onCreditInsufficient: () => setPhase('blocked'),
|
|
103
|
+
onRunError: () => setPhase('failed'),
|
|
104
|
+
onError: (error) => console.error(error),
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const execution = await session.result
|
|
110
|
+
console.log(execution.finalResult)
|
|
111
|
+
} finally {
|
|
112
|
+
// cancel is idempotent and safe during cleanup.
|
|
113
|
+
session.cancel()
|
|
282
114
|
}
|
|
283
115
|
```
|
|
284
116
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
### JSON Schema → zod mapping
|
|
117
|
+
`stream()` starts immediately. `session.result` resolves only on `run_complete`; it rejects on
|
|
118
|
+
terminal workflow errors, transport errors, and abort. For a typed action, `session.result` remains
|
|
119
|
+
the raw execution result because streaming callbacks/events expose protocol data. Use `.run()` when
|
|
120
|
+
the only desired result is the validated typed output.
|
|
290
121
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
| JSON Schema type | zod |
|
|
294
|
-
|---|---|
|
|
295
|
-
| `string` | `z.string()` |
|
|
296
|
-
| `number` | `z.number()` |
|
|
297
|
-
| `integer` | `z.number().int()` |
|
|
298
|
-
| `boolean` | `z.boolean()` |
|
|
299
|
-
| `array of string` | `z.array(z.string())` |
|
|
300
|
-
| `array of object` | `z.array(z.object({ ... }))` |
|
|
301
|
-
| `object` | `z.object({ ... })` |
|
|
302
|
-
| `enum` (string) | `z.enum(['a', 'b', 'c'])` |
|
|
303
|
-
| optional field (not in `required[]`) | `.optional()` on the field |
|
|
304
|
-
| field with default | `.default(value)` |
|
|
305
|
-
| nullable field | `.nullable()` |
|
|
306
|
-
|
|
307
|
-
### Real examples
|
|
122
|
+
### `events`: ordered async iteration
|
|
308
123
|
|
|
309
124
|
```ts
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
imageUrl: z.string().url(),
|
|
320
|
-
prompt: z.string().optional(),
|
|
321
|
-
outputFormat: z.enum(['json', 'text', 'markdown']).default('json'),
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
// Multi-step generation with options
|
|
325
|
-
export const generatePostInputSchema = z.object({
|
|
326
|
-
topic: z.string().min(1),
|
|
327
|
-
tone: z.enum(['professional', 'casual', 'humorous']),
|
|
328
|
-
platform: z.enum(['twitter', 'linkedin', 'blog']),
|
|
329
|
-
keywords: z.array(z.string()).min(1).max(10),
|
|
330
|
-
includeHashtags: z.boolean().default(true),
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
// Nested object
|
|
334
|
-
export const analyzeDataInputSchema = z.object({
|
|
335
|
-
dataset: z.array(
|
|
336
|
-
z.object({
|
|
337
|
-
id: z.string(),
|
|
338
|
-
value: z.number(),
|
|
339
|
-
label: z.string().optional(),
|
|
340
|
-
})
|
|
341
|
-
),
|
|
342
|
-
aggregationType: z.enum(['sum', 'average', 'median', 'max', 'min']),
|
|
343
|
-
groupBy: z.string().optional(),
|
|
344
|
-
})
|
|
125
|
+
try {
|
|
126
|
+
for await (const event of howone.ai.generateImage.events({ prompt })) {
|
|
127
|
+
if (event.type === 'progress') appendLog(String(event.message))
|
|
128
|
+
if (event.type === 'run_complete') setOutput(event.message)
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// On run_error/credit_insufficient, the terminal event was yielded before this rejection.
|
|
132
|
+
setError(error instanceof Error ? error.message : String(error))
|
|
133
|
+
}
|
|
345
134
|
```
|
|
346
135
|
|
|
347
|
-
|
|
136
|
+
The iterable owns and cleans up its session. On terminal workflow failure it yields the terminal
|
|
137
|
+
event first, then rejects on the next iteration step. Breaking out of the loop cancels the request.
|
|
348
138
|
|
|
349
|
-
##
|
|
139
|
+
## Callback semantics
|
|
350
140
|
|
|
351
141
|
```ts
|
|
352
|
-
type
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// Lifecycle
|
|
142
|
+
type AiOptions = {
|
|
143
|
+
onEvent?: (event: AiEvent, channel: 'workflow') => void
|
|
144
|
+
onDisplayEvent?: (event: WorkflowEventDisplay) => void
|
|
357
145
|
onRunStart?: (event: RunStartEvent) => void
|
|
358
|
-
onRunComplete?: (event: RunCompleteEvent, result: AiResult) => void
|
|
359
|
-
/** Credits / quota exhausted — show top-up UI, NOT a generic error */
|
|
360
|
-
onCreditInsufficient?: (event: CreditInsufficientEvent) => void
|
|
361
|
-
/** Workflow logic failed — show retry/support UI */
|
|
362
|
-
onRunError?: (event: RunErrorEvent) => void
|
|
363
|
-
|
|
364
|
-
// Progress log lines from progress.message
|
|
365
146
|
onMessageChunk?: (text: string, event: ProgressEvent) => void
|
|
366
|
-
|
|
367
|
-
// Progress compatibility callback: progress lines fire as (0, message), success fires as (100)
|
|
368
147
|
onProgress?: (percent: number, message?: string) => void
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
// Called on any error (credit or execution)
|
|
374
|
-
onError?: (error: Error) => void
|
|
375
|
-
|
|
376
|
-
// Called when the stream closes (success or error)
|
|
148
|
+
onRunComplete?: (event: RunCompleteEvent, result: AiResult) => void
|
|
149
|
+
onRunError?: (event: RunErrorEvent) => void
|
|
150
|
+
onCreditInsufficient?: (event: CreditInsufficientEvent) => void
|
|
377
151
|
onComplete?: (result: AiResult) => void
|
|
378
|
-
|
|
379
|
-
// Abort signal — connect to an AbortController for cancellation
|
|
152
|
+
onError?: (error: Error) => void
|
|
380
153
|
signal?: AbortSignal
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
limitExceeded?: {
|
|
384
|
-
onLimitExceeded?: (context: LimitExceededContext) => void
|
|
385
|
-
showUpgradeToast?: boolean
|
|
386
|
-
upgradeUrl?: string
|
|
387
|
-
}
|
|
154
|
+
maxRetainedEvents?: number
|
|
155
|
+
maxRetainedProgressLogs?: number
|
|
388
156
|
}
|
|
389
157
|
```
|
|
390
158
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
159
|
+
Use `onMessageChunk`; old names such as `onStreamChunk`, `onTextChunk`, or legacy multi-channel
|
|
160
|
+
callbacks do not exist. `onEvent` observes every event before terminal-specific callbacks and before
|
|
161
|
+
the result promise rejects. `onComplete` means a terminal event was assembled, including an error
|
|
162
|
+
terminal; inspect `result.outcome`. `onError` then receives the structured error.
|
|
395
163
|
|
|
396
|
-
|
|
164
|
+
Callbacks receive every event even when retained arrays are bounded. Defaults retain at most 1,000
|
|
165
|
+
raw events and 500 progress log lines, preventing long workflows from growing memory without bound.
|
|
397
166
|
|
|
398
|
-
##
|
|
167
|
+
## Error classes
|
|
399
168
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const result = await howone.ai.generateStory.run(input)
|
|
407
|
-
} catch (err) {
|
|
408
|
-
if (err instanceof AiSchemaValidationError) {
|
|
409
|
-
console.error('Validation failed:')
|
|
410
|
-
console.error(' Action:', err.actionId) // 'generateStory'
|
|
411
|
-
console.error(' Direction:', err.direction) // 'input' | 'output'
|
|
412
|
-
console.error(' Issues:', err.issues) // [{ path, message, code }]
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
```
|
|
169
|
+
| Error | Meaning |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `AiWorkflowConfigurationError` | binding has a missing/non-UUID workflow ID |
|
|
172
|
+
| `AiSchemaConfigurationError` | schema does not implement Zod `safeParse` |
|
|
173
|
+
| `AiSchemaValidationError` | input or final output violates the schema |
|
|
174
|
+
| `WorkflowExecutionError` | EAX emitted `run_error` or `credit_insufficient` |
|
|
416
175
|
|
|
417
|
-
|
|
176
|
+
For credit failures, use `error.outcome === 'credit_insufficient'`. SDK limit callbacks are signals;
|
|
177
|
+
the app owns visible upgrade UI. The SDK does not call `window.confirm` unless legacy
|
|
178
|
+
`showUpgradeToast: true` is explicitly enabled.
|
|
418
179
|
|
|
419
|
-
##
|
|
180
|
+
## Persistence
|
|
420
181
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
182
|
+
For history-style products, create a pending entity before calling AI and persist completed/failed
|
|
183
|
+
state with `runAiActionAndPersist()`. Keep workflow execution free of database CRUD; entity writes
|
|
184
|
+
belong to app code.
|
|
424
185
|
|
|
425
186
|
```ts
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const result = await runAiActionAndPersist({
|
|
187
|
+
await runAiActionAndPersist({
|
|
429
188
|
entity: howone.entities.Generation,
|
|
430
|
-
input
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
},
|
|
434
|
-
createPending: (input) => ({
|
|
435
|
-
prompt: input.prompt,
|
|
436
|
-
ageRange: input.ageRange,
|
|
437
|
-
status: 'pending',
|
|
438
|
-
requestedAt: new Date().toISOString(),
|
|
439
|
-
}),
|
|
440
|
-
run: (input) => howone.ai.generateStory.run(input),
|
|
441
|
-
mapCompleted: ({ output }) => ({
|
|
442
|
-
status: 'completed',
|
|
443
|
-
title: output.title,
|
|
444
|
-
content: output.content,
|
|
445
|
-
completedAt: new Date().toISOString(),
|
|
446
|
-
}),
|
|
189
|
+
input,
|
|
190
|
+
createPending: ({ prompt }) => ({ prompt, status: 'pending' }),
|
|
191
|
+
run: (value) => howone.ai.generateImage.run(value),
|
|
192
|
+
mapCompleted: ({ output }) => ({ status: 'completed', imageUrl: output.imageUrl }),
|
|
447
193
|
mapFailed: ({ error }) => ({
|
|
448
194
|
status: 'failed',
|
|
449
|
-
errorMessage: error instanceof Error ? error.message :
|
|
195
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
450
196
|
}),
|
|
451
|
-
onStateChange: (state) => {
|
|
452
|
-
// app-owned UI callback; SDK does not show toasts
|
|
453
|
-
setGenerationState(state.status)
|
|
454
|
-
},
|
|
455
197
|
})
|
|
456
198
|
```
|
|
457
199
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
```ts
|
|
461
|
-
type AiPersistenceResult<TRecord, TOutput> =
|
|
462
|
-
| { status: 'completed'; record: TRecord; output: TOutput }
|
|
463
|
-
| { status: 'failed'; record: TRecord; error: unknown }
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
Rules:
|
|
467
|
-
|
|
468
|
-
- `createPending` must only return fields declared in the entity schema.
|
|
469
|
-
- `mapCompleted` maps durable product fields from AI output to entity update payload.
|
|
470
|
-
- `mapFailed` should persist a failure state if the product shows history or retry.
|
|
471
|
-
- Use `onStateChange` to update app-owned UI; do not add SDK toast behavior.
|
|
472
|
-
- For simple one-shot AI actions that do not need history, call `howone.ai.*.run()` directly.
|
|
473
|
-
|
|
474
|
-
---
|
|
475
|
-
|
|
476
|
-
## React Patterns
|
|
477
|
-
|
|
478
|
-
### One-shot run with loading state
|
|
479
|
-
|
|
480
|
-
```tsx
|
|
481
|
-
import { useState } from 'react'
|
|
482
|
-
import { AiSchemaValidationError } from '@howone/sdk'
|
|
483
|
-
import howone, { type GenerateStoryInput, type GenerateStoryOutput } from '@/lib/sdk'
|
|
484
|
-
|
|
485
|
-
function GenerateStoryButton({ input }: { input: GenerateStoryInput }) {
|
|
486
|
-
const [loading, setLoading] = useState(false)
|
|
487
|
-
const [result, setResult] = useState<GenerateStoryOutput | null>(null)
|
|
488
|
-
const [error, setError] = useState<string | null>(null)
|
|
489
|
-
|
|
490
|
-
async function handleGenerate() {
|
|
491
|
-
setLoading(true)
|
|
492
|
-
setError(null)
|
|
493
|
-
try {
|
|
494
|
-
const output = await howone.ai.generateStory.run(input)
|
|
495
|
-
setResult(output)
|
|
496
|
-
} catch (err) {
|
|
497
|
-
if (err instanceof AiSchemaValidationError) {
|
|
498
|
-
setError(`Validation error: ${err.issues.map(i => i.message).join(', ')}`)
|
|
499
|
-
} else {
|
|
500
|
-
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
501
|
-
}
|
|
502
|
-
} finally {
|
|
503
|
-
setLoading(false)
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return (
|
|
508
|
-
<>
|
|
509
|
-
<button onClick={handleGenerate} disabled={loading}>
|
|
510
|
-
{loading ? 'Generating...' : 'Generate Story'}
|
|
511
|
-
</button>
|
|
512
|
-
{result && <div>{result.title}</div>}
|
|
513
|
-
{error && <div className="error">{error}</div>}
|
|
514
|
-
</>
|
|
515
|
-
)
|
|
516
|
-
}
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### Streaming with live text output
|
|
520
|
-
|
|
521
|
-
```tsx
|
|
522
|
-
import { useRef, useState } from 'react'
|
|
523
|
-
import howone, { type GenerateStoryInput } from '@/lib/sdk'
|
|
524
|
-
import type { AiSession } from '@howone/sdk'
|
|
525
|
-
|
|
526
|
-
function StreamingStoryGenerator({ input }: { input: GenerateStoryInput }) {
|
|
527
|
-
const [text, setText] = useState('')
|
|
528
|
-
const [streaming, setStreaming] = useState(false)
|
|
529
|
-
const sessionRef = useRef<AiSession | null>(null)
|
|
530
|
-
|
|
531
|
-
function startGeneration() {
|
|
532
|
-
setText('')
|
|
533
|
-
setStreaming(true)
|
|
534
|
-
|
|
535
|
-
sessionRef.current = howone.ai.generateStory.stream(input, {
|
|
536
|
-
onStreamChunk: (chunk) => setText(prev => prev + chunk),
|
|
537
|
-
onComplete: () => setStreaming(false),
|
|
538
|
-
onError: (err) => {
|
|
539
|
-
console.error(err)
|
|
540
|
-
setStreaming(false)
|
|
541
|
-
},
|
|
542
|
-
})
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function cancelGeneration() {
|
|
546
|
-
sessionRef.current?.cancel()
|
|
547
|
-
setStreaming(false)
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return (
|
|
551
|
-
<>
|
|
552
|
-
<button onClick={startGeneration} disabled={streaming}>Start</button>
|
|
553
|
-
<button onClick={cancelGeneration} disabled={!streaming}>Cancel</button>
|
|
554
|
-
<pre>{text}</pre>
|
|
555
|
-
</>
|
|
556
|
-
)
|
|
557
|
-
}
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
---
|
|
561
|
-
|
|
562
|
-
## Common Mistakes
|
|
200
|
+
## Common mistakes
|
|
563
201
|
|
|
564
|
-
| Mistake |
|
|
202
|
+
| Mistake | Fix |
|
|
565
203
|
|---|---|
|
|
566
|
-
|
|
|
567
|
-
|
|
|
568
|
-
|
|
|
569
|
-
| `
|
|
570
|
-
|
|
|
571
|
-
|
|
|
572
|
-
|
|
|
573
|
-
| Reading `raw.finalResult`, `raw.data.result`, or `raw.result` after a typed `.run()` | Use the returned value directly when `outputSchema` is configured |
|
|
574
|
-
| Calling `howone.ai.generateStory.run(input)` inside JSX render | Move to event handler or useEffect |
|
|
204
|
+
| Omitting `workflowId` | Re-read the synced manifest; module initialization now throws. |
|
|
205
|
+
| Adding `mode: 'run'` to the action | Every action exposes run/runSafe/stream/events; no mode option exists. |
|
|
206
|
+
| Reading `.finalResult` from a typed `.run()` | Use the returned typed output directly. |
|
|
207
|
+
| Expecting `stream.result` to be typed output | It is the raw execution result. |
|
|
208
|
+
| Swallowing `session.result` | Always await/catch it to avoid unhandled rejection. |
|
|
209
|
+
| Weakening output Zod after validation fails | Fix the manifest/workflow mismatch. |
|
|
210
|
+
| Writing an EAX URL | Set only client `env`; the SDK owns the endpoint mapping. |
|