@springbrand/agent-runtime 0.2.0-alpha.55 → 0.2.0-alpha.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.2.0-alpha.55",
3
+ "version": "0.2.0-alpha.57",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -356,6 +356,7 @@ export class ScopedWorkspace implements WorkspacePort, WorkspaceAdminPort {
356
356
  }
357
357
  return files.map((file) => this.project(file)!);
358
358
  }
359
+
359
360
  }
360
361
 
361
362
  /**
@@ -41,8 +41,24 @@ export interface WorkspaceFileInfo {
41
41
  createdAt: number;
42
42
  updatedAt: number;
43
43
  target?: string;
44
+ attachmentId?: string;
45
+ contentVersion?: string;
46
+ cdnUrl?: string;
47
+ origin?: "user_upload" | "https_import" | "action" | "agent" | "userspace";
48
+ understandingStatus?: "not_requested" | "pending" | "ready" | "failed" | "unsupported";
44
49
  }
45
50
 
51
+ export interface WorkspaceFileEvidence {
52
+ summary: string;
53
+ body: string;
54
+ warnings: string[];
55
+ truncated: boolean;
56
+ }
57
+
58
+ export type WorkspaceFileInspection = WorkspaceFileInfo & {
59
+ evidence?: WorkspaceFileEvidence;
60
+ };
61
+
46
62
  /**
47
63
  * 向 Runtime 提供已限定作用域的文件操作。
48
64
  *
@@ -21,6 +21,19 @@ function attachmentText(
21
21
  part: Extract<UIMessage["parts"][number], { type: "file" }>,
22
22
  ): string {
23
23
  const name = part.filename?.trim() || "attachment";
24
+ const springbrand = recordOf(recordOf(part.providerMetadata).springbrand);
25
+ if (
26
+ typeof springbrand.attachmentId === "string" &&
27
+ typeof springbrand.contentVersion === "string" &&
28
+ typeof springbrand.path === "string"
29
+ ) {
30
+ const evidence = recordOf(springbrand.evidence);
31
+ const evidenceText = springbrand.untrustedFileEvidence === true &&
32
+ typeof evidence.summary === "string" && typeof evidence.body === "string"
33
+ ? `\n[Untrusted file evidence]\n${evidence.summary}\n${evidence.body}\n[/Untrusted file evidence]`
34
+ : "";
35
+ return `[Attachment: ${name} (${part.mediaType}) attachmentId=${springbrand.attachmentId} contentVersion=${springbrand.contentVersion} workspacePath=${springbrand.path} cdnUrl=${part.url}]${evidenceText}`;
36
+ }
24
37
  let attachmentId: string | undefined;
25
38
  try {
26
39
  attachmentId = /^\/attachments\/([^/]+)$/u
@@ -91,8 +104,26 @@ export function captureUIUserSidecar(
91
104
  ): UIUserSidecar {
92
105
  return {
93
106
  ...(message.metadata === undefined ? {} : { metadata: message.metadata }),
94
- parts: message.parts.filter(
95
- (part) => part.type === "text" || part.type === "file",
96
- ),
107
+ parts: message.parts.flatMap<UIMessage["parts"][number]>((part) => {
108
+ if (part.type === "text") return [part];
109
+ if (part.type !== "file") return [];
110
+ const providerMetadata = recordOf(part.providerMetadata);
111
+ const springbrand = recordOf(providerMetadata.springbrand);
112
+ if (
113
+ typeof springbrand.attachmentId !== "string" ||
114
+ typeof springbrand.contentVersion !== "string"
115
+ ) return [part];
116
+ return [{
117
+ ...part,
118
+ providerMetadata: {
119
+ ...providerMetadata,
120
+ springbrand: {
121
+ attachmentId: springbrand.attachmentId,
122
+ contentVersion: springbrand.contentVersion,
123
+ ...(typeof springbrand.size === "number" ? { size: springbrand.size } : {}),
124
+ },
125
+ },
126
+ }];
127
+ }),
97
128
  };
98
129
  }
@@ -45,6 +45,7 @@ import {
45
45
  } from "./assembly";
46
46
  import {
47
47
  isRecoverableAssistantError,
48
+ isOpenRouterServerToolTimeout,
48
49
  isModelStreamStallMessage,
49
50
  withProviderRetry,
50
51
  type PiGenerationLifecycleObserver,
@@ -490,7 +491,7 @@ export interface PiTurnDurability {
490
491
  export interface CreatePreparedPiTurnOptions {
491
492
  readonly prepared: PreparedPiRuntime;
492
493
  readonly pinnedDescriptor: string;
493
- /** 同一 Session 的所有 Submission 共用的稳定 Provider 路由身份。 */
494
+ /** Session 当前的稳定 Provider 路由身份;终态 Provider 故障可在下一 Submission 前旋转它。 */
494
495
  readonly modelSessionId: string;
