@agent-native/core 0.78.0 → 0.78.1

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 (92) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +9 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/types.ts +22 -0
  5. package/corpus/core/src/client/AgentPanel.tsx +8 -0
  6. package/corpus/core/src/client/AssistantChat.tsx +40 -0
  7. package/corpus/core/src/client/agent-chat.ts +173 -0
  8. package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
  9. package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
  10. package/corpus/core/src/client/composer/types.ts +22 -0
  11. package/corpus/core/src/client/index.ts +5 -0
  12. package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
  13. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
  14. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
  15. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
  16. package/corpus/templates/assets/AGENTS.md +18 -3
  17. package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
  18. package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
  19. package/corpus/templates/assets/actions/generate-asset.ts +30 -23
  20. package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
  21. package/corpus/templates/assets/actions/generate-image.ts +46 -30
  22. package/corpus/templates/assets/actions/generate-video.ts +18 -3
  23. package/corpus/templates/assets/actions/list-assets.ts +39 -8
  24. package/corpus/templates/assets/actions/list-libraries.ts +24 -2
  25. package/corpus/templates/assets/actions/navigate.ts +1 -1
  26. package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
  27. package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
  28. package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
  29. package/corpus/templates/assets/actions/variant-slots.ts +131 -28
  30. package/corpus/templates/assets/actions/view-screen.ts +9 -0
  31. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
  32. package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
  33. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  34. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
  35. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
  36. package/corpus/templates/assets/app/i18n-data.ts +1815 -0
  37. package/corpus/templates/assets/app/lib/libraries.ts +6 -0
  38. package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
  39. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  40. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  41. package/corpus/templates/assets/app/routes/audit.tsx +1 -1
  42. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
  43. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
  44. package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
  45. package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
  46. package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
  47. package/corpus/templates/assets/app/routes/library.tsx +1454 -175
  48. package/corpus/templates/assets/server/db/index.ts +1 -1
  49. package/corpus/templates/assets/server/lib/generation.ts +86 -15
  50. package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
  51. package/corpus/templates/assets/server/plugins/db.ts +5 -0
  52. package/corpus/templates/assets/shared/api.ts +6 -3
  53. package/dist/agent/types.d.ts +21 -0
  54. package/dist/agent/types.d.ts.map +1 -1
  55. package/dist/agent/types.js.map +1 -1
  56. package/dist/client/AgentPanel.d.ts +5 -1
  57. package/dist/client/AgentPanel.d.ts.map +1 -1
  58. package/dist/client/AgentPanel.js +2 -2
  59. package/dist/client/AgentPanel.js.map +1 -1
  60. package/dist/client/AssistantChat.d.ts +6 -0
  61. package/dist/client/AssistantChat.d.ts.map +1 -1
  62. package/dist/client/AssistantChat.js +15 -3
  63. package/dist/client/AssistantChat.js.map +1 -1
  64. package/dist/client/agent-chat.d.ts +32 -0
  65. package/dist/client/agent-chat.d.ts.map +1 -1
  66. package/dist/client/agent-chat.js +113 -0
  67. package/dist/client/agent-chat.js.map +1 -1
  68. package/dist/client/composer/TiptapComposer.d.ts +2 -1
  69. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  70. package/dist/client/composer/TiptapComposer.js +228 -56
  71. package/dist/client/composer/TiptapComposer.js.map +1 -1
  72. package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
  73. package/dist/client/composer/extensions/MentionReference.js +3 -0
  74. package/dist/client/composer/extensions/MentionReference.js.map +1 -1
  75. package/dist/client/composer/types.d.ts +21 -0
  76. package/dist/client/composer/types.d.ts.map +1 -1
  77. package/dist/client/composer/types.js.map +1 -1
  78. package/dist/client/index.d.ts +1 -1
  79. package/dist/client/index.d.ts.map +1 -1
  80. package/dist/client/index.js +1 -1
  81. package/dist/client/index.js.map +1 -1
  82. package/dist/collab/routes.d.ts +2 -2
  83. package/dist/notifications/routes.d.ts +3 -3
  84. package/dist/observability/routes.d.ts +6 -6
  85. package/dist/progress/routes.d.ts +1 -1
  86. package/dist/resources/handlers.d.ts +3 -3
  87. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  88. package/dist/server/agent-chat-plugin.js +5 -0
  89. package/dist/server/agent-chat-plugin.js.map +1 -1
  90. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  91. package/dist/server/transcribe-voice.d.ts +1 -1
  92. package/package.json +1 -1
