@promptbook/cli 0.112.0-103 → 0.112.0-104

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 (31) hide show
  1. package/apps/agents-server/src/app/AddAgentButton.tsx +0 -5
  2. package/apps/agents-server/src/app/actions.ts +50 -0
  3. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +3 -4
  4. package/apps/agents-server/src/app/api/health/route.ts +18 -0
  5. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +1 -4
  6. package/apps/agents-server/src/components/Header/Header.tsx +0 -11
  7. package/apps/agents-server/src/components/Header/useHeaderAgentMenus.tsx +0 -5
  8. package/apps/agents-server/src/components/NewAgentDialog/useNewAgentDialog.tsx +39 -16
  9. package/apps/agents-server/src/constants/defaultAgentAvatarVisual.ts +1 -1
  10. package/apps/agents-server/src/database/migrations/2026-06-0200-default-agent-avatar-visual-octopus3d3.sql +16 -0
  11. package/apps/agents-server/src/middleware.ts +2 -1
  12. package/apps/agents-server/src/tools/$provideCdnForServer.ts +43 -2
  13. package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +27 -4
  14. package/apps/agents-server/src/utils/defaultAgents/defaultAgents.ts +168 -0
  15. package/apps/agents-server/src/utils/defaultAgents/installDefaultAgents.ts +139 -0
  16. package/esm/index.es.js +518 -7
  17. package/esm/index.es.js.map +1 -1
  18. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  19. package/esm/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
  20. package/package.json +1 -1
  21. package/src/avatars/types/AvatarVisualDefinition.ts +1 -0
  22. package/src/avatars/visuals/avatarVisualRegistry.ts +2 -0
  23. package/src/avatars/visuals/octopus3d3AvatarVisual.ts +903 -0
  24. package/src/other/templates/getTemplatesPipelineCollection.ts +784 -716
  25. package/src/utils/agents/resolveAgentAvatarImageUrl.ts +1 -1
  26. package/src/version.ts +1 -1
  27. package/src/versions.txt +1 -1
  28. package/umd/index.umd.js +518 -7
  29. package/umd/index.umd.js.map +1 -1
  30. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  31. package/umd/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { useRouter } from 'next/navigation';
4
3
  import { FileCard } from '../components/Homepage/FileCard';
5
4
  import { useAgentNaming } from '../components/AgentNaming/AgentNamingContext';
6
5
  import { showAlert } from '../components/AsyncDialogs/asyncDialogs';
