@belte/belte 0.19.0 → 0.19.2
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/CHANGELOG.md +16 -0
- package/README.md +419 -337
- package/package.json +1 -1
- package/src/lib/browser/page.svelte.ts +33 -9
- package/src/lib/browser/startClient.ts +11 -1
- package/src/lib/mcp/dispatchMcpRequest.ts +7 -9
- package/src/lib/mcp/mcpSurface.ts +32 -7
- package/src/lib/server/agent.ts +10 -1
- package/src/lib/server/runtime/createServer.ts +11 -8
- package/src/lib/server/runtime/types/RequestStore.ts +7 -0
- package/src/lib/shared/activePage.ts +20 -0
- package/src/lib/shared/pageSlot.ts +17 -0
- package/src/lib/shared/setPageResolver.ts +7 -0
- package/src/lib/shared/types/PageSnapshot.ts +11 -0
- package/src/serverEntry.ts +16 -0
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Component } from 'svelte'
|
|
2
|
+
import { activePage } from '../shared/activePage.ts'
|
|
2
3
|
import {
|
|
3
4
|
type NormalizedLayoutPrefix,
|
|
4
5
|
nearestLayoutPrefix,
|
|
5
6
|
normalizeLayoutPrefixes,
|
|
6
7
|
} from '../shared/nearestLayoutPrefix.ts'
|
|
8
|
+
import type { PageSnapshot } from '../shared/types/PageSnapshot.ts'
|
|
7
9
|
import { abortPageStream } from './pageStreamController.ts'
|
|
8
10
|
import type { Layouts } from './types/Layouts.ts'
|
|
9
11
|
import type { Pages } from './types/Pages.ts'
|
|
@@ -42,13 +44,37 @@ export type Page = keyof Routes extends never
|
|
|
42
44
|
? PageStateFor<string>
|
|
43
45
|
: { [R in keyof Routes]: PageStateFor<R> }[keyof Routes]
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
/*
|
|
48
|
+
Client-side singleton the resolver returns. navigate()/applyState mutate it;
|
|
49
|
+
because it's $state, reads taken through the `page` proxy inside a $derived
|
|
50
|
+
re-run on every nav. Never populated server-side — there the resolver reads
|
|
51
|
+
the per-request ALS store instead, so concurrent renders don't share it.
|
|
52
|
+
*/
|
|
53
|
+
export const clientPageState: PageSnapshot = $state({
|
|
47
54
|
route: '',
|
|
48
55
|
params: {},
|
|
49
56
|
url: new URL('http://localhost/'),
|
|
50
57
|
})
|
|
51
58
|
|
|
59
|
+
/*
|
|
60
|
+
Public page state. A getter proxy over the side's registered resolver
|
|
61
|
+
(activePage): client → clientPageState singleton, server → the per-request
|
|
62
|
+
ALS snapshot. Property reads on the client resolve to the $state singleton, so
|
|
63
|
+
reactivity flows; on the server each render reads its own request scope. Users
|
|
64
|
+
only ever see route/params/url.
|
|
65
|
+
*/
|
|
66
|
+
export const page = {
|
|
67
|
+
get route() {
|
|
68
|
+
return activePage().route
|
|
69
|
+
},
|
|
70
|
+
get params() {
|
|
71
|
+
return activePage().params
|
|
72
|
+
},
|
|
73
|
+
get url() {
|
|
74
|
+
return activePage().url
|
|
75
|
+
},
|
|
76
|
+
} as unknown as Page
|
|
77
|
+
|
|
52
78
|
/*
|
|
53
79
|
Internal renderer state — the Layout/Page components App.svelte mounts.
|
|
54
80
|
Kept on a separate $state object so it doesn't leak into the public `page`
|
|
@@ -116,15 +142,13 @@ function applyState(
|
|
|
116
142
|
): void {
|
|
117
143
|
renderState.Layout = Layout
|
|
118
144
|
renderState.Page = Page
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
mutable.params = params
|
|
145
|
+
clientPageState.route = route
|
|
146
|
+
clientPageState.params = params
|
|
122
147
|
syncUrl()
|
|
123
148
|
}
|
|
124
149
|
|
|
125
150
|
function syncUrl(): void {
|
|
126
|
-
|
|
127
|
-
mutable.url = new URL(window.location.href)
|
|
151
|
+
clientPageState.url = new URL(window.location.href)
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
/*
|
|
@@ -207,7 +231,7 @@ export async function navigate(href: string, options: NavigateOptions = {}): Pro
|
|
|
207
231
|
return
|
|
208
232
|
}
|
|
209
233
|
const fullTarget = `${target.pathname}${target.search}${target.hash}`
|
|
210
|
-
if (target.pathname ===
|
|
234
|
+
if (target.pathname === clientPageState.url.pathname) {
|
|
211
235
|
writeHistory(replace, fullTarget)
|
|
212
236
|
syncUrl()
|
|
213
237
|
return
|
|
@@ -234,7 +258,7 @@ only refreshes `page.url`; a pathname change resolves and swaps the page, or
|
|
|
234
258
|
hard-navigates when the restored URL isn't an SPA route.
|
|
235
259
|
*/
|
|
236
260
|
async function applyTarget(pathname: string, fullTarget: string): Promise<void> {
|
|
237
|
-
if (pathname ===
|
|
261
|
+
if (pathname === clientPageState.url.pathname) {
|
|
238
262
|
syncUrl()
|
|
239
263
|
return
|
|
240
264
|
}
|
|
@@ -3,13 +3,21 @@ import App from '../../App.svelte'
|
|
|
3
3
|
import { createCacheStore } from '../shared/createCacheStore.ts'
|
|
4
4
|
import { setCacheStoreResolver } from '../shared/setCacheStoreResolver.ts'
|
|
5
5
|
import { setGlobalCacheStoreResolver } from '../shared/setGlobalCacheStoreResolver.ts'
|
|
6
|
+
import { setPageResolver } from '../shared/setPageResolver.ts'
|
|
6
7
|
import type { CacheSnapshotEntry } from '../shared/types/CacheSnapshotEntry.ts'
|
|
7
8
|
import type { CacheStore } from '../shared/types/CacheStore.ts'
|
|
8
9
|
import type { StreamingPlaceholder } from '../shared/types/StreamingPlaceholder.ts'
|
|
9
10
|
import { cacheEntryFromSnapshot } from './cacheEntryFromSnapshot.ts'
|
|
10
11
|
import { installStreamingPlaceholders } from './installStreamingPlaceholders.ts'
|
|
11
12
|
import { openResolveStream } from './openResolveStream.ts'
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
bindPage,
|
|
15
|
+
clientPageState,
|
|
16
|
+
handlePopstate,
|
|
17
|
+
navigate,
|
|
18
|
+
page,
|
|
19
|
+
renderState,
|
|
20
|
+
} from './page.svelte.ts'
|
|
13
21
|
import type { Layouts } from './types/Layouts.ts'
|
|
14
22
|
import type { Pages } from './types/Pages.ts'
|
|
15
23
|
|
|
@@ -107,6 +115,8 @@ export async function startClient({
|
|
|
107
115
|
setCacheStoreResolver(() => cacheStore)
|
|
108
116
|
/* One tab store: cache(fn, { global: true }) shares it, so global is a no-op here. */
|
|
109
117
|
setGlobalCacheStoreResolver(() => cacheStore)
|
|
118
|
+
/* One document: the `page` proxy resolves to this $state singleton, mutated by navigate(). */
|
|
119
|
+
setPageResolver(() => clientPageState)
|
|
110
120
|
if (window.__SSR__.cache) {
|
|
111
121
|
hydrateCacheFromSnapshot(cacheStore, window.__SSR__.cache)
|
|
112
122
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { promptRegistry } from '../server/prompts/promptRegistry.ts'
|
|
2
1
|
import { ensureRegistriesLoaded } from '../server/runtime/registryManifests.ts'
|
|
3
2
|
import { NO_STORE } from '../shared/CACHE_CONTROL_VALUES.ts'
|
|
4
3
|
import { getMcpResourceServer } from './mcpResourceServerSlot.ts'
|
|
5
|
-
import { buildPrompts, buildTools, callTool } from './mcpSurface.ts'
|
|
4
|
+
import { buildPrompts, buildTools, callTool, renderPrompt } from './mcpSurface.ts'
|
|
6
5
|
import type { JsonRpcRequest } from './types/JsonRpcRequest.ts'
|
|
7
6
|
import type { JsonRpcResponse } from './types/JsonRpcResponse.ts'
|
|
8
7
|
import type { McpServerOptions } from './types/McpServerOptions.ts'
|
|
@@ -33,14 +32,13 @@ function getPrompt(
|
|
|
33
32
|
name: string,
|
|
34
33
|
args: Record<string, unknown> | undefined,
|
|
35
34
|
): Record<string, unknown> {
|
|
36
|
-
const
|
|
37
|
-
if (!entry) {
|
|
38
|
-
throw new Error(`unknown prompt: ${name}`)
|
|
39
|
-
}
|
|
40
|
-
const rendered = entry.prompt.render((args ?? {}) as Record<string, string>)
|
|
35
|
+
const { description, messages } = renderPrompt(name, args)
|
|
41
36
|
return {
|
|
42
|
-
...(
|
|
43
|
-
messages:
|
|
37
|
+
...(description ? { description } : {}),
|
|
38
|
+
messages: messages.map((message) => ({
|
|
39
|
+
role: message.role,
|
|
40
|
+
content: { type: 'text', text: message.text },
|
|
41
|
+
})),
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -230,16 +230,31 @@ export async function callTool(
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
/*
|
|
233
|
-
Renders a prompt
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
Renders a prompt: looks it up, interpolates the caller's args, and returns its
|
|
234
|
+
optional description plus the message(s) that seed a conversation. A markdown
|
|
235
|
+
prompt is a single user turn whose text is the interpolated template. Throws on
|
|
236
|
+
an unknown prompt name. The one place prompt rendering lives — dispatchMcpRequest
|
|
237
|
+
wraps this in the prompts/get wire shape, the agent loop reads the messages plain.
|
|
236
238
|
*/
|
|
237
|
-
export function
|
|
239
|
+
export function renderPrompt(
|
|
240
|
+
name: string,
|
|
241
|
+
args?: Record<string, unknown>,
|
|
242
|
+
): { description?: string; messages: PromptMessage[] } {
|
|
238
243
|
const entry = promptRegistry.get(name)
|
|
239
244
|
if (!entry) {
|
|
240
245
|
throw new Error(`unknown prompt: ${name}`)
|
|
241
246
|
}
|
|
242
|
-
return
|
|
247
|
+
return {
|
|
248
|
+
...(entry.prompt.description ? { description: entry.prompt.description } : {}),
|
|
249
|
+
messages: [
|
|
250
|
+
{ role: 'user', text: entry.prompt.render((args ?? {}) as Record<string, string>) },
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The conversation-seeding messages for a prompt, without the wire-shape wrapping.
|
|
256
|
+
export function getPromptMessages(name: string, args?: Record<string, unknown>): PromptMessage[] {
|
|
257
|
+
return renderPrompt(name, args).messages
|
|
243
258
|
}
|
|
244
259
|
|
|
245
260
|
/*
|
|
@@ -248,10 +263,20 @@ Projects the app's MCP surface for an in-process consumer bound to `request`
|
|
|
248
263
|
the model acts with the caller's identity. Used by `agent()`.
|
|
249
264
|
*/
|
|
250
265
|
export function mcpSurface(request: Request): McpSurface {
|
|
266
|
+
// Built on first read and memoized: an engine that advertises tools but never
|
|
267
|
+
// reads prompts (or reaches tools over HTTP, reading neither) skips the unused build.
|
|
268
|
+
let tools: ToolDescriptor[] | undefined
|
|
269
|
+
let prompts: PromptDescriptor[] | undefined
|
|
251
270
|
return {
|
|
252
|
-
tools
|
|
271
|
+
get tools() {
|
|
272
|
+
tools ??= buildTools()
|
|
273
|
+
return tools
|
|
274
|
+
},
|
|
275
|
+
get prompts() {
|
|
276
|
+
prompts ??= buildPrompts()
|
|
277
|
+
return prompts
|
|
278
|
+
},
|
|
253
279
|
call: (name, args) => callTool(name, args, request),
|
|
254
|
-
prompts: buildPrompts(),
|
|
255
280
|
getPrompt: getPromptMessages,
|
|
256
281
|
async listResources() {
|
|
257
282
|
const server = getMcpResourceServer()
|
package/src/lib/server/agent.ts
CHANGED
|
@@ -41,7 +41,16 @@ export type AgentFrame =
|
|
|
41
41
|
| { type: 'text'; delta: string }
|
|
42
42
|
| { type: 'tool_use'; id: string; name: string; input: unknown }
|
|
43
43
|
| { type: 'tool_result'; id: string; name: string; ok: boolean }
|
|
44
|
-
|
|
44
|
+
/*
|
|
45
|
+
`stop` is the reason the engine's loop ended. `error` covers an abnormal
|
|
46
|
+
stop the model didn't choose — a provider error/limit (e.g. Claude Code's
|
|
47
|
+
max-turns) or the engine's own tool-loop cap — so a client can tell a cut-off
|
|
48
|
+
answer from a clean `end`.
|
|
49
|
+
*/
|
|
50
|
+
| {
|
|
51
|
+
type: 'done'
|
|
52
|
+
stop: 'end' | 'tool_use' | 'max_tokens' | 'refusal' | 'error'
|
|
53
|
+
}
|
|
45
54
|
|
|
46
55
|
// The app's tool/prompt/resource surface handed to an engine (already gated).
|
|
47
56
|
export type AgentSurface = McpSurface
|
|
@@ -262,17 +262,17 @@ export async function createServer({
|
|
|
262
262
|
])
|
|
263
263
|
const ErrorView = errorMod.default as Component
|
|
264
264
|
const Layout = layoutMod?.default as Component | undefined
|
|
265
|
+
// status is a number (and stack optional); the page-params shape is
|
|
266
|
+
// string-keyed generically, so the error props ride through as-is.
|
|
267
|
+
const errorParams = { status, message, stack } as unknown as Record<string, string>
|
|
268
|
+
/* Publish to the store too, so the `page` proxy resolves these during the error render
|
|
269
|
+
(renderError bypasses renderPage, which is where normal renders set them). */
|
|
270
|
+
store.route = pathname
|
|
271
|
+
store.params = errorParams
|
|
265
272
|
const rendered = await render(App, {
|
|
266
273
|
props: {
|
|
267
274
|
state: {
|
|
268
|
-
page: {
|
|
269
|
-
route: pathname,
|
|
270
|
-
// status is a number (and stack optional); the page-params
|
|
271
|
-
// shape is string-keyed generically, so the error props
|
|
272
|
-
// ride through to the component as-is.
|
|
273
|
-
params: { status, message, stack } as unknown as Record<string, string>,
|
|
274
|
-
url: store.url,
|
|
275
|
-
},
|
|
275
|
+
page: { route: pathname, params: errorParams, url: store.url },
|
|
276
276
|
render: { Layout, Page: ErrorView },
|
|
277
277
|
},
|
|
278
278
|
},
|
|
@@ -293,6 +293,9 @@ export async function createServer({
|
|
|
293
293
|
params: Record<string, string>,
|
|
294
294
|
store: RequestStore,
|
|
295
295
|
): Promise<Response> {
|
|
296
|
+
/* Publish the match so the `page` proxy resolves route/params during SSR render. */
|
|
297
|
+
store.route = routeUrl
|
|
298
|
+
store.params = params
|
|
296
299
|
const json = wantsJson(store.req)
|
|
297
300
|
if (json) {
|
|
298
301
|
return Response.json(
|
|
@@ -12,6 +12,13 @@ export type RequestStore = {
|
|
|
12
12
|
req: Request
|
|
13
13
|
cache: CacheStore
|
|
14
14
|
/*
|
|
15
|
+
The matched page route and its decoded params, set just before the page
|
|
16
|
+
renders so the `page` proxy resolves them inside layout-scoped components
|
|
17
|
+
during SSR. Undefined on rpc/socket requests and until a page match lands.
|
|
18
|
+
*/
|
|
19
|
+
route?: string
|
|
20
|
+
params?: Record<string, string>
|
|
21
|
+
/*
|
|
15
22
|
The request's cookie jar, materialized lazily by the first cookies() call
|
|
16
23
|
and flushed to Set-Cookie headers when the scope returns. Undefined while a
|
|
17
24
|
request never touches cookies, so the common path parses and emits nothing.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { pageSlot } from './pageSlot.ts'
|
|
2
|
+
import type { PageSnapshot } from './types/PageSnapshot.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Resolves the active page snapshot. The runtime is registered via
|
|
6
|
+
`setPageResolver` from the server entry (request-scoped via ALS) or the
|
|
7
|
+
client entry (module-level singleton). If no resolver is registered, a single
|
|
8
|
+
fallback snapshot is created lazily so isolated tests still work. Mirrors
|
|
9
|
+
activeCacheStore.
|
|
10
|
+
*/
|
|
11
|
+
export function activePage(): PageSnapshot {
|
|
12
|
+
const fromResolver = pageSlot.resolver?.()
|
|
13
|
+
if (fromResolver) {
|
|
14
|
+
return fromResolver
|
|
15
|
+
}
|
|
16
|
+
if (!pageSlot.fallback) {
|
|
17
|
+
pageSlot.fallback = { route: '', params: {}, url: new URL('http://localhost/') }
|
|
18
|
+
}
|
|
19
|
+
return pageSlot.fallback
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PageSnapshot } from './types/PageSnapshot.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Internal slot the runtime entries register their page resolver into. The
|
|
5
|
+
server entry installs an ALS-backed resolver (request-scoped, so concurrent
|
|
6
|
+
and streaming renders never share state); the client entry installs a
|
|
7
|
+
module-singleton resolver. `fallback` is a single lazy snapshot used only
|
|
8
|
+
when no resolver is registered — keeps isolated tests working without forcing
|
|
9
|
+
them to spin up the runtime. Mirrors cacheStoreSlot.
|
|
10
|
+
*/
|
|
11
|
+
export const pageSlot: {
|
|
12
|
+
resolver: (() => PageSnapshot | undefined) | undefined
|
|
13
|
+
fallback: PageSnapshot | undefined
|
|
14
|
+
} = {
|
|
15
|
+
resolver: undefined,
|
|
16
|
+
fallback: undefined,
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { pageSlot } from './pageSlot.ts'
|
|
2
|
+
import type { PageSnapshot } from './types/PageSnapshot.ts'
|
|
3
|
+
|
|
4
|
+
// Registers the runtime's page resolver. Called once per side at boot.
|
|
5
|
+
export function setPageResolver(fn: () => PageSnapshot | undefined): void {
|
|
6
|
+
pageSlot.resolver = fn
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Side-agnostic shape of the active page state: the matched route, its decoded
|
|
3
|
+
params, and the live request/location URL. The browser `page` proxy reads its
|
|
4
|
+
public fields through this; the server resolver reads them off the per-request
|
|
5
|
+
store, the client resolver off the module singleton.
|
|
6
|
+
*/
|
|
7
|
+
export type PageSnapshot = {
|
|
8
|
+
route: string
|
|
9
|
+
params: Record<string, string>
|
|
10
|
+
url: URL
|
|
11
|
+
}
|
package/src/serverEntry.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { loadEnvFromDataDir } from './lib/shared/loadEnvFromDataDir.ts'
|
|
|
35
35
|
import { runningAsStandaloneBinary } from './lib/shared/runningAsStandaloneBinary.ts'
|
|
36
36
|
import { setCacheStoreResolver } from './lib/shared/setCacheStoreResolver.ts'
|
|
37
37
|
import { setGlobalCacheStoreResolver } from './lib/shared/setGlobalCacheStoreResolver.ts'
|
|
38
|
+
import { setPageResolver } from './lib/shared/setPageResolver.ts'
|
|
38
39
|
|
|
39
40
|
/*
|
|
40
41
|
Resolve config into process.env before anything reads it (createServer reads
|
|
@@ -67,6 +68,21 @@ exitWithParent()
|
|
|
67
68
|
|
|
68
69
|
setCacheStoreResolver(() => requestContext.getStore()?.cache)
|
|
69
70
|
|
|
71
|
+
/*
|
|
72
|
+
Request-scoped page resolver: the `page` proxy reads route/params/url off the
|
|
73
|
+
ALS store, so layout-scoped components see the live match during SSR without a
|
|
74
|
+
module singleton leaking across concurrent or streaming renders. route/params
|
|
75
|
+
land just before render; url is set at the request boundary, so 404/error
|
|
76
|
+
renders still get a correct page.url.
|
|
77
|
+
*/
|
|
78
|
+
setPageResolver(() => {
|
|
79
|
+
const store = requestContext.getStore()
|
|
80
|
+
if (!store) {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
return { route: store.route ?? '', params: store.params ?? {}, url: store.url }
|
|
84
|
+
})
|
|
85
|
+
|
|
70
86
|
/*
|
|
71
87
|
Process-level store for cache(fn, { global: true }) — one per server process,
|
|
72
88
|
outlives every request so memoised external calls are shared across them.
|