@@ -21,12 +21,19 @@ or generated image/video assets that another app can reference by ID and URL.
21
21
  when `libraryId` is omitted, generates candidates, returns the picker filtered
22
22
  to those run IDs, and works in in-app chat plus external MCP hosts.
23
23
  - Use `open-asset-picker` when a person should browse, search, or select an
24
- existing asset in UI, or when you want the picker to handle generation itself.
24
+ existing asset inside an embedded picker, or when you want the picker to
25
+ handle generation itself. It still opens `/library` with the iframe/bridge
26
+ contract. The normal human Library workspace is `/library` and `/library/:id`.
25
27
  Pass `mediaType: "image"` by default, or `mediaType: "video"` for video
26
28
  libraries.
27
29
  - Use unattended actions when the agent already knows what to do:
28
30
  `search-assets`, `list-assets`, `generate-image`, `generate-image-batch`,
29
31
  `generate-video`, `refresh-generation-run`, and `export-asset`.
32
+ - In chat, consume composer `@` references as structured generation inputs:
33
+ `brand-kit` maps to `libraryId`, `preset` maps to `presetId`, and
34
+ `media-type` chooses image generation versus video generation. If no mention
35
+ is available, use `view-screen`, `list-libraries`, and
36
+ `list-generation-presets` to choose explicit args.
30
37
  - Use generation presets when the user asks for a repeatable output format
31
38
  like social image, blog hero, or diagram. Call `list-generation-presets` for
32
39
  the library and pass `presetId` through generation/refinement actions.
@@ -58,7 +65,8 @@ or generated image/video assets that another app can reference by ID and URL.
58
65
  `generate-image-batch` returns, use its returned `images` / asset fields
59
66
  directly; do not call `get-generation-run`, `refresh-generation-run`, or
60
67
  regenerate just to verify image runs.
61
- 4. For preset-backed work, pass `presetId`; for handoff work, pass `sessionId`.
68
+ 4. For preset-backed work, pass a mentioned or selected `presetId`; for handoff
69
+ work, pass `sessionId`.
62
70
  5. Let the server choose a small deterministic reference set unless the user
63
71
  named exact assets. Canonical style anchors come from
64
72
  `assetLibraries.settings.canonicalStyleAssetIds` and
@@ -10,8 +10,13 @@ Use this skill before calling `generate-image`, `generate-image-batch`, or
10
10
 
11
11
  ## Rules
12
12
 
13
- - Start from the current library. Call `view-screen` when the user says "this
14
- library" or "this image" and you need fresh IDs.
13
+ - Start from composer `@` mentions when the user tags generation inputs.
14
+ `brand-kit` references map to `libraryId`, `preset` references map to
15
+ `presetId`, and `media-type` references choose image generation versus video
16
+ generation. Call `view-screen` when the user says "this library" or "this
17
+ image" and you need fresh IDs. The image model may default from the composer
18
+ image-model picker; all other generation settings should be explicit action
19
+ args, preset values, or action schema defaults.
15
20
  - Use category-tagged references. Blog heroes should prefer `hero`; diagrams
16
21
  should prefer `diagram`; product imagery should include `product` and `logo`
17
22
  references.
@@ -20,16 +25,18 @@ Use this skill before calling `generate-image`, `generate-image-batch`, or
20
25
  `assets.metadata.isStyleAnchor` before sampling other relevant references.
21
26
  - Honor library custom instructions. They are persistent prompt guidance and
22
27
  should be updated when the user wants durable generation behavior.
23
- - Generate 2-4 candidates for open-ended requests. Use `generate-image-batch`
24
- with stable `slotId`s so the UI can show live slots.
28
+ - Generate the selected candidate count for open-ended requests, usually 2-4.
29
+ Use `generate-image-batch` with stable `slotId`s so the shared generation tray
30
+ can show live slots.
25
31
  - `generate-image` and `generate-image-batch` are synchronous for images. One
26
32
  batch call should produce the requested candidates and return their asset
27
33
  IDs/URLs; do not follow it with `get-generation-run`,
28
34
  `refresh-generation-run`, or more generation unless the user asks for another
29
35
  direction or the returned slot has `ok: false`.
