@theoremjs/react 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +772 -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-images.d.ts +76 -0
- package/client/decision-images.js +139 -0
- package/client/decision-transport.d.ts +70 -0
- package/client/decision-transport.js +175 -0
- package/client/decode-composer-draft.d.ts +11 -0
- package/client/decode-composer-draft.js +12 -0
- 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/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 +29 -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 +47 -0
- package/client/host-transport.d.ts +70 -0
- package/client/host-transport.js +75 -0
- package/client/image-output.d.ts +6 -0
- package/client/image-output.js +13 -0
- package/client/index.d.ts +28 -0
- package/client/index.js +22 -0
- package/client/ink-waveform.d.ts +17 -0
- package/client/ink-waveform.js +68 -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 +53 -0
- package/client/live/live-captions.js +174 -0
- package/client/live/live-mic-forward.d.ts +30 -0
- package/client/live/live-mic-forward.js +46 -0
- 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 +13 -0
- package/client/live/live-state.js +22 -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 +22 -0
- package/client/live/run-live-tool-call.js +70 -0
- 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 +189 -0
- package/client/live-client.js +829 -0
- package/client/live-errors.d.ts +2 -0
- package/client/live-errors.js +9 -0
- package/client/live-messages.d.ts +148 -0
- package/client/live-messages.js +100 -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/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/pcm-downsample.d.ts +4 -0
- package/client/pcm-downsample.js +45 -0
- package/client/run-commit.d.ts +34 -0
- package/client/run-commit.js +49 -0
- package/client/run-session.d.ts +79 -0
- package/client/run-session.js +207 -0
- package/client/schema-fields.d.ts +63 -0
- package/client/schema-fields.js +199 -0
- package/client/shaped-data.d.ts +118 -0
- package/client/shaped-data.js +343 -0
- package/client/source-citations.d.ts +13 -0
- package/client/source-citations.js +56 -0
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -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 +293 -0
- package/client/tool-resume.d.ts +50 -0
- package/client/tool-resume.js +14 -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 +440 -0
- package/client/trace-view.d.ts +130 -0
- package/client/trace-view.js +333 -0
- package/client/transcript-groups.d.ts +140 -0
- package/client/transcript-groups.js +217 -0
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transcript-scroll.d.ts +7 -0
- package/client/transcript-scroll.js +6 -0
- package/client/transport.d.ts +264 -0
- package/client/transport.js +330 -0
- package/client/turn-client.d.ts +44 -0
- package/client/turn-client.js +88 -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 +162 -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 +21 -0
- package/components/InkWaveform.js +131 -0
- package/components/live/use-live-runner-controls.d.ts +47 -0
- package/components/live/use-live-runner-controls.js +96 -0
- package/components/live/use-live-runner-model.d.ts +49 -0
- package/components/live/use-live-runner-model.js +190 -0
- package/components/live/use-live-runner-ui.d.ts +67 -0
- package/components/live/use-live-runner-ui.js +167 -0
- package/components/live/use-live-session-client.d.ts +41 -0
- package/components/live/use-live-session-client.js +126 -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 +19 -0
- package/components/use-voice-playback.d.ts +17 -0
- package/components/use-voice-playback.js +68 -0
- package/hooks/use-described.d.ts +12 -0
- package/hooks/use-described.js +25 -0
- 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 +79 -0
- package/hooks/use-theorem-chat-actions.js +287 -0
- package/hooks/use-theorem-chat-state.d.ts +66 -0
- package/hooks/use-theorem-chat-state.js +122 -0
- package/hooks/use-theorem-chat.d.ts +97 -0
- package/hooks/use-theorem-chat.js +528 -0
- package/hooks/use-theorem-decision.d.ts +18 -0
- package/hooks/use-theorem-decision.js +12 -0
- package/hooks/use-theorem-embedding.d.ts +18 -0
- package/hooks/use-theorem-embedding.js +12 -0
- package/hooks/use-theorem-host.d.ts +45 -0
- package/hooks/use-theorem-host.js +135 -0
- package/hooks/use-theorem-interface.d.ts +22 -0
- package/hooks/use-theorem-interface.js +15 -0
- package/hooks/use-trace-records.d.ts +4 -0
- package/hooks/use-trace-records.js +7 -0
- package/index.d.ts +17 -0
- package/index.js +17 -0
- package/live.d.ts +9 -0
- package/live.js +10 -0
- package/package.json +74 -4
- 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/credential-store.d.ts +24 -0
- package/server/credential-store.js +14 -0
- package/server/decision-handler.d.ts +42 -0
- package/server/decision-handler.js +46 -0
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +152 -0
- package/server/handler.js +771 -0
- package/server/json-response.d.ts +2 -0
- package/server/json-response.js +7 -0
- package/server/mod.d.ts +25 -0
- package/server/mod.js +23 -0
- 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 +21 -0
- package/server/request-check.js +49 -0
- 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 +123 -0
- package/server/session-store.js +264 -0
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.d.ts +35 -0
- package/server/steer-inbox.js +52 -0
- 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 +134 -0
- package/ui/CardWall.d.ts +10 -0
- package/ui/CardWall.js +61 -0
- package/ui/ChatComposerBar.d.ts +62 -0
- package/ui/ChatComposerBar.js +306 -0
- package/ui/ChatTranscript.d.ts +43 -0
- package/ui/ChatTranscript.js +527 -0
- package/ui/ConsoleFrame.d.ts +104 -0
- package/ui/ConsoleFrame.js +146 -0
- package/ui/DecisionAnswers.d.ts +53 -0
- package/ui/DecisionAnswers.js +233 -0
- 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 +30 -0
- package/ui/LiveRunner.js +180 -0
- package/ui/PaneState.d.ts +33 -0
- package/ui/PaneState.js +50 -0
- package/ui/SchemaFields.d.ts +10 -0
- package/ui/SchemaFields.js +150 -0
- package/ui/ShapedData.d.ts +11 -0
- package/ui/ShapedData.js +196 -0
- package/ui/SidePanel.d.ts +126 -0
- package/ui/SidePanel.js +123 -0
- package/ui/StateFields.d.ts +44 -0
- package/ui/StateFields.js +139 -0
- package/ui/TheoremChat.d.ts +122 -0
- package/ui/TheoremChat.js +134 -0
- package/ui/TheoremDecision.d.ts +30 -0
- package/ui/TheoremDecision.js +130 -0
- package/ui/TheoremEmbedding.d.ts +27 -0
- package/ui/TheoremEmbedding.js +122 -0
- package/ui/TheoremHost.d.ts +55 -0
- package/ui/TheoremHost.js +240 -0
- package/ui/ToolGateCard.d.ts +32 -0
- package/ui/ToolGateCard.js +107 -0
- package/ui/ToolResult.d.ts +10 -0
- package/ui/ToolResult.js +62 -0
- package/ui/ToolResultCharts.d.ts +5 -0
- package/ui/ToolResultCharts.js +103 -0
- package/ui/TraceCharts.d.ts +13 -0
- package/ui/TraceCharts.js +266 -0
- package/ui/TraceGuardrails.d.ts +11 -0
- package/ui/TraceGuardrails.js +74 -0
- package/ui/TraceInspector.d.ts +16 -0
- package/ui/TraceInspector.js +206 -0
- package/ui/TraceInspectorPanel.d.ts +46 -0
- package/ui/TraceInspectorPanel.js +95 -0
- package/ui/TraceOverview.d.ts +38 -0
- package/ui/TraceOverview.js +126 -0
- package/ui/TraceSpanDetail.d.ts +10 -0
- package/ui/TraceSpanDetail.js +190 -0
- package/ui/TraceStory.d.ts +20 -0
- package/ui/TraceStory.js +108 -0
- package/ui/TraceValues.d.ts +36 -0
- package/ui/TraceValues.js +123 -0
- package/ui/TraceWaterfall.d.ts +13 -0
- package/ui/TraceWaterfall.js +135 -0
- package/ui/VoiceNote.d.ts +12 -0
- package/ui/VoiceNote.js +28 -0
- 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 +792 -0
- package/ui/built/theme.css.d.ts +9 -0
- package/ui/built/theorem.d.ts +13 -0
- package/ui/built/theorem.js +660 -0
- package/ui/built/theorem.variants.d.ts +22 -0
- package/ui/code-language.d.ts +12 -0
- package/ui/code-language.js +50 -0
- package/ui/disclosure-motion.d.ts +6 -0
- package/ui/disclosure-motion.js +207 -0
- 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.d.ts +4 -0
- package/ui/icons.js +38 -0
- package/ui/index.d.ts +36 -0
- package/ui/index.js +39 -0
- package/ui/labels-provider.d.ts +16 -0
- package/ui/labels-provider.js +64 -0
- package/ui/labels.d.ts +1442 -0
- package/ui/labels.js +1443 -0
- 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 +10 -0
- package/ui/theme.js +13 -0
- package/ui/theorem-theme.d.ts +10 -0
- package/ui/theorem-theme.js +105 -0
- package/ui/trace-palette.d.ts +10 -0
- package/ui/trace-palette.js +10 -0
- package/ui/trace-patterns.d.ts +40 -0
- package/ui/trace-patterns.js +57 -0
- package/ui/transcript-copy-text.d.ts +4 -0
- package/ui/transcript-copy-text.js +40 -0
- package/ui/wall-columns.d.ts +8 -0
- package/ui/wall-columns.js +19 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reply, run against the conversation its host stores: the history goes
|
|
3
|
+
* to the model from the store, never from the browser, and what the reply
|
|
4
|
+
* leaves (its transcript, its history, a paused reply) is written back.
|
|
5
|
+
*
|
|
6
|
+
* Every host runs replies through here: `createTheoremHandler` with the
|
|
7
|
+
* kernel's `runTurn`, the studio with its own scope. Each run streams its
|
|
8
|
+
* events, then one `conversation` line with the ref the store issued, then
|
|
9
|
+
* the run's error when it failed.
|
|
10
|
+
*
|
|
11
|
+
* The conversation owns the transcript, the history, and the paused reply's
|
|
12
|
+
* events. Which paused calls may run, and with what input, stays the host's
|
|
13
|
+
* session store's: a call is answered there first, and its events settle it here.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { attachmentsRefused, labelText, TheoremError, toErrorEvent, } from '@theoremjs/agents';
|
|
18
|
+
import { sanitizeTurnRequestWithEvents, withPublicLabels } from '@theoremjs/agents/guardrails';
|
|
19
|
+
import { forClient } from '@theoremjs/agents/host';
|
|
20
|
+
import { appendAssistantEventsToHistory, appendUserDraftToHistory, applyTurnEventsToSession, emptyInterfaceTurnSession, foldTurnEvents, gatedToolsFromEvents, historyFromTranscriptBlocks, prepareUserTurn, settlesToolCall, streamThoughtsEnabled, } from '@theoremjs/agents/interface';
|
|
21
|
+
import { base64ToBytes } from '@theoremjs/agents/kernel';
|
|
22
|
+
import { stampWorked } from "../client/run-commit.js";
|
|
23
|
+
import { emptyConversation, unknownConversation, } from "./conversation-store.js";
|
|
24
|
+
import { kernelTurnInput } from "./turn-input.js";
|
|
25
|
+
/** The paused call a reference names, when its call id and its operation both match. */
|
|
26
|
+
function waitingOn(conversation, reference) {
|
|
27
|
+
return gatedToolsFromEvents(conversation.paused?.events ?? []).some((call) => call.callId === reference.callId && call.operationId === reference.operationId);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The conversation host for a profile as `forClient` shows it to `clientEvents`:
|
|
31
|
+
* its thoughts as its outputs allow, its failures in its own wording.
|
|
32
|
+
*/
|
|
33
|
+
export function readerConversationHost(args) {
|
|
34
|
+
const { profile, clientEvents } = args;
|
|
35
|
+
return {
|
|
36
|
+
sessionId: args.sessionId,
|
|
37
|
+
store: args.store,
|
|
38
|
+
showThoughts: streamThoughtsEnabled(profile.outputs),
|
|
39
|
+
forReader: (event) => forClient(event, clientEvents),
|
|
40
|
+
failureText(failure) {
|
|
41
|
+
const worded = forClient(withPublicLabels(failure, profile.labels), clientEvents);
|
|
42
|
+
return worded.type === 'error' && worded.error
|
|
43
|
+
? worded.error
|
|
44
|
+
: labelText(`error.${failure.errorKind}`, {}, profile.labels);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The conversation under `ref`, or a new one when the browser has none, refused when it does not
|
|
50
|
+
* allow `use`.
|
|
51
|
+
*/
|
|
52
|
+
export async function openConversation(host, ref, use, restore) {
|
|
53
|
+
const opened = await openedAt(host, ref, restore);
|
|
54
|
+
if (use.kind === 'message')
|
|
55
|
+
requireWalkAway(opened.conversation, use.abandon);
|
|
56
|
+
if (use.kind === 'answer')
|
|
57
|
+
requireWaitingOn(opened.conversation, use.call);
|
|
58
|
+
if (use.kind === 'rewind')
|
|
59
|
+
requireRewindPoint(opened.conversation, use.at);
|
|
60
|
+
return opened;
|
|
61
|
+
}
|
|
62
|
+
async function openedAt(host, ref, restore) {
|
|
63
|
+
if (ref === undefined) {
|
|
64
|
+
if (restore)
|
|
65
|
+
// labels-exempt: internal diagnostic; the user reads error.request
|
|
66
|
+
throw new TheoremError('request', 'a transcript restores a conversation the host lost, and this message names none');
|
|
67
|
+
return { ref: null, revision: null, conversation: emptyConversation() };
|
|
68
|
+
}
|
|
69
|
+
const snapshot = await host.store.read(host.sessionId, ref);
|
|
70
|
+
if (snapshot)
|
|
71
|
+
return { ref, revision: snapshot.revision, conversation: snapshot.conversation };
|
|
72
|
+
if (!restore)
|
|
73
|
+
throw unknownConversation();
|
|
74
|
+
// why: A null ref makes the reply's write a new conversation, whose ref the `conversation` line hands the page.
|
|
75
|
+
const { conversation, events } = conversationFromBlocks(restore.profile, restore.blocks);
|
|
76
|
+
return { ref: null, revision: null, conversation, events };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A conversation from transcript blocks, read as history by the profile's guardrails at the
|
|
80
|
+
* `history` boundary, as every turn's history is. A match set to `block` refuses them. Returns it
|
|
81
|
+
* with the guardrail events for what matched.
|
|
82
|
+
*/
|
|
83
|
+
function conversationFromBlocks(profile, blocks) {
|
|
84
|
+
const { request, events, refusal } = sanitizeTurnRequestWithEvents({ profile: profile.id, input: { history: historyFromTranscriptBlocks(blocks) } }, profile);
|
|
85
|
+
if (refusal)
|
|
86
|
+
throw refusal;
|
|
87
|
+
return {
|
|
88
|
+
conversation: {
|
|
89
|
+
...emptyConversation(),
|
|
90
|
+
transcript: [...blocks],
|
|
91
|
+
history: request.input?.history ?? [],
|
|
92
|
+
},
|
|
93
|
+
events,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Starts a conversation from blocks the host wrote, not ran: a chat that opens on an exchange
|
|
98
|
+
* already under way. The model reads them as history on the next turn, read first by the
|
|
99
|
+
* profile's guardrails at the `history` boundary, as every turn's history is. A match set to
|
|
100
|
+
* `block` refuses them, and nothing is stored. Returns the new conversation's ref, its view for
|
|
101
|
+
* the chat's `conversationView`, and the guardrail events for what matched.
|
|
102
|
+
*/
|
|
103
|
+
export async function startConversation(host, profile, blocks) {
|
|
104
|
+
const { conversation, events } = conversationFromBlocks(profile, blocks);
|
|
105
|
+
const result = await host.store.write(host.sessionId, null, null, conversation);
|
|
106
|
+
if (!result.written) {
|
|
107
|
+
// labels-exempt: internal diagnostic; the user reads error.unavailable
|
|
108
|
+
throw new TheoremError('unavailable', 'the conversation store did not keep the new conversation');
|
|
109
|
+
}
|
|
110
|
+
// why: A new conversation has no paused reply, so its view is the transcript alone.
|
|
111
|
+
return { ref: result.ref, view: { transcript: [...blocks] }, events };
|
|
112
|
+
}
|
|
113
|
+
function foldedFor(host, events) {
|
|
114
|
+
return foldTurnEvents(events.map((event) => host.forReader(event)), { showThoughts: host.showThoughts });
|
|
115
|
+
}
|
|
116
|
+
/** The conversation as the browser draws it. */
|
|
117
|
+
export function conversationView(host, conversation) {
|
|
118
|
+
const { paused } = conversation;
|
|
119
|
+
return {
|
|
120
|
+
transcript: conversation.transcript,
|
|
121
|
+
...(paused
|
|
122
|
+
? {
|
|
123
|
+
paused: {
|
|
124
|
+
events: paused.events.map((event) => host.forReader(event)),
|
|
125
|
+
blocks: stampWorked(foldedFor(host, paused.events), {
|
|
126
|
+
workedMs: paused.workedMs,
|
|
127
|
+
endedAt: paused.endedAt,
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Writes `next` over what the request read; a write another request beat is refused. */
|
|
135
|
+
async function save(host, opened, next) {
|
|
136
|
+
const result = await host.store.write(host.sessionId, opened.ref, opened.revision, next);
|
|
137
|
+
if (!result.written) {
|
|
138
|
+
// labels-exempt: internal diagnostic; the user reads error.unavailable
|
|
139
|
+
throw new TheoremError('unavailable', 'the conversation changed while this reply ran', {
|
|
140
|
+
cause: { code: 'conversation_conflict' },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return { ref: result.ref, revision: result.revision, conversation: next };
|
|
144
|
+
}
|
|
145
|
+
/** The line that tells the browser the ref of what was just written. */
|
|
146
|
+
function refLine(opened, posted) {
|
|
147
|
+
if (opened.ref === null)
|
|
148
|
+
return [];
|
|
149
|
+
return [{ type: 'conversation', ref: opened.ref, ...(posted ? { posted: true } : {}) }];
|
|
150
|
+
}
|
|
151
|
+
/** The model's history once the reply's events enter it. */
|
|
152
|
+
function replyHistory(reply) {
|
|
153
|
+
return appendAssistantEventsToHistory(reply.base, reply.events.slice(reply.baseEvents));
|
|
154
|
+
}
|
|
155
|
+
/** The conversation's meters and checkpoint after the reply's events. */
|
|
156
|
+
function metered(conversation, events) {
|
|
157
|
+
const applied = applyTurnEventsToSession({
|
|
158
|
+
...emptyInterfaceTurnSession(),
|
|
159
|
+
providerState: conversation.providerState,
|
|
160
|
+
inputTokens: conversation.inputTokens,
|
|
161
|
+
historyTokens: conversation.historyTokens,
|
|
162
|
+
}, events);
|
|
163
|
+
return {
|
|
164
|
+
...(applied.providerState ? { providerState: applied.providerState } : {}),
|
|
165
|
+
...(applied.inputTokens === undefined ? {} : { inputTokens: applied.inputTokens }),
|
|
166
|
+
...(applied.historyTokens === undefined ? {} : { historyTokens: applied.historyTokens }),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** The reply is over: its blocks join the transcript and its events the history. */
|
|
170
|
+
function committed(host, conversation, reply) {
|
|
171
|
+
const { paused: _paused, providerState: _state, ...rest } = conversation;
|
|
172
|
+
return {
|
|
173
|
+
...rest,
|
|
174
|
+
...metered(conversation, reply.events),
|
|
175
|
+
transcript: [
|
|
176
|
+
...conversation.transcript,
|
|
177
|
+
...stampWorked(foldedFor(host, reply.events), {
|
|
178
|
+
workedMs: reply.workedMs,
|
|
179
|
+
endedAt: Date.now(),
|
|
180
|
+
}),
|
|
181
|
+
],
|
|
182
|
+
history: replyHistory(reply),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** The reply waits on gates: its events wait beside the transcript until they settle. */
|
|
186
|
+
function pausedOn(conversation, reply, runner) {
|
|
187
|
+
const paused = {
|
|
188
|
+
events: reply.events,
|
|
189
|
+
base: reply.base,
|
|
190
|
+
baseEvents: reply.baseEvents,
|
|
191
|
+
workedMs: reply.workedMs,
|
|
192
|
+
endedAt: Date.now(),
|
|
193
|
+
...(runner.model ? { model: runner.model } : {}),
|
|
194
|
+
...(runner.effort ? { effort: runner.effort } : {}),
|
|
195
|
+
};
|
|
196
|
+
return { ...conversation, ...metered(conversation, reply.events), paused };
|
|
197
|
+
}
|
|
198
|
+
/** The reply failed: it commits as far as it got, closed by its error. */
|
|
199
|
+
function failed(host, conversation, reply, failure) {
|
|
200
|
+
const next = committed(host, conversation, reply);
|
|
201
|
+
if (next.transcript.slice(conversation.transcript.length).some((b) => b.kind === 'error')) {
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
...next,
|
|
206
|
+
transcript: [
|
|
207
|
+
...next.transcript,
|
|
208
|
+
{ id: crypto.randomUUID(), kind: 'error', message: host.failureText(failure) },
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The person stopped the reply: it leaves the transcript, and the history
|
|
214
|
+
* keeps what came before this run, the calls it settled included.
|
|
215
|
+
*/
|
|
216
|
+
function stopped(conversation, reply) {
|
|
217
|
+
const { paused: _paused, ...rest } = conversation;
|
|
218
|
+
return {
|
|
219
|
+
...rest,
|
|
220
|
+
transcript: [
|
|
221
|
+
...conversation.transcript,
|
|
222
|
+
{ id: crypto.randomUUID(), kind: 'turn-done', stop: { kind: 'cancelled' } },
|
|
223
|
+
],
|
|
224
|
+
history: appendAssistantEventsToHistory(reply.base, reply.events.slice(reply.baseEvents, Math.max(reply.baseEvents, reply.seed))),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Runs the model for `reply` and writes what it leaves. A compaction moves the
|
|
229
|
+
* reply's base to the compacted history; `withMessage` puts the person's
|
|
230
|
+
* message back on it when the reply answers one.
|
|
231
|
+
*/
|
|
232
|
+
async function* runReply(host, opened, reply, runner, input, withMessage) {
|
|
233
|
+
const { conversation } = opened;
|
|
234
|
+
const startedAt = Date.now();
|
|
235
|
+
const worked = () => ({ ...reply, workedMs: reply.workedMs + (Date.now() - startedAt) });
|
|
236
|
+
let errorEvent;
|
|
237
|
+
let current = opened;
|
|
238
|
+
try {
|
|
239
|
+
const events = runner.run({
|
|
240
|
+
input,
|
|
241
|
+
providerState: conversation.providerState,
|
|
242
|
+
model: runner.model,
|
|
243
|
+
effort: runner.effort,
|
|
244
|
+
});
|
|
245
|
+
for await (const event of events) {
|
|
246
|
+
reply.events.push(event);
|
|
247
|
+
if (event.type === 'error') {
|
|
248
|
+
// why: The browser throws on an error line, so it goes out after the conversation line.
|
|
249
|
+
errorEvent = event;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (event.type === 'compaction') {
|
|
253
|
+
reply.base = withMessage(event.history);
|
|
254
|
+
reply.baseEvents = reply.events.length;
|
|
255
|
+
}
|
|
256
|
+
yield event;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
current = await save(host, current, runner.signal.aborted
|
|
261
|
+
? stopped(conversation, worked())
|
|
262
|
+
: failed(host, conversation, worked(), toErrorEvent(err)));
|
|
263
|
+
yield* refLine(current);
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
const settled = worked();
|
|
267
|
+
if (runner.signal.aborted) {
|
|
268
|
+
current = await save(host, current, stopped(conversation, settled));
|
|
269
|
+
}
|
|
270
|
+
else if (errorEvent) {
|
|
271
|
+
current = await save(host, current, failed(host, conversation, settled, errorEvent));
|
|
272
|
+
}
|
|
273
|
+
else if (gatedToolsFromEvents(settled.events).length) {
|
|
274
|
+
current = await save(host, current, pausedOn(conversation, settled, runner));
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
current = await save(host, current, committed(host, conversation, settled));
|
|
278
|
+
yield* refLine(current);
|
|
279
|
+
// why: The reply is the browser's before compaction runs, so a failed compaction loses none of it.
|
|
280
|
+
const compacted = await compactedAfter(host, current, settled, runner, withMessage);
|
|
281
|
+
if (compacted)
|
|
282
|
+
yield* refLine(compacted);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
yield* refLine(current);
|
|
286
|
+
if (errorEvent)
|
|
287
|
+
yield errorEvent;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The profile compacts after a reply (`done.compaction.needed`): the history
|
|
291
|
+
* before the reply is compacted, and the reply's own messages follow it.
|
|
292
|
+
*/
|
|
293
|
+
async function compactedAfter(host, opened, reply, runner, withMessage) {
|
|
294
|
+
const done = reply.events.findLast((event) => event.type === 'done');
|
|
295
|
+
const signal = done?.type === 'done' ? done.compaction : undefined;
|
|
296
|
+
if (!signal?.needed)
|
|
297
|
+
return undefined;
|
|
298
|
+
const compacted = await runner.compact(signal.history, signal.tokens, runner.model);
|
|
299
|
+
if (!compacted)
|
|
300
|
+
return undefined;
|
|
301
|
+
const history = replyHistory({ ...reply, base: withMessage(compacted), baseEvents: 0 });
|
|
302
|
+
return save(host, opened, { ...opened.conversation, history });
|
|
303
|
+
}
|
|
304
|
+
/** A file off the wire as the profile's input limits read it. */
|
|
305
|
+
function pendingOf(blob) {
|
|
306
|
+
let sizeBytes;
|
|
307
|
+
try {
|
|
308
|
+
sizeBytes = base64ToBytes(blob.data).length;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// invariant: A file that is not base64 is a malformed request; its bytes must not enter diagnostics.
|
|
312
|
+
throw new TheoremError('request', labelText('error.request'), {
|
|
313
|
+
cause: { code: 'request_file_encoding' },
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return { name: blob.name, mimeType: blob.mimeType, sizeBytes, data: blob.data };
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The person's message as the profile accepts it: refused when its files break the profile's
|
|
320
|
+
* limits. `server` is the host's own context for the run (the handler's `context` option).
|
|
321
|
+
*/
|
|
322
|
+
export function postedMessage(iface, input, server) {
|
|
323
|
+
const prepared = prepareUserTurn(iface.inputs, {
|
|
324
|
+
...(input.text !== undefined ? { text: input.text } : {}),
|
|
325
|
+
...(input.attachments ? { attachments: input.attachments.map(pendingOf) } : {}),
|
|
326
|
+
...(input.voice ? { voice: input.voice.map(pendingOf) } : {}),
|
|
327
|
+
}, iface.guardrails);
|
|
328
|
+
if (!prepared.ok)
|
|
329
|
+
throw attachmentsRefused(prepared.issues);
|
|
330
|
+
const { text: _sent, ...rest } = input;
|
|
331
|
+
const { text } = prepared.draft;
|
|
332
|
+
return {
|
|
333
|
+
draft: prepared.draft,
|
|
334
|
+
blocks: prepared.blocks,
|
|
335
|
+
media: {
|
|
336
|
+
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
337
|
+
...(input.voice ? { voice: input.voice } : {}),
|
|
338
|
+
},
|
|
339
|
+
input: kernelTurnInput(text === undefined ? rest : { ...rest, text }, server),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Refuses a walk-away that does not name exactly the calls the paused reply
|
|
344
|
+
* waits on, by call id and operation; a message to a paused reply must walk away.
|
|
345
|
+
*/
|
|
346
|
+
function requireWalkAway(conversation, abandon) {
|
|
347
|
+
const waiting = gatedToolsFromEvents(conversation.paused?.events ?? []);
|
|
348
|
+
if (waiting.length && !abandon) {
|
|
349
|
+
// labels-exempt: internal diagnostic; the user reads the copy key's wording
|
|
350
|
+
throw new TheoremError('request', 'a message to a paused reply must walk away from it', {
|
|
351
|
+
copy: { key: 'session.gate_pending' },
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const named = abandon ?? [];
|
|
355
|
+
if (waiting.length !== named.length ||
|
|
356
|
+
!named.every((reference) => waitingOn(conversation, reference))) {
|
|
357
|
+
// labels-exempt: internal diagnostic; the user reads the copy key's wording
|
|
358
|
+
throw new TheoremError('request', 'the message walks away from calls the reply is not waiting on', {
|
|
359
|
+
copy: { key: 'session.gate_expired' },
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** Refuses an answer to a call the paused reply is not waiting on, by call id and operation. */
|
|
364
|
+
function requireWaitingOn(conversation, reference) {
|
|
365
|
+
if (waitingOn(conversation, reference))
|
|
366
|
+
return;
|
|
367
|
+
// labels-exempt: internal diagnostic; the user reads the copy key's wording
|
|
368
|
+
throw new TheoremError('request', `the reply is not waiting on ${reference.callId}`, {
|
|
369
|
+
copy: { key: 'session.gate_expired' },
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Streams each walked-away call's tool events into the paused reply as it
|
|
374
|
+
* settles; each run's other events stay with that run. Returns the error that
|
|
375
|
+
* ended a run, or undefined once every call settled.
|
|
376
|
+
*/
|
|
377
|
+
async function* settleWalkedAway(paused, calls) {
|
|
378
|
+
for (const call of calls) {
|
|
379
|
+
for await (const event of call.events) {
|
|
380
|
+
if (event.type === 'error')
|
|
381
|
+
return event;
|
|
382
|
+
if (event.type !== 'tool')
|
|
383
|
+
continue;
|
|
384
|
+
paused.events.push(event);
|
|
385
|
+
yield event;
|
|
386
|
+
}
|
|
387
|
+
if (!paused.events.some((event) => settlesToolCall(event, call.callId))) {
|
|
388
|
+
// labels-exempt: internal diagnostic; the user reads error.internal
|
|
389
|
+
throw new TheoremError('internal', `walked-away call ${call.callId} ended without settling`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
function pausedReply(paused) {
|
|
395
|
+
return {
|
|
396
|
+
base: paused.base,
|
|
397
|
+
events: [...paused.events],
|
|
398
|
+
baseEvents: paused.baseEvents,
|
|
399
|
+
seed: paused.events.length,
|
|
400
|
+
workedMs: paused.workedMs,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/** A run's error as a failure to throw. */
|
|
404
|
+
function failureOf(error) {
|
|
405
|
+
return new TheoremError(error.errorKind, error.error ?? labelText(`error.${error.errorKind}`));
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The person sends a message. A reply that waits on gates is walked away
|
|
409
|
+
* from first: each call settles cancelled and the paused reply commits; then
|
|
410
|
+
* the message posts and its reply runs.
|
|
411
|
+
*/
|
|
412
|
+
export async function* messageReply(args) {
|
|
413
|
+
const { host, message, runner } = args;
|
|
414
|
+
const { events: restored = [], ...stored } = args.opened;
|
|
415
|
+
let opened = stored;
|
|
416
|
+
yield* restored;
|
|
417
|
+
const { paused } = opened.conversation;
|
|
418
|
+
requireWalkAway(opened.conversation, args.walkAway);
|
|
419
|
+
if (paused) {
|
|
420
|
+
const walked = pausedReply(paused);
|
|
421
|
+
const error = yield* settleWalkedAway(walked, args.walkAway ?? []);
|
|
422
|
+
if (error) {
|
|
423
|
+
opened = await save(host, opened, {
|
|
424
|
+
...opened.conversation,
|
|
425
|
+
paused: { ...paused, events: walked.events, endedAt: Date.now() },
|
|
426
|
+
});
|
|
427
|
+
yield* refLine(opened);
|
|
428
|
+
yield error;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
opened = { ...opened, conversation: committed(host, opened.conversation, walked) };
|
|
432
|
+
}
|
|
433
|
+
const { conversation } = opened;
|
|
434
|
+
const withMessage = (history) => appendUserDraftToHistory(history, message.draft, message.media);
|
|
435
|
+
// why: The message posts before the model runs, so a reply the browser stops still leaves it, in the transcript and the history.
|
|
436
|
+
opened = await save(host, opened, {
|
|
437
|
+
...conversation,
|
|
438
|
+
transcript: [...conversation.transcript, ...message.blocks],
|
|
439
|
+
history: withMessage(conversation.history),
|
|
440
|
+
});
|
|
441
|
+
yield* refLine(opened, 'posted');
|
|
442
|
+
const reply = {
|
|
443
|
+
base: withMessage(conversation.history),
|
|
444
|
+
events: [],
|
|
445
|
+
baseEvents: 0,
|
|
446
|
+
seed: 0,
|
|
447
|
+
workedMs: 0,
|
|
448
|
+
};
|
|
449
|
+
yield* runReply(host, opened, reply, runner, {
|
|
450
|
+
...message.input,
|
|
451
|
+
history: conversation.history,
|
|
452
|
+
...(conversation.inputTokens === undefined ? {} : { inputTokens: conversation.inputTokens }),
|
|
453
|
+
...(conversation.historyTokens === undefined
|
|
454
|
+
? {}
|
|
455
|
+
: { historyTokens: conversation.historyTokens }),
|
|
456
|
+
}, withMessage);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* The person answers one call the reply waits on. `settling` streams the
|
|
460
|
+
* call's events; once no call waits, the reply goes on in the same stream.
|
|
461
|
+
*/
|
|
462
|
+
export async function* answerReply(args) {
|
|
463
|
+
const { host, runner } = args;
|
|
464
|
+
let opened = args.opened;
|
|
465
|
+
requireWaitingOn(opened.conversation, args.call);
|
|
466
|
+
const { paused, ...waiting } = opened.conversation;
|
|
467
|
+
if (!paused)
|
|
468
|
+
return;
|
|
469
|
+
const reply = pausedReply(paused);
|
|
470
|
+
const startedAt = Date.now();
|
|
471
|
+
let error;
|
|
472
|
+
for await (const event of args.settling) {
|
|
473
|
+
if (event.type === 'error') {
|
|
474
|
+
error = event;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
reply.events.push(event);
|
|
478
|
+
yield event;
|
|
479
|
+
}
|
|
480
|
+
reply.workedMs += Date.now() - startedAt;
|
|
481
|
+
reply.seed = reply.events.length;
|
|
482
|
+
if (error || gatedToolsFromEvents(reply.events).length) {
|
|
483
|
+
// why: An answer that failed before its call settled leaves the reply as it was, to answer again.
|
|
484
|
+
const settledCall = reply.events.some((event) => settlesToolCall(event, args.call.callId));
|
|
485
|
+
const events = error && !settledCall ? paused.events : reply.events;
|
|
486
|
+
opened = await save(host, opened, {
|
|
487
|
+
...opened.conversation,
|
|
488
|
+
paused: { ...paused, events, workedMs: reply.workedMs, endedAt: Date.now() },
|
|
489
|
+
});
|
|
490
|
+
yield* refLine(opened);
|
|
491
|
+
if (error)
|
|
492
|
+
yield error;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
yield* runReply(host, { ...opened, conversation: waiting }, reply, { ...runner, model: runner.model ?? paused.model, effort: runner.effort ?? paused.effort }, {
|
|
496
|
+
history: replyHistory(reply),
|
|
497
|
+
...(waiting.inputTokens === undefined ? {} : { inputTokens: waiting.inputTokens }),
|
|
498
|
+
...(waiting.historyTokens === undefined ? {} : { historyTokens: waiting.historyTokens }),
|
|
499
|
+
}, (history) => history);
|
|
500
|
+
}
|
|
501
|
+
/** Refuses a rewind to a block the transcript does not hold; -1 empties it. */
|
|
502
|
+
function requireRewindPoint(conversation, at) {
|
|
503
|
+
if (Number.isSafeInteger(at) && at >= -1 && at < conversation.transcript.length)
|
|
504
|
+
return;
|
|
505
|
+
// labels-exempt: internal diagnostic; the user reads error.request
|
|
506
|
+
throw new TheoremError('request', `no transcript block ${at} to rewind to`);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* The conversation cut after transcript block `at` (-1 empties it): the
|
|
510
|
+
* history is rebuilt from what is kept, and the model's checkpoint and meters
|
|
511
|
+
* start over. A paused reply is walked away from first, and commits before the cut.
|
|
512
|
+
*/
|
|
513
|
+
export async function rewindConversation(args) {
|
|
514
|
+
const { host, opened, at } = args;
|
|
515
|
+
let { conversation } = opened;
|
|
516
|
+
requireRewindPoint(conversation, at);
|
|
517
|
+
requireWalkAway(conversation, args.walkAway);
|
|
518
|
+
if (conversation.paused) {
|
|
519
|
+
const walked = pausedReply(conversation.paused);
|
|
520
|
+
const settling = settleWalkedAway(walked, args.walkAway ?? []);
|
|
521
|
+
let step = await settling.next();
|
|
522
|
+
while (!step.done)
|
|
523
|
+
step = await settling.next();
|
|
524
|
+
if (step.value)
|
|
525
|
+
throw failureOf(step.value);
|
|
526
|
+
conversation = committed(host, conversation, walked);
|
|
527
|
+
}
|
|
528
|
+
const transcript = conversation.transcript.slice(0, at + 1);
|
|
529
|
+
return save(host, opened, {
|
|
530
|
+
version: 1,
|
|
531
|
+
transcript,
|
|
532
|
+
history: historyFromTranscriptBlocks(transcript),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/** A paused call's operation: every call a session issued carries one. */
|
|
536
|
+
export function requiredOperationId(call) {
|
|
537
|
+
if (!call.operationId)
|
|
538
|
+
throw new TheoremError('internal', labelText('error.internal'));
|
|
539
|
+
return call.operationId;
|
|
540
|
+
}
|
|
541
|
+
/** The calls a paused reply waits on: the ones a message or a rewind walks away from. */
|
|
542
|
+
export function waitingCalls(conversation) {
|
|
543
|
+
return gatedToolsFromEvents(conversation.paused?.events ?? []).map((call) => ({
|
|
544
|
+
callId: call.callId,
|
|
545
|
+
operationId: requiredOperationId(call),
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
/** Refuses what only a kept conversation answers, for a profile that keeps none. */
|
|
549
|
+
export function statelessRefusal(what) {
|
|
550
|
+
// labels-exempt: internal diagnostic; the user reads error.request
|
|
551
|
+
return new TheoremError('request', `a stateless chat keeps no conversation to ${what}`);
|
|
552
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool credentials for `createTheoremHandler`, held on the server by session.
|
|
3
|
+
*
|
|
4
|
+
* The browser never holds a credential: the host's OAuth callback route saves
|
|
5
|
+
* a token here, a key the user types at a gate is saved here as it arrives,
|
|
6
|
+
* and every turn reads a slot from here only when a tool that signs in runs. A
|
|
7
|
+
* refreshed OAuth token is saved here before the call goes on.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { ToolCredential } from '@theoremjs/agents/kernel';
|
|
12
|
+
import { type MemorySessionStoreOptions } from './session-store.ts';
|
|
13
|
+
/** A session's credentials, keyed by the tools' auth slot. */
|
|
14
|
+
export type TheoremCredentials = Record<string, ToolCredential>;
|
|
15
|
+
/**
|
|
16
|
+
* Where tool credentials live. The default is process memory: use a vault or
|
|
17
|
+
* an encrypted row when requests can reach different instances, and so the
|
|
18
|
+
* credentials outlive the process.
|
|
19
|
+
*/
|
|
20
|
+
export interface TheoremCredentialStore {
|
|
21
|
+
load(sessionId: string): TheoremCredentials | undefined | Promise<TheoremCredentials | undefined>;
|
|
22
|
+
save(sessionId: string, credentials: TheoremCredentials): void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createMemoryCredentialStore(options?: MemorySessionStoreOptions): TheoremCredentialStore;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool credentials for `createTheoremHandler`, held on the server by session.
|
|
3
|
+
*
|
|
4
|
+
* The browser never holds a credential: the host's OAuth callback route saves
|
|
5
|
+
* a token here, a key the user types at a gate is saved here as it arrives,
|
|
6
|
+
* and every turn reads a slot from here only when a tool that signs in runs. A
|
|
7
|
+
* refreshed OAuth token is saved here before the call goes on.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { createMemorySessionMap } from "./session-store.js";
|
|
12
|
+
export function createMemoryCredentialStore(options = {}) {
|
|
13
|
+
return createMemorySessionMap(options);
|
|
14
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Web-standard handler that serves a decision profile to `<TheoremDecision />`.
|
|
3
|
+
*
|
|
4
|
+
* - `GET <base>` → `{ interface }` the profile's handle, contract, and questions
|
|
5
|
+
* - `POST <base>/decide` → `{ result }` the answers for the posted `{ state }`
|
|
6
|
+
*
|
|
7
|
+
* The host owns the questions: the browser sends only the state and the images it names,
|
|
8
|
+
* so a visitor can't spend the host's key asking questions of their own.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { type DecisionAsset, type DecisionProfile, type DecisionQuestion, type KeyVault, type TraceSink } from '@theoremjs/agents';
|
|
13
|
+
import type { ClientTurnOptions } from '@theoremjs/agents/host';
|
|
14
|
+
import type { DecisionProfileDefinition } from '@theoremjs/agents/kernel';
|
|
15
|
+
export type TheoremDecisionHandlerOptions = {
|
|
16
|
+
/** Decision profile to serve. Registered with the kernel when the handler is created. */
|
|
17
|
+
profile: DecisionProfile | DecisionProfileDefinition;
|
|
18
|
+
/** The questions every decision asks, by id. */
|
|
19
|
+
questions: Record<string, DecisionQuestion>;
|
|
20
|
+
/**
|
|
21
|
+
* Images every decision sends, by the reference its questions write, such as `<ghs02>`. The page
|
|
22
|
+
* sees them, to draw a box on one; a page image with one of their names is refused.
|
|
23
|
+
*/
|
|
24
|
+
images?: Record<string, DecisionAsset>;
|
|
25
|
+
/** The host's keys by slot; the profile's key slot reads from it. */
|
|
26
|
+
vault?: KeyVault;
|
|
27
|
+
fetch?: typeof globalThis.fetch;
|
|
28
|
+
/** Replaces the profile's `observability.writeTo` for every decision. */
|
|
29
|
+
sink?: TraceSink;
|
|
30
|
+
/** Host metadata kept on each decision's trace record, e.g. the signed-in user. */
|
|
31
|
+
metadata?: (request: Request) => Record<string, unknown> | undefined;
|
|
32
|
+
/** Who reads this host's replies: forwarded to `clientError` for a failure the handler catches. */
|
|
33
|
+
clientEvents?: ClientTurnOptions;
|
|
34
|
+
/**
|
|
35
|
+
* Called with any error the handler catches, for reporting. Users read the
|
|
36
|
+
* profile's labels wording for the error's kind, never the error itself.
|
|
37
|
+
*/
|
|
38
|
+
onError?: (err: unknown, ctx: {
|
|
39
|
+
request: Request;
|
|
40
|
+
}) => void;
|
|
41
|
+
};
|
|
42
|
+
export declare function createTheoremDecisionHandler(options: TheoremDecisionHandlerOptions): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Web-standard handler that serves a decision profile to `<TheoremDecision />`.
|
|
3
|
+
*
|
|
4
|
+
* - `GET <base>` → `{ interface }` the profile's handle, contract, and questions
|
|
5
|
+
* - `POST <base>/decide` → `{ result }` the answers for the posted `{ state }`
|
|
6
|
+
*
|
|
7
|
+
* The host owns the questions: the browser sends only the state and the images it names,
|
|
8
|
+
* so a visitor can't spend the host's key asking questions of their own.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { defineProfile, registerProfile, runDecision, z, } from '@theoremjs/agents';
|
|
13
|
+
import { decisionAssets, decisionInterface } from "../client/decision-transport.js";
|
|
14
|
+
import { readBody } from "./handler.js";
|
|
15
|
+
import { singleActionHandler } from "./single-action.js";
|
|
16
|
+
const decideBodySchema = z.object({
|
|
17
|
+
state: z.unknown(),
|
|
18
|
+
assets: z.record(z.string(), z.object({ mimeType: z.string(), data: z.string() })).optional(),
|
|
19
|
+
});
|
|
20
|
+
export function createTheoremDecisionHandler(options) {
|
|
21
|
+
const profile = defineProfile(options.profile);
|
|
22
|
+
if (profile.type !== 'decision') {
|
|
23
|
+
// labels-exempt: builder config error at setup; no user sees it
|
|
24
|
+
throw new Error(`createTheoremDecisionHandler serves type 'decision'; got type '${profile.type}'.`);
|
|
25
|
+
}
|
|
26
|
+
registerProfile(profile);
|
|
27
|
+
return singleActionHandler({
|
|
28
|
+
action: 'decide',
|
|
29
|
+
iface: decisionInterface(profile, options.questions, options.images),
|
|
30
|
+
labels: profile.labels,
|
|
31
|
+
clientEvents: options.clientEvents,
|
|
32
|
+
onError: options.onError,
|
|
33
|
+
}, async (request) => {
|
|
34
|
+
const body = await readBody(request, decideBodySchema);
|
|
35
|
+
return runDecision({
|
|
36
|
+
profile: profile.id,
|
|
37
|
+
// why: The kernel checks the state is non-null JSON within the profile's size.
|
|
38
|
+
state: body.state,
|
|
39
|
+
// why: The kernel checks each image's type and bytes before any request leaves.
|
|
40
|
+
assets: decisionAssets(options.images, body.assets),
|
|
41
|
+
questions: options.questions,
|
|
42
|
+
signal: request.signal,
|
|
43
|
+
metadata: options.metadata?.(request),
|
|
44
|
+
}, { vault: options.vault, fetch: options.fetch, sink: options.sink });
|
|
45
|
+
});
|
|
46
|
+
}
|