@theoremjs/react 0.3.0 → 0.3.1
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/README.md +728 -202
- package/client/composer-hints.js +1 -1
- package/client/decision-images.d.ts +76 -0
- package/client/decision-images.js +139 -0
- package/client/decision-transport.d.ts +26 -4
- package/client/decision-transport.js +79 -6
- package/client/embedding-input.d.ts +84 -0
- package/client/embedding-input.js +194 -0
- package/client/embedding-space.d.ts +82 -0
- package/client/embedding-space.js +389 -0
- package/client/embedding-transport.d.ts +40 -0
- package/client/embedding-transport.js +71 -0
- package/client/failure.d.ts +7 -10
- package/client/failure.js +9 -9
- package/client/host-call.d.ts +1 -1
- package/client/host-call.js +7 -2
- package/client/host-transport.d.ts +2 -0
- package/client/host-transport.js +3 -2
- package/client/index.d.ts +19 -11
- package/client/index.js +14 -10
- package/client/ink-waveform.d.ts +1 -1
- package/client/ink-waveform.js +1 -2
- package/client/live/apply-live-turn-tool-event.js +1 -1
- package/client/live/live-captions.d.ts +30 -0
- package/client/live/live-captions.js +105 -1
- package/client/live/live-mic-forward.d.ts +1 -0
- package/client/live/live-mic-forward.js +11 -2
- package/client/live/live-page-tool.d.ts +34 -0
- package/client/live/live-page-tool.js +11 -0
- package/client/live/live-state.d.ts +4 -2
- package/client/live/live-state.js +2 -0
- package/client/live/live-video.js +1 -1
- package/client/live/run-live-tool-call.d.ts +7 -2
- package/client/live/run-live-tool-call.js +48 -10
- package/client/live/use-live-caption-log.d.ts +17 -0
- package/client/live/use-live-caption-log.js +51 -0
- package/client/live-client.d.ts +52 -4
- package/client/live-client.js +250 -59
- package/client/live-messages.d.ts +51 -13
- package/client/live-messages.js +42 -9
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/run-commit.d.ts +16 -43
- package/client/run-commit.js +26 -101
- package/client/run-session.d.ts +40 -41
- package/client/run-session.js +104 -112
- package/client/schema-fields.js +27 -6
- package/client/shaped-data.js +37 -8
- package/client/source-citations.js +1 -1
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/state-fields.js +1 -1
- package/client/tool-result.js +30 -12
- package/client/tool-resume.d.ts +18 -1
- package/client/tool-resume.js +4 -1
- package/client/trace-feed.d.ts +1 -1
- package/client/trace-story.d.ts +2 -2
- package/client/trace-story.js +43 -19
- package/client/trace-view.d.ts +2 -2
- package/client/trace-view.js +56 -16
- package/client/transcript-groups.d.ts +28 -2
- package/client/transcript-groups.js +63 -13
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transport.d.ts +114 -26
- package/client/transport.js +106 -32
- package/client/turn-client.d.ts +17 -8
- package/client/turn-client.js +26 -31
- package/client/voice-recorder.d.ts +1 -1
- package/client/voice-recorder.js +2 -5
- package/client/wire-line.js +5 -5
- package/client/worklets/mic-capture.js +9 -9
- package/components/InkWaveform.d.ts +12 -1
- package/components/InkWaveform.js +65 -42
- package/components/live/use-live-runner-controls.d.ts +3 -4
- package/components/live/use-live-runner-controls.js +6 -10
- package/components/live/use-live-runner-model.d.ts +10 -6
- package/components/live/use-live-runner-model.js +29 -14
- package/components/live/use-live-runner-ui.d.ts +10 -8
- package/components/live/use-live-runner-ui.js +19 -11
- package/components/live/use-live-session-client.d.ts +4 -4
- package/components/live/use-live-session-client.js +11 -8
- package/components/use-composer-voice.d.ts +4 -4
- package/components/use-composer-voice.js +8 -8
- package/components/use-editor-selection.js +4 -1
- package/components/use-voice-playback.d.ts +2 -1
- package/components/use-voice-playback.js +4 -4
- package/hooks/use-described.d.ts +5 -1
- package/hooks/use-described.js +11 -9
- package/hooks/use-embedding-index.d.ts +27 -0
- package/hooks/use-embedding-index.js +43 -0
- package/hooks/use-one-call.d.ts +31 -0
- package/hooks/use-one-call.js +76 -0
- package/hooks/use-page-tool-warnings.d.ts +6 -0
- package/hooks/use-page-tool-warnings.js +25 -0
- package/hooks/use-second-ticker.d.ts +2 -0
- package/hooks/use-second-ticker.js +13 -0
- package/hooks/use-theorem-chat-actions.d.ts +25 -21
- package/hooks/use-theorem-chat-actions.js +67 -45
- package/hooks/use-theorem-chat-state.d.ts +6 -6
- package/hooks/use-theorem-chat-state.js +13 -9
- package/hooks/use-theorem-chat.d.ts +48 -14
- package/hooks/use-theorem-chat.js +329 -141
- package/hooks/use-theorem-decision.d.ts +5 -10
- package/hooks/use-theorem-decision.js +4 -67
- package/hooks/use-theorem-embedding.d.ts +18 -0
- package/hooks/use-theorem-embedding.js +12 -0
- package/hooks/use-theorem-host.d.ts +5 -4
- package/hooks/use-theorem-host.js +26 -8
- package/hooks/use-theorem-interface.d.ts +6 -3
- package/hooks/use-theorem-interface.js +12 -22
- package/index.d.ts +6 -3
- package/index.js +5 -2
- package/live.d.ts +1 -0
- package/live.js +1 -1
- package/package.json +6 -5
- package/server/checkpoint-ownership.d.ts +6 -0
- package/server/checkpoint-ownership.js +42 -0
- package/server/conversation-store.d.ts +92 -0
- package/server/conversation-store.js +131 -0
- package/server/conversation.d.ts +193 -0
- package/server/conversation.js +552 -0
- package/server/decision-handler.d.ts +12 -4
- package/server/decision-handler.js +29 -38
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +58 -20
- package/server/handler.js +441 -238
- package/server/mod.d.ts +15 -6
- package/server/mod.js +13 -5
- package/server/page-dispatch.d.ts +22 -0
- package/server/page-dispatch.js +127 -0
- package/server/request-body.d.ts +2 -0
- package/server/request-body.js +42 -0
- package/server/request-check.d.ts +6 -1
- package/server/request-check.js +27 -12
- package/server/session-authority.d.ts +8 -0
- package/server/session-authority.js +78 -0
- package/server/session-boundary.d.ts +26 -0
- package/server/session-boundary.js +64 -0
- package/server/session-identity.d.ts +16 -0
- package/server/session-identity.js +41 -0
- package/server/session-limits.d.ts +12 -0
- package/server/session-limits.js +59 -0
- package/server/session-store.d.ts +69 -20
- package/server/session-store.js +220 -5
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.js +1 -1
- package/server/tool-operations.d.ts +6 -0
- package/server/tool-operations.js +132 -0
- package/server/turn-input.d.ts +10 -0
- package/server/turn-input.js +29 -0
- package/styles/ink-controls.css +78 -78
- package/ui/CardWall.d.ts +10 -0
- package/ui/CardWall.js +61 -0
- package/ui/ChatComposerBar.d.ts +28 -5
- package/ui/ChatComposerBar.js +85 -30
- package/ui/ChatTranscript.d.ts +14 -1
- package/ui/ChatTranscript.js +110 -57
- package/ui/ConsoleFrame.d.ts +61 -4
- package/ui/ConsoleFrame.js +109 -9
- package/ui/DecisionAnswers.d.ts +40 -2
- package/ui/DecisionAnswers.js +166 -35
- package/ui/DecisionImages.d.ts +16 -0
- package/ui/DecisionImages.js +46 -0
- package/ui/EmbeddingDocuments.d.ts +78 -0
- package/ui/EmbeddingDocuments.js +278 -0
- package/ui/EmbeddingQuery.d.ts +27 -0
- package/ui/EmbeddingQuery.js +95 -0
- package/ui/EmbeddingResults.d.ts +41 -0
- package/ui/EmbeddingResults.js +165 -0
- package/ui/EmbeddingSpace.d.ts +26 -0
- package/ui/EmbeddingSpace.js +106 -0
- package/ui/LiveCaptionsPanel.d.ts +22 -0
- package/ui/LiveCaptionsPanel.js +27 -0
- package/ui/LiveRunner.d.ts +6 -4
- package/ui/LiveRunner.js +42 -55
- package/ui/PaneState.d.ts +33 -0
- package/ui/PaneState.js +50 -0
- package/ui/SchemaFields.d.ts +1 -1
- package/ui/SchemaFields.js +54 -26
- package/ui/ShapedData.js +73 -28
- package/ui/SidePanel.d.ts +74 -5
- package/ui/SidePanel.js +82 -9
- package/ui/StateFields.d.ts +10 -2
- package/ui/StateFields.js +56 -13
- package/ui/TheoremChat.d.ts +65 -14
- package/ui/TheoremChat.js +66 -78
- package/ui/TheoremDecision.d.ts +12 -20
- package/ui/TheoremDecision.js +78 -35
- package/ui/TheoremEmbedding.d.ts +27 -0
- package/ui/TheoremEmbedding.js +122 -0
- package/ui/TheoremHost.d.ts +22 -5
- package/ui/TheoremHost.js +127 -37
- package/ui/ToolGateCard.d.ts +5 -1
- package/ui/ToolGateCard.js +30 -23
- package/ui/ToolResult.js +12 -10
- package/ui/ToolResultCharts.js +29 -12
- package/ui/TraceCharts.d.ts +2 -2
- package/ui/TraceCharts.js +48 -17
- package/ui/TraceGuardrails.d.ts +3 -2
- package/ui/TraceGuardrails.js +24 -6
- package/ui/TraceInspector.d.ts +11 -1
- package/ui/TraceInspector.js +45 -28
- package/ui/TraceInspectorPanel.d.ts +31 -9
- package/ui/TraceInspectorPanel.js +77 -39
- package/ui/TraceOverview.d.ts +7 -3
- package/ui/TraceOverview.js +44 -14
- package/ui/TraceSpanDetail.js +24 -22
- package/ui/TraceStory.js +9 -8
- package/ui/TraceValues.js +19 -9
- package/ui/TraceWaterfall.js +12 -9
- package/ui/VoiceNote.js +4 -4
- package/ui/approval-heading.d.ts +4 -0
- package/ui/approval-heading.js +9 -0
- package/ui/arrive.d.ts +18 -0
- package/ui/arrive.js +46 -0
- package/ui/built/theme.css +86 -68
- package/ui/built/theme.css.d.ts +9 -0
- package/ui/built/theorem.d.ts +3 -3
- package/ui/built/theorem.js +34 -5
- package/ui/built/theorem.variants.d.ts +8 -2
- package/ui/code-language.d.ts +12 -0
- package/ui/code-language.js +50 -0
- package/ui/disclosure-motion.js +53 -9
- package/ui/embedding-space-gl.d.ts +37 -0
- package/ui/embedding-space-gl.js +663 -0
- package/ui/embedding-space-view.d.ts +8 -0
- package/ui/embedding-space-view.js +8 -0
- package/ui/glide.d.ts +7 -0
- package/ui/glide.js +44 -0
- package/ui/icons.js +2 -1
- package/ui/index.d.ts +20 -9
- package/ui/index.js +20 -10
- package/ui/labels-provider.js +5 -2
- package/ui/labels.d.ts +374 -85
- package/ui/labels.js +1028 -188
- package/ui/markdown-plugins.d.ts +9 -0
- package/ui/markdown-plugins.js +13 -0
- package/ui/mcp-icon.d.ts +6 -0
- package/ui/mcp-icon.js +8 -0
- package/ui/row-keys.d.ts +19 -0
- package/ui/row-keys.js +33 -0
- package/ui/theme.d.ts +1 -1
- package/ui/theme.js +2 -2
- package/ui/theorem-theme.js +32 -10
- package/ui/trace-palette.d.ts +2 -16
- package/ui/trace-palette.js +2 -16
- package/ui/trace-patterns.d.ts +11 -3
- package/ui/trace-patterns.js +16 -3
- package/ui/transcript-copy-text.js +1 -0
- package/ui/wall-columns.d.ts +8 -0
- package/ui/wall-columns.js +19 -0
- package/client/walk-away.d.ts +0 -34
- package/client/walk-away.js +0 -48
- package/server/walk-away.d.ts +0 -22
- package/server/walk-away.js +0 -41
package/server/handler.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withPublicLabels } from '@theoremjs/agents/guardrails';
|
|
2
|
+
import { createMemoryPageToolBroker, pageToolResultSchema, sessionPageDispatch, } from "./page-dispatch.js";
|
|
1
3
|
/**
|
|
2
4
|
* One Web-standard handler that serves a Theorem chat UI for a single profile.
|
|
3
5
|
*
|
|
@@ -9,36 +11,47 @@
|
|
|
9
11
|
* waits on gates walks away from them first (`abandon`)
|
|
10
12
|
* - `POST <base>/invoke` → NDJSON events for the user's answer to a paused tool call
|
|
11
13
|
* - `POST <base>/steer` → inject messages into a running turn
|
|
14
|
+
* - `POST <base>/conversation` → the transcript and the paused reply, to draw
|
|
15
|
+
* - `POST <base>/rewind` → `{ conversation, view }` the conversation cut after one block, and its new ref
|
|
12
16
|
*
|
|
13
17
|
* `createTheoremHostHandler` serves a host profile the same way, with no model:
|
|
14
18
|
*
|
|
15
19
|
* - `GET <base>` → `{ interface }` the tools the page may call, with their schemas
|
|
16
20
|
* - `POST <base>/call` → NDJSON events for one call of one of those tools
|
|
17
21
|
* - `POST <base>/invoke` → NDJSON events for the user's answer to a paused call
|
|
22
|
+
* - `POST <base>/page-result` → grant-correlated browser result receipt
|
|
18
23
|
*
|
|
19
|
-
* Trust boundary: the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* Trust boundary: the host owns the conversation and every decision that grants
|
|
25
|
+
* authority. Two stores keep them. The `conversationStore` owns the transcript,
|
|
26
|
+
* the history the model reads and the paused reply; the browser sends only what
|
|
27
|
+
* the person did now, with the opaque ref the store issued. The session store
|
|
28
|
+
* owns which paused calls may run (and with what input) and the tool
|
|
29
|
+
* permissions — request bodies can't grant any of them. A profile whose `providerContinuation.history` is
|
|
30
|
+
* `'stateless'` keeps no conversation, and takes no store.
|
|
23
31
|
* Tool credentials are held on the server too: a key the user types at a gate
|
|
24
32
|
* is sent once and saved, and no credential is ever sent back to the browser.
|
|
25
33
|
*
|
|
26
34
|
* @module
|
|
27
35
|
*/
|
|
28
|
-
import {
|
|
29
|
-
import { caughtStatus, forClient, HTTP_METHOD, } from '@theoremjs/agents/host';
|
|
30
|
-
import {
|
|
36
|
+
import { authorizeToolReplay, compactHistory, defaultKernelScope, defineProfile, invokeTool, jsonObjectSchema, labelText, registerProfile, runTurn, TheoremError, toolInvocationHash, } from '@theoremjs/agents';
|
|
37
|
+
import { caughtStatus, clientError, forClient, HTTP_METHOD, } from '@theoremjs/agents/host';
|
|
38
|
+
import { clientProfileInterface, gatedToolsFromEvents, interfaceFromProfile, promotedToolIdsFromEvents, toolSnapshotFromEvents, } from '@theoremjs/agents/interface';
|
|
39
|
+
import { answerGatedCall, resolveGateTtlMs, } from '@theoremjs/agents/kernel';
|
|
31
40
|
import { hostInterface, theoremHostCallRequestSchema, } from "../client/host-transport.js";
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
41
|
+
import { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremRewindRequestSchema, theoremSteerRequestSchema, theoremTurnRequestSchema, } from "../client/transport.js";
|
|
42
|
+
import { answerReply, conversationView, messageReply, openConversation, postedMessage, readerConversationHost, requiredOperationId, rewindConversation, statelessRefusal, waitingCalls, } from "./conversation.js";
|
|
43
|
+
import { unknownConversation } from "./conversation-store.js";
|
|
34
44
|
import { createMemoryCredentialStore, } from "./credential-store.js";
|
|
45
|
+
import { sessionStateStore } from "./session-authority.js";
|
|
46
|
+
import { theoremSession } from "./session-identity.js";
|
|
47
|
+
export { theoremSession, theoremSessionId } from "./session-identity.js";
|
|
48
|
+
import { jsonResponse } from "./json-response.js";
|
|
49
|
+
import { boundedRequestJson, requestByteLimit } from "./request-body.js";
|
|
35
50
|
import { checkRequest } from "./request-check.js";
|
|
36
|
-
import {
|
|
51
|
+
import { resolveSessionLimits } from "./session-limits.js";
|
|
52
|
+
import { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, } from "./session-store.js";
|
|
37
53
|
import { createMemorySteerInbox, steerStage, steerUnitOf } from "./steer-inbox.js";
|
|
38
|
-
import {
|
|
39
|
-
import { jsonResponse } from "./json-response.js";
|
|
40
|
-
const SESSION_COOKIE = 'theorem_session';
|
|
41
|
-
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
|
|
54
|
+
import { sameAcceptedAnswer, sessionToolCoordinator } from "./tool-operations.js";
|
|
42
55
|
const NDJSON_HEADERS = {
|
|
43
56
|
'content-type': 'application/x-ndjson; charset=utf-8',
|
|
44
57
|
'cache-control': 'no-store',
|
|
@@ -48,82 +61,41 @@ function withSessionCookie(response, session) {
|
|
|
48
61
|
response.headers.append('set-cookie', session.setCookie);
|
|
49
62
|
return response;
|
|
50
63
|
}
|
|
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
64
|
/** 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.
|
|
65
|
+
export async function readBody(request, schema, maxBytes) {
|
|
66
|
+
// why: JSON-only POSTs can't be sent by a cross-site form, and force a CORS preflight.
|
|
59
67
|
const type = request.headers.get('content-type') ?? '';
|
|
60
68
|
if (!type.toLowerCase().startsWith('application/json')) {
|
|
61
|
-
//
|
|
69
|
+
// labels-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
62
70
|
throw new TheoremError('request', 'Content-Type must be application/json');
|
|
63
71
|
}
|
|
64
72
|
let raw;
|
|
65
73
|
try {
|
|
66
|
-
raw = await request
|
|
74
|
+
raw = await boundedRequestJson(request, requestByteLimit(maxBytes));
|
|
67
75
|
}
|
|
68
|
-
catch (
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof TheoremError)
|
|
78
|
+
throw error;
|
|
79
|
+
// invariant: A JSON parser's message can contain request values and must not enter diagnostics.
|
|
80
|
+
throw new TheoremError('request', labelText('error.request'), {
|
|
81
|
+
cause: { code: 'request_json' },
|
|
82
|
+
});
|
|
71
83
|
}
|
|
72
84
|
return checkRequest(schema, raw, 'request body');
|
|
73
85
|
}
|
|
74
|
-
/**
|
|
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'. */
|
|
86
|
+
/** Last path segment after the mount point: '', a chat's route, or a host's 'call'. */
|
|
92
87
|
function routeOf(request) {
|
|
93
88
|
const segments = new URL(request.url).pathname.split('/').filter(Boolean);
|
|
94
89
|
const last = segments.at(-1) ?? '';
|
|
95
|
-
return last === 'turn' ||
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
};
|
|
90
|
+
return last === 'turn' ||
|
|
91
|
+
last === 'invoke' ||
|
|
92
|
+
last === 'steer' ||
|
|
93
|
+
last === 'conversation' ||
|
|
94
|
+
last === 'rewind' ||
|
|
95
|
+
last === 'call' ||
|
|
96
|
+
last === 'page-result'
|
|
97
|
+
? last
|
|
98
|
+
: '';
|
|
127
99
|
}
|
|
128
100
|
function lockedStore(store, loaded) {
|
|
129
101
|
const locks = new Map();
|
|
@@ -147,59 +119,64 @@ function lockedStore(store, loaded) {
|
|
|
147
119
|
},
|
|
148
120
|
};
|
|
149
121
|
}
|
|
150
|
-
/**
|
|
151
|
-
function
|
|
152
|
-
return
|
|
153
|
-
...state,
|
|
154
|
-
gates: pruneGates(state.gates, Date.now(), gateTtlMs),
|
|
155
|
-
settled: pruneGates(state.settled, Date.now(), gateTtlMs),
|
|
156
|
-
}));
|
|
122
|
+
/** The profile as the server reads a message against it, its tools as registered now. */
|
|
123
|
+
function chatInterface(chat) {
|
|
124
|
+
return interfaceFromProfile(chat.profile, defaultKernelScope.tools);
|
|
157
125
|
}
|
|
158
126
|
async function sessionOf(ctx, request) {
|
|
159
127
|
if (!ctx.options.session)
|
|
160
|
-
return
|
|
128
|
+
return theoremSession(request);
|
|
161
129
|
const id = (await ctx.options.session(request))?.trim();
|
|
162
130
|
if (!id) {
|
|
163
|
-
//
|
|
131
|
+
// labels-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
164
132
|
throw new TheoremError('auth', 'the session resolver refused the request', {
|
|
165
133
|
copy: { key: 'session.sign_in' },
|
|
166
134
|
});
|
|
167
135
|
}
|
|
168
136
|
return { id };
|
|
169
137
|
}
|
|
170
|
-
/** Reports the error to the host, then words it for
|
|
171
|
-
function
|
|
138
|
+
/** Reports the error to the host, then words it for whoever reads this host's replies. */
|
|
139
|
+
function caughtError(ctx, err, request) {
|
|
172
140
|
ctx.options.onError?.(err, { request });
|
|
173
|
-
return
|
|
141
|
+
return clientError(err, ctx.profile.labels, ctx.options.clientEvents);
|
|
174
142
|
}
|
|
175
143
|
function eventStream(ctx, request, source) {
|
|
176
144
|
const encoder = new TextEncoder();
|
|
177
145
|
const line = (value) => encoder.encode(`${JSON.stringify(value)}\n`);
|
|
146
|
+
// invariant: The source runs to its end after the browser goes, so a stopped reply still reaches its save.
|
|
147
|
+
let cancelled = false;
|
|
178
148
|
const stream = new ReadableStream({
|
|
179
149
|
async start(controller) {
|
|
150
|
+
const send = (value) => {
|
|
151
|
+
if (!cancelled)
|
|
152
|
+
controller.enqueue(line(value));
|
|
153
|
+
};
|
|
180
154
|
try {
|
|
181
155
|
for await (const event of source()) {
|
|
182
|
-
|
|
156
|
+
send(event.type === 'conversation' ? event : forClient(event, ctx.options.clientEvents));
|
|
183
157
|
}
|
|
184
158
|
}
|
|
185
159
|
catch (err) {
|
|
186
|
-
if (!request.signal.aborted)
|
|
187
|
-
|
|
188
|
-
}
|
|
160
|
+
if (!request.signal.aborted)
|
|
161
|
+
send({ type: 'error', ...caughtError(ctx, err, request) });
|
|
189
162
|
}
|
|
190
|
-
|
|
163
|
+
if (!cancelled)
|
|
164
|
+
controller.close();
|
|
165
|
+
},
|
|
166
|
+
cancel() {
|
|
167
|
+
cancelled = true;
|
|
191
168
|
},
|
|
192
169
|
});
|
|
193
170
|
return new Response(stream, { headers: NDJSON_HEADERS });
|
|
194
171
|
}
|
|
195
172
|
function providerFor(ctx, request, model) {
|
|
196
173
|
const { provider } = ctx.options;
|
|
197
|
-
//
|
|
174
|
+
// labels-exempt: builder config error; the user reads error.config
|
|
198
175
|
if (!provider)
|
|
199
176
|
throw new TheoremError('config', `profile ${ctx.profile.id} has no provider`);
|
|
200
177
|
if (typeof provider === 'function')
|
|
201
178
|
return provider({ request, model });
|
|
202
|
-
return
|
|
179
|
+
return provider;
|
|
203
180
|
}
|
|
204
181
|
/** Steer inboxes are scoped to the session, so a turn id alone can't reach another user's turn. */
|
|
205
182
|
function inboxKey(sessionId, turnId) {
|
|
@@ -210,9 +187,16 @@ function pendingGatesFrom(events, context) {
|
|
|
210
187
|
return gatedToolsFromEvents(events).map((gated) => ({
|
|
211
188
|
callId: gated.callId,
|
|
212
189
|
gate: {
|
|
190
|
+
profileId: context.profileId,
|
|
191
|
+
operationId: requiredOperationId(gated),
|
|
192
|
+
callId: gated.callId,
|
|
213
193
|
name: gated.name,
|
|
214
194
|
arguments: gated.arguments,
|
|
215
|
-
gate: {
|
|
195
|
+
gate: {
|
|
196
|
+
kind: gated.gateKind,
|
|
197
|
+
permission: gated.permission,
|
|
198
|
+
...(gated.auth ? { auth: gated.auth } : {}),
|
|
199
|
+
},
|
|
216
200
|
snapshot: toolSnapshotFromEvents(events),
|
|
217
201
|
promoted: [...new Set([...(context.promoted ?? []), ...promotedToolIdsFromEvents(events)])],
|
|
218
202
|
turnInput: context.turnInput,
|
|
@@ -221,26 +205,31 @@ function pendingGatesFrom(events, context) {
|
|
|
221
205
|
},
|
|
222
206
|
}));
|
|
223
207
|
}
|
|
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
|
-
*/
|
|
208
|
+
/** When the stream paused on gates, record the exact calls the user may now approve. */
|
|
228
209
|
async function recordOutcome(sessions, sessionId, events, context) {
|
|
229
|
-
const interactionIds = events.flatMap((event) => event.type === 'done' && event.interactionId ? [event.interactionId] : []);
|
|
230
210
|
const pending = pendingGatesFrom(events, context);
|
|
231
|
-
if (!
|
|
211
|
+
if (!pending.length)
|
|
232
212
|
return;
|
|
233
213
|
await sessions.mutate(sessionId, (state) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
214
|
+
for (const { gate } of pending) {
|
|
215
|
+
const operation = state.operations[gate.operationId];
|
|
216
|
+
if (operation && operation.status !== 'ready')
|
|
217
|
+
throw new TheoremError('unavailable', labelText('error.unavailable'));
|
|
218
|
+
delete state.operations[gate.operationId];
|
|
219
|
+
Object.defineProperty(state.gates, gate.operationId, {
|
|
220
|
+
value: gate,
|
|
221
|
+
writable: true,
|
|
222
|
+
enumerable: true,
|
|
223
|
+
configurable: true,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
237
226
|
});
|
|
238
227
|
}
|
|
239
|
-
async function* recorded(sessions, sessionId, events, context) {
|
|
240
|
-
const seen = [];
|
|
228
|
+
async function* recorded(sessions, sessionId, events, context, seed = []) {
|
|
229
|
+
const seen = [...seed];
|
|
241
230
|
for await (const event of events) {
|
|
242
231
|
seen.push(event);
|
|
243
|
-
if (event.type === 'done')
|
|
232
|
+
if (event.type === 'done' || event.type === 'provider_checkpoint')
|
|
244
233
|
await recordOutcome(sessions, sessionId, seen, context);
|
|
245
234
|
yield event;
|
|
246
235
|
}
|
|
@@ -268,7 +257,7 @@ async function withAuthorizationUrl(ctx, request, sessionId, event) {
|
|
|
268
257
|
return event;
|
|
269
258
|
const { tool } = event;
|
|
270
259
|
const gate = tool.gate.kind === 'auth' ? tool.gate : undefined;
|
|
271
|
-
if (
|
|
260
|
+
if (gate?.authChallenge.authType !== 'oauth2' || !ctx.options.authorizationUrl)
|
|
272
261
|
return event;
|
|
273
262
|
const challenge = gate.authChallenge;
|
|
274
263
|
const authorizationUrl = await ctx.options.authorizationUrl(challenge, { request, sessionId });
|
|
@@ -282,78 +271,106 @@ async function* withAuthorizationUrls(ctx, request, sessionId, events) {
|
|
|
282
271
|
for await (const event of events)
|
|
283
272
|
yield await withAuthorizationUrl(ctx, request, sessionId, event);
|
|
284
273
|
}
|
|
285
|
-
function answerPendingGate(state, request) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
274
|
+
function answerPendingGate(state, request, profileId) {
|
|
275
|
+
const operation = Object.hasOwn(state.operations, request.operationId)
|
|
276
|
+
? state.operations[request.operationId]
|
|
277
|
+
: undefined;
|
|
278
|
+
if (operation?.status === 'ready' &&
|
|
279
|
+
operation.profileId === profileId &&
|
|
280
|
+
operation.pending.callId === request.callId) {
|
|
281
|
+
const accepted = answerGatedCall(request, {
|
|
282
|
+
name: operation.pending.name,
|
|
283
|
+
arguments: operation.pending.arguments,
|
|
284
|
+
permission: operation.pending.gate.permission,
|
|
285
|
+
gateKind: operation.pending.gate.kind,
|
|
286
|
+
...(operation.pending.gate.kind === 'auth' && operation.pending.gate.auth
|
|
287
|
+
? { auth: operation.pending.gate.auth }
|
|
288
|
+
: {}),
|
|
289
|
+
}, operation.answer.sessionPermissions);
|
|
290
|
+
const { typed: _typed, ...plain } = accepted;
|
|
291
|
+
if (!sameAcceptedAnswer(plain, operation.answer))
|
|
292
|
+
throw new TheoremError('request', labelText('error.request'));
|
|
293
|
+
return {
|
|
294
|
+
callId: request.callId,
|
|
295
|
+
pending: operation.pending,
|
|
296
|
+
answer: { ...operation.answer, ...(accepted.typed ? { typed: accepted.typed } : {}) },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const pending = Object.hasOwn(state.gates, request.operationId)
|
|
300
|
+
? state.gates[request.operationId]
|
|
301
|
+
: undefined;
|
|
302
|
+
if (!pending || pending.profileId !== profileId || pending.callId !== request.callId) {
|
|
303
|
+
// labels-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
289
304
|
throw new TheoremError('request', `gate ${request.callId} is not pending`, {
|
|
290
305
|
copy: { key: 'session.gate_expired' },
|
|
291
306
|
});
|
|
292
307
|
}
|
|
293
|
-
// A refused answer (a refused secret, an edit without an approval) throws here and leaves the gate pending.
|
|
308
|
+
// why: A refused answer (a refused secret, an edit without an approval) throws here and leaves the gate pending.
|
|
294
309
|
const answer = answerGatedCall(request, {
|
|
295
310
|
name: pending.name,
|
|
296
311
|
arguments: pending.arguments,
|
|
297
312
|
permission: pending.gate.permission,
|
|
313
|
+
gateKind: pending.gate.kind,
|
|
298
314
|
...(pending.gate.kind === 'auth' && pending.gate.auth ? { auth: pending.gate.auth } : {}),
|
|
299
315
|
}, state.permissions);
|
|
300
|
-
delete state.gates[request.
|
|
316
|
+
delete state.gates[request.operationId];
|
|
317
|
+
const { typed: _typed, ...accepted } = answer;
|
|
318
|
+
state.operations[request.operationId] = {
|
|
319
|
+
status: 'ready',
|
|
320
|
+
profileId,
|
|
321
|
+
pending,
|
|
322
|
+
answer: accepted,
|
|
323
|
+
readyAt: Date.now(),
|
|
324
|
+
};
|
|
301
325
|
state.permissions = answer.sessionPermissions;
|
|
302
326
|
return { callId: request.callId, pending, answer };
|
|
303
327
|
}
|
|
304
|
-
function walkedCallOf(state,
|
|
305
|
-
const settled = state.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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)));
|
|
328
|
+
function walkedCallOf(state, reference, profileId) {
|
|
329
|
+
const settled = Object.hasOwn(state.operations, reference.operationId)
|
|
330
|
+
? state.operations[reference.operationId]
|
|
331
|
+
: undefined;
|
|
332
|
+
return settled?.status === 'settled' &&
|
|
333
|
+
settled.callId === reference.callId &&
|
|
334
|
+
settled.profileId === profileId
|
|
335
|
+
? { callId: reference.callId, settled }
|
|
336
|
+
: answerPendingGate(state, { ...reference, decision: 'abandon' }, profileId);
|
|
317
337
|
}
|
|
318
338
|
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
339
|
+
* Take the calls a message or a rewind walks away from out of the session,
|
|
340
|
+
* answered `abandon`: every one of them, or none when one is neither waiting
|
|
341
|
+
* nor settled. Each comes back with the run that settles it.
|
|
322
342
|
*/
|
|
323
|
-
async function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
343
|
+
async function takeWalkedAway(ctx, request, session, abandon) {
|
|
344
|
+
// why: A throw leaves the session unsaved, so no call is taken unless all are.
|
|
345
|
+
const calls = await ctx.sessions.mutate(session.id, (state) => abandon.map((reference) => ({
|
|
346
|
+
reference,
|
|
347
|
+
call: walkedCallOf(state, reference, ctx.profile.id),
|
|
348
|
+
})));
|
|
349
|
+
return calls.map(({ reference, call }) => ({
|
|
350
|
+
...reference,
|
|
351
|
+
events: 'settled' in call
|
|
352
|
+
? replayOperation(ctx, request, call.settled)
|
|
353
|
+
: settleAnswered(ctx, request, session, call),
|
|
354
|
+
}));
|
|
334
355
|
}
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
});
|
|
356
|
+
async function* settleAnswered(ctx, request, session, answered) {
|
|
357
|
+
yield* invokeAnswered(ctx, request, session.id, answered);
|
|
349
358
|
}
|
|
350
|
-
function invokeAnswered(ctx, request, sessionId, { callId, pending, answer }) {
|
|
351
|
-
|
|
359
|
+
async function* invokeAnswered(ctx, request, sessionId, { callId, pending, answer }) {
|
|
360
|
+
const host = await ctx.options.host?.(request);
|
|
361
|
+
yield* invokeTool({
|
|
352
362
|
profile: ctx.profile.id,
|
|
363
|
+
execution: sessionToolCoordinator(ctx.sessions, sessionId),
|
|
364
|
+
pageExecution: sessionPageDispatch(ctx.sessions, sessionId, ctx.pageToolBroker).execution,
|
|
353
365
|
name: pending.name,
|
|
354
366
|
callId,
|
|
367
|
+
operationId: pending.operationId,
|
|
355
368
|
input: answer.input,
|
|
356
369
|
resume: answer.resume,
|
|
370
|
+
// why: An approval that names no answer is the page saying it has none, so the call is not held again.
|
|
371
|
+
...(pending.gate.kind === 'page' && answer.resume.granted
|
|
372
|
+
? { page: answer.page ?? { unanswered: true } }
|
|
373
|
+
: {}),
|
|
357
374
|
sessionPermissions: answer.sessionPermissions,
|
|
358
375
|
credentials: sessionCredentials(ctx, sessionId),
|
|
359
376
|
turnInput: pending.turnInput,
|
|
@@ -361,58 +378,40 @@ function invokeAnswered(ctx, request, sessionId, { callId, pending, answer }) {
|
|
|
361
378
|
promoted: pending.promoted,
|
|
362
379
|
model: pending.model,
|
|
363
380
|
signal: request.signal,
|
|
364
|
-
host
|
|
381
|
+
host,
|
|
365
382
|
});
|
|
366
383
|
}
|
|
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
384
|
/** A settled call's answer, streamed again. */
|
|
377
|
-
async function* once(event) {
|
|
378
|
-
yield event;
|
|
385
|
+
async function* once(event, labels) {
|
|
386
|
+
yield withPublicLabels(event, labels);
|
|
379
387
|
}
|
|
380
|
-
|
|
381
|
-
|
|
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;
|
|
388
|
+
/** One model run of a chat's reply, its gates recorded in the session as they pause. */
|
|
389
|
+
async function* chatTurn(ctx, request, session, turn, turnId) {
|
|
391
390
|
const state = await ctx.sessions.read(session.id);
|
|
392
|
-
const provider = await providerFor(ctx, request,
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
? body.previousInteractionId
|
|
396
|
-
: undefined;
|
|
397
|
-
const key = body.turnId ? inboxKey(session.id, body.turnId) : undefined;
|
|
391
|
+
const provider = await providerFor(ctx, request, turn.model);
|
|
392
|
+
const host = await ctx.options.host?.(request);
|
|
393
|
+
const key = turnId ? inboxKey(session.id, turnId) : undefined;
|
|
398
394
|
if (key)
|
|
399
395
|
await ctx.inbox.open(key);
|
|
400
396
|
try {
|
|
401
397
|
const events = runTurn({
|
|
402
398
|
profile: ctx.profile.id,
|
|
403
|
-
|
|
404
|
-
|
|
399
|
+
execution: sessionToolCoordinator(ctx.sessions, session.id),
|
|
400
|
+
pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker).execution,
|
|
401
|
+
input: turn.input,
|
|
402
|
+
...(turn.providerState ? { providerState: turn.providerState } : {}),
|
|
405
403
|
sessionPermissions: state.permissions,
|
|
406
404
|
credentials: sessionCredentials(ctx, session.id),
|
|
407
405
|
signal: request.signal,
|
|
408
|
-
host
|
|
409
|
-
...(
|
|
410
|
-
...(
|
|
406
|
+
host,
|
|
407
|
+
...(turn.model ? { model: turn.model } : {}),
|
|
408
|
+
...(turn.effort ? { effort: turn.effort } : {}),
|
|
411
409
|
...(key ? { onStage: steerStage(ctx.inbox, key) } : {}),
|
|
412
410
|
}, provider);
|
|
413
411
|
yield* recorded(ctx.sessions, session.id, withAuthorizationUrls(ctx, request, session.id, events), {
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
profileId: ctx.profile.id,
|
|
413
|
+
turnInput: turn.input,
|
|
414
|
+
model: turn.model,
|
|
416
415
|
});
|
|
417
416
|
}
|
|
418
417
|
finally {
|
|
@@ -420,27 +419,122 @@ async function* turnEvents(ctx, request, session, body, walked) {
|
|
|
420
419
|
await ctx.inbox.close(key);
|
|
421
420
|
}
|
|
422
421
|
}
|
|
422
|
+
/** How this request runs the model for a reply: its generation, and the request's abort. */
|
|
423
|
+
function chatRunner(ctx, request, session, generation) {
|
|
424
|
+
return {
|
|
425
|
+
run: (turn) => chatTurn(ctx, request, session, turn, generation.turnId),
|
|
426
|
+
async compact(history, tokens, model) {
|
|
427
|
+
const provider = await providerFor(ctx, request, model);
|
|
428
|
+
const compacted = await compactHistory({
|
|
429
|
+
profile: ctx.profile.id,
|
|
430
|
+
history,
|
|
431
|
+
tokens,
|
|
432
|
+
signal: request.signal,
|
|
433
|
+
...(model ? { model } : {}),
|
|
434
|
+
}, provider);
|
|
435
|
+
return compacted?.history;
|
|
436
|
+
},
|
|
437
|
+
...(generation.model ? { model: generation.model } : {}),
|
|
438
|
+
...(generation.effort ? { effort: generation.effort } : {}),
|
|
439
|
+
signal: request.signal,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/** The chat's conversation as this session's runs read and write it; undefined when the profile keeps none. */
|
|
443
|
+
function conversationHost(ctx, session) {
|
|
444
|
+
const store = ctx.chat?.conversations;
|
|
445
|
+
if (!ctx.chat || !store)
|
|
446
|
+
return undefined;
|
|
447
|
+
return readerConversationHost({
|
|
448
|
+
sessionId: session.id,
|
|
449
|
+
store,
|
|
450
|
+
profile: { outputs: ctx.chat.profile.outputs, labels: ctx.profile.labels },
|
|
451
|
+
clientEvents: ctx.options.clientEvents,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/** The person's message: a new reply, run against the conversation the host keeps. */
|
|
455
|
+
async function turn(ctx, chat, request, session) {
|
|
456
|
+
const body = await readBody(request, theoremTurnRequestSchema, ctx.options.maxRequestBytes);
|
|
457
|
+
const message = postedMessage(chatInterface(chat), body.input, await ctx.options.context?.(request));
|
|
458
|
+
const runner = chatRunner(ctx, request, session, body);
|
|
459
|
+
const host = conversationHost(ctx, session);
|
|
460
|
+
if (!host) {
|
|
461
|
+
if (body.conversation !== undefined)
|
|
462
|
+
throw statelessRefusal('continue');
|
|
463
|
+
// labels-exempt: internal diagnostic; the user reads error.request
|
|
464
|
+
if (body.abandon)
|
|
465
|
+
throw statelessRefusal('walk away from');
|
|
466
|
+
return eventStream(ctx, request, () => runner.run({ input: message.input, model: body.model, effort: body.effort }));
|
|
467
|
+
}
|
|
468
|
+
const opened = await openConversation(host, body.conversation, {
|
|
469
|
+
kind: 'message',
|
|
470
|
+
...(body.abandon ? { abandon: body.abandon } : {}),
|
|
471
|
+
});
|
|
472
|
+
// why: Take the walked-away calls before streaming, so a stale one is a reply status, not a stream error.
|
|
473
|
+
const walkAway = body.abandon
|
|
474
|
+
? await takeWalkedAway(ctx, request, session, body.abandon)
|
|
475
|
+
: undefined;
|
|
476
|
+
return eventStream(ctx, request, () => messageReply({ host, opened, message, ...(walkAway ? { walkAway } : {}), runner }));
|
|
477
|
+
}
|
|
478
|
+
/** The conversation as the browser draws it. */
|
|
479
|
+
async function conversation(ctx, request, session) {
|
|
480
|
+
const body = await readBody(request, theoremConversationRequestSchema, ctx.options.maxRequestBytes);
|
|
481
|
+
const host = conversationHost(ctx, session);
|
|
482
|
+
if (!host)
|
|
483
|
+
throw statelessRefusal('read');
|
|
484
|
+
const opened = await openConversation(host, body.conversation, { kind: 'read' });
|
|
485
|
+
return jsonResponse(200, conversationView(host, opened.conversation));
|
|
486
|
+
}
|
|
487
|
+
/** The conversation cut after one block; a reply that waits on gates is walked away from first. */
|
|
488
|
+
async function rewind(ctx, request, session) {
|
|
489
|
+
const body = await readBody(request, theoremRewindRequestSchema, ctx.options.maxRequestBytes);
|
|
490
|
+
const host = conversationHost(ctx, session);
|
|
491
|
+
if (!host)
|
|
492
|
+
throw statelessRefusal('rewind');
|
|
493
|
+
const opened = await openConversation(host, body.conversation, { kind: 'rewind', at: body.at });
|
|
494
|
+
const waiting = waitingCalls(opened.conversation);
|
|
495
|
+
const walkAway = waiting.length
|
|
496
|
+
? await takeWalkedAway(ctx, request, session, waiting)
|
|
497
|
+
: undefined;
|
|
498
|
+
const next = await rewindConversation({
|
|
499
|
+
host,
|
|
500
|
+
opened,
|
|
501
|
+
at: body.at,
|
|
502
|
+
...(walkAway ? { walkAway } : {}),
|
|
503
|
+
});
|
|
504
|
+
if (next.ref === null)
|
|
505
|
+
throw new TheoremError('internal', labelText('error.internal'));
|
|
506
|
+
const rewound = {
|
|
507
|
+
conversation: next.ref,
|
|
508
|
+
view: conversationView(host, next.conversation),
|
|
509
|
+
};
|
|
510
|
+
return jsonResponse(200, rewound);
|
|
511
|
+
}
|
|
423
512
|
async function* invokeEvents(ctx, request, session, answered) {
|
|
424
513
|
const { pending, answer } = answered;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
514
|
+
if (answer.typed)
|
|
515
|
+
await saveCredential(ctx, session.id, answer.typed.slot, answer.typed.credential);
|
|
516
|
+
const events = settleAnswered(ctx, request, session, answered);
|
|
517
|
+
yield* recorded(ctx.sessions, session.id, withAuthorizationUrls(ctx, request, session.id, events), {
|
|
518
|
+
profileId: ctx.profile.id,
|
|
519
|
+
turnInput: pending.turnInput,
|
|
520
|
+
model: pending.model,
|
|
521
|
+
promoted: pending.promoted,
|
|
522
|
+
}, [
|
|
523
|
+
{
|
|
524
|
+
type: 'tool',
|
|
525
|
+
tool: {
|
|
526
|
+
name: pending.name,
|
|
527
|
+
callId: pending.callId,
|
|
528
|
+
operationId: pending.operationId,
|
|
529
|
+
arguments: jsonObjectSchema.parse(answer.input),
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
]);
|
|
439
533
|
}
|
|
440
534
|
async function steer(ctx, request, session) {
|
|
441
|
-
const body = await readBody(request, theoremSteerRequestSchema);
|
|
535
|
+
const body = await readBody(request, theoremSteerRequestSchema, ctx.options.maxRequestBytes);
|
|
442
536
|
if (!(await ctx.inbox.enqueue(inboxKey(session.id, body.turnId), steerUnitOf(body)))) {
|
|
443
|
-
//
|
|
537
|
+
// labels-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
444
538
|
throw new TheoremError('request', `turn ${body.turnId} is not running`, {
|
|
445
539
|
copy: { key: 'session.turn_ended' },
|
|
446
540
|
});
|
|
@@ -450,35 +544,111 @@ async function steer(ctx, request, session) {
|
|
|
450
544
|
/** 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
545
|
async function* callEvents(ctx, request, session, body) {
|
|
452
546
|
const state = await ctx.sessions.read(session.id);
|
|
547
|
+
const host = await ctx.options.host?.(request);
|
|
453
548
|
const events = invokeTool({
|
|
454
549
|
profile: ctx.profile.id,
|
|
550
|
+
execution: sessionToolCoordinator(ctx.sessions, session.id),
|
|
551
|
+
pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker).execution,
|
|
455
552
|
name: body.name,
|
|
456
553
|
input: body.input,
|
|
457
554
|
sessionPermissions: state.permissions,
|
|
458
555
|
credentials: sessionCredentials(ctx, session.id),
|
|
459
556
|
signal: request.signal,
|
|
460
|
-
host
|
|
557
|
+
host,
|
|
461
558
|
});
|
|
462
559
|
yield* recorded(ctx.sessions, session.id, withAuthorizationUrls(ctx, request, session.id, events), {
|
|
560
|
+
profileId: ctx.profile.id,
|
|
463
561
|
turnInput: {},
|
|
464
562
|
});
|
|
465
563
|
}
|
|
466
564
|
function answerRequest(body) {
|
|
467
|
-
return {
|
|
565
|
+
return {
|
|
566
|
+
callId: body.gateId,
|
|
567
|
+
operationId: body.operationId,
|
|
568
|
+
decision: body.decision,
|
|
569
|
+
input: body.input,
|
|
570
|
+
secret: body.secret,
|
|
571
|
+
page: body.page,
|
|
572
|
+
};
|
|
468
573
|
}
|
|
469
574
|
/** A route this profile doesn't serve. */
|
|
470
575
|
function refused(ctx) {
|
|
471
576
|
return jsonResponse(HTTP_METHOD, {
|
|
472
|
-
error:
|
|
577
|
+
error: labelText('error.request', {}, ctx.profile.labels),
|
|
473
578
|
errorKind: 'request',
|
|
474
579
|
});
|
|
475
580
|
}
|
|
476
|
-
|
|
581
|
+
async function* replayOperation(ctx, request, operation) {
|
|
582
|
+
const tool = defaultKernelScope.tools.get(operation.toolId);
|
|
583
|
+
if (operation.profileId !== ctx.profile.id ||
|
|
584
|
+
!tool ||
|
|
585
|
+
tool.type === 'builtin' ||
|
|
586
|
+
!('tools' in ctx.profile) ||
|
|
587
|
+
!ctx.profile.tools?.allow.includes(operation.toolId) ||
|
|
588
|
+
(ctx.options.authorizeToolReplay &&
|
|
589
|
+
!(await ctx.options.authorizeToolReplay({ request, operation }))))
|
|
590
|
+
throw new TheoremError('blocked', labelText('error.blocked'));
|
|
591
|
+
authorizeToolReplay({
|
|
592
|
+
tools: defaultKernelScope.tools,
|
|
593
|
+
profile: ctx.profile,
|
|
594
|
+
name: operation.toolId,
|
|
595
|
+
resume: undefined,
|
|
596
|
+
outcome: operation.outcome,
|
|
597
|
+
});
|
|
598
|
+
yield* once(operation.outcome.event, ctx.profile.labels);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* The user's answer to a paused call. In a kept conversation the reply goes on
|
|
602
|
+
* in the same stream once no call waits; otherwise the stream is the call's.
|
|
603
|
+
*/
|
|
477
604
|
async function invoke(ctx, request, session) {
|
|
478
|
-
const body = await readBody(request, theoremInvokeRequestSchema);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
605
|
+
const body = await readBody(request, theoremInvokeRequestSchema, ctx.options.maxRequestBytes);
|
|
606
|
+
if (body.page && 'output' in body.page)
|
|
607
|
+
throw new TheoremError('request', labelText('error.request'));
|
|
608
|
+
const host = conversationHost(ctx, session);
|
|
609
|
+
if (!host) {
|
|
610
|
+
if (body.conversation !== undefined)
|
|
611
|
+
throw statelessRefusal('continue');
|
|
612
|
+
return eventStream(ctx, request, await settlingOf(ctx, request, session, body));
|
|
613
|
+
}
|
|
614
|
+
if (body.conversation === undefined)
|
|
615
|
+
throw unknownConversation();
|
|
616
|
+
const call = { callId: body.gateId, operationId: body.operationId };
|
|
617
|
+
const opened = await openConversation(host, body.conversation, { kind: 'answer', call });
|
|
618
|
+
const settling = await settlingOf(ctx, request, session, body);
|
|
619
|
+
const runner = chatRunner(ctx, request, session, body);
|
|
620
|
+
return eventStream(ctx, request, () => answerReply({ host, opened, call, settling: settling(), runner }));
|
|
621
|
+
}
|
|
622
|
+
/** The answered call's events: its saved outcome again, or the call run as answered. */
|
|
623
|
+
async function settlingOf(ctx, request, session, body) {
|
|
624
|
+
const saved = await ctx.sessions.read(session.id);
|
|
625
|
+
const operation = Object.hasOwn(saved.operations, body.operationId)
|
|
626
|
+
? saved.operations[body.operationId]
|
|
627
|
+
: undefined;
|
|
628
|
+
if (operation &&
|
|
629
|
+
operation.status !== 'ready' &&
|
|
630
|
+
operation.callId === body.gateId &&
|
|
631
|
+
operation.profileId === ctx.profile.id &&
|
|
632
|
+
(operation.status === 'running' || operation.status === 'uncertain')) {
|
|
633
|
+
throw new TheoremError('unavailable', labelText('tool.execution_uncertain'), {
|
|
634
|
+
copy: { key: 'tool.execution_uncertain' },
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (operation?.status === 'settled' &&
|
|
638
|
+
operation.profileId === ctx.profile.id &&
|
|
639
|
+
operation.callId === body.gateId) {
|
|
640
|
+
if (body.input !== undefined &&
|
|
641
|
+
(!operation.invocation ||
|
|
642
|
+
(await toolInvocationHash(body.input)) !==
|
|
643
|
+
(await toolInvocationHash(operation.invocation.accepted.input))))
|
|
644
|
+
throw new TheoremError('request', labelText('error.request'), {
|
|
645
|
+
copy: { key: 'error.request' },
|
|
646
|
+
});
|
|
647
|
+
return () => replayOperation(ctx, request, operation);
|
|
648
|
+
}
|
|
649
|
+
// why: Take the answered call before streaming, so a stale answer is a reply status, not a stream error.
|
|
650
|
+
const answered = await ctx.sessions.mutate(session.id, (state) => answerPendingGate(state, answerRequest(body), ctx.profile.id));
|
|
651
|
+
return () => invokeEvents(ctx, request, session, answered);
|
|
482
652
|
}
|
|
483
653
|
async function hostRoute(ctx, request, session) {
|
|
484
654
|
const target = routeOf(request);
|
|
@@ -490,7 +660,7 @@ async function hostRoute(ctx, request, session) {
|
|
|
490
660
|
return jsonResponse(200, { interface: hostInterface(ctx.profile, tool) });
|
|
491
661
|
}
|
|
492
662
|
if (request.method === 'POST' && target === 'call') {
|
|
493
|
-
const body = await readBody(request, theoremHostCallRequestSchema);
|
|
663
|
+
const body = await readBody(request, theoremHostCallRequestSchema, ctx.options.maxRequestBytes);
|
|
494
664
|
return eventStream(ctx, request, () => callEvents(ctx, request, session, body));
|
|
495
665
|
}
|
|
496
666
|
if (request.method === 'POST' && target === 'invoke')
|
|
@@ -498,39 +668,63 @@ async function hostRoute(ctx, request, session) {
|
|
|
498
668
|
return refused(ctx);
|
|
499
669
|
}
|
|
500
670
|
async function route(ctx, request, session) {
|
|
671
|
+
if (request.method === 'POST' && routeOf(request) === 'page-result') {
|
|
672
|
+
const body = await readBody(request, pageToolResultSchema, ctx.options.maxRequestBytes);
|
|
673
|
+
await sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker).answer(body);
|
|
674
|
+
return jsonResponse(200, { accepted: true });
|
|
675
|
+
}
|
|
501
676
|
if (ctx.profile.type === 'host')
|
|
502
677
|
return hostRoute({ ...ctx, profile: ctx.profile }, request, session);
|
|
678
|
+
const { chat } = ctx;
|
|
503
679
|
const target = routeOf(request);
|
|
680
|
+
if (!chat)
|
|
681
|
+
return refused(ctx);
|
|
504
682
|
if (request.method === 'GET' && target === '')
|
|
505
|
-
return jsonResponse(200, {
|
|
683
|
+
return jsonResponse(200, {
|
|
684
|
+
interface: clientProfileInterface(chat.profile, defaultKernelScope.tools),
|
|
685
|
+
});
|
|
506
686
|
if (request.method !== 'POST' || target === '' || target === 'call')
|
|
507
687
|
return refused(ctx);
|
|
508
|
-
if (target === 'turn')
|
|
509
|
-
|
|
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
|
-
}
|
|
688
|
+
if (target === 'turn')
|
|
689
|
+
return turn(ctx, chat, request, session);
|
|
515
690
|
if (target === 'invoke')
|
|
516
691
|
return invoke(ctx, request, session);
|
|
692
|
+
if (target === 'conversation')
|
|
693
|
+
return conversation(ctx, request, session);
|
|
694
|
+
if (target === 'rewind')
|
|
695
|
+
return rewind(ctx, request, session);
|
|
517
696
|
return steer(ctx, request, session);
|
|
518
697
|
}
|
|
519
698
|
export function createTheoremHandler(options) {
|
|
520
699
|
const profile = defineProfile(options.profile);
|
|
521
700
|
if (profile.type === 'decision') {
|
|
522
|
-
//
|
|
701
|
+
// labels-exempt: builder config error at setup; no user sees it
|
|
523
702
|
throw new Error("createTheoremHandler serves turn-based profiles; serve type 'decision' with createTheoremDecisionHandler.");
|
|
524
703
|
}
|
|
525
704
|
if (profile.type === 'host') {
|
|
526
|
-
//
|
|
705
|
+
// labels-exempt: builder config error at setup; no user sees it
|
|
527
706
|
throw new Error("createTheoremHandler serves turn-based profiles; serve type 'host' with createTheoremHostHandler.");
|
|
528
707
|
}
|
|
529
|
-
if (profile.type === 'live') {
|
|
530
|
-
//
|
|
708
|
+
if (profile.type === 'live' || profile.type === 'embedding') {
|
|
709
|
+
// labels-exempt: builder config error at setup; no user sees it
|
|
531
710
|
throw new Error(`createTheoremHandler serves turn-based profiles; got type '${profile.type}'.`);
|
|
532
711
|
}
|
|
533
|
-
|
|
712
|
+
const stateless = profile.providerContinuation?.history === 'stateless';
|
|
713
|
+
if (stateless === (options.conversationStore !== undefined)) {
|
|
714
|
+
throw new TheoremError('config', labelText('error.config'), {
|
|
715
|
+
cause: {
|
|
716
|
+
code: stateless ? 'conversation_store_stateless' : 'conversation_store_missing',
|
|
717
|
+
profile: profile.id,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return serve('createTheoremHandler', profile, options, {
|
|
722
|
+
steerInbox: options.steerInbox,
|
|
723
|
+
chat: {
|
|
724
|
+
profile,
|
|
725
|
+
...(options.conversationStore ? { conversations: options.conversationStore } : {}),
|
|
726
|
+
},
|
|
727
|
+
});
|
|
534
728
|
}
|
|
535
729
|
/**
|
|
536
730
|
* Serves a host profile to `<TheoremHost />`: the page calls the profile's tools
|
|
@@ -539,19 +733,29 @@ export function createTheoremHandler(options) {
|
|
|
539
733
|
export function createTheoremHostHandler(options) {
|
|
540
734
|
const profile = defineProfile(options.profile);
|
|
541
735
|
if (profile.type !== 'host') {
|
|
542
|
-
//
|
|
736
|
+
// labels-exempt: builder config error at setup; no user sees it
|
|
543
737
|
throw new Error(`createTheoremHostHandler serves type 'host'; got type '${profile.type}'.`);
|
|
544
738
|
}
|
|
545
739
|
return serve('createTheoremHostHandler', profile, options);
|
|
546
740
|
}
|
|
547
|
-
function serve(name, profile, options,
|
|
741
|
+
function serve(name, profile, options, chatOptions = {}) {
|
|
548
742
|
const gateTtlMs = resolveGateTtlMs(name, options.gateTtlMs);
|
|
743
|
+
const operationRetentionMs = options.operationRetentionMs ?? DEFAULT_OPERATION_RETENTION_MS;
|
|
744
|
+
if (!Number.isSafeInteger(operationRetentionMs) || operationRetentionMs <= 0) {
|
|
745
|
+
throw new TheoremError('config', labelText('error.config'), {
|
|
746
|
+
cause: { code: 'operation_retention' },
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
const limits = resolveSessionLimits(options.sessionLimits);
|
|
750
|
+
requestByteLimit(options.maxRequestBytes);
|
|
549
751
|
registerProfile(profile);
|
|
550
752
|
const ctx = {
|
|
551
753
|
options,
|
|
754
|
+
pageToolBroker: options.pageToolBroker ?? createMemoryPageToolBroker(),
|
|
552
755
|
profile,
|
|
553
|
-
|
|
554
|
-
|
|
756
|
+
...(chatOptions.chat ? { chat: chatOptions.chat } : {}),
|
|
757
|
+
inbox: chatOptions.steerInbox ?? createMemorySteerInbox(),
|
|
758
|
+
sessions: sessionStateStore(options.sessionStore ?? createMemorySessionStore({ sessionLimits: limits }), gateTtlMs, operationRetentionMs, limits),
|
|
555
759
|
credentials: lockedStore(options.credentialStore ?? createMemoryCredentialStore(), (saved = {}) => saved),
|
|
556
760
|
};
|
|
557
761
|
return async (request) => {
|
|
@@ -561,8 +765,7 @@ function serve(name, profile, options, steerInbox) {
|
|
|
561
765
|
return withSessionCookie(await route(ctx, request, session), session);
|
|
562
766
|
}
|
|
563
767
|
catch (err) {
|
|
564
|
-
|
|
565
|
-
return withSessionCookie(jsonResponse(caughtStatus(err), { error, errorKind: errorKind(err) }), session);
|
|
768
|
+
return withSessionCookie(jsonResponse(caughtStatus(err), caughtError(ctx, err, request)), session);
|
|
566
769
|
}
|
|
567
770
|
};
|
|
568
771
|
}
|