@theoremjs/react 0.0.0-stage → 0.3.0
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/LICENSE +21 -0
- package/README.md +246 -2
- package/client/attachment-hover-preview.d.ts +11 -0
- package/client/attachment-hover-preview.js +36 -0
- package/client/audio-level.d.ts +5 -0
- package/client/audio-level.js +24 -0
- package/client/composer-attachments.d.ts +30 -0
- package/client/composer-attachments.js +33 -0
- package/client/composer-drawer.d.ts +19 -0
- package/client/composer-drawer.js +15 -0
- package/client/composer-hints.d.ts +25 -0
- package/client/composer-hints.js +20 -0
- package/client/composer-primary.d.ts +16 -0
- package/client/composer-primary.js +25 -0
- package/client/decision-transport.d.ts +48 -0
- package/client/decision-transport.js +102 -0
- package/client/decode-composer-draft.d.ts +11 -0
- package/client/decode-composer-draft.js +12 -0
- package/client/encode-composer-draft.d.ts +9 -0
- package/client/encode-composer-draft.js +34 -0
- package/client/encode-files.d.ts +18 -0
- package/client/encode-files.js +54 -0
- package/client/failure.d.ts +32 -0
- package/client/failure.js +34 -0
- package/client/generation-selection.d.ts +15 -0
- package/client/generation-selection.js +24 -0
- package/client/host-call.d.ts +51 -0
- package/client/host-call.js +42 -0
- package/client/host-transport.d.ts +68 -0
- package/client/host-transport.js +74 -0
- package/client/image-output.d.ts +6 -0
- package/client/image-output.js +13 -0
- package/client/index.d.ts +20 -0
- package/client/index.js +18 -0
- package/client/ink-waveform.d.ts +17 -0
- package/client/ink-waveform.js +69 -0
- package/client/live/apply-live-turn-tool-event.d.ts +14 -0
- package/client/live/apply-live-turn-tool-event.js +39 -0
- package/client/live/live-captions.d.ts +23 -0
- package/client/live/live-captions.js +70 -0
- package/client/live/live-mic-forward.d.ts +29 -0
- package/client/live/live-mic-forward.js +37 -0
- package/client/live/live-state.d.ts +11 -0
- package/client/live/live-state.js +20 -0
- package/client/live/live-tool.d.ts +11 -0
- package/client/live/live-tool.js +1 -0
- package/client/live/live-video.d.ts +10 -0
- package/client/live/live-video.js +71 -0
- package/client/live/run-live-tool-call.d.ts +17 -0
- package/client/live/run-live-tool-call.js +32 -0
- package/client/live-client.d.ts +141 -0
- package/client/live-client.js +638 -0
- package/client/live-errors.d.ts +2 -0
- package/client/live-errors.js +9 -0
- package/client/live-messages.d.ts +110 -0
- package/client/live-messages.js +67 -0
- package/client/mic-capture.d.ts +4 -0
- package/client/mic-capture.js +6 -0
- package/client/oauth-popup.d.ts +26 -0
- package/client/oauth-popup.js +30 -0
- package/client/pcm-downsample.d.ts +4 -0
- package/client/pcm-downsample.js +45 -0
- package/client/run-commit.d.ts +61 -0
- package/client/run-commit.js +124 -0
- package/client/run-session.d.ts +80 -0
- package/client/run-session.js +215 -0
- package/client/schema-fields.d.ts +63 -0
- package/client/schema-fields.js +178 -0
- package/client/shaped-data.d.ts +118 -0
- package/client/shaped-data.js +314 -0
- package/client/source-citations.d.ts +13 -0
- package/client/source-citations.js +56 -0
- package/client/state-fields.d.ts +16 -0
- package/client/state-fields.js +44 -0
- package/client/tool-result.d.ts +78 -0
- package/client/tool-result.js +275 -0
- package/client/tool-resume.d.ts +33 -0
- package/client/tool-resume.js +11 -0
- package/client/trace-feed.d.ts +13 -0
- package/client/trace-feed.js +18 -0
- package/client/trace-story.d.ts +163 -0
- package/client/trace-story.js +416 -0
- package/client/trace-view.d.ts +130 -0
- package/client/trace-view.js +293 -0
- package/client/transcript-groups.d.ts +114 -0
- package/client/transcript-groups.js +167 -0
- package/client/transcript-scroll.d.ts +7 -0
- package/client/transcript-scroll.js +6 -0
- package/client/transport.d.ts +176 -0
- package/client/transport.js +256 -0
- package/client/turn-client.d.ts +35 -0
- package/client/turn-client.js +93 -0
- package/client/voice-label.d.ts +2 -0
- package/client/voice-label.js +15 -0
- package/client/voice-recorder.d.ts +27 -0
- package/client/voice-recorder.js +165 -0
- package/client/walk-away.d.ts +34 -0
- package/client/walk-away.js +48 -0
- package/client/wire-line.d.ts +56 -0
- package/client/wire-line.js +90 -0
- package/client/worklets/mic-capture.js +16 -0
- package/components/InkWaveform.d.ts +10 -0
- package/components/InkWaveform.js +108 -0
- package/components/live/use-live-runner-controls.d.ts +48 -0
- package/components/live/use-live-runner-controls.js +100 -0
- package/components/live/use-live-runner-model.d.ts +45 -0
- package/components/live/use-live-runner-model.js +175 -0
- package/components/live/use-live-runner-ui.d.ts +65 -0
- package/components/live/use-live-runner-ui.js +159 -0
- package/components/live/use-live-session-client.d.ts +41 -0
- package/components/live/use-live-session-client.js +123 -0
- package/components/use-composer-voice.d.ts +28 -0
- package/components/use-composer-voice.js +90 -0
- package/components/use-editor-selection.d.ts +3 -0
- package/components/use-editor-selection.js +16 -0
- package/components/use-voice-playback.d.ts +16 -0
- package/components/use-voice-playback.js +68 -0
- package/hooks/use-described.d.ts +8 -0
- package/hooks/use-described.js +23 -0
- package/hooks/use-theorem-chat-actions.d.ts +75 -0
- package/hooks/use-theorem-chat-actions.js +265 -0
- package/hooks/use-theorem-chat-state.d.ts +66 -0
- package/hooks/use-theorem-chat-state.js +118 -0
- package/hooks/use-theorem-chat.d.ts +63 -0
- package/hooks/use-theorem-chat.js +340 -0
- package/hooks/use-theorem-decision.d.ts +23 -0
- package/hooks/use-theorem-decision.js +75 -0
- package/hooks/use-theorem-host.d.ts +44 -0
- package/hooks/use-theorem-host.js +117 -0
- package/hooks/use-theorem-interface.d.ts +19 -0
- package/hooks/use-theorem-interface.js +25 -0
- package/hooks/use-trace-records.d.ts +4 -0
- package/hooks/use-trace-records.js +7 -0
- package/index.d.ts +14 -0
- package/index.js +14 -0
- package/live.d.ts +8 -0
- package/live.js +10 -0
- package/package.json +73 -4
- package/server/credential-store.d.ts +24 -0
- package/server/credential-store.js +14 -0
- package/server/decision-handler.d.ts +34 -0
- package/server/decision-handler.js +55 -0
- package/server/handler.d.ts +114 -0
- package/server/handler.js +568 -0
- package/server/json-response.d.ts +2 -0
- package/server/json-response.js +7 -0
- package/server/mod.d.ts +16 -0
- package/server/mod.js +15 -0
- package/server/request-check.d.ts +16 -0
- package/server/request-check.js +34 -0
- package/server/session-store.d.ts +74 -0
- package/server/session-store.js +49 -0
- package/server/steer-inbox.d.ts +35 -0
- package/server/steer-inbox.js +52 -0
- package/server/walk-away.d.ts +22 -0
- package/server/walk-away.js +41 -0
- package/styles/ink-controls.css +134 -0
- package/ui/ChatComposerBar.d.ts +39 -0
- package/ui/ChatComposerBar.js +251 -0
- package/ui/ChatTranscript.d.ts +30 -0
- package/ui/ChatTranscript.js +474 -0
- package/ui/ConsoleFrame.d.ts +47 -0
- package/ui/ConsoleFrame.js +46 -0
- package/ui/DecisionAnswers.d.ts +15 -0
- package/ui/DecisionAnswers.js +102 -0
- package/ui/LiveRunner.d.ts +28 -0
- package/ui/LiveRunner.js +193 -0
- package/ui/SchemaFields.d.ts +10 -0
- package/ui/SchemaFields.js +122 -0
- package/ui/ShapedData.d.ts +11 -0
- package/ui/ShapedData.js +151 -0
- package/ui/SidePanel.d.ts +57 -0
- package/ui/SidePanel.js +50 -0
- package/ui/StateFields.d.ts +36 -0
- package/ui/StateFields.js +96 -0
- package/ui/TheoremChat.d.ts +71 -0
- package/ui/TheoremChat.js +146 -0
- package/ui/TheoremDecision.d.ts +38 -0
- package/ui/TheoremDecision.js +87 -0
- package/ui/TheoremHost.d.ts +38 -0
- package/ui/TheoremHost.js +150 -0
- package/ui/ToolGateCard.d.ts +28 -0
- package/ui/ToolGateCard.js +100 -0
- package/ui/ToolResult.d.ts +10 -0
- package/ui/ToolResult.js +60 -0
- package/ui/ToolResultCharts.d.ts +5 -0
- package/ui/ToolResultCharts.js +86 -0
- package/ui/TraceCharts.d.ts +13 -0
- package/ui/TraceCharts.js +235 -0
- package/ui/TraceGuardrails.d.ts +10 -0
- package/ui/TraceGuardrails.js +56 -0
- package/ui/TraceInspector.d.ts +6 -0
- package/ui/TraceInspector.js +189 -0
- package/ui/TraceInspectorPanel.d.ts +24 -0
- package/ui/TraceInspectorPanel.js +57 -0
- package/ui/TraceOverview.d.ts +34 -0
- package/ui/TraceOverview.js +96 -0
- package/ui/TraceSpanDetail.d.ts +10 -0
- package/ui/TraceSpanDetail.js +188 -0
- package/ui/TraceStory.d.ts +20 -0
- package/ui/TraceStory.js +107 -0
- package/ui/TraceValues.d.ts +36 -0
- package/ui/TraceValues.js +113 -0
- package/ui/TraceWaterfall.d.ts +13 -0
- package/ui/TraceWaterfall.js +132 -0
- package/ui/VoiceNote.d.ts +12 -0
- package/ui/VoiceNote.js +28 -0
- package/ui/built/theme.css +774 -0
- package/ui/built/theorem.d.ts +13 -0
- package/ui/built/theorem.js +631 -0
- package/ui/built/theorem.variants.d.ts +16 -0
- package/ui/disclosure-motion.d.ts +6 -0
- package/ui/disclosure-motion.js +163 -0
- package/ui/icons.d.ts +4 -0
- package/ui/icons.js +37 -0
- package/ui/index.d.ts +25 -0
- package/ui/index.js +29 -0
- package/ui/labels-provider.d.ts +16 -0
- package/ui/labels-provider.js +61 -0
- package/ui/labels.d.ts +1153 -0
- package/ui/labels.js +603 -0
- package/ui/theme.d.ts +10 -0
- package/ui/theme.js +13 -0
- package/ui/theorem-theme.d.ts +10 -0
- package/ui/theorem-theme.js +83 -0
- package/ui/trace-palette.d.ts +24 -0
- package/ui/trace-palette.js +24 -0
- package/ui/trace-patterns.d.ts +32 -0
- package/ui/trace-patterns.js +44 -0
- package/ui/transcript-copy-text.d.ts +4 -0
- package/ui/transcript-copy-text.js +39 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Web-standard handler that serves a Theorem chat UI for a single profile.
|
|
3
|
+
*
|
|
4
|
+
* Mount it on any framework that speaks `Request` → `Response` (Deno.serve,
|
|
5
|
+
* Hono, Next route handlers, SvelteKit, Workers) under a catch-all route:
|
|
6
|
+
*
|
|
7
|
+
* - `GET <base>` → `{ interface }` client-safe profile interface
|
|
8
|
+
* - `POST <base>/turn` → NDJSON turn events; a message sent while the reply
|
|
9
|
+
* waits on gates walks away from them first (`abandon`)
|
|
10
|
+
* - `POST <base>/invoke` → NDJSON events for the user's answer to a paused tool call
|
|
11
|
+
* - `POST <base>/steer` → inject messages into a running turn
|
|
12
|
+
*
|
|
13
|
+
* `createTheoremHostHandler` serves a host profile the same way, with no model:
|
|
14
|
+
*
|
|
15
|
+
* - `GET <base>` → `{ interface }` the tools the page may call, with their schemas
|
|
16
|
+
* - `POST <base>/call` → NDJSON events for one call of one of those tools
|
|
17
|
+
* - `POST <base>/invoke` → NDJSON events for the user's answer to a paused call
|
|
18
|
+
*
|
|
19
|
+
* Trust boundary: the browser owns the conversation text; the server owns every
|
|
20
|
+
* decision that grants authority. Tool permissions, which paused calls may run
|
|
21
|
+
* (and with what input), and which provider interactions may be continued are
|
|
22
|
+
* kept in a server-side session — request bodies can't grant any of them.
|
|
23
|
+
* Tool credentials are held on the server too: a key the user types at a gate
|
|
24
|
+
* is sent once and saved, and no credential is ever sent back to the browser.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import { createProvider, defaultKernelScope, defineProfile, errorKind, invokeTool, lexiconText, publicError, registerProfile, runTurn, TheoremError, } from '@theoremjs/agents';
|
|
29
|
+
import { caughtStatus, forClient, HTTP_METHOD, } from '@theoremjs/agents/host';
|
|
30
|
+
import { answerGatedCall, resolveGateTtlMs, toBase64Url, } from '@theoremjs/agents/kernel';
|
|
31
|
+
import { hostInterface, theoremHostCallRequestSchema, } from "../client/host-transport.js";
|
|
32
|
+
import { gatedToolsFromEvents, interfaceFromProfile, promotedToolIdsFromEvents, settlesToolCall, toolSnapshotFromEvents, } from '@theoremjs/agents/interface';
|
|
33
|
+
import { theoremInvokeRequestSchema, theoremSteerRequestSchema, theoremTurnRequestSchema, } from "../client/transport.js";
|
|
34
|
+
import { createMemoryCredentialStore, } from "./credential-store.js";
|
|
35
|
+
import { checkRequest } from "./request-check.js";
|
|
36
|
+
import { createMemorySessionStore, emptySessionState, pruneGates, } from "./session-store.js";
|
|
37
|
+
import { createMemorySteerInbox, steerStage, steerUnitOf } from "./steer-inbox.js";
|
|
38
|
+
import { checkWalkAway, walkAway } from "./walk-away.js";
|
|
39
|
+
import { jsonResponse } from "./json-response.js";
|
|
40
|
+
const SESSION_COOKIE = 'theorem_session';
|
|
41
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
|
|
42
|
+
const NDJSON_HEADERS = {
|
|
43
|
+
'content-type': 'application/x-ndjson; charset=utf-8',
|
|
44
|
+
'cache-control': 'no-store',
|
|
45
|
+
};
|
|
46
|
+
function withSessionCookie(response, session) {
|
|
47
|
+
if (session?.setCookie)
|
|
48
|
+
response.headers.append('set-cookie', session.setCookie);
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
/** Strip server-only identity (system prompts) from the projected interface. */
|
|
52
|
+
function clientInterface(profile) {
|
|
53
|
+
const iface = interfaceFromProfile(profile, defaultKernelScope.tools);
|
|
54
|
+
return { ...iface, identity: { handle: iface.identity.handle } };
|
|
55
|
+
}
|
|
56
|
+
/** A JSON body checked against `schema`; a missing or malformed field is a `request` error (400). */
|
|
57
|
+
export async function readBody(request, schema) {
|
|
58
|
+
// JSON-only POSTs can't be sent by a cross-site form, and force a CORS preflight.
|
|
59
|
+
const type = request.headers.get('content-type') ?? '';
|
|
60
|
+
if (!type.toLowerCase().startsWith('application/json')) {
|
|
61
|
+
// lexicon-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
62
|
+
throw new TheoremError('request', 'Content-Type must be application/json');
|
|
63
|
+
}
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = await request.json();
|
|
67
|
+
}
|
|
68
|
+
catch (cause) {
|
|
69
|
+
// lexicon-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
70
|
+
throw new TheoremError('request', 'Request body must be JSON', { cause });
|
|
71
|
+
}
|
|
72
|
+
return checkRequest(schema, raw, 'request body');
|
|
73
|
+
}
|
|
74
|
+
/** Conversation messages a browser may supply — never system instructions. */
|
|
75
|
+
function conversationOnly(messages) {
|
|
76
|
+
return messages.filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'tool');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Keep the user-authored parts of a turn input. Drops fields that would let a
|
|
80
|
+
* client speak as the host: system-role history, `role`, `repair` guidance,
|
|
81
|
+
* token-meter overrides, and live resumption handles.
|
|
82
|
+
*/
|
|
83
|
+
function userTurnInput(input) {
|
|
84
|
+
return {
|
|
85
|
+
...(input.text !== undefined ? { text: input.text } : {}),
|
|
86
|
+
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
87
|
+
...(input.voice ? { voice: input.voice } : {}),
|
|
88
|
+
...(input.history ? { history: conversationOnly(input.history) } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Last path segment after the mount point: '', 'turn', 'invoke', 'steer', or a host's 'call'. */
|
|
92
|
+
function routeOf(request) {
|
|
93
|
+
const segments = new URL(request.url).pathname.split('/').filter(Boolean);
|
|
94
|
+
const last = segments.at(-1) ?? '';
|
|
95
|
+
return last === 'turn' || last === 'invoke' || last === 'steer' || last === 'call' ? last : '';
|
|
96
|
+
}
|
|
97
|
+
function readCookie(request, name) {
|
|
98
|
+
for (const part of (request.headers.get('cookie') ?? '').split(';')) {
|
|
99
|
+
const [key, ...rest] = part.trim().split('=');
|
|
100
|
+
if (key === name)
|
|
101
|
+
return rest.join('=');
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
function newSessionId() {
|
|
106
|
+
return toBase64Url(crypto.getRandomValues(new Uint8Array(32)));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The session id the handler issued in its cookie, for a host route outside the
|
|
110
|
+
* handler (an OAuth callback) that saves credentials under it. Hosts that pass
|
|
111
|
+
* a `session` resolver use their own id instead.
|
|
112
|
+
*/
|
|
113
|
+
export function theoremSessionId(request) {
|
|
114
|
+
const existing = readCookie(request, SESSION_COOKIE);
|
|
115
|
+
return existing && SESSION_ID_PATTERN.test(existing) ? existing : undefined;
|
|
116
|
+
}
|
|
117
|
+
function cookieSession(request) {
|
|
118
|
+
const existing = theoremSessionId(request);
|
|
119
|
+
if (existing)
|
|
120
|
+
return { id: existing };
|
|
121
|
+
const id = newSessionId();
|
|
122
|
+
const secure = new URL(request.url).protocol === 'https:' ? '; Secure' : '';
|
|
123
|
+
return {
|
|
124
|
+
id,
|
|
125
|
+
setCookie: `${SESSION_COOKIE}=${id}; Path=/; HttpOnly; SameSite=Lax${secure}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function lockedStore(store, loaded) {
|
|
129
|
+
const locks = new Map();
|
|
130
|
+
const read = async (sessionId) => loaded(await store.load(sessionId));
|
|
131
|
+
return {
|
|
132
|
+
read,
|
|
133
|
+
mutate(sessionId, change) {
|
|
134
|
+
const next = (locks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
135
|
+
const value = await read(sessionId);
|
|
136
|
+
const result = change(value);
|
|
137
|
+
await store.save(sessionId, value);
|
|
138
|
+
return result;
|
|
139
|
+
});
|
|
140
|
+
const settled = next.catch(() => { });
|
|
141
|
+
locks.set(sessionId, settled);
|
|
142
|
+
void settled.then(() => {
|
|
143
|
+
if (locks.get(sessionId) === settled)
|
|
144
|
+
locks.delete(sessionId);
|
|
145
|
+
});
|
|
146
|
+
return next;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Session state, with expired gates dropped as it loads so they can't be answered. */
|
|
151
|
+
function sessionStateStore(store, gateTtlMs) {
|
|
152
|
+
return lockedStore(store, (state = emptySessionState()) => ({
|
|
153
|
+
...state,
|
|
154
|
+
gates: pruneGates(state.gates, Date.now(), gateTtlMs),
|
|
155
|
+
settled: pruneGates(state.settled, Date.now(), gateTtlMs),
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
async function sessionOf(ctx, request) {
|
|
159
|
+
if (!ctx.options.session)
|
|
160
|
+
return cookieSession(request);
|
|
161
|
+
const id = (await ctx.options.session(request))?.trim();
|
|
162
|
+
if (!id) {
|
|
163
|
+
// lexicon-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
164
|
+
throw new TheoremError('auth', 'the session resolver refused the request', {
|
|
165
|
+
copy: { key: 'session.sign_in' },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return { id };
|
|
169
|
+
}
|
|
170
|
+
/** Reports the error to the host, then words it for the user from the profile's lexicon. */
|
|
171
|
+
function publicMessage(ctx, err, request) {
|
|
172
|
+
ctx.options.onError?.(err, { request });
|
|
173
|
+
return publicError(err, ctx.profile.lexicon);
|
|
174
|
+
}
|
|
175
|
+
function eventStream(ctx, request, source) {
|
|
176
|
+
const encoder = new TextEncoder();
|
|
177
|
+
const line = (value) => encoder.encode(`${JSON.stringify(value)}\n`);
|
|
178
|
+
const stream = new ReadableStream({
|
|
179
|
+
async start(controller) {
|
|
180
|
+
try {
|
|
181
|
+
for await (const event of source()) {
|
|
182
|
+
controller.enqueue(line(forClient(event, ctx.options.clientEvents)));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
if (!request.signal.aborted) {
|
|
187
|
+
controller.enqueue(line({ type: 'error', error: publicMessage(ctx, err, request), errorKind: errorKind(err) }));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
controller.close();
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
return new Response(stream, { headers: NDJSON_HEADERS });
|
|
194
|
+
}
|
|
195
|
+
function providerFor(ctx, request, model) {
|
|
196
|
+
const { provider } = ctx.options;
|
|
197
|
+
// lexicon-exempt: builder config error; the user reads error.config
|
|
198
|
+
if (!provider)
|
|
199
|
+
throw new TheoremError('config', `profile ${ctx.profile.id} has no provider`);
|
|
200
|
+
if (typeof provider === 'function')
|
|
201
|
+
return provider({ request, model });
|
|
202
|
+
return createProvider(ctx.profile, provider, model);
|
|
203
|
+
}
|
|
204
|
+
/** Steer inboxes are scoped to the session, so a turn id alone can't reach another user's turn. */
|
|
205
|
+
function inboxKey(sessionId, turnId) {
|
|
206
|
+
return `${sessionId}\u0000${turnId}`;
|
|
207
|
+
}
|
|
208
|
+
/** The exact paused calls the user may now approve, when the stream stopped on gates. */
|
|
209
|
+
function pendingGatesFrom(events, context) {
|
|
210
|
+
return gatedToolsFromEvents(events).map((gated) => ({
|
|
211
|
+
callId: gated.callId,
|
|
212
|
+
gate: {
|
|
213
|
+
name: gated.name,
|
|
214
|
+
arguments: gated.arguments,
|
|
215
|
+
gate: { kind: gated.gateKind, permission: gated.permission, ...(gated.auth ? { auth: gated.auth } : {}) },
|
|
216
|
+
snapshot: toolSnapshotFromEvents(events),
|
|
217
|
+
promoted: [...new Set([...(context.promoted ?? []), ...promotedToolIdsFromEvents(events)])],
|
|
218
|
+
turnInput: context.turnInput,
|
|
219
|
+
model: context.model,
|
|
220
|
+
createdAt: Date.now(),
|
|
221
|
+
},
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Record what the stream established: interaction ids for continuation and,
|
|
226
|
+
* when it paused on gates, the exact calls the user may now approve.
|
|
227
|
+
*/
|
|
228
|
+
async function recordOutcome(sessions, sessionId, events, context) {
|
|
229
|
+
const interactionIds = events.flatMap((event) => event.type === 'done' && event.interactionId ? [event.interactionId] : []);
|
|
230
|
+
const pending = pendingGatesFrom(events, context);
|
|
231
|
+
if (!interactionIds.length && !pending.length)
|
|
232
|
+
return;
|
|
233
|
+
await sessions.mutate(sessionId, (state) => {
|
|
234
|
+
state.interactions.push(...interactionIds);
|
|
235
|
+
for (const { callId, gate } of pending)
|
|
236
|
+
state.gates[callId] = gate;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async function* recorded(sessions, sessionId, events, context) {
|
|
240
|
+
const seen = [];
|
|
241
|
+
for await (const event of events) {
|
|
242
|
+
seen.push(event);
|
|
243
|
+
if (event.type === 'done')
|
|
244
|
+
await recordOutcome(sessions, sessionId, seen, context);
|
|
245
|
+
yield event;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Save one credential, leaving the session's others as they are. */
|
|
249
|
+
function saveCredential(ctx, sessionId, slot, credential) {
|
|
250
|
+
return ctx.credentials.mutate(sessionId, (saved) => {
|
|
251
|
+
saved[slot] = credential;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The session's credentials as a call reads them: one slot when a tool needs it,
|
|
256
|
+
* saved the moment the kernel sets one, so a rotated refresh token is never lost
|
|
257
|
+
* to a closed stream.
|
|
258
|
+
*/
|
|
259
|
+
function sessionCredentials(ctx, sessionId) {
|
|
260
|
+
return {
|
|
261
|
+
get: async (slot) => (await ctx.credentials.read(sessionId))[slot],
|
|
262
|
+
set: (slot, credential) => saveCredential(ctx, sessionId, slot, credential),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** An OAuth gate given the host's sign-in URL. */
|
|
266
|
+
async function withAuthorizationUrl(ctx, request, sessionId, event) {
|
|
267
|
+
if (event.type !== 'tool' || event.tool.phase !== 'gate')
|
|
268
|
+
return event;
|
|
269
|
+
const { tool } = event;
|
|
270
|
+
const gate = tool.gate.kind === 'auth' ? tool.gate : undefined;
|
|
271
|
+
if (!gate || gate.authChallenge.authType !== 'oauth2' || !ctx.options.authorizationUrl)
|
|
272
|
+
return event;
|
|
273
|
+
const challenge = gate.authChallenge;
|
|
274
|
+
const authorizationUrl = await ctx.options.authorizationUrl(challenge, { request, sessionId });
|
|
275
|
+
return {
|
|
276
|
+
...event,
|
|
277
|
+
tool: { ...tool, gate: { ...gate, authChallenge: { ...challenge, authorizationUrl } } },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** Every OAuth gate in the stream, given the host's sign-in URL. */
|
|
281
|
+
async function* withAuthorizationUrls(ctx, request, sessionId, events) {
|
|
282
|
+
for await (const event of events)
|
|
283
|
+
yield await withAuthorizationUrl(ctx, request, sessionId, event);
|
|
284
|
+
}
|
|
285
|
+
function answerPendingGate(state, request) {
|
|
286
|
+
const pending = state.gates[request.callId];
|
|
287
|
+
if (!pending) {
|
|
288
|
+
// lexicon-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
289
|
+
throw new TheoremError('request', `gate ${request.callId} is not pending`, {
|
|
290
|
+
copy: { key: 'session.gate_expired' },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// A refused answer (a refused secret, an edit without an approval) throws here and leaves the gate pending.
|
|
294
|
+
const answer = answerGatedCall(request, {
|
|
295
|
+
name: pending.name,
|
|
296
|
+
arguments: pending.arguments,
|
|
297
|
+
permission: pending.gate.permission,
|
|
298
|
+
...(pending.gate.kind === 'auth' && pending.gate.auth ? { auth: pending.gate.auth } : {}),
|
|
299
|
+
}, state.permissions);
|
|
300
|
+
delete state.gates[request.callId];
|
|
301
|
+
state.permissions = answer.sessionPermissions;
|
|
302
|
+
return { callId: request.callId, pending, answer };
|
|
303
|
+
}
|
|
304
|
+
function walkedCallOf(state, callId) {
|
|
305
|
+
const settled = state.settled[callId];
|
|
306
|
+
return settled ? { callId, settled } : answerPendingGate(state, { callId, decision: 'abandon' });
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Take the calls a message walks away from out of the session, answered
|
|
310
|
+
* `abandon`: every one of them, or none when one is neither waiting nor
|
|
311
|
+
* settled. The message's history must leave exactly those calls open.
|
|
312
|
+
*/
|
|
313
|
+
function takeWalkedAway(ctx, session, input, abandon) {
|
|
314
|
+
checkWalkAway(input, abandon);
|
|
315
|
+
// A throw leaves the session unsaved, so no call is taken unless all are.
|
|
316
|
+
return ctx.sessions.mutate(session.id, (state) => abandon.map((callId) => walkedCallOf(state, callId)));
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The run that settles an answered call, as the model asked it. The session
|
|
320
|
+
* keeps the event that settles it, and `ended` names the call once it has
|
|
321
|
+
* (see `restoreUnended`).
|
|
322
|
+
*/
|
|
323
|
+
async function* settleAnswered(ctx, request, session, answered, ended) {
|
|
324
|
+
const { callId, pending } = answered;
|
|
325
|
+
for await (const event of invokeAnswered(ctx, request, session.id, answered)) {
|
|
326
|
+
if (!ended.has(callId) && settlesToolCall(event, callId)) {
|
|
327
|
+
ended.add(callId);
|
|
328
|
+
await ctx.sessions.mutate(session.id, (state) => {
|
|
329
|
+
state.settled[callId] = { event, createdAt: pending.createdAt };
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
yield event;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* A request that took calls and ended before they settled (the client went
|
|
337
|
+
* away, the host failed, an earlier call's run threw) puts each back to wait
|
|
338
|
+
* as it did, to be answered again, unless it paused on a gate again: the run
|
|
339
|
+
* recorded that gate with its outcome.
|
|
340
|
+
*/
|
|
341
|
+
async function restoreUnended(ctx, session, calls, ended) {
|
|
342
|
+
const unended = calls.flatMap((call) => ('pending' in call && !ended.has(call.callId) ? [call] : []));
|
|
343
|
+
if (!unended.length)
|
|
344
|
+
return;
|
|
345
|
+
await ctx.sessions.mutate(session.id, (state) => {
|
|
346
|
+
for (const { callId, pending } of unended)
|
|
347
|
+
state.gates[callId] ??= pending;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function invokeAnswered(ctx, request, sessionId, { callId, pending, answer }) {
|
|
351
|
+
return invokeTool({
|
|
352
|
+
profile: ctx.profile.id,
|
|
353
|
+
name: pending.name,
|
|
354
|
+
callId,
|
|
355
|
+
input: answer.input,
|
|
356
|
+
resume: answer.resume,
|
|
357
|
+
sessionPermissions: answer.sessionPermissions,
|
|
358
|
+
credentials: sessionCredentials(ctx, sessionId),
|
|
359
|
+
turnInput: pending.turnInput,
|
|
360
|
+
snapshot: pending.snapshot,
|
|
361
|
+
promoted: pending.promoted,
|
|
362
|
+
model: pending.model,
|
|
363
|
+
signal: request.signal,
|
|
364
|
+
host: ctx.options.host?.(request),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/** Settles each walked-away call, and returns the input with their answers. */
|
|
368
|
+
async function* walkAwayFrom(ctx, request, session, walked, ended) {
|
|
369
|
+
return yield* walkAway(walked.input, walked.calls.map((call) => ({
|
|
370
|
+
callId: call.callId,
|
|
371
|
+
events: 'settled' in call
|
|
372
|
+
? once(call.settled.event)
|
|
373
|
+
: settleAnswered(ctx, request, session, call, ended),
|
|
374
|
+
})));
|
|
375
|
+
}
|
|
376
|
+
/** A settled call's answer, streamed again. */
|
|
377
|
+
async function* once(event) {
|
|
378
|
+
yield event;
|
|
379
|
+
}
|
|
380
|
+
async function* turnEvents(ctx, request, session, body, walked) {
|
|
381
|
+
const ended = new Set();
|
|
382
|
+
let input;
|
|
383
|
+
try {
|
|
384
|
+
input = walked.calls.length ? yield* walkAwayFrom(ctx, request, session, walked, ended) : walked.input;
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
await restoreUnended(ctx, session, walked.calls, ended);
|
|
388
|
+
}
|
|
389
|
+
if (!input)
|
|
390
|
+
return;
|
|
391
|
+
const state = await ctx.sessions.read(session.id);
|
|
392
|
+
const provider = await providerFor(ctx, request, body.model);
|
|
393
|
+
// Only continue provider-side conversations this session started.
|
|
394
|
+
const previousInteractionId = body.previousInteractionId && state.interactions.includes(body.previousInteractionId)
|
|
395
|
+
? body.previousInteractionId
|
|
396
|
+
: undefined;
|
|
397
|
+
const key = body.turnId ? inboxKey(session.id, body.turnId) : undefined;
|
|
398
|
+
if (key)
|
|
399
|
+
await ctx.inbox.open(key);
|
|
400
|
+
try {
|
|
401
|
+
const events = runTurn({
|
|
402
|
+
profile: ctx.profile.id,
|
|
403
|
+
input,
|
|
404
|
+
previousInteractionId,
|
|
405
|
+
sessionPermissions: state.permissions,
|
|
406
|
+
credentials: sessionCredentials(ctx, session.id),
|
|
407
|
+
signal: request.signal,
|
|
408
|
+
host: ctx.options.host?.(request),
|
|
409
|
+
...(body.model ? { model: body.model } : {}),
|
|
410
|
+
...(body.effort ? { effort: body.effort } : {}),
|
|
411
|
+
...(key ? { onStage: steerStage(ctx.inbox, key) } : {}),
|
|
412
|
+
}, provider);
|
|
413
|
+
yield* recorded(ctx.sessions, session.id, withAuthorizationUrls(ctx, request, session.id, events), {
|
|
414
|
+
turnInput: input,
|
|
415
|
+
model: body.model,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
if (key)
|
|
420
|
+
await ctx.inbox.close(key);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function* invokeEvents(ctx, request, session, answered) {
|
|
424
|
+
const { pending, answer } = answered;
|
|
425
|
+
const ended = new Set();
|
|
426
|
+
try {
|
|
427
|
+
if (answer.typed)
|
|
428
|
+
await saveCredential(ctx, session.id, answer.typed.slot, answer.typed.credential);
|
|
429
|
+
const events = settleAnswered(ctx, request, session, answered, ended);
|
|
430
|
+
yield* recorded(ctx.sessions, session.id, withAuthorizationUrls(ctx, request, session.id, events), {
|
|
431
|
+
turnInput: pending.turnInput,
|
|
432
|
+
model: pending.model,
|
|
433
|
+
promoted: pending.promoted,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await restoreUnended(ctx, session, [answered], ended);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function steer(ctx, request, session) {
|
|
441
|
+
const body = await readBody(request, theoremSteerRequestSchema);
|
|
442
|
+
if (!(await ctx.inbox.enqueue(inboxKey(session.id, body.turnId), steerUnitOf(body)))) {
|
|
443
|
+
// lexicon-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
444
|
+
throw new TheoremError('request', `turn ${body.turnId} is not running`, {
|
|
445
|
+
copy: { key: 'session.turn_ended' },
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return jsonResponse(200, { ok: true });
|
|
449
|
+
}
|
|
450
|
+
/** One call of one of a host's tools, run as the page asked it; a gate it pauses on waits in the session. */
|
|
451
|
+
async function* callEvents(ctx, request, session, body) {
|
|
452
|
+
const state = await ctx.sessions.read(session.id);
|
|
453
|
+
const events = invokeTool({
|
|
454
|
+
profile: ctx.profile.id,
|
|
455
|
+
name: body.name,
|
|
456
|
+
input: body.input,
|
|
457
|
+
sessionPermissions: state.permissions,
|
|
458
|
+
credentials: sessionCredentials(ctx, session.id),
|
|
459
|
+
signal: request.signal,
|
|
460
|
+
host: ctx.options.host?.(request),
|
|
461
|
+
});
|
|
462
|
+
yield* recorded(ctx.sessions, session.id, withAuthorizationUrls(ctx, request, session.id, events), {
|
|
463
|
+
turnInput: {},
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function answerRequest(body) {
|
|
467
|
+
return { callId: body.gateId, decision: body.decision, input: body.input, secret: body.secret };
|
|
468
|
+
}
|
|
469
|
+
/** A route this profile doesn't serve. */
|
|
470
|
+
function refused(ctx) {
|
|
471
|
+
return jsonResponse(HTTP_METHOD, {
|
|
472
|
+
error: lexiconText('error.request', {}, ctx.profile.lexicon),
|
|
473
|
+
errorKind: 'request',
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/** The user's answer to a paused call. */
|
|
477
|
+
async function invoke(ctx, request, session) {
|
|
478
|
+
const body = await readBody(request, theoremInvokeRequestSchema);
|
|
479
|
+
// Take the answered call before streaming, so a stale answer is a reply status, not a stream error.
|
|
480
|
+
const answered = await ctx.sessions.mutate(session.id, (state) => answerPendingGate(state, answerRequest(body)));
|
|
481
|
+
return eventStream(ctx, request, () => invokeEvents(ctx, request, session, answered));
|
|
482
|
+
}
|
|
483
|
+
async function hostRoute(ctx, request, session) {
|
|
484
|
+
const target = routeOf(request);
|
|
485
|
+
if (request.method === 'GET' && target === '') {
|
|
486
|
+
const tool = (name) => {
|
|
487
|
+
const registered = defaultKernelScope.tools.get(name);
|
|
488
|
+
return registered?.type === 'builtin' ? undefined : registered;
|
|
489
|
+
};
|
|
490
|
+
return jsonResponse(200, { interface: hostInterface(ctx.profile, tool) });
|
|
491
|
+
}
|
|
492
|
+
if (request.method === 'POST' && target === 'call') {
|
|
493
|
+
const body = await readBody(request, theoremHostCallRequestSchema);
|
|
494
|
+
return eventStream(ctx, request, () => callEvents(ctx, request, session, body));
|
|
495
|
+
}
|
|
496
|
+
if (request.method === 'POST' && target === 'invoke')
|
|
497
|
+
return invoke(ctx, request, session);
|
|
498
|
+
return refused(ctx);
|
|
499
|
+
}
|
|
500
|
+
async function route(ctx, request, session) {
|
|
501
|
+
if (ctx.profile.type === 'host')
|
|
502
|
+
return hostRoute({ ...ctx, profile: ctx.profile }, request, session);
|
|
503
|
+
const target = routeOf(request);
|
|
504
|
+
if (request.method === 'GET' && target === '')
|
|
505
|
+
return jsonResponse(200, { interface: clientInterface(ctx.profile) });
|
|
506
|
+
if (request.method !== 'POST' || target === '' || target === 'call')
|
|
507
|
+
return refused(ctx);
|
|
508
|
+
if (target === 'turn') {
|
|
509
|
+
const body = await readBody(request, theoremTurnRequestSchema);
|
|
510
|
+
const input = userTurnInput(body.input);
|
|
511
|
+
// Take the walked-away calls before streaming, so a stale one is a reply status, not a stream error.
|
|
512
|
+
const calls = body.abandon ? await takeWalkedAway(ctx, session, input, body.abandon) : [];
|
|
513
|
+
return eventStream(ctx, request, () => turnEvents(ctx, request, session, body, { input, calls }));
|
|
514
|
+
}
|
|
515
|
+
if (target === 'invoke')
|
|
516
|
+
return invoke(ctx, request, session);
|
|
517
|
+
return steer(ctx, request, session);
|
|
518
|
+
}
|
|
519
|
+
export function createTheoremHandler(options) {
|
|
520
|
+
const profile = defineProfile(options.profile);
|
|
521
|
+
if (profile.type === 'decision') {
|
|
522
|
+
// lexicon-exempt: builder config error at setup; no user sees it
|
|
523
|
+
throw new Error("createTheoremHandler serves turn-based profiles; serve type 'decision' with createTheoremDecisionHandler.");
|
|
524
|
+
}
|
|
525
|
+
if (profile.type === 'host') {
|
|
526
|
+
// lexicon-exempt: builder config error at setup; no user sees it
|
|
527
|
+
throw new Error("createTheoremHandler serves turn-based profiles; serve type 'host' with createTheoremHostHandler.");
|
|
528
|
+
}
|
|
529
|
+
if (profile.type === 'live') {
|
|
530
|
+
// lexicon-exempt: builder config error at setup; no user sees it
|
|
531
|
+
throw new Error(`createTheoremHandler serves turn-based profiles; got type '${profile.type}'.`);
|
|
532
|
+
}
|
|
533
|
+
return serve('createTheoremHandler', profile, options, options.steerInbox);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Serves a host profile to `<TheoremHost />`: the page calls the profile's tools
|
|
537
|
+
* directly, and every call runs through the kernel's gates and guardrails.
|
|
538
|
+
*/
|
|
539
|
+
export function createTheoremHostHandler(options) {
|
|
540
|
+
const profile = defineProfile(options.profile);
|
|
541
|
+
if (profile.type !== 'host') {
|
|
542
|
+
// lexicon-exempt: builder config error at setup; no user sees it
|
|
543
|
+
throw new Error(`createTheoremHostHandler serves type 'host'; got type '${profile.type}'.`);
|
|
544
|
+
}
|
|
545
|
+
return serve('createTheoremHostHandler', profile, options);
|
|
546
|
+
}
|
|
547
|
+
function serve(name, profile, options, steerInbox) {
|
|
548
|
+
const gateTtlMs = resolveGateTtlMs(name, options.gateTtlMs);
|
|
549
|
+
registerProfile(profile);
|
|
550
|
+
const ctx = {
|
|
551
|
+
options,
|
|
552
|
+
profile,
|
|
553
|
+
inbox: steerInbox ?? createMemorySteerInbox(),
|
|
554
|
+
sessions: sessionStateStore(options.sessionStore ?? createMemorySessionStore(), gateTtlMs),
|
|
555
|
+
credentials: lockedStore(options.credentialStore ?? createMemoryCredentialStore(), (saved = {}) => saved),
|
|
556
|
+
};
|
|
557
|
+
return async (request) => {
|
|
558
|
+
let session;
|
|
559
|
+
try {
|
|
560
|
+
session = await sessionOf(ctx, request);
|
|
561
|
+
return withSessionCookie(await route(ctx, request, session), session);
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
const error = publicMessage(ctx, err, request);
|
|
565
|
+
return withSessionCookie(jsonResponse(caughtStatus(err), { error, errorKind: errorKind(err) }), session);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
package/server/mod.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server half of `@theoremjs/react`: serve one profile to the chat UI, a
|
|
3
|
+
* decision profile to the decision UI, or a host profile's tools to the host UI.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
export { theoremHostCallRequestSchema } from '../client/host-transport.ts';
|
|
8
|
+
export type { LiveClientMessage } from '../client/live-messages.ts';
|
|
9
|
+
export { theoremInvokeRequestSchema, theoremReplaySchema, theoremSteerRequestSchema, theoremTurnRequestSchema, } from '../client/transport.ts';
|
|
10
|
+
export { createMemoryCredentialStore, type TheoremCredentialStore, type TheoremCredentials, } from './credential-store.ts';
|
|
11
|
+
export { createTheoremDecisionHandler, type TheoremDecisionHandlerOptions, } from './decision-handler.ts';
|
|
12
|
+
export { createTheoremHandler, createTheoremHostHandler, readBody, type TheoremHandlerOptions, type TheoremHostHandlerOptions, type TheoremRequestContext, theoremSessionId, } from './handler.ts';
|
|
13
|
+
export { checkRequest, parseLiveClientMessage } from './request-check.ts';
|
|
14
|
+
export { createMemorySessionStore, type MemorySessionStoreOptions, type PendingToolGate, type SettledToolGate, type TheoremSessionState, type TheoremSessionStore, } from './session-store.ts';
|
|
15
|
+
export { createMemorySteerInbox, type SteerInbox, type SteerUnit, steerStage, steerUnitOf, } from './steer-inbox.ts';
|
|
16
|
+
export { checkWalkAway, type WalkedAwayCall, walkAway } from './walk-away.ts';
|
package/server/mod.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server half of `@theoremjs/react`: serve one profile to the chat UI, a
|
|
3
|
+
* decision profile to the decision UI, or a host profile's tools to the host UI.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
export { theoremHostCallRequestSchema } from "../client/host-transport.js";
|
|
8
|
+
export { theoremInvokeRequestSchema, theoremReplaySchema, theoremSteerRequestSchema, theoremTurnRequestSchema, } from "../client/transport.js";
|
|
9
|
+
export { createMemoryCredentialStore, } from "./credential-store.js";
|
|
10
|
+
export { createTheoremDecisionHandler, } from "./decision-handler.js";
|
|
11
|
+
export { createTheoremHandler, createTheoremHostHandler, readBody, theoremSessionId, } from "./handler.js";
|
|
12
|
+
export { checkRequest, parseLiveClientMessage } from "./request-check.js";
|
|
13
|
+
export { createMemorySessionStore, } from "./session-store.js";
|
|
14
|
+
export { createMemorySteerInbox, steerStage, steerUnitOf, } from "./steer-inbox.js";
|
|
15
|
+
export { checkWalkAway, walkAway } from "./walk-away.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a browser sends a host, checked: the handler's request bodies and a
|
|
3
|
+
* relay's live messages. A malformed one is a `request` error, which names
|
|
4
|
+
* only the paths and codes that broke, never the value.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { type z } from '@theoremjs/agents';
|
|
9
|
+
import { type LiveClientMessage } from '../client/live-messages.ts';
|
|
10
|
+
/** `raw` checked against `schema`; a missing or malformed field is a `request` error. */
|
|
11
|
+
export declare function checkRequest<T>(schema: z.ZodType<T>, raw: unknown, what: string): T;
|
|
12
|
+
/**
|
|
13
|
+
* One text frame from the live client, as a relay reads it: JSON that passes
|
|
14
|
+
* `liveClientMessageSchema`, else a `request` error.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseLiveClientMessage(text: string): LiveClientMessage;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a browser sends a host, checked: the handler's request bodies and a
|
|
3
|
+
* relay's live messages. A malformed one is a `request` error, which names
|
|
4
|
+
* only the paths and codes that broke, never the value.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { TheoremError } from '@theoremjs/agents';
|
|
9
|
+
import { liveClientMessageSchema } from "../client/live-messages.js";
|
|
10
|
+
import { issueSummary } from "../client/wire-line.js";
|
|
11
|
+
/** `raw` checked against `schema`; a missing or malformed field is a `request` error. */
|
|
12
|
+
export function checkRequest(schema, raw, what) {
|
|
13
|
+
const parsed = schema.safeParse(raw);
|
|
14
|
+
if (!parsed.success) {
|
|
15
|
+
// lexicon-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
16
|
+
throw new TheoremError('request', `${what} failed its check: ${issueSummary(parsed.error)}`);
|
|
17
|
+
}
|
|
18
|
+
return parsed.data;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* One text frame from the live client, as a relay reads it: JSON that passes
|
|
22
|
+
* `liveClientMessageSchema`, else a `request` error.
|
|
23
|
+
*/
|
|
24
|
+
export function parseLiveClientMessage(text) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = JSON.parse(text);
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
// lexicon-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
31
|
+
throw new TheoremError('request', 'live message must be JSON', { cause });
|
|
32
|
+
}
|
|
33
|
+
return checkRequest(liveClientMessageSchema, raw, 'live message');
|
|
34
|
+
}
|