30
- - For repeatable deliverables, call `list-generation-presets` and pass the
31
- selected `presetId` to `generate-image`, `generate-image-batch`,
32
- `refine-image`, or `rerun-generation-run`.
36
+ - For repeatable deliverables, honor a `preset` @mention as `presetId` or call
37
+ `list-generation-presets` when choosing one. Pass the preset through
38
+ `generate-image`, `generate-image-batch`, `refine-image`, or
39
+ `rerun-generation-run`.
33
40
  - For designer handoff, preserve `sessionId` and call
34
41
  `update-generation-session` after each new candidate so the active asset,
35
42
  feedback, and run lineage stay resumable.
@@ -7,6 +7,14 @@ description: Schema, CRUD, sharing, and cascade-delete patterns for asset librar
7
7
 
8
8
  Use this skill before adding fields, changing access checks, or modifying delete behavior.
9
9
 
10
+ ## User surface
11
+
12
+ The human Library workspace is canonical at `/library`. The root selects
13
+ "All assets" and browses assets across every accessible brand kit; `/library/:id`
14
+ opens one kit's management detail. Legacy `/brand-kits` URLs redirect here.
15
+ Embedded picker hosts still load `/library` in an iframe and keep the existing
16
+ bridge contract.
17
+
10
18
  ## Schema overview
11
19
 