@@ -21,15 +20,11 @@ type AddAgentButtonProps = {
21
20
  * Renders the add-agent card and creation dialog workflow.
22
21
  */
23
22
  export function AddAgentButton({ currentFolderId }: AddAgentButtonProps) {
24
- const router = useRouter();
25
23
  const { formatText } = useAgentNaming();
26
24
  const { t } = useServerLanguage();
27
25
  const addButtonLabel = formatText(t('agentCreation.addButtonLabel'));
28
26
 
29
27
  const { isPreparingDialog, openNewAgentDialog, dialog } = useNewAgentDialog({
30
- onCreated: ({ targetPath }) => {
31
- router.push(targetPath);
32
- },
33
28
  onCreateFailed: async (error) => {
34
29
  console.error('Failed to create agent:', error);
35
30
  await showAlert({
@@ -8,6 +8,9 @@ import { DEFAULT_NAME_POOL, NAME_POOL_METADATA_KEY, parseNamePool } from '../con
8
8
  import { NEW_AGENT_WIZZARD_METADATA_KEY, parseNewAgentWizardMode } from '../constants/newAgentWizard';
9
9
  import { getMetadata } from '../database/getMetadata';
10
10
  import { $provideAgentCollectionForServer } from '../tools/$provideAgentCollectionForServer';
11
+ import { invalidateCachedActiveOrganizationSnapshots } from '../utils/agentOrganization/loadAgentOrganizationState';
12
+ import { resolveAgentRouteTarget } from '../utils/agentRouting/resolveAgentRouteTarget';
13
+ import { buildAgentChatHref, buildAgentProfileHref } from '../utils/agentRouting/agentRouteHrefs';
11
14
  import { type AgentVisibility, parseAgentVisibility } from '../utils/agentVisibility';
12
15
  import { authenticateUser } from '../utils/authenticateUser';
13
16
  import { createAgentWithDefaultVisibility } from '../utils/createAgentWithDefaultVisibility';
@@ -15,6 +18,16 @@ import { resolveCurrentUserIdentity } from '../utils/currentUserIdentity';
15
18
  import { isUserAdmin } from '../utils/isUserAdmin';
16
19
  import { clearSession, setSession } from '../utils/session';
17
20
 
21
+ /**
22
+ * Maximum attempts used to confirm a freshly created agent route resolves before navigation starts.
23
+ */
24
+ const CREATED_AGENT_ROUTE_READY_ATTEMPTS = 20;
25
+
26
+ /**
27
+ * Delay between created-agent route-resolution retries.
28
+ */
29
+ const CREATED_AGENT_ROUTE_READY_DELAY_MS = 100;
30
+
18
31
  /**
19
32
  * Creates a new agent from the generated boilerplate template.
20
33
  *
@@ -34,6 +47,8 @@ export async function $createAgentAction(): Promise<{ agentName: string_agent_na
34
47
  const { agentName, permanentId } = await createAgentWithDefaultVisibility(collection, agentSource, {
35
48
  userId: currentUserIdentity?.userId,
36
49
  });
50
+ revalidateCreatedAgentPaths(permanentId);
51
+ await waitForCreatedAgentRoute(permanentId);
37
52
 
38
53
  return { agentName, permanentId };
39
54
  }
@@ -63,6 +78,39 @@ export async function $getNewAgentCreationSettingsAction(): Promise<{
63
78
  };
64
79
  }
65
80
 
81
+ /**
82
+ * Clears cached organization snapshots and route payloads after a new agent is created.
83
+ *
84
+ * @param permanentId - Canonical identifier of the newly created agent.
85
+ */
86
+ function revalidateCreatedAgentPaths(permanentId: string_agent_permanent_id): void {
87
+ invalidateCachedActiveOrganizationSnapshots();
88
+ revalidatePath('/', 'layout');
89
+ revalidatePath('/');
90
+ revalidatePath('/agents');
91
+ revalidatePath('/dashboard');
92
+ revalidatePath(buildAgentProfileHref(permanentId));
93
+ revalidatePath(buildAgentChatHref(permanentId));
94
+ }
95
+
96
+ /**
97
+ * Waits until the new agent can be resolved by the same routing helper the chat page uses.
98
+ *
99
+ * @param permanentId - Canonical identifier of the newly created agent.
100
+ */
101
+ async function waitForCreatedAgentRoute(permanentId: string_agent_permanent_id): Promise<void> {
102
+ for (let attempt = 0; attempt < CREATED_AGENT_ROUTE_READY_ATTEMPTS; attempt++) {
103
+ const routeTarget = await resolveAgentRouteTarget(permanentId, { forceRefresh: true });
104
+ if (routeTarget?.kind === 'local' && routeTarget.canonicalAgentId === permanentId) {
105
+ return;
106
+ }
107
+
108
+ await new Promise((resolve) => setTimeout(resolve, CREATED_AGENT_ROUTE_READY_DELAY_MS));
109
+ }
110
+
111
+ throw new Error(`Created agent "${permanentId}" could not be resolved for routing immediately after creation.`);
112
+ }
113
+
66
114
  /**
67
115
  * Creates a new agent using provided book content.
68
116
  *
@@ -89,6 +137,8 @@ export async function $createAgentFromBookAction(
89
137
  visibility: visibility ?? undefined,
90
138
  userId: currentUserIdentity?.userId,
91
139
  });
140
+ revalidateCreatedAgentPaths(permanentId);
141
+ await waitForCreatedAgentRoute(permanentId);
92
142
 
93
143
  return { agentName, permanentId };
94
144
  }
@@ -4,7 +4,6 @@ import { usePromise } from '@common/hooks/usePromise';
4
4
  import { Chat } from '@promptbook-local/components';
5
5
  import { RemoteAgent } from '@promptbook-local/core';
6
6
  import { string_book, type ChatMessage } from '@promptbook-local/types';
7
- import { useRouter } from 'next/navigation';
8
7
  import { useCallback, useMemo, useState, type MouseEvent as ReactMouseEvent } from 'react';
9
8
  import { spaceTrim } from 'spacetrim';
10
9
  import { string_agent_url, string_color } from '../../../../../../src/types/typeAliases';
@@ -24,6 +23,7 @@ import { useServerLanguage } from '../../../components/ServerLanguage/ServerLang
24
23
  import { ChatThreadLoadingSkeleton } from '../../../components/Skeleton/ChatThreadLoadingSkeleton';
25
24
  import { usePromptbookTheme } from '../../../components/ThemeMode/usePromptbookTheme';
26
25
  import type { ServerLanguageCode } from '../../../languages/ServerLanguageRegistry';
26
+ import { buildFreshAgentChatHref } from '../../../utils/agentRouting/agentRouteHrefs';
27
27
  import { executeQuickActionButton } from '../../../utils/chat/executeQuickActionButton';
28
28
  import { resolveChatMessageValidationIssue } from '../../../utils/chat/validateChatMessageContent';
29
29
  import { createServerLanguageMoment } from '../../../utils/localization/createServerLanguageMoment';
@@ -277,7 +277,6 @@ export function AgentProfileChat({
277
277
  isHistoryEnabled = false,
278
278
  areFileAttachmentsEnabled = true,
279
279
  }: AgentProfileChatProps) {
280
- const router = useRouter();
281
280
  const [isCreatingAgent, setIsCreatingAgent] = useState(false);
282
281
  const [optimisticNavigationState, setOptimisticNavigationState] = useState<OptimisticChatNavigationState | null>(null);
283
282
  const { formatText } = useAgentNaming();
@@ -409,7 +408,7 @@ export function AgentProfileChat({
409
408
  try {
410
409
  const { permanentId } = await $createAgentFromBookAction(bookContent as string_book);
411
410
  if (permanentId) {
412
- router.push(`/agents/${permanentId}`);
411
+ window.location.assign(buildFreshAgentChatHref(permanentId));
413
412
  }
414
413
  } catch (error) {
415
414
  console.error('Failed to create agent:', error);
@@ -421,7 +420,7 @@ export function AgentProfileChat({
421
420
  setIsCreatingAgent(false);
422
421
  }
423
422
  },
424
- [formatText, router],
423
+ [formatText],
425
424
  );
426
425
  const handleFileUpload = useCallback(async (file: File) => chatFileUploadHandler(file), []);
427
426
  const fallbackInitialMessage = useMemo(() => {
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * Ensures the readiness route runs in the same runtime as the standalone server.
5
+ */
6
+ export const runtime = 'nodejs';
7
+
8
+ /**
9
+ * Prevents a cached response from hiding readiness failures during deployment handoff.
10
+ */
11
+ export const dynamic = 'force-dynamic';
12
+
13
+ /**
14
+ * Lightweight readiness endpoint used by standalone VPS pm2/nginx handoffs.
15
+ */
16
+ export async function GET() {
17
+ return NextResponse.json({ status: 'ok' });
18
+ }
@@ -186,10 +186,7 @@ export function AgentProfile(props: AgentProfileProps) {
186
186
  ) : (
187
187
  <div className="flex h-full w-full items-center justify-center overflow-hidden p-4 md:p-8">
188
188
  {/* Keep built-in visuals inside a centered square stage so different avatar renderers fit the tall profile card consistently. */}
189
- <div
190
- className="flex h-full w-full max-h-[80%] max-w-[80%] items-center justify-center"
191
- style={{ aspectRatio: '1 / 1' }}
192
- >
189
+ <div className="flex aspect-square w-[80%] items-center justify-center">
193
190
  <AgentAvatar
194
191
  agent={agent}
195
192
  baseUrl={publicUrl}
@@ -9,7 +9,6 @@ import { HamburgerMenu } from '../../../../../src/book-components/_common/Hambur
9
9
  import { useMenuHoisting } from '../../../../../src/book-components/_common/MenuHoisting/MenuHoistingContext';
10
10
  import { just } from '../../../../../src/utils/organization/just';
11
11
  import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
12
- import { pushWithHeadless, useIsHeadless } from '../_utils/headlessParam';
13
12
  import { useInstallPromptState, type AgentContextMenuRenamePayload } from '../AgentContextMenu/AgentContextMenu';
14
13
  import { useAgentNaming } from '../AgentNaming/AgentNamingContext';
15
14
  import { QrCodeModal } from '../AgentProfile/QrCodeModal';
@@ -58,11 +57,9 @@ export function Header(props: HeaderProps) {
58
57
  feedbackMode = 'stars',
59
58
  shibbolethAuthenticationStatus,
60
59
  } = props;
61
-
62
60
  const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
63
61
  const router = useRouter();
64
62
  const pathname = usePathname();
65
- const isHeadless = useIsHeadless();
66
63
  const menuHoisting = useMenuHoisting();
67
64
  const mobileMenuHoisting = useMobileMenuHoisting();
68
65
  const { naming } = useAgentNaming();
@@ -73,13 +70,6 @@ export function Header(props: HeaderProps) {
73
70
  () => buildDocumentationDropdownItems(visibleDocumentationCommitments, t),
74
71
  [t, visibleDocumentationCommitments],
75
72
  );
76
-
77
- const fallbackNavigateToHref = useCallback(
78
- (href: string) => {
79
- pushWithHeadless(router, href, isHeadless);
80
- },
81
- [isHeadless, router],
82
- );
83
73
  const hoistedMobileMenuItems = mobileMenuHoisting?.menuItems || EMPTY_HOISTED_MOBILE_MENU_ITEMS;
84
74
  const {
85
75
  cancelMenuClose,
@@ -201,7 +191,6 @@ export function Header(props: HeaderProps) {
201
191
  isAdmin,
202
192
  isAuthenticated: Boolean(currentUser),
203
193
  isInstalled,
204
- navigateToHref: fallbackNavigateToHref,
205
194
  namingPlural: naming.plural,
206
195
  namingSingular: naming.singular,
207
196
  onAgentRenamed: handleAgentRenamedFromHeader,
@@ -44,7 +44,6 @@ type UseHeaderAgentMenusOptions = {
44
44
  readonly isAdmin: boolean;
45
45
  readonly isAuthenticated: boolean;
46
46
  readonly isInstalled: boolean;
47
- readonly navigateToHref: (href: string) => void;
48
47
  readonly namingPlural: string;
49
48
  readonly namingSingular: string;
50
49
  readonly onAgentRenamed: (payload: AgentContextMenuRenamePayload) => void;
@@ -160,7 +159,6 @@ export function useHeaderAgentMenus({
160
159
  isAdmin,
161
160
  isAuthenticated,
162
161
  isInstalled,
163
- navigateToHref,
164
162
  namingPlural,
165
163
  namingSingular,
166
164
  onAgentRenamed,
@@ -214,9 +212,6 @@ export function useHeaderAgentMenus({
214
212
  openNewAgentDialog,
215
213
  dialog: newAgentDialog,
216
214
  } = useNewAgentDialog({
217
- onCreated: ({ targetPath }) => {
218
- navigateToHref(targetPath);
219
- },
220
215
  onCreateFailed: async (error) => {
221
216
  await showNewAgentFailure('Failed to create agent:', error, namingSingular, translate);
222
217
  },
@@ -3,7 +3,9 @@
3
3
  import type { string_book } from '@promptbook-local/types';
4
4
  import type { ReactElement } from 'react';
5
5
  import { useCallback, useState } from 'react';
6
+ import { appendHeadlessParam, useIsHeadless } from '../_utils/headlessParam';
6
7
  import type { AgentVisibility } from '../../utils/agentVisibility';
8
+ import { buildFreshAgentChatHref } from '../../utils/agentRouting/agentRouteHrefs';
7
9
  import {
8
10
  $createAgentFromBookAction,
9
11
  $generateAgentBoilerplateAction,
@@ -51,9 +53,9 @@ type CreatedAgentPayload = {
51
53
  */
52
54
  type UseNewAgentDialogOptions = {
53
55
  /**
54
- * Called after a new agent is created successfully.
56
+ * Optional callback invoked after the new agent payload is prepared and before navigation starts.
55
57
  */
56
- readonly onCreated: (agent: CreatedAgentPayload) => Promise<void> | void;
58
+ readonly onCreated?: (agent: CreatedAgentPayload) => Promise<void> | void;
57
59
  /**
58
60
  * Optional callback invoked when creating an agent fails.
59
61
  */
@@ -120,11 +122,27 @@ function extractAgentNameFromBoilerplate(boilerplate: string_book): string {
120
122
  );
121
123
  }
122
124
 
125
+ /**
126
+ * Creates the navigation payload returned after one agent is persisted.
127
+ *
128
+ * @param agentName - Persisted display name of the agent.
129
+ * @param permanentId - Canonical immutable identifier of the agent.
130
+ * @returns Shared created-agent payload used by all creation surfaces.
131
+ */
132
+ function createCreatedAgentPayload(agentName: string, permanentId: string): CreatedAgentPayload {
133
+ return {
134
+ agentName,
135
+ permanentId,
136
+ targetPath: buildFreshAgentChatHref(permanentId),
137
+ };
138
+ }
139
+
123
140
  /**
124
141
  * Provides a shared "create new agent" workflow with boilerplate loading and a book-editing dialog.
125
142
  */
126
143
  export function useNewAgentDialog(options: UseNewAgentDialogOptions): UseNewAgentDialogResult {
127
144
  const { onCreated, onCreateFailed, onPrepareFailed } = options;
145
+ const isHeadless = useIsHeadless();
128
146
  const [isPreparingDialog, setIsPreparingDialog] = useState(false);
129
147
  const [dialogState, setDialogState] = useState<NewAgentDialogState | null>(null);
130
148
 
@@ -132,6 +150,21 @@ export function useNewAgentDialog(options: UseNewAgentDialogOptions): UseNewAgen
132
150
  setDialogState(null);
133
151
  }, []);
134
152
 
153
+ /**
154
+ * Finalizes one successful creation by hard-navigating to the new chat route.
155
+ *
156
+ * The App Router can transiently keep the just-created dynamic route in a stale not-found
157
+ * state, so new-agent creation intentionally uses a full navigation once the route is ready.
158
+ */
159
+ const handleCreatedAgent = useCallback(
160
+ async (agent: CreatedAgentPayload) => {
161
+ await onCreated?.(agent);
162
+ setDialogState(null);
163
+ window.location.assign(appendHeadlessParam(agent.targetPath, isHeadless));
164
+ },
165
+ [isHeadless, onCreated],
166
+ );
167
+
135
168
  const openNewAgentDialog = useCallback(
136
169
  async (openOptions?: OpenNewAgentDialogOptions) => {
137
170
  setIsPreparingDialog(true);
@@ -200,17 +233,12 @@ export function useNewAgentDialog(options: UseNewAgentDialogOptions): UseNewAgen
200
233
  surface: 'editor',
201
234
  folderId: dialogState.targetFolderId,
202
235
  });
203
- await onCreated({
204
- agentName,
205
- permanentId,
206
- targetPath: `/agents/${encodeURIComponent(permanentId)}`,
207
- });
208
- setDialogState(null);
236
+ await handleCreatedAgent(createCreatedAgentPayload(agentName, permanentId));
209
237
  } catch (error) {
210
238
  await onCreateFailed?.(error);
211
239
  }
212
240
  },
213
- [dialogState, onCreateFailed, onCreated],
241
+ [dialogState, handleCreatedAgent, onCreateFailed],
214
242
  );
215
243
 
216
244
  const handleCreateFromWizard = useCallback(
@@ -232,17 +260,12 @@ export function useNewAgentDialog(options: UseNewAgentDialogOptions): UseNewAgen
232
260
  knowledgeCount: request.knowledgeCount,
233
261
  });
234
262
 
235
- await onCreated({
236
- agentName,
237
- permanentId,
238
- targetPath: `/agents/${encodeURIComponent(permanentId)}`,
239
- });
240
- setDialogState(null);
263
+ await handleCreatedAgent(createCreatedAgentPayload(agentName, permanentId));
241
264
  } catch (error) {
242
265
  await onCreateFailed?.(error);
243
266
  }
244
267
  },
245
- [dialogState, onCreateFailed, onCreated],
268
+ [dialogState, handleCreatedAgent, onCreateFailed],
246
269
  );
247
270
 
248
271
  const handleOpenEditorFromWizard = useCallback((request: NewAgentWizardOpenEditorRequest) => {
@@ -53,7 +53,7 @@ export const DEFAULT_AGENT_AVATAR_VISUAL_METADATA_VALUES = DEFAULT_AGENT_AVATAR_
53
53
  export const DEFAULT_AGENT_AVATAR_VISUAL_METADATA_VALUE =
54
54
  DEFAULT_AGENT_AVATAR_VISUAL_METADATA_OPTIONS.find(
55
55
  ({ visualId }) => visualId === SHARED_DEFAULT_AGENT_AVATAR_VISUAL_ID,
56
- )?.metadataValue || 'OCTOPUS3';
56
+ )?.metadataValue || 'OCTOPUS3D3';
57
57
 
58
58
  /**
59
59
  * Resolves one raw metadata value to a supported built-in avatar visual id.
@@ -0,0 +1,16 @@
1
+ INSERT INTO "prefix_Metadata" ("key", "value", "note", "createdAt", "updatedAt")
2
+ SELECT 'DEFAULT_AGENT_AVATAR_VISUAL',
3
+ 'OCTOPUS3D3',
4
+ 'Default built-in avatar visual used for agents without `META IMAGE` or `META AVATAR`. Allowed values: PIXEL_ART, OCTOPUS, OCTOPUS2, OCTOPUS3, OCTOPUS3D, OCTOPUS3D2, OCTOPUS3D3, ASCII_OCTOPUS, MINECRAFT, MINECRAFT2, FRACTAL, ORB.',
5
+ NOW(),
6
+ NOW()
7
+ WHERE NOT EXISTS (SELECT 1 FROM "prefix_Metadata" WHERE "key" = 'DEFAULT_AGENT_AVATAR_VISUAL');
8
+
9
+ UPDATE "prefix_Metadata"
10
+ SET "value" = CASE
11
+ WHEN UPPER(COALESCE("value", '')) = 'OCTOPUS3' THEN 'OCTOPUS3D3'
12
+ ELSE "value"
13
+ END,
14
+ "note" = 'Default built-in avatar visual used for agents without `META IMAGE` or `META AVATAR`. Allowed values: PIXEL_ART, OCTOPUS, OCTOPUS2, OCTOPUS3, OCTOPUS3D, OCTOPUS3D2, OCTOPUS3D3, ASCII_OCTOPUS, MINECRAFT, MINECRAFT2, FRACTAL, ORB.',
15
+ "updatedAt" = NOW()
16
+ WHERE "key" = 'DEFAULT_AGENT_AVATAR_VISUAL';
@@ -53,8 +53,9 @@ export const config = {
53
53
  * - favicon.ico (favicon file)
54
54
  * - robots.txt (should not block on middleware DB lookups)
55
55
  * - public folder
56
+ * - api/health (standalone VPS readiness probe)
56
57
  * - api/internal (worker/cron routes are authorized separately)
57
58
  */
58
- '/((?!_next/static|_next/image|favicon.ico|logo-|fonts/|robots.txt|api/internal).*)',
59
+ '/((?!_next/static|_next/image|favicon.ico|logo-|fonts/|robots.txt|api/health|api/internal).*)',
59
60
  ],
60
61
  };
@@ -4,6 +4,20 @@ import { TrackedFilesStorage } from '../utils/cdn/classes/TrackedFilesStorage';
4
4
  import { VercelBlobStorage } from '../utils/cdn/classes/VercelBlobStorage';
5
5
  import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
6
6
 
7
+ /**
8
+ * Region expected by the bundled VersityGW S3-compatible storage.
9
+ *
10
+ * @private internal default for `$provideCdnForServer`
11
+ */
12
+ const SELF_CONTAINED_S3_DEFAULT_REGION = 'us-east-1';
13
+
14
+ /**
15
+ * Legacy fallback used by Cloudflare R2-style external S3 configuration.
16
+ *
17
+ * @private internal default for `$provideCdnForServer`
18
+ */
19
+ const EXTERNAL_S3_DEFAULT_REGION = 'auto';
20
+
7
21
  /**
8
22
  * Cache of CDN instance
9
23
  *
@@ -40,7 +54,7 @@ function createCdnStorageForServer(): IIFilesStorageWithCdn {
40
54
  cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
41
55
  gzip: true,
42
56
  forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === 'true',
43
- region: process.env.CDN_REGION || 'auto',
57
+ region: resolveS3CompatibleStorageRegion(),
44
58
  });
45
59
  }
46
60
 
@@ -57,7 +71,7 @@ function createCdnStorageForServer(): IIFilesStorageWithCdn {
57
71
  * @private helper of `$provideCdnForServer`
58
72
  */
59
73
  function isS3CompatibleStorageSelected(): boolean {
60
- const storageMode = (process.env.PTBK_FILE_STORAGE_MODE || process.env.CDN_PROVIDER || '').toLowerCase();
74
+ const storageMode = getS3CompatibleStorageMode();
61
75
  const isS3StorageMode =
62
76
  storageMode === 's3' || storageMode === 'external-s3' || storageMode === 'self-contained-s3';
63
77
 
@@ -68,6 +82,33 @@ function isS3CompatibleStorageSelected(): boolean {
68
82
  return !process.env.VERCEL_BLOB_READ_WRITE_TOKEN && hasS3CompatibleStorageConfiguration();
69
83
  }
70
84
 
85
+ /**
86
+ * Resolves the configured S3-compatible storage mode.
87
+ *
88
+ * @private helper of `$provideCdnForServer`
89
+ */
90
+ function getS3CompatibleStorageMode(): string {
91
+ return (process.env.PTBK_FILE_STORAGE_MODE || process.env.CDN_PROVIDER || '').toLowerCase();
92
+ }
93
+
94
+ /**
95
+ * Resolves the S3 signing region used by AWS SDK requests.
96
+ *
97
+ * @private helper of `$provideCdnForServer`
98
+ */
99
+ function resolveS3CompatibleStorageRegion(): string {
100
+ const configuredRegion = process.env.CDN_REGION?.trim();
101
+ if (configuredRegion) {
102
+ return configuredRegion;
103
+ }
104
+
105
+ if (getS3CompatibleStorageMode() === 'self-contained-s3') {
106
+ return SELF_CONTAINED_S3_DEFAULT_REGION;
107
+ }
108
+
109
+ return EXTERNAL_S3_DEFAULT_REGION;
110
+ }
111
+
71
112
  /**
72
113
  * Checks whether all S3-compatible storage environment variables are present.
73
114
  *
@@ -56,15 +56,21 @@ type PseudoAgentRouteTarget = {
56
56
  export type AgentRouteTarget = LocalAgentRouteTarget | RemoteAgentRouteTarget | PseudoAgentRouteTarget;
57
57
 
58
58
  /**
59
- * Resolves any incoming `/agents/:agentId` token into a canonical target URL.
59
+ * Resolves any incoming `/agents/:agentId` token into a canonical target URL without memoization.
60
60
  *
61
61
  * Supported inputs include plain IDs/names, `@name`, `{name}`, `{id}`, and absolute `/agents/...` URLs.
62
62
  * Pseudo-agent tokens such as `{User}`, `{Void}`, or `{Null}` resolve to dedicated documentation pages.
63
63
  *
64
64
  * @param rawReference - Raw decoded route parameter value.
65
+ * @param options - Optional cache-bypass controls used by create-agent flows.
65
66
  * @returns Canonical local/remote route target or `null` when the reference cannot be resolved.
66
67
  */
67
- const getCachedAgentRouteTarget = cache(async (rawReference: string): Promise<AgentRouteTarget | null> => {
68
+ async function resolveAgentRouteTargetUncached(
69
+ rawReference: string,
70
+ options?: {
71
+ readonly forceRefresh?: boolean;
72
+ },
73
+ ): Promise<AgentRouteTarget | null> {
68
74
  const parsedBookScopedAgentIdentifier = parseBookScopedAgentIdentifier(rawReference);
69
75
  if (parsedBookScopedAgentIdentifier) {
70
76
  const { publicUrl } = await $provideServer();
@@ -96,7 +102,7 @@ const getCachedAgentRouteTarget = cache(async (rawReference: string): Promise<Ag
96
102
  };
97
103
  }
98
104
 
99
- const resolver = await $provideAgentReferenceResolver();
105
+ const resolver = await $provideAgentReferenceResolver({ forceRefresh: options?.forceRefresh });
100
106
  let resolvedUrlValue: string;
101
107
 
102
108
  try {
@@ -137,15 +143,32 @@ const getCachedAgentRouteTarget = cache(async (rawReference: string): Promise<Ag
137
143
  canonicalAgentId,
138
144
  canonicalUrl: `${localServerUrl}${AGENT_PATH_PREFIX}${encodeURIComponent(canonicalAgentId)}`,
139
145
  };
146
+ }
147
+
148
+ /**
149
+ * Memoized route-target resolver used for normal page rendering.
150
+ */
151
+ const getCachedAgentRouteTarget = cache(async (rawReference: string): Promise<AgentRouteTarget | null> => {
152
+ return resolveAgentRouteTargetUncached(rawReference);
140
153
  });
141
154
 
142
155
  /**
143
156
  * Resolves any incoming `/agents/:agentId` token into a canonical target URL.
144
157
  *
145
158
  * @param rawReference - Raw decoded route parameter value.
159
+ * @param options - Optional cache-bypass controls used by create-agent flows.
146
160
  * @returns Canonical local/remote route target or `null` when the reference cannot be resolved.
147
161
  */
148
- export async function resolveAgentRouteTarget(rawReference: string): Promise<AgentRouteTarget | null> {
162
+ export async function resolveAgentRouteTarget(
163
+ rawReference: string,
164
+ options?: {
165
+ readonly forceRefresh?: boolean;
166
+ },
167
+ ): Promise<AgentRouteTarget | null> {
168
+ if (options?.forceRefresh) {
169
+ return resolveAgentRouteTargetUncached(rawReference, options);
170
+ }
171
+
149
172
  return getCachedAgentRouteTarget(rawReference);
150
173
  }
151
174