495
496
  readonly submission: {
496
497
  readonly id: string;
@@ -609,6 +610,7 @@ export type PiCanonicalMessageCommit =
609
610
  export interface PiTurnTerminalIntent {
610
611
  readonly outcome: "succeeded" | "failed" | "aborted";
611
612
  readonly message?: string;
613
+ readonly resetModelSession?: boolean;
612
614
  }
613
615
 
614
616
  export interface PiPreparedTurnRunOptions {
@@ -1134,6 +1136,9 @@ export class PreparedPiTurnAdapter {
1134
1136
  );
1135
1137
  this.terminalIntent = {
1136
1138
  outcome: terminal.outcome,
1139
+ ...(isOpenRouterServerToolTimeout(authoritativeMessage)
1140
+ ? { resetModelSession: true }
1141
+ : {}),
1137
1142
  ...(authoritativeMessage.errorMessage
1138
1143
  ? { message: authoritativeMessage.errorMessage }
1139
1144
  : terminal.message
@@ -182,6 +182,32 @@ export function readModelStreamStallDetails(
182
182
  }
183
183
  }
184
184
 
185
+ export function isOpenRouterServerToolTimeout(
186
+ message: AssistantMessage,
187
+ ): boolean {
188
+ if (
189
+ message.stopReason !== "error" ||
190
+ !message.provider.startsWith("model-api-openrouter-messages-") ||
191
+ !message.errorMessage
192
+ ) return false;
193
+ try {
194
+ const payload = JSON.parse(message.errorMessage) as {
195
+ type?: unknown;
196
+ error?: {
197
+ type?: unknown;
198
+ message?: unknown;
199
+ error_type?: unknown;
200
+ };
201
+ };
202
+ return payload.type === "error" &&
203
+ payload.error?.type === "timeout_error" &&
204
+ payload.error.message === "Server tool request failed" &&
205
+ payload.error.error_type === "timeout";
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
185
211
  export function isRecoverableAssistantError(
186
212
  message: AssistantMessage,
187
213
  ): boolean {
@@ -189,6 +215,7 @@ export function isRecoverableAssistantError(
189
215
  message.stopReason === "error" &&
190
216
  /thought[_ ]signature/i.test(message.errorMessage ?? "")
191
217
  ) return false;
218
+ if (isOpenRouterServerToolTimeout(message)) return false;
192
219
  return isRetryableAssistantError(message) ||
193
220
  (message.stopReason === "error" &&
194
221
  /upstream http\/2 stream failed/i.test(message.errorMessage ?? ""));
@@ -88,7 +88,7 @@ state.writeFileBytes is not available. Use state.writeFile or state.appendFile f
88
88
 
89
89
  Bundled Skill resources are not Workspace files. Read text with tools.read_skill_resource, run bundled scripts with tools.run_skill_script, and copy templates or assets with tools.materialize_skill_resource.
90
90
 
91
- User Attachments are not Workspace files until copied with tools.copy_attachment_to_workspace({ id }).
91
+ User Attachments are already Workspace files below /inputs. Inspect their exact path and immutable cdnUrl before using them.
92
92
 
93
93
  When an asset already exists in Workspace, use state.cp to copy it into the current Website directory. Use actual task paths and reference the destination with a relative URL.
94
94
 
package/src/runtime.ts CHANGED
@@ -134,6 +134,7 @@ import {
134
134
  const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
135
135
  const TURN_EVENT_RETRY_SECONDS = 10;
136
136
  const RUNTIME_EVENT_MAX_ATTEMPTS = 5;
137
+ const MODEL_SESSION_ID_STORAGE_KEY = "universal-agent:model-session-id";
137
138
 
138
139
  // 计划续跑丢失多久后由看门狗接手。必须显著长于一个正常执行片的时长,
139
140
  // 否则会在健康的长执行片上误判并派发出重复的一片。
@@ -442,6 +443,8 @@ export abstract class AgentRuntimeKernel<
442
443
  private readonly approvals: ApprovalLifecycle<StoredSubmission>;
443
444
  private readonly interactions: InteractionLifecycle<StoredSubmission>;
444
445
  private runtimeLoadTracker?: RuntimeLoadTracker;
446
+ private modelSessionId?: string;
447
+ private readonly modelSessionIdReady?: Promise<void>;
445
448
  private runtimeEventDrainTail?: Promise<void>;
446
449
  private readonly streamBySubmission = new Map<string, string>();
447
450
  private migratedSubmissionIds?: Set<string>;
@@ -521,6 +524,11 @@ export abstract class AgentRuntimeKernel<
521
524
  snapshotKey: "__cfPiFiberSnapshot",
522
525
  snapshotKind: "pi-turn",
523
526
  });
527
+ this.modelSessionIdReady = ctx.storage.get<string>(
528
+ MODEL_SESSION_ID_STORAGE_KEY,
529
+ ).then((stored) => {
530
+ this.modelSessionId = stored;
531
+ });
524
532
  this.piAdapter = pi;
525
533
  this.db = new RuntimeDatabase(
526
534
  this.sql.bind(this),
@@ -734,6 +742,7 @@ export abstract class AgentRuntimeKernel<
734
742
  }
735
743
 
736
744
  protected async initCandidate(candidate: RuntimeCandidate): Promise<void> {
745
+ await this.modelSessionIdReady;
737
746
  const previous = this.activeRuntime;
738
747
  const candidateSnapshot = candidate.snapshot;
739
748
  this.runtimeLoad.advance("assembly");
@@ -870,6 +879,7 @@ export abstract class AgentRuntimeKernel<
870
879
  terminal: {
871
880
  outcome: "succeeded" | "failed" | "aborted";
872
881
  message?: string;
882
+ resetModelSession?: boolean;
873
883
  },
874
884
  ) => void | Promise<void>;
875
885
  } = {},
@@ -909,7 +919,7 @@ export abstract class AgentRuntimeKernel<
909
919
  return this.pi.createTurn({
910
920
  prepared: this.preparedPi(),
911
921
  pinnedDescriptor: submission.assemblyDescriptor,
912
- modelSessionId: this.name,
922
+ modelSessionId: this.modelSessionId ?? this.name,
913
923
  submission: {
914
924
  id: submission.submissionId,
915
925
  requestId: submission.requestId,
@@ -3038,6 +3048,7 @@ export abstract class AgentRuntimeKernel<
3038
3048
  | {
3039
3049
  outcome: "succeeded" | "failed" | "aborted";
3040
3050
  message?: string;
3051
+ resetModelSession?: boolean;
3041
3052
  }
3042
3053
  | undefined;
3043
3054
  const startedAt = ready.createdAt;
@@ -3050,13 +3061,20 @@ export abstract class AgentRuntimeKernel<
3050
3061
  this.migratedSubmissions.has(submissionId)
3051
3062
  ? undefined
3052
3063
  : this.persistAndBroadcastRecord(turn, record),
3053
- onTerminal: (intent) => {
3064
+ onTerminal: async (intent) => {
3054
3065
  terminalIntent = intent;
3055
3066
  this.appendTerminalIntent(
3056
3067
  submission,
3057
3068
  intent.outcome,
3058
3069
  intent.message,
3059
3070
  );
3071
+ if (intent.resetModelSession) {
3072
+ this.modelSessionId = crypto.randomUUID();
3073
+ await this.ctx.storage.put(
3074
+ MODEL_SESSION_ID_STORAGE_KEY,
3075
+ this.modelSessionId,
3076
+ );
3077
+ }
3060
3078
  },
3061
3079
  });
3062
3080
  for (const pending of this.db.steers.listForSubmission(submissionId)) {
@@ -3605,7 +3623,12 @@ export abstract class AgentRuntimeKernel<
3605
3623
  const messages = await this.getMessages();
3606
3624
  const running = this.db.submissions.findRunning();
3607
3625
  return running?.assistantMessageId
3608
- ? messages.filter(({ id }) => id !== running.assistantMessageId)
3626
+ ? messages.filter((message) =>
3627
+ message.role !== "assistant" ||
3628
+ (message.id !== running.assistantMessageId &&
3629
+ (message.metadata as { turnId?: unknown } | undefined)?.turnId !==
3630
+ running.submissionId)
3631
+ )
3609
3632
  : messages;
3610
3633
  }
3611
3634
 
@@ -1,15 +1,32 @@
1
1
  export const SPRINGBRAND_WORKER_WEBSITE_SKILL = {
2
2
  name: "springbrand-worker-website",
3
3
  description:
4
- "Use when an Agent creates, restructures, previews, verifies, or deploys a Website in a SpringBrand Workspace; provide the complete Cloudflare Worker project layout, asset conventions, and browser validation workflow.",
4
+ "Use when an Agent creates, restructures, previews, verifies, saves, or publishes a Website in a SpringBrand Workspace; covers static multi-page sites, SPAs, App Facet backends, deployment-safe paths, and end-to-end browser validation.",
5
5
  body: [
6
6
  "# SpringBrand Worker Website",
7
7
  "",
8
- "Keep the Website in one stable project directory under `/creations/<website-name>/`. Treat that directory as the complete deployable Website project, not as a directory containing only one HTML file.",
8
+ "Build one complete, deployable Website project in one stable directory under `/creations/<website-name>/`. Do not scatter one Website across several Creation directories, and do not treat a single entry HTML file as the whole project when it references other files.",
9
+ "",
10
+ "## Delivery pipeline",
11
+ "",
12
+ "1. Workspace files below `/creations/<website-name>/` are the editable source of the Website.",
13
+ "2. `deploy_workspace_preview` builds that directory into one target-neutral Deployment Bundle. Static assets are copied to the immutable Workspace Site data plane; Worker modules run through the matching SpringBrand App Facet.",
14
+ "3. The Workspace Site gateway resolves multi-page HTML routes and prefixes root-relative HTML navigation with the immutable deployment path. Static bytes do not pass through the App Facet.",
15
+ "4. Browser evidence verifies the deployed Website, not merely its source files.",
16
+ "5. `save_creation` saves the verified Website bundle as a Creation Version. Saving does not publish it.",
17
+ "6. Public production publication is a separate, explicit, person-approved operation. It promotes the Deployment Bundle to Workers Static Assets and replaces only the deployment-specific Runtime Config.",
18
+ "",
19
+ "## Choose one project shape",
20
+ "",
21
+ "- Static multi-page Website (MPA): several HTML documents, such as `public/index.html`, `public/about/index.html`, and `public/trails/index.html`. Use `404-page` handling so broken links remain visible.",
22
+ "- Single-page application (SPA): one `public/index.html` with client-side routing. Use `single-page-application` fallback so browser deep links return the entry document.",
23
+ "- Full-stack Website: either MPA or SPA assets plus `src/worker.ts`, which exports the SpringBrand `App` Durable Object for `/api/*`, WebSocket, or persistent SQLite behavior.",
24
+ "",
25
+ "Do not combine MPA and SPA fallback accidentally. A multi-page Website hidden behind an SPA fallback can return the home page for a missing child document and make a broken link look successful.",
9
26
  "",
10
27
  "## Recommended layout",
11
28
  "",
12
- "When creating a new Website and no existing project structure needs to be preserved, prefer:",
29
+ "Preserve an existing valid framework or build layout. For a new Website, prefer:",
13
30
  "",
14
31
  "```text",
15
32
  "/creations/<website-name>/",
@@ -18,14 +35,14 @@ export const SPRINGBRAND_WORKER_WEBSITE_SKILL = {
18
35
  "├── src/",
19
36
  "│ ├── worker.ts",
20
37
  "│ ├── routes/",
21
- "│ ├── lib/",
22
- "│ └── client/",
38
+ "│ └── lib/",
23
39
  "└── public/",
24
40
  " ├── index.html",
25
41
  " ├── about/",
26
42
  " │ └── index.html",
27
43
  " ├── products/",
28
- " │ └── index.html",
44
+ " │ ├── index.html",
45
+ " │ └── detail.html",
29
46
  " ├── assets/",
30
47
  " │ ├── css/",
31
48
  " │ ├── js/",
@@ -38,36 +55,141 @@ export const SPRINGBRAND_WORKER_WEBSITE_SKILL = {
38
55
  " └── manifest.webmanifest",
39
56
  "```",
40
57
  "",
41
- "This is a recommended default, not a required layout. Preserve an existing valid framework or project convention such as `dist/`, `build/`, another Worker entry path, or another static asset directory, and configure Wrangler accordingly.",
42
- "",
43
- "## Project rules",
44
- "",
45
- "- Keep Worker configuration, package metadata, and build configuration at the Website project root.",
46
- "- Keep Worker code, routes, application logic, and editable source files under `src/`.",
47
- "- Keep browser-accessible, deployment-ready static files under `public/`.",
48
- "- Use `public/index.html` as the recommended home page. Put additional pages under clear routes such as `public/about/index.html` and `public/products/index.html`.",
49
- "- Put stylesheets, browser scripts, images, fonts, icons, and other static resources under the corresponding `public/assets/` subdirectory.",
50
- "- Put publicly accessible static data under `public/data/` when needed.",
51
- "- Keep every file required to build and deploy the Website inside its project directory.",
52
- "- Do not duplicate the same entry page at both the project root and `public/`.",
53
- "- Do not place secrets, temporary files, logs, raw tool output, or unprocessed attachments inside the Website project.",
54
- "",
55
- "## Worker and browser requirements",
56
- "",
57
- "- Configure Wrangler `main` to point to the actual Worker source file.",
58
- "- Export `class App extends DurableObject` from that module when the Website uses the SpringBrand App Facet.",
59
- "- Configure `assets.directory` to point to the actual browser asset directory, preferably `public/`.",
60
- "- Use `./` or `../` for local Website files, never a root-relative Workspace path, so preview and deployed URL prefixes both work.",
61
- "- Load CSS from HTML instead of importing it from browser JavaScript.",
62
- "- Without a build step, load browser-native third-party ESM through a full pinned HTTPS URL or an import map to a pinned CORS-enabled CDN.",
63
- "- With a build step, produce deployment-ready assets before preview instead of depending on a development server.",
64
- "",
65
- "## Workflow",
66
- "",
67
- "1. Inspect the existing Website directory before changing it and preserve valid conventions.",
68
- "2. Create or update the complete project rather than only an entry HTML file.",
69
- "3. Build when the project requires a build step.",
70
- "4. Preview the Website through the available Workspace or Space capability.",
71
- "5. Open the previewed Website with the browser tools you have and check runtime errors, failed resources, CSP violations, responsive layout, clipping, overlap, and visual hierarchy. Capture a screenshot when visual appearance matters, fix discovered problems, and verify once more.",
58
+ "Keep configuration and editable Worker code at the project root or under `src/`. Keep every browser-addressable, deployment-ready file under the configured assets directory. Do not duplicate the entry document at both the project root and `public/`.",
59
+ "",
60
+ "## Wrangler configuration",
61
+ "",
62
+ "### Static multi-page Website",
63
+ "",
64
+ "```json",
65
+ "{",
66
+ " \"main\": \"src/worker.ts\",",
67
+ " \"compatibility_date\": \"2026-08-01\",",
68
+ " \"assets\": {",
69
+ " \"directory\": \"public\",",
70
+ " \"html_handling\": \"auto-trailing-slash\",",
71
+ " \"not_found_handling\": \"404-page\"",
72
+ " }",
73
+ "}",
74
+ "```",
75
+ "",
76
+ "With `auto-trailing-slash`, `/about/` serves `public/about/index.html`, `/about` serves `public/about.html`, and `/` serves `public/index.html`. Pick one route shape and use it consistently in navigation.",
77
+ "",
78
+ "### Single-page application",
79
+ "",
80
+ "```json",
81
+ "{",
82
+ " \"main\": \"src/worker.ts\",",
83
+ " \"compatibility_date\": \"2026-08-01\",",
84
+ " \"assets\": {",
85
+ " \"directory\": \"public\",",
86
+ " \"html_handling\": \"auto-trailing-slash\",",
87
+ " \"not_found_handling\": \"single-page-application\"",
88
+ " }",
89
+ "}",
90
+ "```",
91
+ "",
92
+ "Use SPA fallback only when the client router owns those paths. If the Website is plain HTML and has no Worker behavior, the platform can configure it automatically; keep an explicit Wrangler file when route policy or an App Facet matters.",
93
+ "",
94
+ "## Static-only Worker entry",
95
+ "",
96
+ "Every explicit Wrangler `main` must name a real file. When a static MPA needs explicit asset routing but no backend behavior, use this minimal App Facet entry instead of inventing API routes:",
97
+ "",
98
+ "```ts",
99
+ "import { DurableObject } from \"cloudflare:workers\";",
100
+ "",
101
+ "export class App extends DurableObject {",
102
+ " fetch() {",
103
+ " return new Response(\"Not Found\", { status: 404 });",
104
+ " }",
105
+ "}",
106
+ "```",
107
+ "",
108
+ "Static assets are served before this class. A request reaches it only when no asset route matched.",
109
+ "",
110
+ "## Multi-page navigation contract",
111
+ "",
112
+ "- The portable default is an explicit document-relative HTML path. From `public/index.html`, use `href=\"./about/index.html\"` and `href=\"./contact.html\"`.",
113
+ "- From `public/about/index.html`, use `href=\"../index.html\"` for home, `href=\"../products/index.html\"` for another root directory, and `href=\"./team.html\"` for a file in the same directory.",
114
+ "- Clean links such as `href=\"./about/\"` are valid in Workspace Site preview and Workers Static Assets publication when `auto-trailing-slash` is active. Use them only when that clean URL is part of the requested public design; explicit HTML paths also work when an immutable Creation Version is opened directly from object storage.",
115
+ "- Every primary page must contain working navigation back to the home page. Do not create visual cards that look clickable but have no link or handler.",
116
+ "- Preserve query strings and fragments when they carry page state, for example `./products/index.html?category=books` and `./about/index.html#team`.",
117
+ "- Never use a Workspace path such as `/creations/site/public/about/index.html` in browser code. Workspace identity and Website URL identity are different.",
118
+ "",
119
+ "## URL behavior under preview and publication",
120
+ "",
121
+ "Root-relative HTML attributes (`href`, `src`, and `action` values beginning with one `/`) are rewritten by the Workspace Site gateway so they retain the deployment prefix. Protocol-relative URLs beginning with `//` are external and are not rewritten.",
122
+ "",
123
+ "Prefer explicit `./` and `../` file URLs for local Website pages and assets. They remain valid in Workspace preview, immutable Creation Version content, and production publication, while a root-relative or directory-only URL depends on the delivery host providing routing or prefix rewriting.",
124
+ "",
125
+ "JavaScript strings are not rewritten. Do not use `fetch(\"/api/items\")`, `new URL(\"/asset\", location.origin)`, or dynamic `import(\"/module.js\")` in generated browser code. CSS `url(...)` values are not HTML attributes either; make them relative to the CSS file.",
126
+ "",
127
+ "Do not add a `<base>` element merely to repair incorrect links. Use correct document-relative URLs first. If an existing framework requires `<base href=\"/\">`, verify every route in the real preview because it changes how all relative URLs resolve.",
128
+ "",
129
+ "## App Facet and Deployment Runtime Config",
130
+ "",
131
+ "A Website backend exports one `class App extends DurableObject` from the configured Worker main module. Do not declare your own Durable Object, D1, R2, or KV bindings in the generated Wrangler file. Persistent Website state belongs in the App Facet's `this.ctx.storage`.",
132
+ "",
133
+ "Full-stack browser code must load the generated Deployment Runtime Config before application code and construct HTTP or WebSocket URLs from `globalThis.__APP_CONFIG__.apiBaseUrl`. Use the correct relative depth on every HTML page: `./.springbrand/runtime-config.js` from the root entry and `../.springbrand/runtime-config.js` from `about/index.html`.",
134
+ "",
135
+ "```html",
136
+ "<script src=\"../.springbrand/runtime-config.js\"></script>",
137
+ "<script type=\"module\" src=\"../assets/js/about.js\"></script>",
138
+ "```",
139
+ "",
140
+ "```js",
141
+ "const apiBaseUrl = globalThis.__APP_CONFIG__.apiBaseUrl;",
142
+ "const response = await fetch(new URL(\"items\", apiBaseUrl));",
143
+ "```",
144
+ "",
145
+ "Never compile a preview URL, user ID, Chat ID, branch, or production host into the main static bundle. Preview and production use the same main assets and different Runtime Config values.",
146
+ "",
147
+ "## Browser asset rules",
148
+ "",
149
+ "- Copy every local image, video, audio file, font, icon, and downloadable asset into the Website directory and reference it by a Website-relative URL.",
150
+ "- Never embed binary assets as base64 or `data:` URLs. Never use a Workspace `cdnUrl` in final HTML.",
151
+ "- Load CSS from HTML. Keep CSS URLs relative to the CSS file.",
152
+ "- Browser-loaded JavaScript must already be browser-executable. Do not ship raw TypeScript or JSX in ordinary `<script>` elements.",
153
+ "- When a build step exists, run it and deploy only its completed output directory. Do not depend on a Vite or framework development server.",
154
+ "- Without a build step, use browser-native JavaScript and full, pinned, CORS-enabled HTTPS module URLs or a complete import map.",
155
+ "- Do not place secrets, logs, temporary build output, raw uploads, or unrelated Tool output inside the Website project.",
156
+ "",
157
+ "## Build, preview, verify, and save",
158
+ "",
159
+ "1. Inspect the current Website directory and preserve valid framework conventions.",
160
+ "2. Create or update the complete project in its stable `/creations/<website-name>/` directory.",
161
+ "3. Build when required and confirm the configured assets directory contains the final entry document and every referenced local file.",
162
+ "4. Call `deploy_workspace_preview` with the Website project directory, then use the returned Workspace Site URL as the browser source of truth.",
163
+ "5. Call `get_browser_console_logs` for the home page and for every primary internal page. Check navigation responses, missing assets, exceptions, CSP or CORS failures, and client fetch failures.",
164
+ "6. Open the Website with the available browser evidence tools. For a multi-page Website, follow real links from the home page to every primary internal page and verify navigation back to the home page. Check at least the normal desktop viewport and one narrow viewport; capture and inspect a screenshot when visual appearance matters.",
165
+ "7. Fix every failure, redeploy, and repeat the same checks. A clean home page does not prove a multi-page Website works.",
166
+ "8. After browser verification succeeds, call `save_creation` exactly once with the existing entry HTML Working Copy path. For the recommended layout, that is `/creations/<website-name>/public/index.html`.",
167
+ "9. Do not call the Website complete until `save_creation` returns an `artifactId`. Do not save referenced assets as separate Creations.",
168
+ "",
169
+ "## Publication contract",
170
+ "",
171
+ "`deploy_workspace_preview` does not publish the Website. `save_creation` does not publish the Website. They create a public-by-address Workspace preview and a persistent private Creation Version respectively.",
172
+ "",
173
+ "Only call `propose_publication` when the person explicitly asks to publish. The Tool starts the platform's explicit publication approval and its returned result is the only authority for publication success; never invent a public URL or claim that a preview URL is the published Website.",
174
+ "",
175
+ "Production publication consumes the same Deployment Bundle, including `html_handling` and `not_found_handling`, through Workers Static Assets. The platform supplies the production Runtime Config; do not rebuild the main browser bundle merely to change `apiBaseUrl`.",
176
+ "",
177
+ "If publication is not requested, unavailable, denied, or fails, leave the verified Creation intact and say precisely that it was saved but not published.",
178
+ "",
179
+ "## Pre-flight checklist",
180
+ "",
181
+ "Before the final response, confirm all of the following:",
182
+ "",
183
+ "- One stable Website project exists below `/creations/`, with exactly one entry document selected for `save_creation`.",
184
+ "- `wrangler.json` points to the real Worker main and browser assets directory when explicit configuration is needed.",
185
+ "- MPA uses `auto-trailing-slash` plus `404-page`; SPA uses `single-page-application` only when a client router owns the routes.",
186
+ "- Every local page, stylesheet, script, image, font, and media URL resolves inside the Website bundle; direct Creation Version viewing uses explicit relative HTML paths unless a clean URL target was explicitly chosen.",
187
+ "- JavaScript and CSS do not depend on root-relative prefix rewriting.",
188
+ "- Full-stack pages load Deployment Runtime Config at the correct relative depth and use its `apiBaseUrl`.",
189
+ "- The deployed home page and every primary internal page were opened successfully in a real browser.",
190
+ "- Every primary internal page can navigate back to the home page.",
191
+ "- Browser console, failed-request evidence, responsive layout, and important interactions are clean.",
192
+ "- The verified entry HTML was saved once and returned an `artifactId`.",
193
+ "- The final response distinguishes previewed, saved, and published states accurately.",
72
194
  ].join("\n"),
73
195
  };