@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
|
@@ -1,75 +1,12 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { clientFailure } from "../client/failure.js";
|
|
3
1
|
import { useDescribed } from "./use-described.js";
|
|
2
|
+
import { useOneCall } from "./use-one-call.js";
|
|
3
|
+
const decideWith = (transport, signal, state, assets) => transport.decide(state, signal, assets);
|
|
4
4
|
/**
|
|
5
5
|
* One decision profile: describe it once, then `decide(state)` as often as
|
|
6
6
|
* needed. A new decision aborts the one still running.
|
|
7
7
|
*/
|
|
8
8
|
export function useTheoremDecision(transport) {
|
|
9
9
|
const { iface, describeFailure } = useDescribed(transport);
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
status: 'idle',
|
|
13
|
-
result: null,
|
|
14
|
-
elapsedMs: null,
|
|
15
|
-
failure: null,
|
|
16
|
-
});
|
|
17
|
-
const running = useRef(null);
|
|
18
|
-
useEffect(() => () => running.current?.abort(), [transport]);
|
|
19
|
-
const cancel = useCallback(() => {
|
|
20
|
-
running.current?.abort();
|
|
21
|
-
running.current = null;
|
|
22
|
-
setRun((previous) => previous.status === 'deciding'
|
|
23
|
-
? { ...previous, status: previous.result ? 'done' : 'idle' }
|
|
24
|
-
: previous);
|
|
25
|
-
}, []);
|
|
26
|
-
const decide = useCallback(async (state) => {
|
|
27
|
-
running.current?.abort();
|
|
28
|
-
const controller = new AbortController();
|
|
29
|
-
running.current = controller;
|
|
30
|
-
setRun((previous) => ({
|
|
31
|
-
transport,
|
|
32
|
-
status: 'deciding',
|
|
33
|
-
result: previous.transport === transport ? previous.result : null,
|
|
34
|
-
elapsedMs: previous.transport === transport ? previous.elapsedMs : null,
|
|
35
|
-
failure: null,
|
|
36
|
-
}));
|
|
37
|
-
const started = performance.now();
|
|
38
|
-
try {
|
|
39
|
-
const reply = await transport.decide(state, controller.signal);
|
|
40
|
-
if (controller.signal.aborted)
|
|
41
|
-
return;
|
|
42
|
-
for (const record of reply.traces ?? [])
|
|
43
|
-
transport.traces?.push(record);
|
|
44
|
-
setRun({
|
|
45
|
-
transport,
|
|
46
|
-
status: 'done',
|
|
47
|
-
result: reply.result,
|
|
48
|
-
elapsedMs: performance.now() - started,
|
|
49
|
-
failure: null,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
if (controller.signal.aborted)
|
|
54
|
-
return;
|
|
55
|
-
setRun((previous) => ({ ...previous, status: 'error', failure: clientFailure(err) }));
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
if (running.current === controller)
|
|
59
|
-
running.current = null;
|
|
60
|
-
}
|
|
61
|
-
}, [transport]);
|
|
62
|
-
const visible = run.transport === transport
|
|
63
|
-
? run
|
|
64
|
-
: { status: 'idle', result: null, elapsedMs: null, failure: null };
|
|
65
|
-
return {
|
|
66
|
-
iface,
|
|
67
|
-
describeFailure,
|
|
68
|
-
status: visible.status,
|
|
69
|
-
result: visible.result,
|
|
70
|
-
elapsedMs: visible.elapsedMs,
|
|
71
|
-
failure: visible.failure,
|
|
72
|
-
decide,
|
|
73
|
-
cancel,
|
|
74
|
-
};
|
|
10
|
+
const { call, ...run } = useOneCall(transport, 'deciding', decideWith);
|
|
11
|
+
return { iface, describeFailure, ...run, decide: call };
|
|
75
12
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EmbeddingInput, EmbeddingResult } from '@theoremjs/agents';
|
|
2
|
+
import type { EmbeddingInterface, EmbeddingTransport, EmbedOptions } from '../client/embedding-transport.ts';
|
|
3
|
+
import type { ClientFailure } from '../client/failure.ts';
|
|
4
|
+
import { type OneCallState } from './use-one-call.ts';
|
|
5
|
+
export type TheoremEmbeddingState = {
|
|
6
|
+
/** The profile as the page sees it; null until the host describes it. */
|
|
7
|
+
iface: EmbeddingInterface | null;
|
|
8
|
+
/** Why the host could not describe the profile. */
|
|
9
|
+
describeFailure: ClientFailure | null;
|
|
10
|
+
} & OneCallState<'embedding', EmbeddingResult>;
|
|
11
|
+
/**
|
|
12
|
+
* One embedding profile: describe it once, then `embed(inputs)` as often as
|
|
13
|
+
* needed. A new embedding aborts the one still running.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useTheoremEmbedding(transport: EmbeddingTransport): TheoremEmbeddingState & {
|
|
16
|
+
embed: (inputs: readonly EmbeddingInput[], options?: EmbedOptions) => Promise<void>;
|
|
17
|
+
cancel: () => void;
|
|
18
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useDescribed } from "./use-described.js";
|
|
2
|
+
import { useOneCall } from "./use-one-call.js";
|
|
3
|
+
const embedWith = (transport, signal, inputs, options) => transport.embed(inputs, options, signal);
|
|
4
|
+
/**
|
|
5
|
+
* One embedding profile: describe it once, then `embed(inputs)` as often as
|
|
6
|
+
* needed. A new embedding aborts the one still running.
|
|
7
|
+
*/
|
|
8
|
+
export function useTheoremEmbedding(transport) {
|
|
9
|
+
const { iface, describeFailure } = useDescribed(transport);
|
|
10
|
+
const { call, ...run } = useOneCall(transport, 'embedding', embedWith);
|
|
11
|
+
return { iface, describeFailure, ...run, embed: call };
|
|
12
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { TurnEvent } from '@theoremjs/agents';
|
|
|
2
2
|
import { type ClientFailure } from '../client/failure.ts';
|
|
3
3
|
import type { ToolCall } from '../client/host-call.ts';
|
|
4
4
|
import type { HostInterface, HostTransport } from '../client/host-transport.ts';
|
|
5
|
-
import {
|
|
5
|
+
import type { PageTools } from '../client/live/live-page-tool.ts';
|
|
6
|
+
import { type PersonGateResolution } from '../client/tool-resume.ts';
|
|
6
7
|
export type { ToolCall };
|
|
7
8
|
/** One call of one of the host's tools, as it streams. */
|
|
8
9
|
export type TheoremHostCall = {
|
|
@@ -20,7 +21,7 @@ export type TheoremHostCall = {
|
|
|
20
21
|
/** The call never reached its tool, or its stream broke. */
|
|
21
22
|
failure: ClientFailure | null;
|
|
22
23
|
/** The answer on its way to the gate the call paused on. */
|
|
23
|
-
answering:
|
|
24
|
+
answering: PersonGateResolution['action'] | null;
|
|
24
25
|
startedAt: number;
|
|
25
26
|
/** Round trip, once it settles or pauses. */
|
|
26
27
|
elapsedMs: number | null;
|
|
@@ -37,8 +38,8 @@ export type TheoremHostState = {
|
|
|
37
38
|
* as often as needed, and answer the gates a call pauses on. `run` returns the
|
|
38
39
|
* call's id at once; calls run side by side, and `cancel` stops one.
|
|
39
40
|
*/
|
|
40
|
-
export declare function useTheoremHost(
|
|
41
|
+
export declare function useTheoremHost(source: HostTransport, pageTools?: PageTools): TheoremHostState & {
|
|
41
42
|
run: (name: string, input: unknown) => string;
|
|
42
|
-
answer: (id: string, resolution:
|
|
43
|
+
answer: (id: string, resolution: PersonGateResolution) => Promise<void>;
|
|
43
44
|
cancel: (id: string) => void;
|
|
44
45
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
1
|
import { toolCallsOf, toolSnapshotFromEvents } from '@theoremjs/agents/interface';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { clientFailure } from "../client/failure.js";
|
|
4
|
+
import { withPageToolDispatch } from "../client/page-dispatch.js";
|
|
4
5
|
import { continueGatedToolInvocation } from "../client/tool-resume.js";
|
|
6
|
+
import { issuedOperationId } from "../client/turn-client.js";
|
|
5
7
|
import { useDescribed } from "./use-described.js";
|
|
6
8
|
function callId() {
|
|
7
9
|
return typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
@@ -18,11 +20,11 @@ function withEvent(call, event) {
|
|
|
18
20
|
* as often as needed, and answer the gates a call pauses on. `run` returns the
|
|
19
21
|
* call's id at once; calls run side by side, and `cancel` stops one.
|
|
20
22
|
*/
|
|
21
|
-
export function useTheoremHost(
|
|
23
|
+
export function useTheoremHost(source, pageTools) {
|
|
24
|
+
const transport = useMemo(() => withPageToolDispatch(source, pageTools), [source, pageTools]);
|
|
22
25
|
const { iface, describeFailure } = useDescribed(transport);
|
|
23
26
|
const [calls, setCalls] = useState([]);
|
|
24
27
|
const running = useRef(new Map());
|
|
25
|
-
// Tools the user allowed for the rest of the page, for a host that replays paused calls.
|
|
26
28
|
const permissions = useRef([]);
|
|
27
29
|
const latest = useRef(calls);
|
|
28
30
|
latest.current = calls;
|
|
@@ -48,11 +50,22 @@ export function useTheoremHost(transport) {
|
|
|
48
50
|
return;
|
|
49
51
|
update(id, (call) => withEvent(call, event));
|
|
50
52
|
}, controller.signal);
|
|
51
|
-
update(id, (call) => ({
|
|
53
|
+
update(id, (call) => ({
|
|
54
|
+
...call,
|
|
55
|
+
isRunning: false,
|
|
56
|
+
answering: null,
|
|
57
|
+
elapsedMs: performance.now() - started,
|
|
58
|
+
}));
|
|
52
59
|
}
|
|
53
60
|
catch (err) {
|
|
54
61
|
const failure = controller.signal.aborted ? null : clientFailure(err);
|
|
55
|
-
update(id, (call) => ({
|
|
62
|
+
update(id, (call) => ({
|
|
63
|
+
...call,
|
|
64
|
+
isRunning: false,
|
|
65
|
+
answering: null,
|
|
66
|
+
failure,
|
|
67
|
+
elapsedMs: performance.now() - started,
|
|
68
|
+
}));
|
|
56
69
|
}
|
|
57
70
|
finally {
|
|
58
71
|
if (running.current.get(id) === controller)
|
|
@@ -86,6 +99,7 @@ export function useTheoremHost(transport) {
|
|
|
86
99
|
if (!held?.call || state?.phase !== 'gate')
|
|
87
100
|
return;
|
|
88
101
|
const gateId = held.call.callId;
|
|
102
|
+
const operationId = issuedOperationId(held.call);
|
|
89
103
|
const reply = continueGatedToolInvocation({
|
|
90
104
|
toolName: held.name,
|
|
91
105
|
gate: state.gate,
|
|
@@ -98,12 +112,16 @@ export function useTheoremHost(transport) {
|
|
|
98
112
|
update(id, (call) => ({ ...call, answering: resolution.action }));
|
|
99
113
|
await stream(id, (onEvent, signal) => transport.invoke({
|
|
100
114
|
gateId,
|
|
115
|
+
operationId,
|
|
101
116
|
decision: reply.decision,
|
|
102
|
-
...(reply.decision === 'approve' && reply.secret !== undefined
|
|
103
|
-
|
|
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.
|
|
104
121
|
replay: {
|
|
105
122
|
name: held.name,
|
|
106
|
-
input: held.input,
|
|
123
|
+
input: state.gate.preparedArguments ?? held.input,
|
|
124
|
+
gateKind: state.gate.kind,
|
|
107
125
|
sessionPermissions: permissions.current,
|
|
108
126
|
...(snapshot ? { snapshot } : {}),
|
|
109
127
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ProfileInterface } from '@theoremjs/agents/interface';
|
|
2
|
-
import {
|
|
2
|
+
import type { ClientFailure } from '../client/failure.ts';
|
|
3
3
|
import type { TheoremTransport } from '../client/transport.ts';
|
|
4
|
-
/** `failure` is worded with the default
|
|
4
|
+
/** `failure` is worded with the default labels: the profile's is not known until it loads. */
|
|
5
5
|
export type TheoremInterfaceState = {
|
|
6
6
|
status: 'loading';
|
|
7
7
|
iface: null;
|
|
@@ -15,5 +15,8 @@ export type TheoremInterfaceState = {
|
|
|
15
15
|
iface: null;
|
|
16
16
|
failure: ClientFailure;
|
|
17
17
|
};
|
|
18
|
-
/**
|
|
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
|
+
*/
|
|
19
22
|
export declare function useTheoremInterface(transport: TheoremTransport): TheoremInterfaceState;
|
|
@@ -1,25 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useDescribed } from "./use-described.js";
|
|
3
3
|
const LOADING = { status: 'loading', iface: null, failure: null };
|
|
4
|
-
/**
|
|
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
|
+
*/
|
|
5
8
|
export function useTheoremInterface(transport) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
transport.describe(controller.signal).then((iface) => {
|
|
13
|
-
if (!controller.signal.aborted)
|
|
14
|
-
setState({ status: 'ready', iface, failure: null });
|
|
15
|
-
}, (err) => {
|
|
16
|
-
if (controller.signal.aborted)
|
|
17
|
-
return;
|
|
18
|
-
setState({ status: 'error', iface: null, failure: clientFailure(err) });
|
|
19
|
-
});
|
|
20
|
-
return () => {
|
|
21
|
-
controller.abort();
|
|
22
|
-
};
|
|
23
|
-
}, [transport]);
|
|
24
|
-
return state;
|
|
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]);
|
|
25
15
|
}
|
package/index.d.ts
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - `@theoremjs/react` hooks + transport (this entry)
|
|
5
5
|
* - `@theoremjs/react/ui` Astryx chat UI built on these hooks
|
|
6
|
-
* - `@theoremjs/react/server` `createTheoremHandler` (and `createTheoremDecisionHandler`, `createTheoremHostHandler`) for the host side
|
|
6
|
+
* - `@theoremjs/react/server` `createTheoremHandler` (and `createTheoremDecisionHandler`, `createTheoremEmbeddingHandler`, `createTheoremHostHandler`) for the host side
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
export * from './client/index.ts';
|
|
11
|
-
export { type
|
|
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';
|
|
12
14
|
export { type TheoremDecisionState, useTheoremDecision } from './hooks/use-theorem-decision.ts';
|
|
13
|
-
export { type
|
|
15
|
+
export { type TheoremEmbeddingState, useTheoremEmbedding, } from './hooks/use-theorem-embedding.ts';
|
|
16
|
+
export { type TheoremHostCall, type TheoremHostState, useTheoremHost, } from './hooks/use-theorem-host.ts';
|
|
14
17
|
export { type TheoremInterfaceState, useTheoremInterface } from './hooks/use-theorem-interface.ts';
|
package/index.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - `@theoremjs/react` hooks + transport (this entry)
|
|
5
5
|
* - `@theoremjs/react/ui` Astryx chat UI built on these hooks
|
|
6
|
-
* - `@theoremjs/react/server` `createTheoremHandler` (and `createTheoremDecisionHandler`, `createTheoremHostHandler`) for the host side
|
|
6
|
+
* - `@theoremjs/react/server` `createTheoremHandler` (and `createTheoremDecisionHandler`, `createTheoremEmbeddingHandler`, `createTheoremHostHandler`) for the host side
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
export * from "./client/index.js";
|
|
11
|
+
export { withPageToolDispatch } from "./client/page-dispatch.js";
|
|
12
|
+
export { useEmbeddingIndex, } from "./hooks/use-embedding-index.js";
|
|
11
13
|
export { useTheoremChat, } from "./hooks/use-theorem-chat.js";
|
|
12
14
|
export { useTheoremDecision } from "./hooks/use-theorem-decision.js";
|
|
13
|
-
export {
|
|
15
|
+
export { useTheoremEmbedding, } from "./hooks/use-theorem-embedding.js";
|
|
16
|
+
export { useTheoremHost, } from "./hooks/use-theorem-host.js";
|
|
14
17
|
export { useTheoremInterface } from "./hooks/use-theorem-interface.js";
|
package/live.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
+
export type { LiveCallOptions, PageTool, PageToolAnswer, PageTools, } from './client/live/live-page-tool.ts';
|
|
6
7
|
export type { LiveConnection } from './client/live-client.ts';
|
|
7
8
|
export type { LiveRunnerProps } from './ui/LiveRunner.tsx';
|
|
8
9
|
export { LiveRunner } from './ui/LiveRunner.tsx';
|
package/live.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoremjs/react",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "React bindings for Theorem: headless hooks + transport, an Astryx chat UI, and a Web-standard server handler.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/masudl-hub/theoremai",
|
|
12
|
+
"url": "git+https://github.com/masudl-hub/theoremai.git",
|
|
13
13
|
"directory": "react"
|
|
14
14
|
},
|
|
15
15
|
"sideEffects": [
|
|
@@ -49,17 +49,18 @@
|
|
|
49
49
|
"./styles/*": "./styles/*"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
+
"@speed-highlight/core": "2.1.0",
|
|
52
53
|
"@tabler/icons-react": "^3.46.0",
|
|
53
54
|
"intl-messageformat": "^11.2.9",
|
|
54
55
|
"recharts": "^3.10.1"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
|
-
"@astryxdesign/core": "0.
|
|
58
|
-
"@astryxdesign/theme-neutral": "0.
|
|
58
|
+
"@astryxdesign/core": "0.7.0",
|
|
59
|
+
"@astryxdesign/theme-neutral": "0.7.0",
|
|
59
60
|
"@stylexjs/stylex": "0.19.0",
|
|
60
61
|
"react": "^19.0.0",
|
|
61
62
|
"react-dom": "^19.0.0",
|
|
62
|
-
"@theoremjs/agents": "^0.3.
|
|
63
|
+
"@theoremjs/agents": "^0.3.1"
|
|
63
64
|
},
|
|
64
65
|
"peerDependenciesMeta": {
|
|
65
66
|
"@astryxdesign/core": {
|
|
@@ -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
|
+
}
|