@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,135 @@
|
|
|
1
|
+
import { toolCallsOf, toolSnapshotFromEvents } from '@theoremjs/agents/interface';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { clientFailure } from "../client/failure.js";
|
|
4
|
+
import { withPageToolDispatch } from "../client/page-dispatch.js";
|
|
5
|
+
import { continueGatedToolInvocation } from "../client/tool-resume.js";
|
|
6
|
+
import { issuedOperationId } from "../client/turn-client.js";
|
|
7
|
+
import { useDescribed } from "./use-described.js";
|
|
8
|
+
function callId() {
|
|
9
|
+
return typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
10
|
+
? crypto.randomUUID()
|
|
11
|
+
: `call-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
12
|
+
}
|
|
13
|
+
function withEvent(call, event) {
|
|
14
|
+
const events = [...call.events, event];
|
|
15
|
+
const progress = event.type === 'tool' && event.tool.phase === 'progress' ? event.tool.data : call.progress;
|
|
16
|
+
return { ...call, events, call: toolCallsOf(events)[0] ?? null, progress };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One host profile: describe it once, then `run(name, input)` any of its tools
|
|
20
|
+
* as often as needed, and answer the gates a call pauses on. `run` returns the
|
|
21
|
+
* call's id at once; calls run side by side, and `cancel` stops one.
|
|
22
|
+
*/
|
|
23
|
+
export function useTheoremHost(source, pageTools) {
|
|
24
|
+
const transport = useMemo(() => withPageToolDispatch(source, pageTools), [source, pageTools]);
|
|
25
|
+
const { iface, describeFailure } = useDescribed(transport);
|
|
26
|
+
const [calls, setCalls] = useState([]);
|
|
27
|
+
const running = useRef(new Map());
|
|
28
|
+
const permissions = useRef([]);
|
|
29
|
+
const latest = useRef(calls);
|
|
30
|
+
latest.current = calls;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const open = running.current;
|
|
33
|
+
return () => {
|
|
34
|
+
for (const controller of open.values())
|
|
35
|
+
controller.abort();
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
const update = useCallback((id, change) => {
|
|
39
|
+
setCalls((previous) => previous.map((call) => (call.id === id ? change(call) : call)));
|
|
40
|
+
}, []);
|
|
41
|
+
/** Streams one request's events into the call, then marks it settled or failed. */
|
|
42
|
+
const stream = useCallback(async (id, send) => {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
running.current.set(id, controller);
|
|
45
|
+
const started = performance.now();
|
|
46
|
+
update(id, (call) => ({ ...call, isRunning: true, failure: null }));
|
|
47
|
+
try {
|
|
48
|
+
await send((event) => {
|
|
49
|
+
if (event.type === 'unsupported' || event.type === 'malformed')
|
|
50
|
+
return;
|
|
51
|
+
update(id, (call) => withEvent(call, event));
|
|
52
|
+
}, controller.signal);
|
|
53
|
+
update(id, (call) => ({
|
|
54
|
+
...call,
|
|
55
|
+
isRunning: false,
|
|
56
|
+
answering: null,
|
|
57
|
+
elapsedMs: performance.now() - started,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const failure = controller.signal.aborted ? null : clientFailure(err);
|
|
62
|
+
update(id, (call) => ({
|
|
63
|
+
...call,
|
|
64
|
+
isRunning: false,
|
|
65
|
+
answering: null,
|
|
66
|
+
failure,
|
|
67
|
+
elapsedMs: performance.now() - started,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (running.current.get(id) === controller)
|
|
72
|
+
running.current.delete(id);
|
|
73
|
+
}
|
|
74
|
+
}, [update]);
|
|
75
|
+
const run = useCallback((name, input) => {
|
|
76
|
+
const id = callId();
|
|
77
|
+
setCalls((previous) => [
|
|
78
|
+
{
|
|
79
|
+
id,
|
|
80
|
+
name,
|
|
81
|
+
input,
|
|
82
|
+
events: [],
|
|
83
|
+
call: null,
|
|
84
|
+
progress: undefined,
|
|
85
|
+
isRunning: true,
|
|
86
|
+
failure: null,
|
|
87
|
+
answering: null,
|
|
88
|
+
startedAt: Date.now(),
|
|
89
|
+
elapsedMs: null,
|
|
90
|
+
},
|
|
91
|
+
...previous,
|
|
92
|
+
]);
|
|
93
|
+
void stream(id, (onEvent, signal) => transport.call({ name, input }, onEvent, signal));
|
|
94
|
+
return id;
|
|
95
|
+
}, [stream, transport]);
|
|
96
|
+
const answer = useCallback(async (id, resolution) => {
|
|
97
|
+
const held = latest.current.find((call) => call.id === id);
|
|
98
|
+
const state = held?.call?.state;
|
|
99
|
+
if (!held?.call || state?.phase !== 'gate')
|
|
100
|
+
return;
|
|
101
|
+
const gateId = held.call.callId;
|
|
102
|
+
const operationId = issuedOperationId(held.call);
|
|
103
|
+
const reply = continueGatedToolInvocation({
|
|
104
|
+
toolName: held.name,
|
|
105
|
+
gate: state.gate,
|
|
106
|
+
sessionPermissions: permissions.current,
|
|
107
|
+
resolution,
|
|
108
|
+
});
|
|
109
|
+
if (reply.decision === 'approve')
|
|
110
|
+
permissions.current = reply.sessionPermissions;
|
|
111
|
+
const snapshot = toolSnapshotFromEvents(held.events);
|
|
112
|
+
update(id, (call) => ({ ...call, answering: resolution.action }));
|
|
113
|
+
await stream(id, (onEvent, signal) => transport.invoke({
|
|
114
|
+
gateId,
|
|
115
|
+
operationId,
|
|
116
|
+
decision: reply.decision,
|
|
117
|
+
...(reply.decision === 'approve' && reply.secret !== undefined
|
|
118
|
+
? { secret: reply.secret }
|
|
119
|
+
: {}),
|
|
120
|
+
// why: A host without sessions (the studio) replays the paused call; others ignore it.
|
|
121
|
+
replay: {
|
|
122
|
+
name: held.name,
|
|
123
|
+
input: state.gate.preparedArguments ?? held.input,
|
|
124
|
+
gateKind: state.gate.kind,
|
|
125
|
+
sessionPermissions: permissions.current,
|
|
126
|
+
...(snapshot ? { snapshot } : {}),
|
|
127
|
+
},
|
|
128
|
+
}, onEvent, signal));
|
|
129
|
+
}, [stream, transport, update]);
|
|
130
|
+
const cancel = useCallback((id) => {
|
|
131
|
+
running.current.get(id)?.abort();
|
|
132
|
+
running.current.delete(id);
|
|
133
|
+
}, []);
|
|
134
|
+
return { iface, describeFailure, calls, run, answer, cancel };
|
|
135
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ProfileInterface } from '@theoremjs/agents/interface';
|
|
2
|
+
import type { ClientFailure } from '../client/failure.ts';
|
|
3
|
+
import type { TheoremTransport } from '../client/transport.ts';
|
|
4
|
+
/** `failure` is worded with the default labels: the profile's is not known until it loads. */
|
|
5
|
+
export type TheoremInterfaceState = {
|
|
6
|
+
status: 'loading';
|
|
7
|
+
iface: null;
|
|
8
|
+
failure: null;
|
|
9
|
+
} | {
|
|
10
|
+
status: 'ready';
|
|
11
|
+
iface: ProfileInterface;
|
|
12
|
+
failure: null;
|
|
13
|
+
} | {
|
|
14
|
+
status: 'error';
|
|
15
|
+
iface: null;
|
|
16
|
+
failure: ClientFailure;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Ask the transport for the host profile's client-safe interface. A new transport keeps the last
|
|
20
|
+
* interface up until it describes itself, as {@link useDescribed} does.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useTheoremInterface(transport: TheoremTransport): TheoremInterfaceState;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useDescribed } from "./use-described.js";
|
|
3
|
+
const LOADING = { status: 'loading', iface: null, failure: null };
|
|
4
|
+
/**
|
|
5
|
+
* Ask the transport for the host profile's client-safe interface. A new transport keeps the last
|
|
6
|
+
* interface up until it describes itself, as {@link useDescribed} does.
|
|
7
|
+
*/
|
|
8
|
+
export function useTheoremInterface(transport) {
|
|
9
|
+
const { iface, describeFailure } = useDescribed(transport);
|
|
10
|
+
return useMemo(() => {
|
|
11
|
+
if (describeFailure)
|
|
12
|
+
return { status: 'error', iface: null, failure: describeFailure };
|
|
13
|
+
return iface ? { status: 'ready', iface, failure: null } : LOADING;
|
|
14
|
+
}, [iface, describeFailure]);
|
|
15
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TraceRecord } from '@theoremjs/agents';
|
|
2
|
+
import type { TraceFeed } from '../client/trace-feed.ts';
|
|
3
|
+
/** The records a feed has delivered so far; none without a feed. */
|
|
4
|
+
export declare function useTraceRecords(feed: TraceFeed | undefined): readonly TraceRecord[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
const NONE = [];
|
|
3
|
+
const noSubscription = () => () => { };
|
|
4
|
+
/** The records a feed has delivered so far; none without a feed. */
|
|
5
|
+
export function useTraceRecords(feed) {
|
|
6
|
+
return useSyncExternalStore(feed?.subscribe ?? noSubscription, feed?.records ?? (() => NONE));
|
|
7
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless React bindings for Theorem — hooks and a transport, no styling.
|
|
3
|
+
*
|
|
4
|
+
* - `@theoremjs/react` hooks + transport (this entry)
|
|
5
|
+
* - `@theoremjs/react/ui` Astryx chat UI built on these hooks
|
|
6
|
+
* - `@theoremjs/react/server` `createTheoremHandler` (and `createTheoremDecisionHandler`, `createTheoremEmbeddingHandler`, `createTheoremHostHandler`) for the host side
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
export * from './client/index.ts';
|
|
11
|
+
export { type PageDispatchResult, withPageToolDispatch } from './client/page-dispatch.ts';
|
|
12
|
+
export { type EmbeddingIndex, type IndexProgress, useEmbeddingIndex, } from './hooks/use-embedding-index.ts';
|
|
13
|
+
export { type SentTurn, type UseTheoremChatOptions, useTheoremChat, } from './hooks/use-theorem-chat.ts';
|
|
14
|
+
export { type TheoremDecisionState, useTheoremDecision } from './hooks/use-theorem-decision.ts';
|
|
15
|
+
export { type TheoremEmbeddingState, useTheoremEmbedding, } from './hooks/use-theorem-embedding.ts';
|
|
16
|
+
export { type TheoremHostCall, type TheoremHostState, useTheoremHost, } from './hooks/use-theorem-host.ts';
|
|
17
|
+
export { type TheoremInterfaceState, useTheoremInterface } from './hooks/use-theorem-interface.ts';
|
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless React bindings for Theorem — hooks and a transport, no styling.
|
|
3
|
+
*
|
|
4
|
+
* - `@theoremjs/react` hooks + transport (this entry)
|
|
5
|
+
* - `@theoremjs/react/ui` Astryx chat UI built on these hooks
|
|
6
|
+
* - `@theoremjs/react/server` `createTheoremHandler` (and `createTheoremDecisionHandler`, `createTheoremEmbeddingHandler`, `createTheoremHostHandler`) for the host side
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
export * from "./client/index.js";
|
|
11
|
+
export { withPageToolDispatch } from "./client/page-dispatch.js";
|
|
12
|
+
export { useEmbeddingIndex, } from "./hooks/use-embedding-index.js";
|
|
13
|
+
export { useTheoremChat, } from "./hooks/use-theorem-chat.js";
|
|
14
|
+
export { useTheoremDecision } from "./hooks/use-theorem-decision.js";
|
|
15
|
+
export { useTheoremEmbedding, } from "./hooks/use-theorem-embedding.js";
|
|
16
|
+
export { useTheoremHost, } from "./hooks/use-theorem-host.js";
|
|
17
|
+
export { useTheoremInterface } from "./hooks/use-theorem-interface.js";
|
package/live.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live (voice / video) runner, on the Astryx UI in the Theorem theme.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
export type { LiveCallOptions, PageTool, PageToolAnswer, PageTools, } from './client/live/live-page-tool.ts';
|
|
7
|
+
export type { LiveConnection } from './client/live-client.ts';
|
|
8
|
+
export type { LiveRunnerProps } from './ui/LiveRunner.tsx';
|
|
9
|
+
export { LiveRunner } from './ui/LiveRunner.tsx';
|
package/live.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live (voice / video) runner, on the Astryx UI in the Theorem theme.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
// why: Astryx's documented order: reset → components → theme.
|
|
7
|
+
import '@astryxdesign/core/reset.css';
|
|
8
|
+
import '@astryxdesign/core/astryx.css';
|
|
9
|
+
import './ui/built/theme.css';
|
|
10
|
+
export { LiveRunner } from "./ui/LiveRunner.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoremjs/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "React bindings for Theorem: headless hooks + transport, an Astryx chat UI, and a Web-standard server handler.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/masudl-hub/theoremai.git",
|
|
13
|
+
"directory": "react"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": [
|
|
16
|
+
"**/*.css",
|
|
17
|
+
"./live.js",
|
|
18
|
+
"./ui/index.js"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./index.d.ts",
|
|
23
|
+
"default": "./index.js"
|
|
24
|
+
},
|
|
25
|
+
"./ui": {
|
|
26
|
+
"types": "./ui/index.d.ts",
|
|
27
|
+
"default": "./ui/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./ui/theme": {
|
|
30
|
+
"types": "./ui/theorem-theme.d.ts",
|
|
31
|
+
"default": "./ui/theorem-theme.js"
|
|
32
|
+
},
|
|
33
|
+
"./ui/icons": {
|
|
34
|
+
"types": "./ui/icons.d.ts",
|
|
35
|
+
"default": "./ui/icons.js"
|
|
36
|
+
},
|
|
37
|
+
"./server": {
|
|
38
|
+
"types": "./server/mod.d.ts",
|
|
39
|
+
"default": "./server/mod.js"
|
|
40
|
+
},
|
|
41
|
+
"./live": {
|
|
42
|
+
"types": "./live.d.ts",
|
|
43
|
+
"default": "./live.js"
|
|
44
|
+
},
|
|
45
|
+
"./client": {
|
|
46
|
+
"types": "./client/index.d.ts",
|
|
47
|
+
"default": "./client/index.js"
|
|
48
|
+
},
|
|
49
|
+
"./styles/*": "./styles/*"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@speed-highlight/core": "2.1.0",
|
|
53
|
+
"@tabler/icons-react": "^3.46.0",
|
|
54
|
+
"intl-messageformat": "^11.2.9",
|
|
55
|
+
"recharts": "^3.10.1"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@astryxdesign/core": "0.7.0",
|
|
59
|
+
"@astryxdesign/theme-neutral": "0.7.0",
|
|
60
|
+
"@stylexjs/stylex": "0.19.0",
|
|
61
|
+
"react": "^19.0.0",
|
|
62
|
+
"react-dom": "^19.0.0",
|
|
63
|
+
"@theoremjs/agents": "^0.3.1"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"@astryxdesign/core": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@astryxdesign/theme-neutral": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@stylexjs/stylex": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ProviderCheckpoint, type TurnHistoryMessage } from '@theoremjs/agents';
|
|
2
|
+
import type { TheoremSessionState } from './session-store.ts';
|
|
3
|
+
export declare function checkpointIdentity(checkpoint: ProviderCheckpoint): Promise<string>;
|
|
4
|
+
export declare function checkpointOwnershipKey(identity: string, operations: TheoremSessionState['operations']): Promise<string>;
|
|
5
|
+
export declare function requireOwnedCheckpoint(identities: readonly string[], checkpoint: ProviderCheckpoint | undefined, operations?: TheoremSessionState['operations'], portableHistory?: readonly TurnHistoryMessage[]): Promise<ProviderCheckpoint | undefined>;
|
|
6
|
+
export declare function retainedCheckpointIdentities(existing: readonly string[], added: readonly string[], limit: number): string[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { labelText, providerCheckpointSchema, TheoremError, toolInvocationHash, } from '@theoremjs/agents';
|
|
2
|
+
export function checkpointIdentity(checkpoint) {
|
|
3
|
+
return toolInvocationHash(providerCheckpointSchema.parse(checkpoint));
|
|
4
|
+
}
|
|
5
|
+
export async function checkpointOwnershipKey(identity, operations) {
|
|
6
|
+
const rows = Object.entries(operations)
|
|
7
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
8
|
+
.map(([id, operation]) => operation.status === 'ready'
|
|
9
|
+
? [id, operation.status, operation.profileId, operation.readyAt]
|
|
10
|
+
: [id, operation.status, operation.profileId, operation.toolId, operation.invocationHash]);
|
|
11
|
+
const epoch = await toolInvocationHash(rows);
|
|
12
|
+
return `${identity}:${epoch}`;
|
|
13
|
+
}
|
|
14
|
+
export async function requireOwnedCheckpoint(identities, checkpoint, operations = {}, portableHistory) {
|
|
15
|
+
if (checkpoint === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const parsed = providerCheckpointSchema.parse(checkpoint);
|
|
18
|
+
const identity = await checkpointIdentity(parsed);
|
|
19
|
+
const key = await checkpointOwnershipKey(identity, operations);
|
|
20
|
+
const records = Object.values(operations);
|
|
21
|
+
const portableSettlements = portableHistory !== undefined &&
|
|
22
|
+
records.length > 0 &&
|
|
23
|
+
identities.some((saved) => saved.startsWith(`${identity}:`)) &&
|
|
24
|
+
records.every((operation) => operation.status === 'settled' &&
|
|
25
|
+
operation.outcome.effects !== 'unknown' &&
|
|
26
|
+
operation.outcome.event.tool.readBack !== undefined &&
|
|
27
|
+
portableHistory.some((message) => message.role === 'tool' &&
|
|
28
|
+
message.tool_call_id === operation.callId &&
|
|
29
|
+
message.name === operation.toolId &&
|
|
30
|
+
message.content === operation.outcome.event.tool.readBack));
|
|
31
|
+
if (!identities.includes(key) &&
|
|
32
|
+
!portableSettlements &&
|
|
33
|
+
!(records.length === 0 && identities.includes(identity))) {
|
|
34
|
+
throw new TheoremError('request', labelText('session.provider_state_unowned'), {
|
|
35
|
+
copy: { key: 'session.provider_state_unowned' },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
export function retainedCheckpointIdentities(existing, added, limit) {
|
|
41
|
+
return [...new Set([...existing, ...added].reverse())].slice(0, limit).reverse();
|
|
42
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a chat's conversation lives for `createTheoremHandler`: the transcript
|
|
3
|
+
* the person reads and the history the model reads next. The host owns it; the
|
|
4
|
+
* browser holds only an opaque ref the store issued, and never sends history.
|
|
5
|
+
*
|
|
6
|
+
* A database store keys the conversation by its id. The sealed store keeps
|
|
7
|
+
* nothing: its ref is the conversation itself, encrypted and signed with the
|
|
8
|
+
* host's key, so the browser carries what it cannot read or change.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { type ProviderCheckpoint, TheoremError, type TurnEvent, type TurnHistoryMessage, z } from '@theoremjs/agents';
|
|
13
|
+
import type { TranscriptBlock } from '@theoremjs/agents/interface';
|
|
14
|
+
import { type MemorySessionStoreOptions } from './session-store.ts';
|
|
15
|
+
/** A reply paused on gates: its events so far, and the history it was given. */
|
|
16
|
+
export type PausedReply = {
|
|
17
|
+
events: TurnEvent[];
|
|
18
|
+
/** The history the reply was given; its events append to this when it commits. */
|
|
19
|
+
base: TurnHistoryMessage[];
|
|
20
|
+
/** How many of `events` `base` already holds: a compaction mid-reply moves the base. */
|
|
21
|
+
baseEvents: number;
|
|
22
|
+
/** How long the reply worked before it paused (ms), approval waits excluded. */
|
|
23
|
+
workedMs: number;
|
|
24
|
+
/** When the reply last paused (epoch ms). */
|
|
25
|
+
endedAt: number;
|
|
26
|
+
/** The model and effort the reply ran with, for the run that goes on after its gates. */
|
|
27
|
+
model?: string;
|
|
28
|
+
effort?: string;
|
|
29
|
+
};
|
|
30
|
+
/** One conversation as the host stores it. */
|
|
31
|
+
export type StoredConversation = {
|
|
32
|
+
version: 1;
|
|
33
|
+
/** What the person reads, without the paused reply. */
|
|
34
|
+
transcript: TranscriptBlock[];
|
|
35
|
+
/** What the model reads on the next turn, compacted when the profile compacts. */
|
|
36
|
+
history: TurnHistoryMessage[];
|
|
37
|
+
providerState?: ProviderCheckpoint;
|
|
38
|
+
/** The last turn's input tokens, for the compaction meter. */
|
|
39
|
+
inputTokens?: number;
|
|
40
|
+
/** The history's size in tokens as the last compaction measured it. */
|
|
41
|
+
historyTokens?: number;
|
|
42
|
+
paused?: PausedReply;
|
|
43
|
+
};
|
|
44
|
+
/** A stored conversation as a host's own store reads it back. */
|
|
45
|
+
export declare const storedConversationSchema: z.ZodType<StoredConversation>;
|
|
46
|
+
/** An empty conversation: what a chat with no ref starts from. */
|
|
47
|
+
export declare function emptyConversation(): StoredConversation;
|
|
48
|
+
export interface ConversationSnapshot {
|
|
49
|
+
revision: string;
|
|
50
|
+
conversation: StoredConversation;
|
|
51
|
+
}
|
|
52
|
+
export type ConversationWriteResult = {
|
|
53
|
+
written: true;
|
|
54
|
+
ref: string;
|
|
55
|
+
revision: string;
|
|
56
|
+
} | {
|
|
57
|
+
written: false;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Where conversations live. `ref` is the store's own: the handler hands it to
|
|
61
|
+
* the browser and back, and never reads it. Every read and write names the
|
|
62
|
+
* session, so a ref from another session opens nothing.
|
|
63
|
+
*/
|
|
64
|
+
export interface TheoremConversationStore {
|
|
65
|
+
/** The conversation, or undefined when this session has none under `ref`. */
|
|
66
|
+
read(sessionId: string, ref: string): Promise<ConversationSnapshot | undefined>;
|
|
67
|
+
/**
|
|
68
|
+
* Compare and replace. A null `ref` creates a conversation; otherwise the
|
|
69
|
+
* write lands only on a conversation this session can read, while its
|
|
70
|
+
* stored revision is still `expectedRevision`.
|
|
71
|
+
*/
|
|
72
|
+
write(sessionId: string, ref: string | null, expectedRevision: string | null, next: StoredConversation): Promise<ConversationWriteResult>;
|
|
73
|
+
}
|
|
74
|
+
/** Refuses a ref the store does not know or cannot open: the browser reads `session.conversation_unknown`. */
|
|
75
|
+
export declare function unknownConversation(cause?: unknown): TheoremError;
|
|
76
|
+
/** Conversations in process memory: forgotten when idle, oldest evicted first. For one instance. */
|
|
77
|
+
export declare function createMemoryConversationStore(options?: Omit<MemorySessionStoreOptions, 'sessionLimits'>): TheoremConversationStore;
|
|
78
|
+
export type SealedConversationStoreOptions = {
|
|
79
|
+
/** Every sealing key still in use, by version: each at least 32 bytes, kept off the browser. */
|
|
80
|
+
keys: Readonly<Record<number, string>>;
|
|
81
|
+
/** The key that seals new writes. */
|
|
82
|
+
keyVersion: number;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* A store for a host that keeps nothing. The ref is the conversation sealed
|
|
86
|
+
* with the host's key and bound to the session: the browser carries it and
|
|
87
|
+
* cannot read or change it, and it opens in no other session.
|
|
88
|
+
*
|
|
89
|
+
* It cannot tell an older seal from the newest, so an older seal opens: like a
|
|
90
|
+
* rewind, it returns a conversation the host wrote. Its revision is the seal.
|
|
91
|
+
*/
|
|
92
|
+
export declare function createSealedConversationStore(options: SealedConversationStoreOptions): TheoremConversationStore;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a chat's conversation lives for `createTheoremHandler`: the transcript
|
|
3
|
+
* the person reads and the history the model reads next. The host owns it; the
|
|
4
|
+
* browser holds only an opaque ref the store issued, and never sends history.
|
|
5
|
+
*
|
|
6
|
+
* A database store keys the conversation by its id. The sealed store keeps
|
|
7
|
+
* nothing: its ref is the conversation itself, encrypted and signed with the
|
|
8
|
+
* host's key, so the browser carries what it cannot read or change.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { labelText, providerCheckpointSchema, TheoremError, turnEventSchema, turnHistoryMessageSchema, z, } from '@theoremjs/agents';
|
|
13
|
+
import { openSecret, sealSecret } from '@theoremjs/agents/kernel';
|
|
14
|
+
import { transcriptBlockSchema } from "../client/transcript-schema.js";
|
|
15
|
+
import { memoryStoreLimits } from "./session-store.js";
|
|
16
|
+
const pausedReply = z.object({
|
|
17
|
+
events: z.array(turnEventSchema),
|
|
18
|
+
base: z.array(turnHistoryMessageSchema),
|
|
19
|
+
baseEvents: z.number().int().nonnegative(),
|
|
20
|
+
workedMs: z.number().nonnegative(),
|
|
21
|
+
endedAt: z.number(),
|
|
22
|
+
model: z.string().optional(),
|
|
23
|
+
effort: z.string().optional(),
|
|
24
|
+
});
|
|
25
|
+
true;
|
|
26
|
+
const storedConversation = z.object({
|
|
27
|
+
version: z.literal(1),
|
|
28
|
+
transcript: z.array(transcriptBlockSchema),
|
|
29
|
+
history: z.array(turnHistoryMessageSchema),
|
|
30
|
+
providerState: providerCheckpointSchema.optional(),
|
|
31
|
+
inputTokens: z.number().optional(),
|
|
32
|
+
historyTokens: z.number().optional(),
|
|
33
|
+
paused: pausedReply.optional(),
|
|
34
|
+
});
|
|
35
|
+
true;
|
|
36
|
+
/** A stored conversation as a host's own store reads it back. */
|
|
37
|
+
export const storedConversationSchema = storedConversation;
|
|
38
|
+
/** An empty conversation: what a chat with no ref starts from. */
|
|
39
|
+
export function emptyConversation() {
|
|
40
|
+
return { version: 1, transcript: [], history: [] };
|
|
41
|
+
}
|
|
42
|
+
/** Refuses a ref the store does not know or cannot open: the browser reads `session.conversation_unknown`. */
|
|
43
|
+
export function unknownConversation(cause) {
|
|
44
|
+
return new TheoremError('request', labelText('session.conversation_unknown'), {
|
|
45
|
+
copy: { key: 'session.conversation_unknown' },
|
|
46
|
+
...(cause === undefined ? {} : { cause }),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** Conversations in process memory: forgotten when idle, oldest evicted first. For one instance. */
|
|
50
|
+
export function createMemoryConversationStore(options = {}) {
|
|
51
|
+
const { ttlMs, maxSessions } = memoryStoreLimits(options);
|
|
52
|
+
const conversations = new Map();
|
|
53
|
+
const current = (sessionId, ref) => {
|
|
54
|
+
const entry = conversations.get(ref);
|
|
55
|
+
if (!entry || entry.sessionId !== sessionId)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (Date.now() - entry.touched >= ttlMs) {
|
|
58
|
+
conversations.delete(ref);
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return entry.snapshot;
|
|
62
|
+
};
|
|
63
|
+
const written = (...[sessionId, ref, expectedRevision, next]) => {
|
|
64
|
+
const snapshot = ref === null ? undefined : current(sessionId, ref);
|
|
65
|
+
// invariant: A ref this session cannot read takes no write, so no session overwrites another's conversation.
|
|
66
|
+
if (ref !== null && snapshot === undefined)
|
|
67
|
+
return { written: false };
|
|
68
|
+
if ((snapshot?.revision ?? null) !== expectedRevision)
|
|
69
|
+
return { written: false };
|
|
70
|
+
const conversation = storedConversation.parse(next);
|
|
71
|
+
const key = ref ?? crypto.randomUUID();
|
|
72
|
+
const revision = crypto.randomUUID();
|
|
73
|
+
conversations.delete(key);
|
|
74
|
+
conversations.set(key, {
|
|
75
|
+
sessionId,
|
|
76
|
+
snapshot: { revision, conversation: structuredClone(conversation) },
|
|
77
|
+
touched: Date.now(),
|
|
78
|
+
});
|
|
79
|
+
while (conversations.size > maxSessions) {
|
|
80
|
+
const oldest = conversations.keys().next().value;
|
|
81
|
+
if (oldest === undefined)
|
|
82
|
+
break;
|
|
83
|
+
conversations.delete(oldest);
|
|
84
|
+
}
|
|
85
|
+
return { written: true, ref: key, revision };
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
read: (sessionId, ref) => Promise.resolve(structuredClone(current(sessionId, ref))),
|
|
89
|
+
// why: A write that fails its check rejects, as any store's does, rather than throwing at the call.
|
|
90
|
+
write: (...args) => new Promise((resolve) => resolve(written(...args))),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A store for a host that keeps nothing. The ref is the conversation sealed
|
|
95
|
+
* with the host's key and bound to the session: the browser carries it and
|
|
96
|
+
* cannot read or change it, and it opens in no other session.
|
|
97
|
+
*
|
|
98
|
+
* It cannot tell an older seal from the newest, so an older seal opens: like a
|
|
99
|
+
* rewind, it returns a conversation the host wrote. Its revision is the seal.
|
|
100
|
+
*/
|
|
101
|
+
export function createSealedConversationStore(options) {
|
|
102
|
+
const sealKey = options.keys[options.keyVersion];
|
|
103
|
+
if (sealKey === undefined) {
|
|
104
|
+
throw new TheoremError('config', labelText('error.config'), {
|
|
105
|
+
cause: { code: 'conversation_seal_key_missing', keyVersion: options.keyVersion },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
async read(sessionId, ref) {
|
|
110
|
+
const plaintext = await openSecret({ sealed: ref, keys: options.keys, binding: [sessionId] })
|
|
111
|
+
// why: A seal that does not open is a ref this session does not own; the caller refuses it.
|
|
112
|
+
.catch(() => undefined);
|
|
113
|
+
if (plaintext === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
const conversation = storedConversation.parse(JSON.parse(plaintext));
|
|
116
|
+
return { revision: ref, conversation };
|
|
117
|
+
},
|
|
118
|
+
async write(sessionId, ref, expectedRevision, next) {
|
|
119
|
+
if (ref !== expectedRevision)
|
|
120
|
+
return { written: false };
|
|
121
|
+
const conversation = storedConversation.parse(next);
|
|
122
|
+
const sealed = await sealSecret({
|
|
123
|
+
plaintext: JSON.stringify(conversation),
|
|
124
|
+
key: sealKey,
|
|
125
|
+
binding: [sessionId],
|
|
126
|
+
keyVersion: options.keyVersion,
|
|
127
|
+
});
|
|
128
|
+
return { written: true, ref: sealed, revision: sealed };
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|