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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/templates/nextjs/lib/sdk.ts +3 -0
  3. package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +183 -69
  4. package/templates/vite/.howone/skills/howone-sdk/01-architect/02-manifest-codegen.md +98 -23
  5. package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +463 -69
  6. package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +366 -64
  7. package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +204 -67
  8. package/templates/vite/.howone/skills/howone-sdk/02-database/04-query-dsl-and-responses.md +237 -0
  9. package/templates/vite/.howone/skills/howone-sdk/02-database/05-ai-persistence-patterns.md +372 -0
  10. package/templates/vite/.howone/skills/howone-sdk/03-sdk/01-client-setup.md +58 -36
  11. package/templates/vite/.howone/skills/howone-sdk/03-sdk/02-entity-operations.md +67 -0
  12. package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +267 -469
  13. package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +113 -322
  14. package/templates/vite/.howone/skills/howone-sdk/03-sdk/07-ai-action-calls.md +95 -48
  15. package/templates/vite/.howone/skills/howone-sdk/03-sdk/08-extension-boundaries.md +226 -0
  16. package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +205 -0
  17. package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +426 -0
  18. package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +219 -0
  19. package/templates/vite/.howone/skills/howone-sdk/04-ai/04-service-capability-catalog.md +281 -0
  20. package/templates/vite/.howone/skills/howone-sdk/04-ai/05-workflow-operations.md +256 -0
  21. package/templates/vite/.howone/skills/howone-sdk/04-ai/06-ai-feature-playbooks.md +296 -0
  22. package/templates/vite/.howone/skills/howone-sdk/SKILL.md +83 -15
  23. package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +2 -2
  24. package/templates/vite/package.json +1 -1
  25. package/templates/vite/src/lib/sdk.ts +3 -0
  26. 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. Call `defineAiAction(name, { workflowId, inputSchema })` **`workflowId` is mandatory**
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 a confirmed `workflowId`.**
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. `ai_capability_design` — design the capability schema
27
+ 1. `ai-capability-design` — design the capability contract
20
28
  2. `sync_ai_artifacts` — sync manifest to disk
21
- 3. `external_ai_capability` — submit workflow to EAX; returns confirmed `workflowId`
22
- 4. Confirmed `workflowId` is written back to the backend capability record
23
- 5. `sync_ai_artifacts` again manifest now contains `workflowId`
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 full ExecutionResult envelope (see caution below)
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 (unwrap finalResult manually)
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
- // outputSchema: omit — cast manually
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() — await the full result
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 result = await howone.ai.generateStory.run(input)
181
- // result is AiResult
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 (same as awaiting run())
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
- // 1. Run the AI action
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
- if (!result.success) throw new Error(result.errors.join(', '))
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
- const output = result.finalResult as GenerateStoryOutput
462
+ Return shape:
417
463
 
418
- // 2. Save to entity
419
- const saved = await howone.entities.Story.create({
420
- title: output.title,
421
- content: output.content,
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 res = await howone.ai.generateStory.run(input)
452
- if (!res.success) throw new Error(res.errors.join(', '))
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 `sync_ai_artifacts` has a confirmed `workflowId` | Run `external_ai_capability` → `sync_ai_artifacts` first; only write bindings after manifest has `workflowId` |
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
- | `outputSchema: z.object({ title: z.string() })` expecting `finalResult` shape | Omit `outputSchema`; cast `result.finalResult` manually |
530
- | Not checking `result.success` before using `finalResult` | Always check `if (!result.success) throw new Error(...)` |
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.