@theoremjs/react 0.3.4-next.649.gf83cea1 → 0.3.4-next.671.gfa09ab2
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 +2 -2
- package/components/live/use-live-session-client.d.ts +1 -1
- package/components/live/use-live-session-client.js +24 -35
- package/hooks/use-theorem-chat.d.ts +5 -3
- package/hooks/use-theorem-chat.js +2 -0
- package/index.d.ts +1 -1
- package/package.json +3 -3
- package/ui/TheoremChat.d.ts +9 -4
- package/ui/TheoremChat.js +3 -2
- package/client/live/run-live-tool-call.d.ts +0 -20
- package/client/live/run-live-tool-call.js +0 -70
package/README.md
CHANGED
|
@@ -203,8 +203,8 @@ A profile whose `providerContinuation.history` is `'stateless'` keeps no convers
|
|
|
203
203
|
|
|
204
204
|
### Send a message from your code
|
|
205
205
|
|
|
206
|
-
`chatRef` gives you `send(text,
|
|
207
|
-
`chatRef` also gives you `stop()`. It stops the reply that streams, as the composer's stop button does.
|
|
206
|
+
`chatRef` gives you `send(text, options?)`. It sends the text as the user. `options` can add `files` and `voice`, arrays of browser files, which use the same profile validation and encoding as composer attachments. It can also name a `model` and its `effort` for the turn. The model becomes the chat's pick, as if chosen in the composer. A model or file the profile does not take is refused, as the composer refuses it, and the refusal shows in the chat. When the reply is complete, it returns the blocks that the turn added. It returns `null` if the chat cannot accept a message now. This happens while a reply streams or waits for a gate.
|
|
207
|
+
`chatRef` also gives you `stop()`. It stops the reply that streams, as the composer's stop button does. It gives you `focus()`, which puts the keyboard in the composer, for example after your own control clears the chat. And it gives you `controller`, the chat's `ChatController`, for a host that drives the chat from outside, as a composer shared by several chats does.
|
|
208
208
|
|
|
209
209
|
```tsx
|
|
210
210
|
const chat = useRef<TheoremChatHandle>(null);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SessionEventOf, TurnEvent } from '@theoremjs/agents';
|
|
2
2
|
import type { LiveCallOptions, LiveConnectPhase, LiveGateAnswer, LiveSessionStatus, LiveToolGatePrompt, TraceFeed } from '@theoremjs/web/client';
|
|
3
3
|
import { type LiveConnection, LiveSessionClient } from '@theoremjs/web/client';
|
|
4
4
|
import { type Dispatch, type MutableRefObject, type SetStateAction } from 'react';
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { LiveSessionClient } from '@theoremjs/web/client';
|
|
1
|
+
import { continueGatedToolInvocation, LiveSessionClient, } from '@theoremjs/web/client';
|
|
3
2
|
import { useCallback, useRef, } from 'react';
|
|
4
3
|
import { applyLiveTranscript } from "../../client/live/live-captions.js";
|
|
5
|
-
import { runLiveToolCall } from "../../client/live/run-live-tool-call.js";
|
|
6
4
|
function onLiveStatusChange(next, bindings) {
|
|
7
5
|
bindings.setStatus(next);
|
|
8
6
|
if (next === 'listening' || next === 'ready') {
|
|
@@ -26,38 +24,22 @@ function onLiveTranscript(text, isUser, meta, bindings) {
|
|
|
26
24
|
// why: A thought or a tool call in the same batch is already queued; each folds onto the last.
|
|
27
25
|
bindings.setCaptions((prev) => applyLiveTranscript(prev, text, isUser, meta?.interim));
|
|
28
26
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
bindings.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
toolArgs,
|
|
44
|
-
callId: meta.callId,
|
|
45
|
-
operationId: meta.operationId,
|
|
46
|
-
sessionPermissions: bindings.sessionPermissionsRef.current,
|
|
47
|
-
setSessionPermissions: (next) => {
|
|
48
|
-
bindings.sessionPermissionsRef.current = next;
|
|
49
|
-
bindings.setSessionPermissions(next);
|
|
50
|
-
},
|
|
51
|
-
waitForGateDecision: bindings.waitForGateDecision,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
bindings.reportFailure(err);
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
if (!bindings.gatePromptRef.current)
|
|
59
|
-
bindings.setActiveTool(null);
|
|
27
|
+
/** The user's answer to a gate, as the session takes it; an approval records the permissions it grants. */
|
|
28
|
+
async function onLiveGate(prompt, bindings) {
|
|
29
|
+
const resolution = await bindings.waitForGateDecision(prompt);
|
|
30
|
+
if (resolution === 'withdrawn')
|
|
31
|
+
return 'withdrawn';
|
|
32
|
+
const reply = continueGatedToolInvocation({
|
|
33
|
+
toolName: prompt.toolName,
|
|
34
|
+
gate: prompt.gate,
|
|
35
|
+
sessionPermissions: bindings.sessionPermissionsRef.current,
|
|
36
|
+
resolution,
|
|
37
|
+
});
|
|
38
|
+
if (reply.decision === 'approve') {
|
|
39
|
+
bindings.sessionPermissionsRef.current = reply.sessionPermissions;
|
|
40
|
+
bindings.setSessionPermissions(reply.sessionPermissions);
|
|
60
41
|
}
|
|
42
|
+
return reply;
|
|
61
43
|
}
|
|
62
44
|
/** Owns LiveSessionClient construction + reconnect identity. */
|
|
63
45
|
export function useLiveSessionClient(bindings) {
|
|
@@ -91,6 +73,9 @@ export function useLiveSessionClient(bindings) {
|
|
|
91
73
|
bindingsRef.current.reportFailure(event.error);
|
|
92
74
|
return;
|
|
93
75
|
}
|
|
76
|
+
// why: A new call from the model clears the last failure.
|
|
77
|
+
if (event.type === 'tool' && event.tool.phase === undefined)
|
|
78
|
+
bindingsRef.current.clearFailure();
|
|
94
79
|
bindingsRef.current.handleLiveTurnEvent(event);
|
|
95
80
|
},
|
|
96
81
|
onTrace: (record) => {
|
|
@@ -111,7 +96,11 @@ export function useLiveSessionClient(bindings) {
|
|
|
111
96
|
onSessionEnded: (session) => {
|
|
112
97
|
bindingsRef.current.reportSessionEnded(session);
|
|
113
98
|
},
|
|
114
|
-
|
|
99
|
+
// why: Read at each call, so a handler the host renders anew is the one that answers.
|
|
100
|
+
get pageTools() {
|
|
101
|
+
return bindingsRef.current.pageTools;
|
|
102
|
+
},
|
|
103
|
+
onGate: (prompt) => onLiveGate(prompt, bindingsRef.current),
|
|
115
104
|
});
|
|
116
105
|
clientRef.current = client;
|
|
117
106
|
return client;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ChatOptions, type SentTurn, type ToolDecisionAction } from '@theoremjs/web/client';
|
|
2
|
-
export type { SentTurn };
|
|
1
|
+
import { type ChatOptions, type SendTextOptions, type SentTurn, type ToolDecisionAction } from '@theoremjs/web/client';
|
|
2
|
+
export type { SendTextOptions, SentTurn };
|
|
3
3
|
export type UseTheoremChatOptions = ChatOptions;
|
|
4
4
|
/**
|
|
5
5
|
* Headless chat model: transcript, streaming, composer drafts, pending
|
|
@@ -32,7 +32,9 @@ export declare function useTheoremChat(options: UseTheoremChatOptions): {
|
|
|
32
32
|
modelId: string;
|
|
33
33
|
effort?: string;
|
|
34
34
|
}) => void;
|
|
35
|
-
sendText: (text: string,
|
|
35
|
+
sendText: (text: string, options?: SendTextOptions) => Promise<SentTurn | null>;
|
|
36
|
+
/** The controller under it all, for a host that drives the chat from outside. */
|
|
37
|
+
controller: import("@theoremjs/web/client").ChatController;
|
|
36
38
|
iface: import("../../../src/interface/types").ComposerProfileInterface | null;
|
|
37
39
|
blocks: import("../../../src/interface/types").TranscriptBlock[];
|
|
38
40
|
streamBlocks: import("../../../src/interface/types").TranscriptBlock[];
|
package/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export * from '@theoremjs/web/client';
|
|
11
11
|
export { type EmbeddingIndex, type IndexProgress, useEmbeddingIndex, } from './hooks/use-embedding-index.ts';
|
|
12
|
-
export { type SentTurn, type UseTheoremChatOptions, useTheoremChat, } from './hooks/use-theorem-chat.ts';
|
|
12
|
+
export { type SendTextOptions, type SentTurn, type UseTheoremChatOptions, useTheoremChat, } from './hooks/use-theorem-chat.ts';
|
|
13
13
|
export { type TheoremDecisionState, useTheoremDecision } from './hooks/use-theorem-decision.ts';
|
|
14
14
|
export { type TheoremEmbeddingState, useTheoremEmbedding, } from './hooks/use-theorem-embedding.ts';
|
|
15
15
|
export { type TheoremHostCall, type TheoremHostState, useTheoremHost, } from './hooks/use-theorem-host.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoremjs/react",
|
|
3
|
-
"version": "0.3.4-next.
|
|
3
|
+
"version": "0.3.4-next.671.gfa09ab2",
|
|
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",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"@stylexjs/stylex": "0.19.0",
|
|
61
61
|
"react": "^19.0.0",
|
|
62
62
|
"react-dom": "^19.0.0",
|
|
63
|
-
"@theoremjs/agents": "^0.3.4-next.
|
|
64
|
-
"@theoremjs/web": "^0.3.4-next.
|
|
63
|
+
"@theoremjs/agents": "^0.3.4-next.671.gfa09ab2",
|
|
64
|
+
"@theoremjs/web": "^0.3.4-next.671.gfa09ab2"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@astryxdesign/core": {
|
package/ui/TheoremChat.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { DefinedTheme } from '@astryxdesign/core/theme';
|
|
2
2
|
import { type ComposerPendingMessage } from '@theoremjs/agents/interface';
|
|
3
|
-
import type { PageTools } from '@theoremjs/web/client';
|
|
3
|
+
import type { ChatController, PageTools } from '@theoremjs/web/client';
|
|
4
4
|
import { type ConversationView, type HttpTransportOptions, type TheoremTransport } from '@theoremjs/web/client';
|
|
5
5
|
import { type ReactNode, type Ref } from 'react';
|
|
6
|
-
import { type SentTurn, type UseTheoremChatOptions } from '../hooks/use-theorem-chat.ts';
|
|
6
|
+
import { type SendTextOptions, type SentTurn, type UseTheoremChatOptions } from '../hooks/use-theorem-chat.ts';
|
|
7
7
|
import type { TheoremLabels } from './labels.ts';
|
|
8
8
|
export type TheoremChatProps = {
|
|
9
9
|
/** Where `createTheoremHandler` is mounted. Default `/api/theorem`. Ignored when `transport` is set. */
|
|
@@ -112,14 +112,19 @@ export type TheoremChatProps = {
|
|
|
112
112
|
/** What a host can do to a mounted {@link TheoremChat}. */
|
|
113
113
|
export type TheoremChatHandle = {
|
|
114
114
|
/**
|
|
115
|
-
* Sends `text
|
|
115
|
+
* Sends `text`, and what `options` adds, as the user, and resolves with the blocks the turn added once the reply is done;
|
|
116
116
|
* `null` when the chat can't take a message now (a reply is streaming, or waits on a gate).
|
|
117
117
|
*/
|
|
118
|
-
send: (text: string,
|
|
118
|
+
send: (text: string, options?: SendTextOptions) => Promise<SentTurn | null>;
|
|
119
119
|
/** Stops the reply that is streaming, as the composer's stop button does. */
|
|
120
120
|
stop: () => void;
|
|
121
121
|
/** Puts the keyboard in the composer. */
|
|
122
122
|
focus: () => void;
|
|
123
|
+
/**
|
|
124
|
+
* The chat's controller, for a host that drives it from outside, as a composer shared by
|
|
125
|
+
* several chats does. See `createChatController` in `@theoremjs/web/client`.
|
|
126
|
+
*/
|
|
127
|
+
controller: ChatController;
|
|
123
128
|
};
|
|
124
129
|
/**
|
|
125
130
|
* 50% of the chat's width, but never narrower than 640px (or the full width,
|
package/ui/TheoremChat.js
CHANGED
|
@@ -69,7 +69,7 @@ function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth
|
|
|
69
69
|
pageTools,
|
|
70
70
|
onTurnEvent,
|
|
71
71
|
});
|
|
72
|
-
const { sendText, handleStop } = chat;
|
|
72
|
+
const { sendText, handleStop, controller } = chat;
|
|
73
73
|
const blocks = useMemo(() => [...chat.blocks, ...chat.streamBlocks], [chat.blocks, chat.streamBlocks]);
|
|
74
74
|
const handle = t('@theorem.agent.handle', { handle: iface.identity.handle });
|
|
75
75
|
const landing = blocks.length === 0;
|
|
@@ -80,7 +80,8 @@ function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth
|
|
|
80
80
|
focus: () => {
|
|
81
81
|
inputRef.current?.focus();
|
|
82
82
|
},
|
|
83
|
-
|
|
83
|
+
controller,
|
|
84
|
+
}), [sendText, handleStop, controller]);
|
|
84
85
|
const composerRef = useGlide(landing, () => inputRef.current?.focus());
|
|
85
86
|
const inspector = useTraceInspector(iface, transport.traces, trace);
|
|
86
87
|
const header = _jsx(SidePanelHeader, { floating: true, children: inspector.toggle });
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { ExecuteToolOnRelay, LiveGateAnswer, LiveToolGatePrompt, PageTools } from '@theoremjs/web/client';
|
|
2
|
-
/**
|
|
3
|
-
* Live provider tool-call handler: the page answers the call if it is the
|
|
4
|
-
* page's; else run the model's call on the relay; at a gate, ask the user and
|
|
5
|
-
* send their decision. The session answers the model
|
|
6
|
-
* however the call ends; its tool events tell the user. A call the model
|
|
7
|
-
* cancels while its gate is open is gone: nothing is sent.
|
|
8
|
-
*/
|
|
9
|
-
export declare function runLiveToolCall(args: {
|
|
10
|
-
executeToolOnRelay: ExecuteToolOnRelay;
|
|
11
|
-
/** The host's page tools, if it has any. */
|
|
12
|
-
pageTools?: PageTools;
|
|
13
|
-
name: string;
|
|
14
|
-
toolArgs: Record<string, unknown>;
|
|
15
|
-
callId: string;
|
|
16
|
-
operationId: string;
|
|
17
|
-
sessionPermissions: string[];
|
|
18
|
-
setSessionPermissions: (next: string[]) => void;
|
|
19
|
-
waitForGateDecision: (prompt: LiveToolGatePrompt) => Promise<LiveGateAnswer>;
|
|
20
|
-
}): Promise<void>;
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { continueGatedToolInvocation } from '@theoremjs/web/client';
|
|
2
|
-
/**
|
|
3
|
-
* Live provider tool-call handler: the page answers the call if it is the
|
|
4
|
-
* page's; else run the model's call on the relay; at a gate, ask the user and
|
|
5
|
-
* send their decision. The session answers the model
|
|
6
|
-
* however the call ends; its tool events tell the user. A call the model
|
|
7
|
-
* cancels while its gate is open is gone: nothing is sent.
|
|
8
|
-
*/
|
|
9
|
-
export async function runLiveToolCall(args) {
|
|
10
|
-
const { executeToolOnRelay, name, toolArgs, callId, operationId } = args;
|
|
11
|
-
let sessionPermissions = args.sessionPermissions;
|
|
12
|
-
// why: A name from the model is looked up among the host's own keys only.
|
|
13
|
-
const pageTool = args.pageTools && Object.hasOwn(args.pageTools, name) ? args.pageTools[name] : undefined;
|
|
14
|
-
let step = await executeToolOnRelay({ callId, operationId });
|
|
15
|
-
const dispatched = new Set();
|
|
16
|
-
while (step.status !== 'settled') {
|
|
17
|
-
if (step.status === 'dispatched') {
|
|
18
|
-
if (dispatched.has(step.grantId))
|
|
19
|
-
return;
|
|
20
|
-
dispatched.add(step.grantId);
|
|
21
|
-
const grantId = step.grantId;
|
|
22
|
-
const input = step.arguments;
|
|
23
|
-
const answer = pageTool
|
|
24
|
-
? await Promise.resolve()
|
|
25
|
-
.then(() => pageTool(input, { callId, operationId }))
|
|
26
|
-
.catch(() => undefined)
|
|
27
|
-
: undefined;
|
|
28
|
-
step = await executeToolOnRelay({
|
|
29
|
-
callId,
|
|
30
|
-
operationId,
|
|
31
|
-
grantId,
|
|
32
|
-
...(answer ? { output: answer.output } : { unanswered: true }),
|
|
33
|
-
});
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
const { gate } = step;
|
|
37
|
-
if (gate.kind === 'page') {
|
|
38
|
-
await executeToolOnRelay({ callId, operationId, decision: 'deny' });
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const resolution = await args.waitForGateDecision({
|
|
42
|
-
callId,
|
|
43
|
-
toolName: name,
|
|
44
|
-
input: gate.preparedArguments ?? toolArgs,
|
|
45
|
-
gate,
|
|
46
|
-
});
|
|
47
|
-
if (resolution === 'withdrawn')
|
|
48
|
-
return;
|
|
49
|
-
const reply = continueGatedToolInvocation({
|
|
50
|
-
toolName: name,
|
|
51
|
-
gate,
|
|
52
|
-
sessionPermissions,
|
|
53
|
-
resolution,
|
|
54
|
-
});
|
|
55
|
-
if (reply.decision === 'deny') {
|
|
56
|
-
// why: The session settles the refusal; its tool event tells the user.
|
|
57
|
-
await executeToolOnRelay({ callId, operationId, decision: 'deny' });
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
sessionPermissions = reply.sessionPermissions;
|
|
61
|
-
args.setSessionPermissions(reply.sessionPermissions);
|
|
62
|
-
// why: Signed in: a typed key goes once, with the approval; after an OAuth callback there is none.
|
|
63
|
-
step = await executeToolOnRelay({
|
|
64
|
-
callId,
|
|
65
|
-
decision: 'approve',
|
|
66
|
-
operationId,
|
|
67
|
-
...(reply.secret !== undefined ? { secret: reply.secret } : {}),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|