12
20
  ```
@@ -24,6 +32,8 @@ image_libraries — top-level library, has ownableColumns + shares
24
32
  Every action that touches an ownable resource must scope its queries:
25
33
 
26
34
  - **List queries**: `accessFilter(schema.assetLibraries, schema.assetLibraryShares)` in WHERE.
35
+ Cross-kit asset lists must first resolve the accessible library IDs, then
36
+ query `image_assets` by those IDs and include the parent kit title for UI chips.
27
37
  - **Read by id**: `await resolveAccess("asset-library", libraryId)`. The `requireLibrary(id)` helper in `_helpers.ts` wraps this.
28
38
  - **Write**: `await assertAccess("asset-library", libraryId, "editor")` for updates / inserts; `"admin"` for deletes.
29
39
 
@@ -52,6 +62,7 @@ Example: adding `image_libraries.icon`:
52
62
  ## Sharing
53
63
 
54
64
  Libraries follow the standard framework sharing model:
65
+
55
66
  - `visibility: "private" | "org" | "public"`
56
67
  - Per-user / per-org grants in `image_library_shares` with `viewer | editor | admin` roles.
57
68
  - Use the framework actions `share-resource`, `unshare-resource`, `set-resource-visibility` with `--resourceType=asset-library`. The legacy `image-library` alias remains registered for existing grants.
@@ -61,6 +72,7 @@ Generated assets and references inherit the parent library's visibility. v1 does
61
72
  ## Cascade delete
62
73
 
63
74
  `delete-library` deletes in order:
75
+
64
76
  1. `image_assets WHERE library_id = ?`
65
77
  2. `image_generation_runs WHERE library_id = ?`
66
78
  3. `image_collections WHERE library_id = ?`
@@ -21,7 +21,9 @@ Detailed library, generation, image, embed, and engine rules live in
21
21
  ad hoc provider calls when the app has an action/engine abstraction.
22
22
  - Preserve provenance and metadata for generated or imported assets.
23
23
  - Use `view-screen` when the active library, selected asset, picker, generation,
24
- or embed target is unclear. The picker is also available from the left nav.
24
+ or embed target is unclear. The human Library surface is `/library` for
25
+ cross-kit browsing and `/library/:libraryId` for a single brand kit; embedded
26
+ picker hosts still use `/library` with their iframe/auth bridge params.
25
27
  - The Create tab (`/`) is the full-page Assets chat surface. Use the shared
26
28
  `assets` chat thread storage there, keep past chats in the left sidebar, and
27
29
  use the right agent sidebar only on non-Create routes with view-transition
@@ -33,8 +35,21 @@ Detailed library, generation, image, embed, and engine rules live in
33
35
  ## Application State
34
36
 
35
37
  - `navigation` exposes library, asset, generation, picker, embed, and selection
36
- context. Picker state includes media type, selected library, query, prompt, and
37
- aspect ratio when available.
38
+ context. Human Library state uses
39
+ `{ view: "library", selection: "all" | libraryId, tab, scope, folderId, search }`.
40
+ Embedded picker state keeps `{ view: "picker", mediaType, libraryId, query,
41
+ prompt, aspectRatio }`.
42
+ - Composer `@` mentions are the source of generation inputs. Map
43
+ `brand-kit` references to `libraryId`, `preset` references to `presetId`, and
44
+ `media-type` references to choosing image (`generate-image` /
45
+ `generate-image-batch`) or video (`generate-video`) generation. The current
46
+ library view auto-tags its brand kit as a visible removable chip when the
47
+ composer is empty. The image model is the only remaining composer-side
48
+ default; the image-model picker writes `imageGenerationModel`, which image
49
+ generation actions may use when `model` is omitted.
50
+ - `asset-variants` is the shared live generation tray state. New image
51
+ candidates should appear there through `generate-image` or
52
+ `generate-image-batch`; do not invent page-local progress surfaces.
38
53
  - `navigate` moves the UI to picker, library, generation, asset, and settings
39
54
  surfaces.
40
55
 
@@ -0,0 +1,24 @@
1
+ import { readAppState } from "@agent-native/core/application-state";
2
+
3
+ import { IMAGE_MODELS, type ImageModel } from "../shared/api.js";
4
+
5
+ export const IMAGE_MODEL_STATE_KEY = "imageGenerationModel";
6
+
7
+ function imageModelValue(value: unknown): ImageModel | undefined {
8
+ return typeof value === "string" &&
9
+ (IMAGE_MODELS as readonly string[]).includes(value)
10
+ ? (value as ImageModel)
11
+ : undefined;
12
+ }
13
+
14
+ function recordValue(value: unknown): Record<string, unknown> {
15
+ return value && typeof value === "object" && !Array.isArray(value)
16
+ ? (value as Record<string, unknown>)
17
+ : {};
18
+ }
19
+
20
+ export async function readImageModelDefault(): Promise<ImageModel | undefined> {
21
+ const raw = await readAppState(IMAGE_MODEL_STATE_KEY).catch(() => null);
22
+ const state = recordValue(raw);
23
+ return imageModelValue(state.model) ?? imageModelValue(raw);
24
+ }
@@ -1,38 +1,36 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import {
3
- deleteAppState,
4
- readAppState,
5
- writeAppState,
6
- } from "@agent-native/core/application-state";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
7
3
  import { assertAccess } from "@agent-native/core/sharing";
8
4
  import { eq } from "drizzle-orm";
9
5
  import { z } from "zod";
10
6
 
11
7
  import { getDb, schema } from "../server/db/index.js";
12
- import type { AssetVariantState } from "../shared/api.js";
8
+ import {
9
+ deleteVariantState,
10
+ readVariantState,
11
+ writeVariantState,
12
+ } from "./variant-slots.js";
13
13
 
14
14
  export default defineAction({
15
15
  description:
16
- "Clear one or more live candidate slots from application_state.asset-variants. Use slotId for a single slot, scope='failed' to drop every failed slot, or scope='all' to clear the panel. Any underlying asset rows for cleared slots are deleted (requires editor access on the library).",
16
+ "Clear one or more live candidate slots from application_state.asset-variants. Uses the current chat thread when available. Use slotId for a single slot, scope='failed' to drop every failed slot, or scope='all' to clear the panel. Any underlying asset rows for cleared slots are deleted (requires editor access on the library).",
17
17
  schema: z
18
18
  .object({
19
19
  slotId: z.string().optional(),
20
20
  scope: z.enum(["failed", "all"]).optional(),
21
+ threadId: z.string().nullable().optional(),
21
22
  })
22
23
  .refine((v) => Boolean(v.slotId) !== Boolean(v.scope), {
23
24
  message: "Provide exactly one of `slotId` or `scope`.",
24
25
  }),
25
- run: async ({ slotId, scope }) => {
26
- const raw = (await readAppState("asset-variants")) as unknown | null;
27
- const legacyRaw =
28
- raw ??
29
- ((await readAppState("image-variants").catch(() => null)) as
30
- | unknown
31
- | null);
32
- const state = (legacyRaw ?? null) as AssetVariantState | null;
26
+ run: async ({ slotId, scope, threadId }, context?: ActionRunContext) => {
27
+ const effectiveThreadId = threadId ?? context?.threadId ?? null;
28
+ const state = await readVariantState(effectiveThreadId);
33
29
  if (!state || !Array.isArray(state.slots) || state.slots.length === 0) {
34
30
  return { dismissed: 0, assetsDeleted: 0, cleared: true };
35
31
  }
32
+ const stateThreadId =
33
+ effectiveThreadId ?? state.variantScopeId ?? state.threadId ?? null;
36
34
 
37
35
  await assertAccess("asset-library", state.libraryId, "editor");
38
36
 
@@ -64,8 +62,7 @@ export default defineAction({
64
62
  const remaining = state.slots.filter((s) => !removed.has(s.slotId));
65
63
 
66
64
  if (remaining.length === 0) {
67
- await deleteAppState("asset-variants");
68
- await deleteAppState("image-variants").catch(() => {});
65
+ await deleteVariantState(stateThreadId);
69
66
  return {
70
67
  dismissed: toRemove.length,
71
68
  assetsDeleted,
@@ -75,11 +72,7 @@ export default defineAction({
75
72
 
76
73
  state.slots = remaining;
77
74
  state.updatedAt = new Date().toISOString();
78
- await writeAppState(
79
- "asset-variants",
80
- state as unknown as Record<string, unknown>,
81
- );
82
- await deleteAppState("image-variants").catch(() => {});
75
+ await writeVariantState(state, stateThreadId);
83
76
  return {
84
77
  dismissed: toRemove.length,
85
78
  assetsDeleted,
@@ -1,4 +1,5 @@
1
1
  import { defineAction, embedApp } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { z } from "zod";
3
4
 
4
5
  import {
@@ -116,16 +117,19 @@ const action = defineAction({
116
117
  view: "picker",
117
118
  };
118
119
  },
119
- run: async (args) => {
120
+ run: async (args, context?: ActionRunContext) => {
120
121
  if (args.mediaType === "video") {
121
- const picker = (await openAssetPicker.run({
122
- mediaType: "video",
123
- prompt: args.prompt,
124
- libraryId: args.libraryId,
125
- libraryHint: args.libraryHint,
126
- count: args.count,
127
- callerAppId: args.callerAppId,
128
- })) as Record<string, unknown>;
122
+ const picker = (await openAssetPicker.run(
123
+ {
124
+ mediaType: "video",
125
+ prompt: args.prompt,
126
+ libraryId: args.libraryId,
127
+ libraryHint: args.libraryHint,
128
+ count: args.count,
129
+ callerAppId: args.callerAppId,
130
+ },
131
+ context,
132
+ )) as Record<string, unknown>;
129
133
  return {
130
134
  ...picker,
131
135
  generated: false,
@@ -134,20 +138,23 @@ const action = defineAction({
134
138
  };
135
139
  }
136
140
 
137
- const picker = (await openAssetPicker.run({
138
- mediaType: "image",
139
- prompt: args.prompt,
140
- libraryId: args.libraryId,
141
- libraryHint: args.libraryHint,
142
- aspectRatio: args.aspectRatio,
143
- presetId: args.presetId,
144
- count: args.count,
145
- autoGenerate: true,
146
- tier: args.tier,
147
- styleStrength: args.styleStrength,
148
- includeLogo: args.includeLogo,
149
- callerAppId: args.callerAppId,
150
- })) as Record<string, unknown>;
141
+ const picker = (await openAssetPicker.run(
142
+ {
143
+ mediaType: "image",
144
+ prompt: args.prompt,
145
+ libraryId: args.libraryId,
146
+ libraryHint: args.libraryHint,
147
+ aspectRatio: args.aspectRatio,
148
+ presetId: args.presetId,
149
+ count: args.count,
150
+ autoGenerate: true,
151
+ tier: args.tier,
152
+ styleStrength: args.styleStrength,
153
+ includeLogo: args.includeLogo,
154
+ callerAppId: args.callerAppId,
155
+ },
156
+ context,
157
+ )) as Record<string, unknown>;
151
158
  return {
152
159
  ...picker,
153
160
  generated: false,
@@ -1,4 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { assertAccess } from "@agent-native/core/sharing";
3
4
  import { eq } from "drizzle-orm";
4
5
  import { nanoid } from "nanoid";
@@ -17,13 +18,20 @@ import {
17
18
  STYLE_STRENGTHS,
18
19
  } from "../shared/api.js";
19
20
  import { requireGenerationSessionInLibrary } from "./_helpers.js";
21
+ import { readImageModelDefault } from "./_image-model-default.js";
20
22
  import generateImage from "./generate-image.js";
23
+ import { upsertVariantSlot } from "./variant-slots.js";
21
24
 
22
25
  export default defineAction({
23
26
  description:
24
- "Generate several brand-consistent images in parallel from one library. This is synchronous for images: one call waits for every slot and returns final image artifacts. Use this for slide decks, landing pages, and multi-slot design work. Do not call get-generation-run or refresh-generation-run after a normal image batch result.",
27
+ "Generate several brand-consistent images in parallel from one brand kit/library. Use @brand-kit mentions as libraryId and @preset mentions as presetId when present. This is synchronous for images: one call waits for every slot and returns final image artifacts. Use this for slide decks, landing pages, and multi-slot design work. Do not call get-generation-run or refresh-generation-run after a normal image batch result.",
25
28
  schema: z.object({
26
- libraryId: z.string(),
29
+ libraryId: z
30
+ .string()
31
+ .optional()
32
+ .describe(
33
+ "Brand kit/library ID. Pass the refId from a brand-kit @mention, or choose a kit from view-screen/list-libraries.",
34
+ ),
27
35
  collectionId: z.string().optional(),
28
36
  presetId: z.string().optional(),
29
37
  sessionId: z.string().optional(),
@@ -45,6 +53,12 @@ export default defineAction({
45
53
  )
46
54
  .min(1)
47
55
  .max(12),
56
+ variantScopeId: z
57
+ .string()
58
+ .optional()
59
+ .describe(
60
+ "Internal UI state scope for live candidate slots. Usually omitted; embedded picker UIs pass a browser-tab scope.",
61
+ ),
48
62
  model: z.enum(IMAGE_MODELS).optional(),
49
63
  tier: z.enum(IMAGE_QUALITY_TIERS).optional(),
50
64
  intent: z.enum(GENERATION_INTENTS).default("generate"),
@@ -60,41 +74,74 @@ export default defineAction({
60
74
  ),
61
75
  }),
62
76
  parallelSafe: true,
63
- run: async ({ slots, ...base }) => {
77
+ run: async ({ slots, ...inputBase }, context?: ActionRunContext) => {
78
+ const imageModelDefault = await readImageModelDefault();
79
+ const libraryId = inputBase.libraryId;
80
+ if (!libraryId) {
81
+ throw new Error(
82
+ "No brand kit selected. Tag a brand kit with @ or pass libraryId.",
83
+ );
84
+ }
85
+ const base = {
86
+ ...inputBase,
87
+ libraryId,
88
+ model: inputBase.model ?? imageModelDefault,
89
+ };
64
90
  await assertAccess("asset-library", base.libraryId, "editor");
65
91
  if (base.sessionId) {
66
92
  await requireGenerationSessionInLibrary(base.sessionId, base.libraryId);
67
93
  }
68
- const limit = pLimit(4);
69
94
  const variantBatchId = nanoid();
95
+ await Promise.all(
96
+ slots.map((slot, index) =>
97
+ upsertVariantSlot({
98
+ runId: `pending-${variantBatchId}-${index + 1}`,
99
+ batchId: variantBatchId,
100
+ libraryId: base.libraryId,
101
+ collectionId: base.collectionId ?? null,
102
+ presetId: base.presetId ?? null,
103
+ sessionId: base.sessionId ?? null,
104
+ threadId: context?.threadId ?? null,
105
+ variantScopeId: base.variantScopeId ?? null,
106
+ prompt: slot.prompt,
107
+ slotId: slot.slotId,
108
+ status: "pending",
109
+ }),
110
+ ),
111
+ );
112
+ const limit = pLimit(4);
70
113
  const results = await Promise.allSettled(
71
114
  slots.map((slot) =>
72
115
  limit(() =>
73
- generateImage.run({
74
- libraryId: base.libraryId,
75
- collectionId: base.collectionId,
76
- presetId: base.presetId,
77
- sessionId: base.sessionId,
78
- prompt: slot.prompt,
79
- aspectRatio: slot.aspectRatio,
80
- imageSize: slot.imageSize,
81
- model: base.model,
82
- tier: base.tier,
83
- intent: slot.intent ?? base.intent,
84
- styleStrength: slot.styleStrength ?? base.styleStrength,
85
- categories: slot.categories,
86
- referenceAssetIds: slot.referenceAssetIds,
87
- includeLogo: base.includeLogo,
88
- groundingMode: base.groundingMode,
89
- slotId: slot.slotId,
90
- variantBatchId,
91
- dismissible: slot.dismissible,
92
- sourceAssetId: slot.sourceAssetId,
93
- subjectAssetId: slot.subjectAssetId,
94
- source: base.source,
95
- callerAppId: base.callerAppId,
96
- activateSessionAsset: false,
97
- }),
116
+ generateImage.run(
117
+ {
118
+ libraryId: base.libraryId,
119
+ collectionId: base.collectionId,
120
+ presetId: base.presetId,
121
+ sessionId: base.sessionId,
122
+ prompt: slot.prompt,
123
+ aspectRatio: slot.aspectRatio,
124
+ imageSize: slot.imageSize,
125
+ model: base.model,
126
+ tier: base.tier,
127
+ intent: slot.intent ?? base.intent,
128
+ styleStrength: slot.styleStrength ?? base.styleStrength,
129
+ categories: slot.categories,
130
+ referenceAssetIds: slot.referenceAssetIds,
131
+ includeLogo: base.includeLogo,
132
+ groundingMode: base.groundingMode,
133
+ slotId: slot.slotId,
134
+ variantBatchId,
135
+ variantScopeId: base.variantScopeId,
136
+ dismissible: slot.dismissible,
137
+ sourceAssetId: slot.sourceAssetId,
138
+ subjectAssetId: slot.subjectAssetId,
139
+ source: base.source,
140
+ callerAppId: base.callerAppId,
141
+ activateSessionAsset: false,
142
+ },
143
+ context,
144
+ ),
98
145
  ),
99
146
  ),
100
147
  );
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import {
3
- readAppState,
4
4
  writeAppState,
5
5
  deleteAppState,
6
6
  } from "@agent-native/core/application-state";
@@ -43,6 +43,7 @@ import {
43
43
  requireGenerationSessionInLibrary,
44
44
  serializeAsset,
45
45
  } from "./_helpers.js";
46
+ import { readImageModelDefault } from "./_image-model-default.js";
46
47
  import { upsertVariantSlot, wasVariantSlotDismissed } from "./variant-slots.js";
47
48
 
48
49
  function resolveModelForTier(
@@ -57,33 +58,16 @@ function resolveModelForTier(
57
58
  : "gemini-3.1-flash-image";
58
59
  }
59
60
 
60
- /**
61
- * The user's default image model, chosen from the composer's model picker and
62
- * persisted in per-user application state. Used as a fallback when no explicit
63
- * model, tier, or preset model is supplied. Returns undefined when unset or
64
- * invalid so the hardcoded default still applies.
65
- */
66
- async function readUserDefaultImageModel(): Promise<ImageModel | undefined> {
67
- try {
68
- const stored = await readAppState("imageGenerationModel");
69
- const model = stored?.model;
70
- if (
71
- typeof model === "string" &&
72
- (IMAGE_MODELS as readonly string[]).includes(model)
73
- ) {
74
- return model as ImageModel;
75
- }
76
- } catch {
77
- // No request context or read failure — fall back to defaults below.
78
- }
79
- return undefined;
80
- }
81
-
82
61
  export default defineAction({
83
62
  description:
84
- "Generate one brand-consistent image from a library. This is synchronous for images and returns the final asset with preview/download/embed URLs. Use generate-image-batch for multiple independent slots; do not poll image runs after this action returns.",
63
+ "Generate one brand-consistent image from a brand kit/library. This is synchronous for images and returns the final asset with preview/download/embed URLs. Use @brand-kit mentions as libraryId and @preset mentions as presetId when present. Use generate-image-batch for multiple independent slots; do not poll image runs after this action returns.",
85
64
  schema: z.object({
86
- libraryId: z.string(),
65
+ libraryId: z
66
+ .string()
67
+ .optional()
68
+ .describe(
69
+ "Brand kit/library ID. Pass the refId from a brand-kit @mention, or choose a kit from view-screen/list-libraries.",
70
+ ),
87
71
  collectionId: z.string().optional(),
88
72
  presetId: z.string().optional(),
89
73
  sessionId: z.string().optional(),
@@ -104,6 +88,12 @@ export default defineAction({
104
88
  includeLogo: z.coerce.boolean().default(false),
105
89
  slotId: z.string().optional(),
106
90
  variantBatchId: z.string().optional(),
91
+ variantScopeId: z
92
+ .string()
93
+ .optional()
94
+ .describe(
95
+ "Internal UI state scope for live candidate slots. Usually omitted; embedded picker UIs pass a browser-tab scope.",
96
+ ),
107
97
  dismissible: z.coerce
108
98
  .boolean()
109
99
  .default(true)
@@ -135,7 +125,18 @@ export default defineAction({
135
125
  ),
136
126
  }),
137
127
  parallelSafe: true,
138
- run: async (args) => {
128
+ run: async (input, context?: ActionRunContext) => {
129
+ const imageModelDefault = await readImageModelDefault();
130
+ const libraryId = input.libraryId;
131
+ if (!libraryId) {
132
+ throw new Error(
133
+ "No brand kit selected. Tag a brand kit with @ or pass libraryId.",
134
+ );
135
+ }
136
+ const args = {
137
+ ...input,
138
+ libraryId,
139
+ };
139
140
  await assertAccess("asset-library", args.libraryId, "editor");
140
141
  const db = getDb();
141
142
  const [library] = await db
@@ -248,9 +249,9 @@ export default defineAction({
248
249
  preset?.category ??
249
250
  collection?.category) as ImageCategory | undefined;
250
251
  const resolvedModel = (args.model ??
252
+ imageModelDefault ??
251
253
  resolveModelForTier(resolvedTier, category) ??
252
254
  preset?.model ??
253
- (await readUserDefaultImageModel()) ??
254
255
  "gemini-3.1-flash-image") as (typeof IMAGE_MODELS)[number];
255
256
  const resolvedCategories =
256
257
  args.categories ??
@@ -303,6 +304,8 @@ export default defineAction({
303
304
  });
304
305
  const runId = nanoid();
305
306
  const now = nowIso();
307
+ const slotId = args.slotId ?? runId;
308
+ const variantScopeId = args.variantScopeId ?? context?.threadId ?? null;
306
309
  // Capture identity at insert time so the org-admin audit log can filter
307
310
  // by owner / org without re-resolving who triggered the run later.
308
311
  const ownerEmail = getRequestUserEmail() ?? null;
@@ -342,11 +345,12 @@ export default defineAction({
342
345
  sessionId: session?.id ?? null,
343
346
  customInstructions: library.customInstructions ?? "",
344
347
  };
345
- const slotId = args.slotId ?? runId;
346
348
  const dismissibleSlot = args.dismissible !== false && Boolean(slotId);
347
349
  const baseMetadata = {
348
350
  slotId,
349
351
  variantBatchId: args.variantBatchId ?? null,
352
+ threadId: context?.threadId ?? null,
353
+ variantScopeId,
350
354
  dismissible: dismissibleSlot,
351
355
  sourceAssetId: args.sourceAssetId,
352
356
  subjectAssetId: args.subjectAssetId,
@@ -389,6 +393,8 @@ export default defineAction({
389
393
  collectionId: resolvedCollectionId ?? null,
390
394
  presetId: preset?.id ?? null,
391
395
  sessionId: session?.id ?? null,
396
+ threadId: context?.threadId ?? null,
397
+ variantScopeId,
392
398
  prompt: args.prompt,
393
399
  slotId,
394
400
  status: "pending",
@@ -430,7 +436,10 @@ export default defineAction({
430
436
  }
431
437
  if (
432
438
  dismissibleSlot &&
433
- (await wasVariantSlotDismissed(args.libraryId, slotId))
439
+ (await wasVariantSlotDismissed(args.libraryId, slotId, {
440
+ threadId: context?.threadId ?? null,
441
+ variantScopeId,
442
+ }))
434
443
  ) {
435
444
  await db
436
445
  .update(schema.assetGenerationRuns)
@@ -539,6 +548,8 @@ export default defineAction({
539
548
  collectionId: resolvedCollectionId ?? null,
540
549
  presetId: preset?.id ?? null,
541
550
  sessionId: session?.id ?? null,
551
+ threadId: context?.threadId ?? null,
552
+ variantScopeId,
542
553
  prompt: args.prompt,
543
554
  slotId,
544
555
  status: "ready",
@@ -570,7 +581,10 @@ export default defineAction({
570
581
  .where(eq(schema.assetGenerationRuns.id, runId));
571
582
  if (
572
583
  dismissibleSlot &&
573
- (await wasVariantSlotDismissed(args.libraryId, slotId))
584
+ (await wasVariantSlotDismissed(args.libraryId, slotId, {
585
+ threadId: context?.threadId ?? null,
586
+ variantScopeId,
587
+ }))
574
588
  ) {
575
589
  throw err;
576
590
  }
@@ -581,6 +595,8 @@ export default defineAction({
581
595
  collectionId: resolvedCollectionId ?? null,
582
596
  presetId: preset?.id ?? null,
583
597
  sessionId: session?.id ?? null,
598
+ threadId: context?.threadId ?? null,
599
+ variantScopeId,
584
600
  prompt: args.prompt,
585
601
  slotId,
586
602
  status: "failed",