howone 0.1.23 → 0.1.26
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 +215 -0
- package/templates/vite/.howone/skills/{howone-sdk → howone}/01-architect/02-manifest-codegen.md +67 -4
- package/templates/vite/.howone/skills/howone/02-database/01-schema-design.md +541 -0
- package/templates/vite/.howone/skills/howone/02-database/02-schema-operations.md +398 -0
- package/templates/vite/.howone/skills/howone/02-database/03-data-access-patterns.md +309 -0
- package/templates/vite/.howone/skills/howone/02-database/04-query-dsl-and-responses.md +237 -0
- package/templates/vite/.howone/skills/howone/02-database/05-ai-persistence-patterns.md +372 -0
- package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/01-client-setup.md +58 -36
- package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/02-entity-operations.md +67 -0
- package/templates/vite/.howone/skills/howone/03-sdk/03-auth.md +414 -0
- package/templates/vite/.howone/skills/howone/03-sdk/04-react-integration.md +191 -0
- package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/07-ai-action-calls.md +168 -64
- package/templates/vite/.howone/skills/howone/03-sdk/08-extension-boundaries.md +226 -0
- package/templates/vite/.howone/skills/howone/04-ai/01-ai-capability-architecture.md +205 -0
- package/templates/vite/.howone/skills/howone/04-ai/02-workflow-contract-rules.md +426 -0
- package/templates/vite/.howone/skills/howone/04-ai/03-ai-sdk-handoff.md +234 -0
- package/templates/vite/.howone/skills/howone/04-ai/04-service-capability-catalog.md +281 -0
- package/templates/vite/.howone/skills/howone/04-ai/05-workflow-operations.md +256 -0
- package/templates/vite/.howone/skills/howone/04-ai/06-ai-feature-playbooks.md +296 -0
- package/templates/vite/.howone/skills/{howone-sdk → howone}/SKILL.md +29 -12
- package/templates/vite/.howone/skills/howone/agents/openai.yaml +4 -0
- package/templates/vite/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +0 -126
- package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +0 -147
- package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +0 -96
- package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +0 -172
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +0 -616
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +0 -398
- package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +0 -1
- package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +0 -142
- package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +0 -169
- package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +0 -80
- package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +0 -4
- /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/05-file-upload.md +0 -0
- /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/06-raw-http.md +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# React Integration
|
|
2
|
+
|
|
3
|
+
## What `@howone/sdk/react` Provides
|
|
4
|
+
|
|
5
|
+
Thin integration layer: auth context plus the HowOne floating brand button. **No** entity hooks,
|
|
6
|
+
AI hooks, toast system, redirect overlay, or app-owned UI.
|
|
7
|
+
|
|
8
|
+
Exports:
|
|
9
|
+
|
|
10
|
+
- `HowOneProvider`
|
|
11
|
+
- `useHowoneContext`
|
|
12
|
+
- `FloatingButton`
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Auth: one SDK config + one Provider flag
|
|
17
|
+
|
|
18
|
+
**Step 1 — `src/lib/sdk.ts` (required):**
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const client = createClient({
|
|
22
|
+
projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
|
|
23
|
+
env: import.meta.env.VITE_HOWONE_ENV,
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Step 2 — Provider:**
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { HowOneProvider } from '@howone/sdk/react'
|
|
31
|
+
import './lib/sdk' // must import before Provider so auth config is registered
|
|
32
|
+
|
|
33
|
+
<HowOneProvider auth="none" brand="visible">
|
|
34
|
+
<App />
|
|
35
|
+
</HowOneProvider>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Layer | Setting | Meaning |
|
|
39
|
+
|-------|---------|---------|
|
|
40
|
+
| `createClient` | `{ projectId, env }` (default **hosted**) | Login/logout → HowOne `/auth` |
|
|
41
|
+
| `createClient` | `auth: 'custom'` | Login/logout → your `loginPath`; APIs still HowOne |
|
|
42
|
+
| `HowOneProvider` | `auth="required"` | Default with hosted — redirect to HowOne login |
|
|
43
|
+
| `HowOneProvider` | `auth="none"` | Use with `auth: 'custom'`; guard routes yourself |
|
|
44
|
+
|
|
45
|
+
Default (HowOne hosted login):
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
createClient({ projectId, env })
|
|
49
|
+
<HowOneProvider auth="required" />
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Custom login page (your UI, HowOne auth APIs, keep HowOne logo unless product asks to hide it):
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
createClient({ projectId, env, auth: 'custom', loginPath: '/login' })
|
|
56
|
+
<HowOneProvider auth="none" brand="visible" />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## HowOneProvider
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<HowOneProvider
|
|
65
|
+
auth="none"
|
|
66
|
+
brand="visible"
|
|
67
|
+
onAuthRedirect={({ mode, returnUrl }) => {
|
|
68
|
+
// App may set its own loading/redirect state here.
|
|
69
|
+
}}
|
|
70
|
+
onAuthStateChange={(state) => {
|
|
71
|
+
// App may update analytics or local UI state here.
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<App />
|
|
75
|
+
</HowOneProvider>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### HowOneProviderProps
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
type HowOneProviderAuth = 'required' | 'optional' | 'none'
|
|
82
|
+
|
|
83
|
+
interface HowOneProviderProps {
|
|
84
|
+
children: React.ReactNode
|
|
85
|
+
projectId?: string // prefer createClient projectId
|
|
86
|
+
auth?: HowOneProviderAuth
|
|
87
|
+
brand?: 'visible' | 'hidden'
|
|
88
|
+
showBrandButton?: boolean
|
|
89
|
+
theme?: 'dark' | 'light' | 'system' | 'inherit'
|
|
90
|
+
onAuthStateChange?: (state: AuthState) => void
|
|
91
|
+
onAuthRedirect?: (info: { mode: 'hosted' | 'custom'; returnUrl: string }) => void
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Important:** Provider `auth` is only a **route guard**. Login/logout URLs come from `createClient({ auth: 'custom' })`.
|
|
96
|
+
|
|
97
|
+
The provider must not render app-owned UI. It does not own toasts, dialogs, pages, custom login UI,
|
|
98
|
+
or redirect overlays. It does keep the bottom-right HowOne logo by default through `FloatingButton`.
|
|
99
|
+
Use `brand="hidden"` or `showBrandButton={false}` only when the product explicitly asks to hide it.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## useHowoneContext
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const { user, token, isAuthenticated, logout } = useHowoneContext()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Logout
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<button onClick={() => void logout()}>Sign out</button>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
With `auth: 'custom'`, `logout()` clears session and navigates to `loginPath` — **not** howone.dev.
|
|
116
|
+
|
|
117
|
+
Equivalent:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
await howone.auth.logout()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Custom login page link
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { useNavigate } from 'react-router-dom'
|
|
127
|
+
import howone from '@/lib/sdk'
|
|
128
|
+
|
|
129
|
+
function Header() {
|
|
130
|
+
const navigate = useNavigate()
|
|
131
|
+
const { isAuthenticated, logout } = useHowoneContext()
|
|
132
|
+
|
|
133
|
+
if (!isAuthenticated) {
|
|
134
|
+
return <button onClick={() => navigate(howone.auth.loginPath)}>Sign in</button>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return <button onClick={() => void logout()}>Sign out</button>
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## FloatingButton
|
|
144
|
+
|
|
145
|
+
The bottom-right HowOne logo is part of the SDK React integration and should remain visible by
|
|
146
|
+
default. It does not replace your login page and does not perform app auth. Hide it only with
|
|
147
|
+
`brand="hidden"` or `showBrandButton={false}`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Protected route pattern
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
function ProtectedPage() {
|
|
155
|
+
const [user, setUser] = useState(null)
|
|
156
|
+
const navigate = useNavigate()
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
howone.me()
|
|
160
|
+
.then(setUser)
|
|
161
|
+
.catch(() => navigate(howone.auth.loginPath, { replace: true }))
|
|
162
|
+
}, [navigate])
|
|
163
|
+
|
|
164
|
+
if (!user) return null
|
|
165
|
+
return <div>Welcome {user.name}</div>
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Common mistakes
|
|
172
|
+
|
|
173
|
+
| Mistake | Fix |
|
|
174
|
+
|---------|-----|
|
|
175
|
+
| Custom UI but no `auth: 'custom'` | Add to `createClient` |
|
|
176
|
+
| `HowOneProvider auth="required"` without custom SDK auth | Hosted redirect to howone.ai |
|
|
177
|
+
| `useHowoneContext` without Provider | Wrap app in `HowOneProvider` |
|
|
178
|
+
| Import Provider before `./lib/sdk` | Import sdk module first |
|
|
179
|
+
| Manual redirect to howone.dev on logout | Use `howone.auth.logout()` |
|
|
180
|
+
| Deleting the bottom-right HowOne logo by default | Keep `brand="visible"` unless explicitly asked to hide it |
|
|
181
|
+
| Expecting SDK toast APIs | Implement visible feedback in the frontend app from callbacks/results |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Import map
|
|
186
|
+
|
|
187
|
+
| Need | Import |
|
|
188
|
+
|------|--------|
|
|
189
|
+
| Provider, context | `@howone/sdk/react` |
|
|
190
|
+
| Client, OTP, OAuth | `@howone/sdk` |
|
|
191
|
+
| App singleton | `@/lib/sdk` default export |
|
|
@@ -62,17 +62,23 @@ type AiActionConfig<TInput, TOutput> = {
|
|
|
62
62
|
```ts
|
|
63
63
|
type AiResult = {
|
|
64
64
|
success: boolean
|
|
65
|
-
|
|
65
|
+
/** Terminal outcome of the run */
|
|
66
|
+
outcome: 'success' | 'credit_insufficient' | 'run_error' | null
|
|
67
|
+
finalResult: Record<string, unknown> | null // the workflow output payload (from run_complete)
|
|
68
|
+
/** Accumulated state data from state_update / state_snapshot events */
|
|
69
|
+
stateData: Record<string, unknown>
|
|
66
70
|
nodeExecutions: Array<{
|
|
67
71
|
nodeName: string
|
|
68
|
-
|
|
72
|
+
agentName?: string
|
|
69
73
|
timestamp: number
|
|
70
74
|
}>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
toolExecutions: Array<{
|
|
76
|
+
toolCallId: string
|
|
77
|
+
toolName: string
|
|
78
|
+
args?: Record<string, unknown>
|
|
79
|
+
result?: unknown
|
|
80
|
+
durationMs?: number
|
|
81
|
+
cost?: number
|
|
76
82
|
timestamp: number
|
|
77
83
|
}>
|
|
78
84
|
totalDuration: number
|
|
@@ -92,14 +98,43 @@ type AiSession = {
|
|
|
92
98
|
|
|
93
99
|
### AiEvent (SSE events)
|
|
94
100
|
|
|
101
|
+
All events share a common base, then carry a typed `payload`:
|
|
102
|
+
|
|
95
103
|
```ts
|
|
96
104
|
type AiEvent = {
|
|
97
|
-
type: string
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
type: string // see channel catalog below
|
|
106
|
+
id: string
|
|
107
|
+
run_id: string
|
|
108
|
+
workflow_id: string
|
|
109
|
+
timestamp: string // ISO 8601 UTC
|
|
110
|
+
seq: number // monotonic ordering within the run
|
|
111
|
+
// Context fields — present when applicable
|
|
112
|
+
node_name?: string
|
|
113
|
+
node_status?: 'pending' | 'running' | 'completed' | 'failed'
|
|
114
|
+
action_step?: number
|
|
115
|
+
agent_name?: string
|
|
116
|
+
action_name?: string
|
|
117
|
+
agent_loop_step?: number
|
|
118
|
+
message_id?: string
|
|
119
|
+
tool_call_id?: string
|
|
120
|
+
payload?: Record<string, unknown>
|
|
100
121
|
}
|
|
101
122
|
```
|
|
102
123
|
|
|
124
|
+
**SSE Channels and event types:**
|
|
125
|
+
|
|
126
|
+
| Channel | Key event types |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `lifecycle` | `run_start`, `run_complete`, `credit_insufficient`, `run_error` |
|
|
129
|
+
| `node` | `node_scheduled`, `node_start`, `node_complete`, `node_failed` |
|
|
130
|
+
| `action` | `action_scheduled`, `action_start`, `action_complete` |
|
|
131
|
+
| `messages` | `ai_message_start`, `ai_message_chunk`, `ai_message_end`, `action_output` |
|
|
132
|
+
| `tools` | `tool_call_start`, `tool_call_end`, `tool_call_error` |
|
|
133
|
+
| `state` | `state_update`, `state_snapshot` |
|
|
134
|
+
| `metadata` | `progress` |
|
|
135
|
+
|
|
136
|
+
Stream terminates after exactly one of: `run_complete`, `credit_insufficient`, or `run_error`.
|
|
137
|
+
|
|
103
138
|
---
|
|
104
139
|
|
|
105
140
|
## Defining AI Actions
|
|
@@ -195,17 +230,17 @@ When an action omits `outputSchema`, `run()` returns the raw `AiResult` executio
|
|
|
195
230
|
|
|
196
231
|
```ts
|
|
197
232
|
const result = await howone.ai.generateStory.run(input, {
|
|
198
|
-
|
|
199
|
-
console.log('chunk:',
|
|
233
|
+
onMessageChunk: (text) => {
|
|
234
|
+
console.log('chunk:', text)
|
|
200
235
|
},
|
|
201
|
-
onNodeStart: (
|
|
202
|
-
console.log(`Node ${
|
|
236
|
+
onNodeStart: (event) => {
|
|
237
|
+
console.log(`Node ${event.node_name} started`)
|
|
203
238
|
},
|
|
204
|
-
|
|
205
|
-
console.log(
|
|
239
|
+
onStateUpdate: (delta) => {
|
|
240
|
+
console.log('State delta:', delta)
|
|
206
241
|
},
|
|
207
|
-
onProgress: (
|
|
208
|
-
setProgress(
|
|
242
|
+
onProgress: (percent) => {
|
|
243
|
+
setProgress(percent)
|
|
209
244
|
},
|
|
210
245
|
onError: (error) => {
|
|
211
246
|
console.error('SSE error:', error)
|
|
@@ -213,13 +248,32 @@ const result = await howone.ai.generateStory.run(input, {
|
|
|
213
248
|
})
|
|
214
249
|
```
|
|
215
250
|
|
|
251
|
+
UI feedback belongs in the frontend app. Do not import or expect SDK toast APIs. Use returned
|
|
252
|
+
promises and callbacks to update app-owned state:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
setStatus({ type: 'loading', message: 'Generating story...' })
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const output = await howone.ai.generateStory.run(input, {
|
|
259
|
+
onProgress: (progress) => setProgress(progress),
|
|
260
|
+
})
|
|
261
|
+
setStatus({ type: 'success', message: 'Story ready', output })
|
|
262
|
+
} catch (error) {
|
|
263
|
+
setStatus({
|
|
264
|
+
type: 'error',
|
|
265
|
+
message: error instanceof Error ? error.message : 'Story generation failed',
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
216
270
|
### stream() — start and control a session
|
|
217
271
|
|
|
218
272
|
```ts
|
|
219
273
|
function startStream(input: GenerateStoryInput) {
|
|
220
274
|
const session = howone.ai.generateStory.stream(input, {
|
|
221
|
-
|
|
222
|
-
setOutput(prev => prev +
|
|
275
|
+
onMessageChunk: (text) => {
|
|
276
|
+
setOutput(prev => prev + text)
|
|
223
277
|
},
|
|
224
278
|
onComplete: (result) => {
|
|
225
279
|
console.log('Done:', result.finalResult)
|
|
@@ -248,17 +302,27 @@ const result = await session.result
|
|
|
248
302
|
async function consumeEvents(input: GenerateStoryInput) {
|
|
249
303
|
for await (const event of howone.ai.generateStory.events(input)) {
|
|
250
304
|
switch (event.type) {
|
|
251
|
-
case '
|
|
252
|
-
|
|
305
|
+
case 'ai_message_chunk':
|
|
306
|
+
// streaming LLM text delta
|
|
307
|
+
setOutput(prev => prev + (event.payload?.content_block?.text ?? event.payload?.content ?? ''))
|
|
253
308
|
break
|
|
254
309
|
case 'node_start':
|
|
255
|
-
console.log('Node started:', event.
|
|
310
|
+
console.log('Node started:', event.node_name)
|
|
311
|
+
break
|
|
312
|
+
case 'tool_call_end':
|
|
313
|
+
console.log('Tool result:', event.tool_call_id, event.payload?.result)
|
|
314
|
+
break
|
|
315
|
+
case 'state_update':
|
|
316
|
+
console.log('State delta:', event.payload?.delta)
|
|
256
317
|
break
|
|
257
|
-
case '
|
|
258
|
-
console.log('
|
|
318
|
+
case 'run_complete':
|
|
319
|
+
console.log('Final result:', event.payload?.result)
|
|
259
320
|
break
|
|
260
|
-
case '
|
|
261
|
-
|
|
321
|
+
case 'credit_insufficient':
|
|
322
|
+
showCreditError(event.payload?.details?.reason)
|
|
323
|
+
break
|
|
324
|
+
case 'run_error':
|
|
325
|
+
showExecutionError(event.payload?.details?.reason)
|
|
262
326
|
break
|
|
263
327
|
}
|
|
264
328
|
}
|
|
@@ -333,37 +397,43 @@ export const analyzeDataInputSchema = z.object({
|
|
|
333
397
|
|
|
334
398
|
```ts
|
|
335
399
|
type SSEExecutionOptions = {
|
|
336
|
-
// Called for every
|
|
337
|
-
onEvent?: (event:
|
|
400
|
+
// Called for every parsed event with its SSE channel
|
|
401
|
+
onEvent?: (event: AiEvent, channel: string) => void
|
|
338
402
|
|
|
339
|
-
//
|
|
340
|
-
|
|
403
|
+
// Lifecycle
|
|
404
|
+
onRunStart?: (event: RunStartEvent) => void
|
|
405
|
+
onRunComplete?: (event: RunCompleteEvent, result: AiResult) => void
|
|
406
|
+
/** Credits / quota exhausted — show top-up UI, NOT a generic error */
|
|
407
|
+
onCreditInsufficient?: (event: CreditInsufficientEvent) => void
|
|
408
|
+
/** Workflow logic failed — show retry/support UI */
|
|
409
|
+
onRunError?: (event: RunErrorEvent) => void
|
|
341
410
|
|
|
342
|
-
//
|
|
343
|
-
|
|
411
|
+
// Nodes
|
|
412
|
+
onNodeStart?: (event: NodeStartEvent) => void
|
|
413
|
+
onNodeComplete?: (event: NodeCompleteEvent) => void
|
|
344
414
|
|
|
345
|
-
//
|
|
346
|
-
|
|
415
|
+
// Messages — streaming LLM text
|
|
416
|
+
onMessageChunk?: (text: string, event: AiMessageChunkEvent) => void
|
|
417
|
+
onMessageEnd?: (event: AiMessageEndEvent) => void
|
|
347
418
|
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
cost: number
|
|
353
|
-
totalCost: number
|
|
354
|
-
timestamp: number
|
|
355
|
-
}) => void
|
|
419
|
+
// Tools
|
|
420
|
+
onToolCallStart?: (event: ToolCallStartEvent) => void
|
|
421
|
+
onToolCallEnd?: (event: ToolCallEndEvent) => void
|
|
422
|
+
onToolCallError?: (event: ToolCallErrorEvent) => void
|
|
356
423
|
|
|
357
|
-
//
|
|
358
|
-
|
|
424
|
+
// State — live output panel updates
|
|
425
|
+
onStateUpdate?: (delta: Record<string, unknown>, event: StateUpdateEvent) => void
|
|
359
426
|
|
|
360
|
-
//
|
|
427
|
+
// Progress percent 0-100
|
|
428
|
+
onProgress?: (percent: number, message?: string) => void
|
|
429
|
+
|
|
430
|
+
// Internal transport log
|
|
361
431
|
onLog?: (message: string) => void
|
|
362
432
|
|
|
363
|
-
// Called
|
|
433
|
+
// Called on any error (credit or execution)
|
|
364
434
|
onError?: (error: Error) => void
|
|
365
435
|
|
|
366
|
-
// Called when the
|
|
436
|
+
// Called when the stream closes (success or error)
|
|
367
437
|
onComplete?: (result: AiResult) => void
|
|
368
438
|
|
|
369
439
|
// Abort signal — connect to an AbortController for cancellation
|
|
@@ -378,6 +448,9 @@ type SSEExecutionOptions = {
|
|
|
378
448
|
}
|
|
379
449
|
```
|
|
380
450
|
|
|
451
|
+
> `onCreditInsufficient` and `onRunError` are **mutually exclusive** terminal events.
|
|
452
|
+
> Do not use a generic `onError` to distinguish them — use the dedicated callbacks.
|
|
453
|
+
|
|
381
454
|
---
|
|
382
455
|
|
|
383
456
|
## AiSchemaValidationError
|
|
@@ -403,28 +476,59 @@ try {
|
|
|
403
476
|
|
|
404
477
|
## AI Result Persistence
|
|
405
478
|
|
|
406
|
-
When AI-generated content should be saved to an entity
|
|
479
|
+
When AI-generated content should be saved to an entity, prefer the SDK persistence helper for
|
|
480
|
+
history-style products. It standardizes the pending-first pattern from `02-database/05-ai-persistence-patterns.md`
|
|
481
|
+
without adding UI behavior.
|
|
407
482
|
|
|
408
483
|
```ts
|
|
409
|
-
|
|
410
|
-
const output = await howone.ai.generateStory.run({
|
|
411
|
-
topic: 'Dragons and magic',
|
|
412
|
-
ageRange: '6-8',
|
|
413
|
-
})
|
|
484
|
+
import { runAiActionAndPersist } from '@howone/sdk'
|
|
414
485
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
486
|
+
const result = await runAiActionAndPersist({
|
|
487
|
+
entity: howone.entities.Generation,
|
|
488
|
+
input: {
|
|
489
|
+
prompt: 'Dragons and magic',
|
|
490
|
+
ageRange: '6-8',
|
|
491
|
+
},
|
|
492
|
+
createPending: (input) => ({
|
|
493
|
+
prompt: input.prompt,
|
|
494
|
+
ageRange: input.ageRange,
|
|
495
|
+
status: 'pending',
|
|
496
|
+
requestedAt: new Date().toISOString(),
|
|
497
|
+
}),
|
|
498
|
+
run: (input) => howone.ai.generateStory.run(input),
|
|
499
|
+
mapCompleted: ({ output }) => ({
|
|
500
|
+
status: 'completed',
|
|
501
|
+
title: output.title,
|
|
502
|
+
content: output.content,
|
|
503
|
+
completedAt: new Date().toISOString(),
|
|
504
|
+
}),
|
|
505
|
+
mapFailed: ({ error }) => ({
|
|
506
|
+
status: 'failed',
|
|
507
|
+
errorMessage: error instanceof Error ? error.message : 'Generation failed',
|
|
508
|
+
}),
|
|
509
|
+
onStateChange: (state) => {
|
|
510
|
+
// app-owned UI callback; SDK does not show toasts
|
|
511
|
+
setGenerationState(state.status)
|
|
512
|
+
},
|
|
425
513
|
})
|
|
426
514
|
```
|
|
427
515
|
|
|
516
|
+
Return shape:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
type AiPersistenceResult<TRecord, TOutput> =
|
|
520
|
+
| { status: 'completed'; record: TRecord; output: TOutput }
|
|
521
|
+
| { status: 'failed'; record: TRecord; error: unknown }
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Rules:
|
|
525
|
+
|
|
526
|
+
- `createPending` must only return fields declared in the entity schema.
|
|
527
|
+
- `mapCompleted` maps durable product fields from AI output to entity update payload.
|
|
528
|
+
- `mapFailed` should persist a failure state if the product shows history or retry.
|
|
529
|
+
- Use `onStateChange` to update app-owned UI; do not add SDK toast behavior.
|
|
530
|
+
- For simple one-shot AI actions that do not need history, call `howone.ai.*.run()` directly.
|
|
531
|
+
|
|
428
532
|
---
|
|
429
533
|
|
|
430
534
|
## React Patterns
|