@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 +1 -1
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -0
- package/src/kernel/bindings.ts +16 -0
- package/src/pi/message/conversion.ts +34 -3
- package/src/pi/runtime-adapter/execution.ts +6 -1
- package/src/pi/runtime-adapter/models.ts +27 -0
- package/src/pi/tool/core-host.ts +1 -1
- package/src/runtime.ts +26 -3
- package/src/skills/springbrand-worker-website/index.ts +159 -37
package/package.json
CHANGED
package/src/kernel/bindings.ts
CHANGED
|
@@ -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.
|
|
95
|
-
(part
|
|
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
|
-
/**
|
|
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 ?? ""));
|
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -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
|
|
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((
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"│
|
|
22
|
-
"│ └── client/",
|
|
38
|
+
"│ └── lib/",
|
|
23
39
|
"└── public/",
|
|
24
40
|
" ├── index.html",
|
|
25
41
|
" ├── about/",
|
|
26
42
|
" │ └── index.html",
|
|
27
43
|
" ├── products/",
|
|
28
|
-
" │
|
|
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
|
-
"
|
|
42
|
-
"",
|
|
43
|
-
"##
|
|
44
|
-
"",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"-
|
|
54
|
-
"",
|
|
55
|
-
"
|
|
56
|
-
"",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"-
|
|
60
|
-
"
|
|
61
|
-
"-
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"",
|
|
65
|
-
"
|
|
66
|
-
"",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
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
|
};
|