howone 0.1.22 → 0.1.25
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/nextjs/lib/sdk.ts +3 -0
- package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +183 -69
- package/templates/vite/.howone/skills/howone-sdk/01-architect/02-manifest-codegen.md +98 -23
- package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +463 -69
- package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +366 -64
- package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +204 -67
- package/templates/vite/.howone/skills/howone-sdk/02-database/04-query-dsl-and-responses.md +237 -0
- package/templates/vite/.howone/skills/howone-sdk/02-database/05-ai-persistence-patterns.md +372 -0
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/01-client-setup.md +58 -36
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/02-entity-operations.md +67 -0
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +267 -469
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +113 -322
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/07-ai-action-calls.md +95 -48
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/08-extension-boundaries.md +226 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +205 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +426 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +219 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/04-service-capability-catalog.md +281 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/05-workflow-operations.md +256 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/06-ai-feature-playbooks.md +296 -0
- package/templates/vite/.howone/skills/howone-sdk/SKILL.md +83 -15
- package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +2 -2
- package/templates/vite/package.json +1 -1
- package/templates/vite/src/lib/sdk.ts +3 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +0 -1
|
@@ -4,24 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
**`src/lib/sdk.ts` must be generated from `.howone/ai/manifest.json`. Do not write it from memory or from generic examples.**
|
|
6
6
|
|
|
7
|
+
For AI capability and workflow design, read `04-ai/` first. This file is only for app-side SDK
|
|
8
|
+
bindings and runtime calls after the manifest exists.
|
|
9
|
+
|
|
7
10
|
For every capability in `manifest.json`:
|
|
8
11
|
1. Read `name`, `workflowId`, `inputSchema`, `outputSchema`
|
|
9
12
|
2. Generate a zod schema from `inputSchema.properties`
|
|
10
|
-
3.
|
|
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**
|
|
11
15
|
|
|
12
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".
|
|
13
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
|
+
|
|
14
22
|
## When to Write SDK Bindings
|
|
15
23
|
|
|
16
|
-
**Do NOT write `defineAiAction` until `.howone/ai/manifest.json` contains
|
|
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.**
|
|
17
25
|
|
|
18
26
|
Correct sequence:
|
|
19
|
-
1. `
|
|
27
|
+
1. `ai-capability-design` — design the capability contract
|
|
20
28
|
2. `sync_ai_artifacts` — sync manifest to disk
|
|
21
|
-
3. `
|
|
22
|
-
4.
|
|
23
|
-
5. `
|
|
24
|
-
6. Read `.howone/ai/manifest.json` → write `src/lib/sdk.ts` with `workflowId`
|
|
29
|
+
3. `external-ai-capability` — submit workflow create/update to EAX from the synced manifest
|
|
30
|
+
4. Workflow status/background-task layer confirms readiness and captures `workflowConfigID` for future edits
|
|
31
|
+
5. Read `.howone/ai/manifest.json` → write `src/lib/sdk.ts` with `workflowId`
|
|
25
32
|
|
|
26
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.
|
|
27
34
|
|
|
@@ -45,7 +52,7 @@ Building without errors does **not** mean the AI workflow binding is correct. A
|
|
|
45
52
|
type AiActionConfig<TInput, TOutput> = {
|
|
46
53
|
workflowId: string // REQUIRED — UUID from manifest.json. Without this, SDK uses action name as URL segment (not a UUID → EAX rejects).
|
|
47
54
|
inputSchema?: z.ZodType<TInput> // validates input before calling the workflow
|
|
48
|
-
outputSchema?: z.ZodType<TOutput> // validates the
|
|
55
|
+
outputSchema?: z.ZodType<TOutput> // validates the workflow finalResult payload for run()
|
|
49
56
|
mode?: 'run' | 'stream' | 'events' // default: supports all three modes
|
|
50
57
|
}
|
|
51
58
|
```
|
|
@@ -120,7 +127,7 @@ export const ai = defineAiActions({
|
|
|
120
127
|
})
|
|
121
128
|
```
|
|
122
129
|
|
|
123
|
-
### Action with typed output
|
|
130
|
+
### Action with typed output
|
|
124
131
|
|
|
125
132
|
```ts
|
|
126
133
|
export const generateStoryOutputSchema = z.object({
|
|
@@ -130,13 +137,11 @@ export const generateStoryOutputSchema = z.object({
|
|
|
130
137
|
})
|
|
131
138
|
export type GenerateStoryOutput = z.infer<typeof generateStoryOutputSchema>
|
|
132
139
|
|
|
133
|
-
// Do NOT put outputSchema in defineAiAction unless it matches the full
|
|
134
|
-
// AiResult envelope. Instead, cast finalResult after run():
|
|
135
140
|
export const ai = defineAiActions({
|
|
136
141
|
generateStory: defineAiAction('generateStory', {
|
|
137
142
|
workflowId: 'd69ab648-2c00-4d94-928e-01bd7b2a5bb2', // ← from manifest.json
|
|
138
143
|
inputSchema: generateStoryInputSchema,
|
|
139
|
-
|
|
144
|
+
outputSchema: generateStoryOutputSchema,
|
|
140
145
|
}),
|
|
141
146
|
})
|
|
142
147
|
```
|
|
@@ -171,25 +176,21 @@ export const ai = defineAiActions({
|
|
|
171
176
|
|
|
172
177
|
## Calling AI Actions
|
|
173
178
|
|
|
174
|
-
### run() —
|
|
179
|
+
### run() — typed action result
|
|
175
180
|
|
|
176
181
|
```ts
|
|
177
182
|
import howone, { type GenerateStoryInput, type GenerateStoryOutput } from '@/lib/sdk'
|
|
178
183
|
|
|
179
184
|
async function generateStory(input: GenerateStoryInput) {
|
|
180
|
-
const
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
if (!result.success) {
|
|
184
|
-
throw new Error(result.errors.join(', '))
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Cast finalResult to your output type
|
|
188
|
-
const output = result.finalResult as GenerateStoryOutput
|
|
185
|
+
const output = await howone.ai.generateStory.run(input)
|
|
186
|
+
// output is GenerateStoryOutput when outputSchema is configured.
|
|
189
187
|
return output
|
|
190
188
|
}
|
|
191
189
|
```
|
|
192
190
|
|
|
191
|
+
When an action has `outputSchema`, `run()` returns the validated workflow `finalResult` payload.
|
|
192
|
+
When an action omits `outputSchema`, `run()` returns the raw `AiResult` execution envelope.
|
|
193
|
+
|
|
193
194
|
### run() — with SSE callbacks
|
|
194
195
|
|
|
195
196
|
```ts
|
|
@@ -212,6 +213,25 @@ const result = await howone.ai.generateStory.run(input, {
|
|
|
212
213
|
})
|
|
213
214
|
```
|
|
214
215
|
|
|
216
|
+
UI feedback belongs in the frontend app. Do not import or expect SDK toast APIs. Use returned
|
|
217
|
+
promises and callbacks to update app-owned state:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
setStatus({ type: 'loading', message: 'Generating story...' })
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const output = await howone.ai.generateStory.run(input, {
|
|
224
|
+
onProgress: (progress) => setProgress(progress),
|
|
225
|
+
})
|
|
226
|
+
setStatus({ type: 'success', message: 'Story ready', output })
|
|
227
|
+
} catch (error) {
|
|
228
|
+
setStatus({
|
|
229
|
+
type: 'error',
|
|
230
|
+
message: error instanceof Error ? error.message : 'Story generation failed',
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
215
235
|
### stream() — start and control a session
|
|
216
236
|
|
|
217
237
|
```ts
|
|
@@ -362,7 +382,7 @@ type SSEExecutionOptions = {
|
|
|
362
382
|
// Called if an error occurs
|
|
363
383
|
onError?: (error: Error) => void
|
|
364
384
|
|
|
365
|
-
// Called when the workflow completes
|
|
385
|
+
// Called when the workflow completes with the raw execution envelope
|
|
366
386
|
onComplete?: (result: AiResult) => void
|
|
367
387
|
|
|
368
388
|
// Abort signal — connect to an AbortController for cancellation
|
|
@@ -402,32 +422,59 @@ try {
|
|
|
402
422
|
|
|
403
423
|
## AI Result Persistence
|
|
404
424
|
|
|
405
|
-
When AI-generated content should be saved to an entity
|
|
425
|
+
When AI-generated content should be saved to an entity, prefer the SDK persistence helper for
|
|
426
|
+
history-style products. It standardizes the pending-first pattern from `02-database/05-ai-persistence-patterns.md`
|
|
427
|
+
without adding UI behavior.
|
|
406
428
|
|
|
407
429
|
```ts
|
|
408
|
-
|
|
409
|
-
const result = await howone.ai.generateStory.run({
|
|
410
|
-
topic: 'Dragons and magic',
|
|
411
|
-
ageRange: '6-8',
|
|
412
|
-
})
|
|
430
|
+
import { runAiActionAndPersist } from '@howone/sdk'
|
|
413
431
|
|
|
414
|
-
|
|
432
|
+
const result = await runAiActionAndPersist({
|
|
433
|
+
entity: howone.entities.Generation,
|
|
434
|
+
input: {
|
|
435
|
+
prompt: 'Dragons and magic',
|
|
436
|
+
ageRange: '6-8',
|
|
437
|
+
},
|
|
438
|
+
createPending: (input) => ({
|
|
439
|
+
prompt: input.prompt,
|
|
440
|
+
ageRange: input.ageRange,
|
|
441
|
+
status: 'pending',
|
|
442
|
+
requestedAt: new Date().toISOString(),
|
|
443
|
+
}),
|
|
444
|
+
run: (input) => howone.ai.generateStory.run(input),
|
|
445
|
+
mapCompleted: ({ output }) => ({
|
|
446
|
+
status: 'completed',
|
|
447
|
+
title: output.title,
|
|
448
|
+
content: output.content,
|
|
449
|
+
completedAt: new Date().toISOString(),
|
|
450
|
+
}),
|
|
451
|
+
mapFailed: ({ error }) => ({
|
|
452
|
+
status: 'failed',
|
|
453
|
+
errorMessage: error instanceof Error ? error.message : 'Generation failed',
|
|
454
|
+
}),
|
|
455
|
+
onStateChange: (state) => {
|
|
456
|
+
// app-owned UI callback; SDK does not show toasts
|
|
457
|
+
setGenerationState(state.status)
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
```
|
|
415
461
|
|
|
416
|
-
|
|
462
|
+
Return shape:
|
|
417
463
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
authorId: currentUser.id,
|
|
423
|
-
status: 'draft',
|
|
424
|
-
wordCount: output.content.split(' ').length,
|
|
425
|
-
// Track generation metadata
|
|
426
|
-
promptTopic: 'Dragons and magic',
|
|
427
|
-
generatedAt: new Date().toISOString(),
|
|
428
|
-
})
|
|
464
|
+
```ts
|
|
465
|
+
type AiPersistenceResult<TRecord, TOutput> =
|
|
466
|
+
| { status: 'completed'; record: TRecord; output: TOutput }
|
|
467
|
+
| { status: 'failed'; record: TRecord; error: unknown }
|
|
429
468
|
```
|
|
430
469
|
|
|
470
|
+
Rules:
|
|
471
|
+
|
|
472
|
+
- `createPending` must only return fields declared in the entity schema.
|
|
473
|
+
- `mapCompleted` maps durable product fields from AI output to entity update payload.
|
|
474
|
+
- `mapFailed` should persist a failure state if the product shows history or retry.
|
|
475
|
+
- Use `onStateChange` to update app-owned UI; do not add SDK toast behavior.
|
|
476
|
+
- For simple one-shot AI actions that do not need history, call `howone.ai.*.run()` directly.
|
|
477
|
+
|
|
431
478
|
---
|
|
432
479
|
|
|
433
480
|
## React Patterns
|
|
@@ -448,9 +495,8 @@ function GenerateStoryButton({ input }: { input: GenerateStoryInput }) {
|
|
|
448
495
|
setLoading(true)
|
|
449
496
|
setError(null)
|
|
450
497
|
try {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
setResult(res.finalResult as GenerateStoryOutput)
|
|
498
|
+
const output = await howone.ai.generateStory.run(input)
|
|
499
|
+
setResult(output)
|
|
454
500
|
} catch (err) {
|
|
455
501
|
if (err instanceof AiSchemaValidationError) {
|
|
456
502
|
setError(`Validation error: ${err.issues.map(i => i.message).join(', ')}`)
|
|
@@ -522,10 +568,11 @@ function StreamingStoryGenerator({ input }: { input: GenerateStoryInput }) {
|
|
|
522
568
|
| Mistake | Correct Pattern |
|
|
523
569
|
|---|---|
|
|
524
570
|
| `defineAiAction('generateStory', { inputSchema })` — no `workflowId` | Always include `workflowId` from `manifest.json`; SDK falls back to action name, which is not a UUID → EAX rejects |
|
|
525
|
-
| Writing `src/lib/sdk.ts` before `
|
|
571
|
+
| Writing `src/lib/sdk.ts` before `.howone/ai/manifest.json` has a workflowId | Run `ai-capability-design` → `sync_ai_artifacts` → `external-ai-capability`; only write bindings from the synced manifest |
|
|
526
572
|
| Hardcoding `workflowId` from memory or guessing | Always read from `.howone/ai/manifest.json` — copy the exact UUID |
|
|
527
573
|
| `howone.ai.run.generateStory(input)` | `howone.ai.generateStory.run(input)` |
|
|
528
574
|
| Action named `run`, `stream`, or `events` | Rename to e.g. `executeWorkflow`, `streamContent` |
|
|
529
|
-
|
|
|
530
|
-
|
|
|
575
|
+
| Passing raw JSON Schema from manifest into `defineAiAction` | Convert JSON Schema fields to Zod first |
|
|
576
|
+
| Making every output field `.optional()` or adding `.passthrough()` after validation fails | Keep manifest-required output fields required; inspect `AiSchemaValidationError.issues` and fix the contract/workflow mismatch |
|
|
577
|
+
| Reading `raw.finalResult`, `raw.data.result`, or `raw.result` after a typed `.run()` | Use the returned value directly when `outputSchema` is configured |
|
|
531
578
|
| Calling `howone.ai.generateStory.run(input)` inside JSX render | Move to event handler or useEffect |
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# SDK Extension Boundaries
|
|
2
|
+
|
|
3
|
+
This reference defines the long-term shape of `@howone/sdk`. Use it whenever changing SDK APIs,
|
|
4
|
+
adding new capabilities, or deciding whether behavior belongs in the SDK or in the generated app.
|
|
5
|
+
|
|
6
|
+
## North Star
|
|
7
|
+
|
|
8
|
+
The SDK is an AI-first runtime, not an app UI framework.
|
|
9
|
+
|
|
10
|
+
It should provide:
|
|
11
|
+
|
|
12
|
+
- stable defaults that work with almost no configuration;
|
|
13
|
+
- typed clients for HowOne platform capabilities;
|
|
14
|
+
- adapters for custom behavior;
|
|
15
|
+
- callbacks/events for app UI;
|
|
16
|
+
- predictable names that AI agents can reuse without guessing.
|
|
17
|
+
|
|
18
|
+
It should not provide:
|
|
19
|
+
|
|
20
|
+
- app-owned pages, modals, toasts, or business UI;
|
|
21
|
+
- hardcoded app flows beyond HowOne platform defaults;
|
|
22
|
+
- hidden persistence side effects;
|
|
23
|
+
- provider-specific branches scattered through feature code.
|
|
24
|
+
|
|
25
|
+
## Default + Adapter Rule
|
|
26
|
+
|
|
27
|
+
Every platform capability should follow the same shape:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
createClient({
|
|
31
|
+
projectId,
|
|
32
|
+
env,
|
|
33
|
+
auth: 'hosted', // default
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Advanced usage should opt into typed adapters:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
createClient({
|
|
41
|
+
projectId,
|
|
42
|
+
env,
|
|
43
|
+
auth: {
|
|
44
|
+
mode: 'headless',
|
|
45
|
+
adapter: {
|
|
46
|
+
getToken: () => externalAuth.getToken(),
|
|
47
|
+
setToken: (token) => externalAuth.setToken(token),
|
|
48
|
+
login: ({ returnUrl }) => appRouter.push(`/login?redirect=${encodeURIComponent(returnUrl ?? '/')}`),
|
|
49
|
+
logout: () => appRouter.push('/'),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Do not add one-off flags when an adapter/callback can express the behavior.
|
|
56
|
+
|
|
57
|
+
## Capability Boundaries
|
|
58
|
+
|
|
59
|
+
| Capability | SDK owns | App owns |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| Auth | token source, session state, login/logout destination policy, hosted HowOne defaults | login page visuals, account menu, loading states, auth error UI |
|
|
62
|
+
| React provider | context, auth callbacks, optional bottom-right HowOne `FloatingButton` logo | layout, toasts, overlays, route components, theme system |
|
|
63
|
+
| Entities | typed CRUD/query clients, public/private routing, payload normalization | forms, list rendering, optimistic UI, field-level UX |
|
|
64
|
+
| Schema | definition operations, preview/apply/version/restore calls | migration approval UI, admin experience |
|
|
65
|
+
| Entity contract utilities | payload whitelisting, public query guardrail validation, structured validation issues | deciding product copy, rendering validation errors |
|
|
66
|
+
| AI workflows | run/stream/events, Zod validation, workflowId binding | progress UI, result rendering, failure surfaces |
|
|
67
|
+
| AI persistence | pending-first orchestration helper, state callbacks, completed/failed mapping hook | choosing schema fields, retry UX, visible state UI |
|
|
68
|
+
| Upload | file/image/batch helpers and callbacks | picker UI, previews, validation copy, uploaded-file placement |
|
|
69
|
+
| Raw HTTP | low-level escape hatch | choosing it only when typed SDK surface does not exist |
|
|
70
|
+
|
|
71
|
+
## React Provider Boundary
|
|
72
|
+
|
|
73
|
+
`HowOneProvider` may render the HowOne bottom-right logo via `FloatingButton`. This is platform
|
|
74
|
+
branding and should remain visible by default.
|
|
75
|
+
|
|
76
|
+
It must not render:
|
|
77
|
+
|
|
78
|
+
- toast notifications;
|
|
79
|
+
- redirect overlays;
|
|
80
|
+
- login/register forms;
|
|
81
|
+
- payment dialogs;
|
|
82
|
+
- app theme wrappers;
|
|
83
|
+
- app-specific navigation.
|
|
84
|
+
|
|
85
|
+
Use callbacks instead:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<HowOneProvider
|
|
89
|
+
auth="required"
|
|
90
|
+
brand="visible"
|
|
91
|
+
onAuthRedirect={({ mode, returnUrl }) => {
|
|
92
|
+
setAuthUi({ redirecting: true, mode, returnUrl })
|
|
93
|
+
}}
|
|
94
|
+
onAuthStateChange={(state) => {
|
|
95
|
+
setCurrentUser(state.user)
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<App />
|
|
99
|
+
</HowOneProvider>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Hide the logo only when explicitly requested:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<HowOneProvider brand="hidden" />
|
|
106
|
+
<HowOneProvider showBrandButton={false} />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## UI Feedback Rule
|
|
110
|
+
|
|
111
|
+
Do not add `ClayxToast`, `toast`, or any visible notification API to `@howone/sdk`.
|
|
112
|
+
|
|
113
|
+
Generated apps should write their own UI from SDK results:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
setStatus({ type: 'loading', message: 'Generating...' })
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const output = await howone.ai.generateImage.run({ prompt })
|
|
120
|
+
setStatus({ type: 'success', message: 'Done', output })
|
|
121
|
+
} catch (error) {
|
|
122
|
+
setStatus({
|
|
123
|
+
type: 'error',
|
|
124
|
+
message: error instanceof Error ? error.message : 'Generation failed',
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For streaming workflows, use callbacks/events:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const session = howone.ai.generateImage.stream(
|
|
133
|
+
{ prompt },
|
|
134
|
+
{
|
|
135
|
+
onStreamContent: (delta) => appendLog(delta),
|
|
136
|
+
onProgress: (progress) => setProgress(progress),
|
|
137
|
+
onError: (error) => setStatus({ type: 'error', message: error.message }),
|
|
138
|
+
onComplete: (result) => setResult(result.finalResult),
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Auth Adapter Contract
|
|
144
|
+
|
|
145
|
+
Use `AuthAdapter` for custom/headless auth. It is the only supported extension point for token
|
|
146
|
+
ownership outside the SDK defaults.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
type AuthAdapter = {
|
|
150
|
+
name?: string
|
|
151
|
+
getToken?: () => string | null | Promise<string | null>
|
|
152
|
+
setToken?: (token: string | null) => void | Promise<void>
|
|
153
|
+
getUser?: (token: string | null) => AuthUser | null | Promise<AuthUser | null>
|
|
154
|
+
login?: (options?: { returnUrl?: string }) => void | Promise<void>
|
|
155
|
+
logout?: (options?: { redirect?: false | string | { url: string; external?: boolean } }) => void | Promise<void>
|
|
156
|
+
clearSession?: (options?: { redirect?: false | string | { url: string; external?: boolean } }) => void | Promise<void>
|
|
157
|
+
subscribe?: (listener: (state: AuthState) => void) => (() => void) | void
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Rules:
|
|
162
|
+
|
|
163
|
+
- Default hosted auth must work without an adapter.
|
|
164
|
+
- External providers must use `mode: 'headless'` plus `adapter`.
|
|
165
|
+
- Custom in-app HowOne login should usually use `mode: 'custom'`, `loginPath`, and SDK OTP/OAuth helpers.
|
|
166
|
+
- `client.me()` and `client.requireMe()` are the canonical user APIs.
|
|
167
|
+
- `client.auth.isAuthenticated()` is a quick token check, not a first-load user fetch.
|
|
168
|
+
|
|
169
|
+
## AI Agent Design Rules
|
|
170
|
+
|
|
171
|
+
Generated app code should have one stable SDK singleton:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import howone from '@/lib/sdk'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Agents should prefer:
|
|
178
|
+
|
|
179
|
+
1. `howone.entities.*` for private/authenticated data;
|
|
180
|
+
2. `howone.public.entities.*` for public access;
|
|
181
|
+
3. `pickEntityPayload()` / `assertEntityPayload()` when mapping broad UI or AI objects to entity writes;
|
|
182
|
+
4. `assertPublicEntityQuery()` when generated code has access to the synced definition;
|
|
183
|
+
5. `howone.ai.*` for workflow execution;
|
|
184
|
+
6. `runAiActionAndPersist()` when the product needs durable AI history;
|
|
185
|
+
7. `howone.upload.*` for files;
|
|
186
|
+
8. `howone.schema.*` for schema tools;
|
|
187
|
+
9. `howone.raw` only as escape hatch.
|
|
188
|
+
|
|
189
|
+
Agents must not:
|
|
190
|
+
|
|
191
|
+
- hardcode HowOne URLs;
|
|
192
|
+
- manually build workflow SSE URLs;
|
|
193
|
+
- call workflows by display name instead of UUID;
|
|
194
|
+
- persist UI-only or workflow-extra fields;
|
|
195
|
+
- add frontend UI APIs to the SDK;
|
|
196
|
+
- remove the default HowOne floating logo unless explicitly asked.
|
|
197
|
+
|
|
198
|
+
## Adding New SDK Capabilities
|
|
199
|
+
|
|
200
|
+
When adding a new capability, choose one of these shapes:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
client.capability.method(input, options)
|
|
204
|
+
client.capability.stream(input, callbacks)
|
|
205
|
+
client.capability.configure(adapter)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Prefer these names:
|
|
209
|
+
|
|
210
|
+
- `run` for one-shot AI/workflow execution;
|
|
211
|
+
- `stream` for session-based execution with callbacks;
|
|
212
|
+
- `events` for async iterables;
|
|
213
|
+
- `query/list/get/create/update/delete` for entities;
|
|
214
|
+
- `configure` only for adapters, not app UI.
|
|
215
|
+
|
|
216
|
+
Keep returned values serializable and obvious. AI agents should be able to inspect the method name
|
|
217
|
+
and infer the contract.
|
|
218
|
+
|
|
219
|
+
## Compatibility Rule
|
|
220
|
+
|
|
221
|
+
Do not break existing generated apps lightly. Prefer:
|
|
222
|
+
|
|
223
|
+
- add new adapter/callback options;
|
|
224
|
+
- keep old string shorthand (`auth: 'custom'`) working;
|
|
225
|
+
- mark old UI props as deprecated/no-op only when needed;
|
|
226
|
+
- update this skill and the relevant numbered reference in the same change.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# AI Capability Architecture
|
|
2
|
+
|
|
3
|
+
Use this reference when a HowOne app needs AI generation, editing, analysis, research, media
|
|
4
|
+
creation, file generation, or any workflow-backed behavior.
|
|
5
|
+
|
|
6
|
+
This file answers: **what AI layer should be designed, in what order, and where each responsibility
|
|
7
|
+
belongs?** For schema details read `02-workflow-contract-rules.md`. For workflow service calls read
|
|
8
|
+
`05-workflow-operations.md`.
|
|
9
|
+
|
|
10
|
+
## Platform Mental Model
|
|
11
|
+
|
|
12
|
+
HowOne AI has five distinct layers:
|
|
13
|
+
|
|
14
|
+
| Layer | Owns | Does not own |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Product feature | User-facing goal, UX states, persistence decision | workflow internals |
|
|
17
|
+
| AI capability contract | `name`, `description`, `inputSchema`, `outputSchema`, `outputEntityName`, versions, manifest | database CRUD, UI, auth |
|
|
18
|
+
| External workflow implementation | generated/edited workflow graph behind a `workflowId` | app schema, frontend state |
|
|
19
|
+
| Status/background layer | `request_id` polling, completed/failed state, `workflowConfigID` capture | SDK binding source |
|
|
20
|
+
| SDK binding/app code | `defineAiAction`, Zod schemas, `howone.ai.*`, persistence through entities | workflow generation |
|
|
21
|
+
|
|
22
|
+
Do not collapse these layers. The common mistakes are:
|
|
23
|
+
|
|
24
|
+
- putting database writes into the workflow;
|
|
25
|
+
- generating `src/lib/sdk.ts` before `.howone/ai/manifest.json` is synced;
|
|
26
|
+
- using action names instead of workflow UUIDs;
|
|
27
|
+
- treating workflow `outputSchema` as a database schema;
|
|
28
|
+
- faking unsupported AI with static frontend data.
|
|
29
|
+
|
|
30
|
+
## Source Of Truth
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
user request = intent
|
|
34
|
+
agent AI contract proposal = draft
|
|
35
|
+
applied AI capability version = validated contract
|
|
36
|
+
.howone/ai/manifest.json = local synced source for SDK codegen
|
|
37
|
+
workflow service completed status = source for workflowConfigID
|
|
38
|
+
src/lib/sdk.ts = generated app binding
|
|
39
|
+
frontend UI = SDK consumer
|
|
40
|
+
entity schema = persistence contract, separate from AI contract
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Never generate SDK bindings from the user prompt or from an unsynced draft.
|
|
44
|
+
|
|
45
|
+
## Standard AI Feature Flow
|
|
46
|
+
|
|
47
|
+
Use this flow for new AI features:
|
|
48
|
+
|
|
49
|
+
1. Classify the feature using `04-ai/04-service-capability-catalog.md`.
|
|
50
|
+
2. Decide whether the feature is supported. If not supported, stop and explain the missing capability.
|
|
51
|
+
3. Decide one workflow per user-facing feature. Use two workflows only for RAG.
|
|
52
|
+
4. Design `inputSchema` and `outputSchema` using `02-workflow-contract-rules.md`.
|
|
53
|
+
5. Preview/apply the AI capability patch through the capability tool.
|
|
54
|
+
6. Sync `.howone/ai/manifest.json`.
|
|
55
|
+
7. Submit workflow create through `external-ai-capability` / workflow operate from the synced manifest.
|
|
56
|
+
8. Store returned `request_id` values for polling.
|
|
57
|
+
9. Poll status until `completed` or `failed`.
|
|
58
|
+
10. On completed + `payload.success === true`, store `payload.workflow_details.new_workflow_config_id`.
|
|
59
|
+
11. Generate/update `src/lib/sdk.ts` using `03-ai-sdk-handoff.md` and `01-architect/02-manifest-codegen.md`.
|
|
60
|
+
12. Implement UI calls through `howone.ai.<action>.run()`, `.stream()`, or `.events()`.
|
|
61
|
+
13. If output must persist, design entity schema and use `runAiActionAndPersist()` when appropriate.
|
|
62
|
+
|
|
63
|
+
Do not submit external workflow create/update from a hand-written schema. It should come from the
|
|
64
|
+
synced manifest.
|
|
65
|
+
|
|
66
|
+
## New Feature vs Existing Feature
|
|
67
|
+
|
|
68
|
+
| Situation | Correct path |
|
|
69
|
+
|---|---|
|
|
70
|
+
| New AI feature, no manifest entry | create AI capability, sync manifest, submit workflow create |
|
|
71
|
+
| Manifest entry exists but no workflow created yet | submit workflow create from manifest |
|
|
72
|
+
| User asks to change input/output contract | update capability contract first, sync, then submit workflow update |
|
|
73
|
+
| User asks to improve behavior only | submit workflow update with `workflowConfigID` and `updatePrompt` |
|
|
74
|
+
| User asks to save outputs/history | design/update database entity after AI output contract is known |
|
|
75
|
+
| User asks for public share of AI result | private history entity + public scoped share entity |
|
|
76
|
+
|
|
77
|
+
## Create vs Update
|
|
78
|
+
|
|
79
|
+
Create external workflow when:
|
|
80
|
+
|
|
81
|
+
- capability has a `workflowId`;
|
|
82
|
+
- no confirmed external implementation exists;
|
|
83
|
+
- no `workflowConfigID` has been captured.
|
|
84
|
+
|
|
85
|
+
Update external workflow when:
|
|
86
|
+
|
|
87
|
+
- an external implementation exists;
|
|
88
|
+
- the status layer previously returned `payload.workflow_details.new_workflow_config_id`;
|
|
89
|
+
- you have a concrete `updatePrompt`.
|
|
90
|
+
|
|
91
|
+
`workflowConfigID` is not the same as `workflowId`.
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
workflowId = stable workflow UUID from manifest, used by SDK execution
|
|
95
|
+
workflowConfigID = implementation config ID from completed workflow generation/edit status
|
|
96
|
+
request_id = async operation ID returned by workflow operate endpoint
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Do not invent any of these IDs.
|
|
100
|
+
|
|
101
|
+
## Workflow Count Rule
|
|
102
|
+
|
|
103
|
+
Default: one user-facing AI feature equals one workflow.
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
|
|
107
|
+
| Feature | Workflow count | Why |
|
|
108
|
+
|---|---:|---|
|
|
109
|
+
| Generate illustrated story | 1 | story text + images are one product action |
|
|
110
|
+
| Edit uploaded photo | 1 | one input image + edit prompt -> edited image |
|
|
111
|
+
| Research news briefing | 1 | search + synthesis are one action |
|
|
112
|
+
| Generate video from prompt | 1 | media generation is one action |
|
|
113
|
+
| Chat with uploaded documents | 2 | RAG needs indexing + query workflows |
|
|
114
|
+
|
|
115
|
+
Do not split normal multi-step behavior into separate workflows. The workflow service handles
|
|
116
|
+
internal orchestration.
|
|
117
|
+
|
|
118
|
+
## Persistence Boundary
|
|
119
|
+
|
|
120
|
+
AI workflows produce outputs. They do not own product records.
|
|
121
|
+
|
|
122
|
+
Workflow may do:
|
|
123
|
+
|
|
124
|
+
- generate, summarize, translate, classify, extract;
|
|
125
|
+
- search/crawl and synthesize;
|
|
126
|
+
- generate/edit/analyze images, video, and audio;
|
|
127
|
+
- retrieve financial or academic data;
|
|
128
|
+
- save/read generated files through URL-based storage.
|
|
129
|
+
|
|
130
|
+
Workflow must not do:
|
|
131
|
+
|
|
132
|
+
- database create/read/update/delete;
|
|
133
|
+
- authentication/session logic;
|
|
134
|
+
- file upload from browser raw bytes;
|
|
135
|
+
- payment processing;
|
|
136
|
+
- owner assignment or permissions;
|
|
137
|
+
- app navigation, UI state, toast, or modal logic.
|
|
138
|
+
|
|
139
|
+
If the product needs durable history, use entity persistence outside the workflow:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
await runAiActionAndPersist({
|
|
143
|
+
entity: howone.entities.Generation,
|
|
144
|
+
input,
|
|
145
|
+
createPending: (input) => ({ prompt: input.prompt, status: 'pending' }),
|
|
146
|
+
run: (input) => howone.ai.generateImage.run(input),
|
|
147
|
+
mapCompleted: ({ output }) => ({ status: 'completed', resultUrl: output.generated_image_url }),
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Unsupported AI Behavior
|
|
152
|
+
|
|
153
|
+
If a user explicitly requires behavior not available in the workflow service, stop that AI path.
|
|
154
|
+
|
|
155
|
+
Do not:
|
|
156
|
+
|
|
157
|
+
- fake AI with static templates;
|
|
158
|
+
- hide the unsupported part;
|
|
159
|
+
- build a UI that pretends the workflow exists;
|
|
160
|
+
- replace the requested capability with a different one without saying so;
|
|
161
|
+
- assume private APIs, external datasets, or providers that are not listed.
|
|
162
|
+
|
|
163
|
+
Correct response:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
This exact AI behavior needs <missing capability>. The current workflow service supports <closest
|
|
167
|
+
available capability>. I can build <narrow supported version>, or we need platform support for
|
|
168
|
+
<missing capability> first.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Capability Naming
|
|
172
|
+
|
|
173
|
+
Use stable JavaScript-safe IDs:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
generateIllustration
|
|
177
|
+
summarizeDocument
|
|
178
|
+
researchNewsBriefing
|
|
179
|
+
editProductPhoto
|
|
180
|
+
transcribeAudio
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Avoid:
|
|
184
|
+
|
|
185
|
+
- display labels with spaces;
|
|
186
|
+
- names that collide with base methods: `run`, `stream`, `events`;
|
|
187
|
+
- provider names: `openAiImage`, `geminiAnalyze`;
|
|
188
|
+
- implementation names: `searchThenSummarize`.
|
|
189
|
+
|
|
190
|
+
The description can be human readable. The ID must be stable for codegen.
|
|
191
|
+
|
|
192
|
+
## AI Architecture Checklist
|
|
193
|
+
|
|
194
|
+
Before editing files:
|
|
195
|
+
|
|
196
|
+
- Feature maps to available workflow capabilities.
|
|
197
|
+
- One workflow per feature unless RAG.
|
|
198
|
+
- Description says what the user gets, not how tools run.
|
|
199
|
+
- Input schema accepts URLs for files, not raw bytes.
|
|
200
|
+
- Output schema contains only requested result fields.
|
|
201
|
+
- Input and output property names do not overlap.
|
|
202
|
+
- Text output descriptions specify language behavior.
|
|
203
|
+
- Persistence is modeled as entity schema, not workflow CRUD.
|
|
204
|
+
- `workflowId`, `request_id`, and `workflowConfigID` are not guessed.
|
|
205
|
+
- SDK binding will be generated only after manifest sync.
|