@theoremjs/web 0.3.4-next.548.gbb156b2 → 0.3.4-next.550.gf20b9cd
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 +1 -0
- package/client/decision-transport.d.ts +18 -3
- package/client/decision-transport.js +24 -17
- package/client/index.d.ts +1 -1
- package/client/index.js +1 -1
- package/package.json +2 -2
- package/server/decision-handler.d.ts +24 -8
- package/server/decision-handler.js +46 -9
- package/server/handler.d.ts +10 -2
- package/server/handler.js +10 -4
- package/server/mod.d.ts +1 -1
- package/server/mod.js +1 -1
- package/server/turn-wiring.d.ts +31 -0
- package/server/turn-wiring.js +54 -0
package/README.md
CHANGED
|
@@ -236,6 +236,7 @@ const transport = withPageToolDispatch(
|
|
|
236
236
|
| `provider` | Host options, including the credential vault. A function can return these options per request, for example for tenant-specific credentials. |
|
|
237
237
|
| `session` | Returns the session id of the caller, for example `${userId}:${conversationId}`. Return `undefined` to refuse the request with a 401. Default: a random id in an HttpOnly, SameSite=Lax cookie that lasts 30 days. |
|
|
238
238
|
| `host` | App data for the tool handlers (`ctx.host`), such as the signed-in user. |
|
|
239
|
+
| `prepare` | Your app's own turn, from the one the handler builds (`TurnWiring.prepare` from `@theoremjs/agents`). Set what your app sets per turn, such as `system`, `credentials`, `onStage` and `onAgentCall`; read your app data as `turn.host`. The handler keeps its profile, its tool execution and its abort. Your `onStage` runs before the handler's steering, and your `credentials` are read before the session's. |
|
|
239
240
|
| `pageToolBroker` | Delivers results to active page dispatches. Default: bounded process memory. |
|
|
240
241
|
| `conversationStore` | Where the conversation is kept. Required, except for a stateless profile, which must not have one. See [Keep the conversation](#keep-the-conversation). |
|
|
241
242
|
| `sessionStore`, `steerInbox` | Where open gates are kept, and where steers and stops reach a running turn. Default: process memory, which reaches only turns this process runs. On more than one instance, give a shared `SteerInbox`. |
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { type DecisionAsset, type DecisionJson, type DecisionProfile, type DecisionQuestion, type DecisionResult, type TraceRecord } from '@theoremjs/agents';
|
|
8
|
+
import type { JsonSchema } from './schema-fields.ts';
|
|
8
9
|
import type { TraceFeed } from './trace-feed.ts';
|
|
9
10
|
import { type HttpOptions } from './transport.ts';
|
|
10
11
|
/** One question as the page sees it: its answer type and the names of its options or levels. Instructions stay on the host. */
|
|
@@ -24,13 +25,27 @@ export type DecisionInterface = {
|
|
|
24
25
|
};
|
|
25
26
|
contract: string;
|
|
26
27
|
maxStateBytes?: number;
|
|
28
|
+
/** The questions every decision asks. Empty when the host builds them for each decision. */
|
|
27
29
|
questions: DecisionQuestionView[];
|
|
30
|
+
/**
|
|
31
|
+
* Set when the host builds the questions for each decision, from its state and, when `input`
|
|
32
|
+
* is set, from what the page fills in by that JSON Schema. Each result names the questions it answers.
|
|
33
|
+
*/
|
|
34
|
+
build?: {
|
|
35
|
+
input?: JsonSchema;
|
|
36
|
+
};
|
|
28
37
|
/** The host's own images, by the reference its questions write, sent with every decision. */
|
|
29
38
|
images?: Record<string, DecisionAsset>;
|
|
30
39
|
observability?: {
|
|
31
40
|
record: boolean;
|
|
32
41
|
};
|
|
33
42
|
};
|
|
43
|
+
/** A decision's result: the answers and, when the host built its questions for it, those questions. */
|
|
44
|
+
export type DecisionReplyResult = DecisionResult & {
|
|
45
|
+
questions?: DecisionQuestionView[];
|
|
46
|
+
};
|
|
47
|
+
/** Questions as the page sees them: their answer types and labels, never their instructions. */
|
|
48
|
+
export declare function questionViews(questions: Record<string, DecisionQuestion>): DecisionQuestionView[];
|
|
34
49
|
/** The page's view of a decision profile, the questions its host asks, and the host's own images. */
|
|
35
50
|
export declare function decisionInterface(profile: Pick<DecisionProfile, 'identity' | 'decision' | 'inputs' | 'observability'>, questions: Record<string, DecisionQuestion>, images?: Record<string, DecisionAsset>): DecisionInterface;
|
|
36
51
|
/**
|
|
@@ -40,13 +55,13 @@ export declare function decisionInterface(profile: Pick<DecisionProfile, 'identi
|
|
|
40
55
|
export declare function decisionAssets(host: Record<string, DecisionAsset> | undefined, page: Record<string, DecisionAsset> | undefined): Record<string, DecisionAsset> | undefined;
|
|
41
56
|
/** A decision's reply: the answers, and the trace records a host that delivers them sent back. */
|
|
42
57
|
export type DecisionReply = {
|
|
43
|
-
result:
|
|
58
|
+
result: DecisionReplyResult;
|
|
44
59
|
traces?: TraceRecord[];
|
|
45
60
|
};
|
|
46
61
|
export interface DecisionTransport {
|
|
47
62
|
describe(signal?: AbortSignal): Promise<DecisionInterface>;
|
|
48
|
-
/** `assets` are the images the state names, by reference. */
|
|
49
|
-
decide(state: Exclude<DecisionJson, null>, signal?: AbortSignal, assets?: Record<string, DecisionAsset
|
|
63
|
+
/** `assets` are the images the state names, by reference; `input` is what the host's question builder takes. */
|
|
64
|
+
decide(state: Exclude<DecisionJson, null>, signal?: AbortSignal, assets?: Record<string, DecisionAsset>, input?: unknown): Promise<DecisionReply>;
|
|
50
65
|
/** Where the host's trace records land, for the inspector. */
|
|
51
66
|
traces?: TraceFeed;
|
|
52
67
|
}
|
|
@@ -14,6 +14,19 @@ function entryLabel(entry, fallback) {
|
|
|
14
14
|
const entries = Array.isArray(entry) ? entry : Object.keys(entry);
|
|
15
15
|
return entries.length ? entryLabel(entries[0], fallback) : fallback;
|
|
16
16
|
}
|
|
17
|
+
/** Questions as the page sees them: their answer types and labels, never their instructions. */
|
|
18
|
+
export function questionViews(questions) {
|
|
19
|
+
return Object.entries(questions).map(([id, question]) => ({
|
|
20
|
+
id,
|
|
21
|
+
type: question.type,
|
|
22
|
+
labels: question.type === 'choice'
|
|
23
|
+
? Object.keys(question.criteria)
|
|
24
|
+
: question.type === 'score'
|
|
25
|
+
? question.criteria.map((entry, level) => entryLabel(entry, String(level)))
|
|
26
|
+
: [],
|
|
27
|
+
...(question.type === 'box' && question.image !== undefined ? { image: question.image } : {}),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
17
30
|
/** The page's view of a decision profile, the questions its host asks, and the host's own images. */
|
|
18
31
|
export function decisionInterface(profile, questions, images) {
|
|
19
32
|
return {
|
|
@@ -23,16 +36,7 @@ export function decisionInterface(profile, questions, images) {
|
|
|
23
36
|
...(profile.inputs.maxStateBytes === undefined
|
|
24
37
|
? {}
|
|
25
38
|
: { maxStateBytes: profile.inputs.maxStateBytes }),
|
|
26
|
-
questions:
|
|
27
|
-
id,
|
|
28
|
-
type: question.type,
|
|
29
|
-
labels: question.type === 'choice'
|
|
30
|
-
? Object.keys(question.criteria)
|
|
31
|
-
: question.type === 'score'
|
|
32
|
-
? question.criteria.map((entry, level) => entryLabel(entry, String(level)))
|
|
33
|
-
: [],
|
|
34
|
-
...(question.type === 'box' && question.image !== undefined ? { image: question.image } : {}),
|
|
35
|
-
})),
|
|
39
|
+
questions: questionViews(questions),
|
|
36
40
|
...(images && Object.keys(images).length > 0 ? { images } : {}),
|
|
37
41
|
...(profile.observability
|
|
38
42
|
? { observability: { record: resolveObservabilityPolicy(profile.observability).record } }
|
|
@@ -75,9 +79,16 @@ const answerSchema = z.discriminatedUnion('type', [
|
|
|
75
79
|
probabilities,
|
|
76
80
|
}),
|
|
77
81
|
]);
|
|
82
|
+
const questionViewSchema = z.object({
|
|
83
|
+
id: z.string(),
|
|
84
|
+
type: z.enum(['choice', 'noul', 'score', 'box']),
|
|
85
|
+
labels: z.array(z.string()),
|
|
86
|
+
image: z.string().optional(),
|
|
87
|
+
});
|
|
78
88
|
const replySchema = z.object({
|
|
79
89
|
result: z.object({
|
|
80
90
|
model: z.string().min(1),
|
|
91
|
+
questions: z.array(questionViewSchema).optional(),
|
|
81
92
|
answers: z.record(z.string(), answerSchema),
|
|
82
93
|
usage: z
|
|
83
94
|
.object({
|
|
@@ -132,12 +143,8 @@ const describedSchema = z.object({
|
|
|
132
143
|
identity: z.object({ handle: z.string() }),
|
|
133
144
|
contract: z.string(),
|
|
134
145
|
maxStateBytes: z.number().optional(),
|
|
135
|
-
questions: z.array(
|
|
136
|
-
|
|
137
|
-
type: z.enum(['choice', 'noul', 'score', 'box']),
|
|
138
|
-
labels: z.array(z.string()),
|
|
139
|
-
image: z.string().optional(),
|
|
140
|
-
})),
|
|
146
|
+
questions: z.array(questionViewSchema),
|
|
147
|
+
build: z.object({ input: z.record(z.string(), z.unknown()).optional() }).optional(),
|
|
141
148
|
images: z
|
|
142
149
|
.record(z.string(), z.object({ mimeType: z.enum(['image/png', 'image/jpeg']), data: z.string() }))
|
|
143
150
|
.optional(),
|
|
@@ -151,6 +158,6 @@ export function createDecisionTransport(options = {}) {
|
|
|
151
158
|
async describe(signal) {
|
|
152
159
|
return checkWire(describedSchema, await fetchJson(base, { signal }, options), 'the decision description').interface;
|
|
153
160
|
},
|
|
154
|
-
decide: (state, signal, assets) => postDecision(`${base}/decide`, { state, ...(assets ? { assets } : {}) }, signal, options),
|
|
161
|
+
decide: (state, signal, assets, input) => postDecision(`${base}/decide`, { state, ...(assets ? { assets } : {}), ...(input === undefined ? {} : { input }) }, signal, options),
|
|
155
162
|
};
|
|
156
163
|
}
|
package/client/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { type ChatController, type ChatOptions, type ChatSlotDecl, type ChatSlot
|
|
|
3
3
|
export { type ComposerDrawerKind, type ComposerDrawerSummary, composerDrawerSummary, } from './composer-drawer.ts';
|
|
4
4
|
export { type ComposerHint, isStashShortcut, resolveComposerHint, STASH_SHORTCUT, } from './composer-hints.ts';
|
|
5
5
|
export { type ComposerActionState, composerActionState, composerHasPayload, } from './composer-primary.ts';
|
|
6
|
-
export { createDecisionTransport, type DecisionInterface, type DecisionQuestionView, type DecisionReply, type DecisionTransport, decisionAssets, decisionInterface, failedTraces, postDecision, readDecisionReply, withFailedTraces, } from './decision-transport.ts';
|
|
6
|
+
export { createDecisionTransport, type DecisionInterface, type DecisionQuestionView, type DecisionReply, type DecisionReplyResult, type DecisionTransport, decisionAssets, decisionInterface, failedTraces, postDecision, questionViews, readDecisionReply, withFailedTraces, } from './decision-transport.ts';
|
|
7
7
|
export { type ComposerDraftFields, composerFieldsFromDraft } from './decode-composer-draft.ts';
|
|
8
8
|
export { addUsage, checkEmbeddingInputs, checkInputs, cosineSimilarity, type EmbeddingFields, type EmbeddingInputCheck, type EmbeddingMatch, type EmbeddingMediaPart, type EmbeddingSplit, embeddingBatches, fieldsInput, inputFields, MAX_BATCH_INPUTS, rankMatches, readEmbeddingInput, readEmbeddingInputs, readsAsText, splitText, } from './embedding-input.ts';
|
|
9
9
|
export { type EmbeddingSpace, layoutSpace, layoutSpaceSoon, outlierThreshold, placeQuery, type SpaceCluster, type SpaceDocument, type SpaceLink, type SpacePoint, } from './embedding-space.ts';
|
package/client/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { createChatController, } from "./chat.js";
|
|
|
3
3
|
export { composerDrawerSummary, } from "./composer-drawer.js";
|
|
4
4
|
export { isStashShortcut, resolveComposerHint, STASH_SHORTCUT, } from "./composer-hints.js";
|
|
5
5
|
export { composerActionState, composerHasPayload, } from "./composer-primary.js";
|
|
6
|
-
export { createDecisionTransport, decisionAssets, decisionInterface, failedTraces, postDecision, readDecisionReply, withFailedTraces, } from "./decision-transport.js";
|
|
6
|
+
export { createDecisionTransport, decisionAssets, decisionInterface, failedTraces, postDecision, questionViews, readDecisionReply, withFailedTraces, } from "./decision-transport.js";
|
|
7
7
|
export { composerFieldsFromDraft } from "./decode-composer-draft.js";
|
|
8
8
|
export { addUsage, checkEmbeddingInputs, checkInputs, cosineSimilarity, embeddingBatches, fieldsInput, inputFields, MAX_BATCH_INPUTS, rankMatches, readEmbeddingInput, readEmbeddingInputs, readsAsText, splitText, } from "./embedding-input.js";
|
|
9
9
|
export { layoutSpace, layoutSpaceSoon, outlierThreshold, placeQuery, } from "./embedding-space.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoremjs/web",
|
|
3
|
-
"version": "0.3.4-next.
|
|
3
|
+
"version": "0.3.4-next.550.gf20b9cd",
|
|
4
4
|
"description": "Theorem on the web platform, with no UI framework: Web-standard server handlers that serve a profile, and the browser client that talks to them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"zod": "^4.6.5",
|
|
28
|
-
"@theoremjs/agents": "^0.3.4-next.
|
|
28
|
+
"@theoremjs/agents": "^0.3.4-next.550.gf20b9cd"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -4,19 +4,35 @@
|
|
|
4
4
|
* - `GET <base>` → `{ interface }` the profile's handle, contract, and questions
|
|
5
5
|
* - `POST <base>/decide` → `{ result }` the answers for the posted `{ state }`
|
|
6
6
|
*
|
|
7
|
-
* The host owns the questions: the browser sends only the state
|
|
8
|
-
*
|
|
7
|
+
* The host owns the questions: the browser sends only the state, the images it names and,
|
|
8
|
+
* when the host builds its questions for each decision, the input its builder takes. A
|
|
9
|
+
* visitor can't spend the host's key asking questions of their own.
|
|
9
10
|
*
|
|
10
11
|
* @module
|
|
11
12
|
*/
|
|
12
|
-
import { type DecisionAsset, type DecisionProfile, type DecisionQuestion, type KeyVault, type TraceSink } from '@theoremjs/agents';
|
|
13
|
+
import { type DecisionAsset, type DecisionJson, type DecisionProfile, type DecisionQuestion, type KeyVault, type TraceSink, z } from '@theoremjs/agents';
|
|
13
14
|
import type { ClientTurnOptions } from '@theoremjs/agents/host';
|
|
14
|
-
import type
|
|
15
|
-
|
|
15
|
+
import { type DecisionProfileDefinition } from '@theoremjs/agents/kernel';
|
|
16
|
+
/**
|
|
17
|
+
* Questions built for each decision: from its state and, when `input` is set, from what the page
|
|
18
|
+
* fills in beside it. The handler checks the input against `input` before `build` sees it.
|
|
19
|
+
*/
|
|
20
|
+
export type DecisionQuestionsBuilder<Input = undefined> = {
|
|
21
|
+
/** What the page fills in for each decision, beyond the state. */
|
|
22
|
+
input?: z.ZodType<Input>;
|
|
23
|
+
/** The questions one decision asks, by id. */
|
|
24
|
+
build(call: {
|
|
25
|
+
state: Exclude<DecisionJson, null>;
|
|
26
|
+
input: Input;
|
|
27
|
+
}): Record<string, DecisionQuestion> | Promise<Record<string, DecisionQuestion>>;
|
|
28
|
+
};
|
|
29
|
+
/** Whether `questions` are built for each decision, not fixed. */
|
|
30
|
+
export declare function isQuestionsBuilder(questions: unknown): questions is DecisionQuestionsBuilder<unknown>;
|
|
31
|
+
export type TheoremDecisionHandlerOptions<Input = unknown> = {
|
|
16
32
|
/** Decision profile to serve. Registered with the kernel when the handler is created. */
|
|
17
33
|
profile: DecisionProfile | DecisionProfileDefinition;
|
|
18
|
-
/** The questions every decision asks, by id. */
|
|
19
|
-
questions: Record<string, DecisionQuestion>;
|
|
34
|
+
/** The questions every decision asks, by id, or a builder that makes them for each decision. */
|
|
35
|
+
questions: Record<string, DecisionQuestion> | DecisionQuestionsBuilder<Input>;
|
|
20
36
|
/**
|
|
21
37
|
* Images every decision sends, by the reference its questions write, such as `<ghs02>`. The page
|
|
22
38
|
* sees them, to draw a box on one; a page image with one of their names is refused.
|
|
@@ -39,4 +55,4 @@ export type TheoremDecisionHandlerOptions = {
|
|
|
39
55
|
request: Request;
|
|
40
56
|
}) => void;
|
|
41
57
|
};
|
|
42
|
-
export declare function createTheoremDecisionHandler(options: TheoremDecisionHandlerOptions): (request: Request) => Promise<Response>;
|
|
58
|
+
export declare function createTheoremDecisionHandler<Input = unknown>(options: TheoremDecisionHandlerOptions<Input>): (request: Request) => Promise<Response>;
|
|
@@ -4,18 +4,27 @@
|
|
|
4
4
|
* - `GET <base>` → `{ interface }` the profile's handle, contract, and questions
|
|
5
5
|
* - `POST <base>/decide` → `{ result }` the answers for the posted `{ state }`
|
|
6
6
|
*
|
|
7
|
-
* The host owns the questions: the browser sends only the state
|
|
8
|
-
*
|
|
7
|
+
* The host owns the questions: the browser sends only the state, the images it names and,
|
|
8
|
+
* when the host builds its questions for each decision, the input its builder takes. A
|
|
9
|
+
* visitor can't spend the host's key asking questions of their own.
|
|
9
10
|
*
|
|
10
11
|
* @module
|
|
11
12
|
*/
|
|
12
|
-
import { defineProfile, registerProfile, runDecision, z, } from '@theoremjs/agents';
|
|
13
|
-
import {
|
|
13
|
+
import { defineProfile, registerProfile, runDecision, TheoremError, z, } from '@theoremjs/agents';
|
|
14
|
+
import { jsonSchemaFromZod } from '@theoremjs/agents/kernel';
|
|
15
|
+
import { decisionAssets, decisionInterface, questionViews } from "../client/decision-transport.js";
|
|
14
16
|
import { readBody } from "./handler.js";
|
|
15
17
|
import { singleActionHandler } from "./single-action.js";
|
|
18
|
+
/** Whether `questions` are built for each decision, not fixed. */
|
|
19
|
+
export function isQuestionsBuilder(questions) {
|
|
20
|
+
return (typeof questions === 'object' &&
|
|
21
|
+
questions !== null &&
|
|
22
|
+
typeof questions.build === 'function');
|
|
23
|
+
}
|
|
16
24
|
const decideBodySchema = z.object({
|
|
17
25
|
state: z.unknown(),
|
|
18
26
|
assets: z.record(z.string(), z.object({ mimeType: z.string(), data: z.string() })).optional(),
|
|
27
|
+
input: z.unknown().optional(),
|
|
19
28
|
});
|
|
20
29
|
export function createTheoremDecisionHandler(options) {
|
|
21
30
|
const profile = defineProfile(options.profile);
|
|
@@ -24,23 +33,51 @@ export function createTheoremDecisionHandler(options) {
|
|
|
24
33
|
throw new Error(`createTheoremDecisionHandler serves type 'decision'; got type '${profile.type}'.`);
|
|
25
34
|
}
|
|
26
35
|
registerProfile(profile);
|
|
36
|
+
const { questions } = options;
|
|
37
|
+
const builder = isQuestionsBuilder(questions)
|
|
38
|
+
? questions
|
|
39
|
+
: undefined;
|
|
40
|
+
const fixed = builder ? {} : questions;
|
|
27
41
|
return singleActionHandler({
|
|
28
42
|
action: 'decide',
|
|
29
|
-
iface:
|
|
43
|
+
iface: builder
|
|
44
|
+
? {
|
|
45
|
+
...decisionInterface(profile, {}, options.images),
|
|
46
|
+
build: builder.input ? { input: jsonSchemaFromZod(builder.input, 'input') } : {},
|
|
47
|
+
}
|
|
48
|
+
: decisionInterface(profile, fixed, options.images),
|
|
30
49
|
labels: profile.labels,
|
|
31
50
|
clientEvents: options.clientEvents,
|
|
32
51
|
onError: options.onError,
|
|
33
52
|
}, async (request) => {
|
|
34
53
|
const body = await readBody(request, decideBodySchema);
|
|
35
|
-
|
|
54
|
+
if (builder && (body.state === null || body.state === undefined))
|
|
55
|
+
// labels-exempt: internal diagnostic; the user reads error.request
|
|
56
|
+
throw new TheoremError('request', 'a decision needs a state');
|
|
57
|
+
// why: The kernel checks the state is JSON within the profile's size; a builder is never handed null.
|
|
58
|
+
const state = body.state;
|
|
59
|
+
const asked = builder
|
|
60
|
+
? await builder.build({ state, input: builtInput(builder, body.input) })
|
|
61
|
+
: fixed;
|
|
62
|
+
const result = await runDecision({
|
|
36
63
|
profile: profile.id,
|
|
37
|
-
|
|
38
|
-
state: body.state,
|
|
64
|
+
state,
|
|
39
65
|
// why: The kernel checks each image's type and bytes before any request leaves.
|
|
40
66
|
assets: decisionAssets(options.images, body.assets),
|
|
41
|
-
questions:
|
|
67
|
+
questions: asked,
|
|
42
68
|
signal: request.signal,
|
|
43
69
|
metadata: options.metadata?.(request),
|
|
44
70
|
}, { vault: options.vault, fetch: options.fetch, sink: options.sink });
|
|
71
|
+
return builder ? { ...result, questions: questionViews(asked) } : result;
|
|
45
72
|
});
|
|
46
73
|
}
|
|
74
|
+
/** The page's input, checked against the builder's: refused as a bad request when it does not fit. */
|
|
75
|
+
function builtInput(builder, input) {
|
|
76
|
+
if (!builder.input)
|
|
77
|
+
return undefined;
|
|
78
|
+
const parsed = builder.input.safeParse(input);
|
|
79
|
+
if (!parsed.success)
|
|
80
|
+
// labels-exempt: internal diagnostic; the user reads error.request
|
|
81
|
+
throw new TheoremError('request', `the questions' input does not fit: ${parsed.error.message}`);
|
|
82
|
+
return parsed.data;
|
|
83
|
+
}
|
package/server/handler.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { type PageToolBroker } from './page-dispatch.ts';
|
|
|
32
32
|
*
|
|
33
33
|
* @module
|
|
34
34
|
*/
|
|
35
|
-
import { type Profile, type ProfileDefinition, type ProviderHostOptions, type ToolGate, type z } from '@theoremjs/agents';
|
|
35
|
+
import { type Profile, type ProfileDefinition, type ProviderHostOptions, type ToolGate, type TurnWiring, type z } from '@theoremjs/agents';
|
|
36
36
|
import { type ClientTurnOptions } from '@theoremjs/agents/host';
|
|
37
37
|
import { type TheoremConversationStore } from './conversation-store.ts';
|
|
38
38
|
import { type TheoremCredentialStore } from './credential-store.ts';
|
|
@@ -64,6 +64,14 @@ export type TheoremHandlerOptions = {
|
|
|
64
64
|
* request and is read as untrusted; this one is the host's.
|
|
65
65
|
*/
|
|
66
66
|
context?: (request: Request) => unknown;
|
|
67
|
+
/**
|
|
68
|
+
* The application's own turn, from the one the handler builds: `TurnWiring.prepare`, the
|
|
69
|
+
* value the application's other call sites use too. The handler keeps its profile, its tool
|
|
70
|
+
* execution and its abort. The application's `onStage` runs before the handler's steering, and
|
|
71
|
+
* its `credentials` are read before the session's. A call held at a gate runs on, once
|
|
72
|
+
* answered, with the `onStage` and `credentials` it gives for that call's turn.
|
|
73
|
+
*/
|
|
74
|
+
prepare?: TurnWiring['prepare'];
|
|
67
75
|
/**
|
|
68
76
|
* Resolve the caller's session id — e.g. `${userId}:${conversationId}` from your
|
|
69
77
|
* auth. Return `undefined` to refuse the request (401). Default: an opaque id
|
|
@@ -134,7 +142,7 @@ export type TheoremHandlerOptions = {
|
|
|
134
142
|
}) => void;
|
|
135
143
|
};
|
|
136
144
|
/** A host profile runs no model, and takes no turns to steer. */
|
|
137
|
-
export type TheoremHostHandlerOptions = Omit<TheoremHandlerOptions, 'profile' | 'provider' | 'steerInbox' | 'conversationStore'> & {
|
|
145
|
+
export type TheoremHostHandlerOptions = Omit<TheoremHandlerOptions, 'profile' | 'provider' | 'steerInbox' | 'conversationStore' | 'prepare'> & {
|
|
138
146
|
/** Host profile to serve. Registered with the kernel when the handler is created. */
|
|
139
147
|
profile: Extract<Profile, {
|
|
140
148
|
type: 'host';
|
package/server/handler.js
CHANGED
|
@@ -52,6 +52,7 @@ import { resolveSessionLimits } from "./session-limits.js";
|
|
|
52
52
|
import { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, } from "./session-store.js";
|
|
53
53
|
import { createMemorySteerInbox, steerStage, steerUnitOf, stopOnRequest, } from "./steer-inbox.js";
|
|
54
54
|
import { sameAcceptedAnswer, sessionToolCoordinator } from "./tool-operations.js";
|
|
55
|
+
import { preparedTurn } from "./turn-wiring.js";
|
|
55
56
|
const NDJSON_HEADERS = {
|
|
56
57
|
'content-type': 'application/x-ndjson; charset=utf-8',
|
|
57
58
|
'cache-control': 'no-store',
|
|
@@ -359,6 +360,8 @@ async function* settleAnswered(ctx, request, session, answered) {
|
|
|
359
360
|
}
|
|
360
361
|
async function* invokeAnswered(ctx, request, sessionId, { callId, pending, answer }) {
|
|
361
362
|
const host = await ctx.options.host?.(request);
|
|
363
|
+
// why: The held call goes on with what the application set for its turn: its stage handler and credentials.
|
|
364
|
+
const { credentials, onStage } = await preparedTurn(ctx.options.prepare, { profile: ctx.profile.id, input: pending.turnInput ?? {}, host, signal: request.signal }, { credentials: sessionCredentials(ctx, sessionId) });
|
|
362
365
|
yield* invokeTool({
|
|
363
366
|
profile: ctx.profile.id,
|
|
364
367
|
execution: sessionToolCoordinator(ctx.sessions, sessionId),
|
|
@@ -373,7 +376,8 @@ async function* invokeAnswered(ctx, request, sessionId, { callId, pending, answe
|
|
|
373
376
|
? { page: answer.page ?? { unanswered: true } }
|
|
374
377
|
: {}),
|
|
375
378
|
sessionPermissions: answer.sessionPermissions,
|
|
376
|
-
credentials
|
|
379
|
+
credentials,
|
|
380
|
+
...(onStage ? { onStage } : {}),
|
|
377
381
|
turnInput: pending.turnInput,
|
|
378
382
|
snapshot: pending.snapshot,
|
|
379
383
|
promoted: pending.promoted,
|
|
@@ -396,20 +400,22 @@ async function* chatTurn(ctx, request, session, turn, key, stop) {
|
|
|
396
400
|
stopOnRequest(ctx.inbox, key, stop);
|
|
397
401
|
}
|
|
398
402
|
try {
|
|
399
|
-
const
|
|
403
|
+
const prepared = await preparedTurn(ctx.options.prepare, {
|
|
400
404
|
profile: ctx.profile.id,
|
|
401
405
|
execution: sessionToolCoordinator(ctx.sessions, session.id),
|
|
402
406
|
pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker).execution,
|
|
403
407
|
input: turn.input,
|
|
404
408
|
...(turn.providerState ? { providerState: turn.providerState } : {}),
|
|
405
409
|
sessionPermissions: state.permissions,
|
|
406
|
-
credentials: sessionCredentials(ctx, session.id),
|
|
407
410
|
signal: AbortSignal.any([request.signal, stop.signal]),
|
|
408
411
|
host,
|
|
409
412
|
...(turn.model ? { model: turn.model } : {}),
|
|
410
413
|
...(turn.effort ? { effort: turn.effort } : {}),
|
|
414
|
+
}, {
|
|
415
|
+
credentials: sessionCredentials(ctx, session.id),
|
|
411
416
|
...(key ? { onStage: steerStage(ctx.inbox, key) } : {}),
|
|
412
|
-
}
|
|
417
|
+
});
|
|
418
|
+
const events = runTurn(prepared, provider);
|
|
413
419
|
yield* recorded(ctx.sessions, session.id, withAuthorizationUrls(ctx, request, session.id, events), {
|
|
414
420
|
profileId: ctx.profile.id,
|
|
415
421
|
turnInput: turn.input,
|
package/server/mod.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremRe
|
|
|
11
11
|
export { answerReply, type ConversationHost, type ConversationUse, conversationView, type IssuedWalkAway, messageReply, type OpenedConversation, openConversation, type PostedMessage, postedMessage, type ReplyLine, type ReplyRunner, type ReplyTurn, readerConversationHost, rewindConversation, startConversation, statelessRefusal, type WalkedAwayCall, waitingCalls, } from './conversation.ts';
|
|
12
12
|
export { type ConversationSnapshot, type ConversationWriteResult, createMemoryConversationStore, createSealedConversationStore, emptyConversation, type PausedReply, type SealedConversationStoreOptions, type StoredConversation, storedConversationSchema, type TheoremConversationStore, unknownConversation, } from './conversation-store.ts';
|
|
13
13
|
export { createMemoryCredentialStore, type TheoremCredentialStore, type TheoremCredentials, } from './credential-store.ts';
|
|
14
|
-
export { createTheoremDecisionHandler, type TheoremDecisionHandlerOptions, } from './decision-handler.ts';
|
|
14
|
+
export { createTheoremDecisionHandler, type DecisionQuestionsBuilder, isQuestionsBuilder, type TheoremDecisionHandlerOptions, } from './decision-handler.ts';
|
|
15
15
|
export { createTheoremEmbeddingHandler, embedBodySchema, embedRequest, type TheoremEmbeddingHandlerOptions, } from './embedding-handler.ts';
|
|
16
16
|
export { createTheoremHandler, createTheoremHostHandler, readBody, type TheoremHandlerOptions, type TheoremHostHandlerOptions, type TheoremRequestContext, theoremSession, theoremSessionId, } from './handler.ts';
|
|
17
17
|
export { createTheoremMcpHandler, type TheoremMcpHandlerOptions } from './mcp-handler.ts';
|
package/server/mod.js
CHANGED
|
@@ -10,7 +10,7 @@ export { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremRe
|
|
|
10
10
|
export { answerReply, conversationView, messageReply, openConversation, postedMessage, readerConversationHost, rewindConversation, startConversation, statelessRefusal, waitingCalls, } from "./conversation.js";
|
|
11
11
|
export { createMemoryConversationStore, createSealedConversationStore, emptyConversation, storedConversationSchema, unknownConversation, } from "./conversation-store.js";
|
|
12
12
|
export { createMemoryCredentialStore, } from "./credential-store.js";
|
|
13
|
-
export { createTheoremDecisionHandler, } from "./decision-handler.js";
|
|
13
|
+
export { createTheoremDecisionHandler, isQuestionsBuilder, } from "./decision-handler.js";
|
|
14
14
|
export { createTheoremEmbeddingHandler, embedBodySchema, embedRequest, } from "./embedding-handler.js";
|
|
15
15
|
export { createTheoremHandler, createTheoremHostHandler, readBody, theoremSession, theoremSessionId, } from "./handler.js";
|
|
16
16
|
export { createTheoremMcpHandler } from "./mcp-handler.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The application's turn wiring (`TurnWiring.prepare`) laid over the turn the handler builds.
|
|
3
|
+
* The handler keeps the fields it owns; the application's stage handler and credentials go
|
|
4
|
+
* first, and the handler's own run when the application's have nothing.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { NormalizedTurnRequest, StageHandler, TurnRequest, TurnWiring } from '@theoremjs/agents';
|
|
9
|
+
import type { ToolCredentialSource } from '@theoremjs/agents/kernel';
|
|
10
|
+
/** The fields a handler sets on every turn, which `prepare` cannot replace. */
|
|
11
|
+
export type HandlerOwned = Pick<TurnRequest, 'profile' | 'execution' | 'pageExecution' | 'providerState' | 'signal'>;
|
|
12
|
+
/** What the handler adds to a turn of its own: the session's credentials and its steering. */
|
|
13
|
+
export type HandlerTurnParts = {
|
|
14
|
+
credentials: ToolCredentialSource;
|
|
15
|
+
onStage?: StageHandler;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The turn to run: `turn` through the application's `prepare`, with the handler's own fields
|
|
19
|
+
* kept, and its credentials and steering after the application's.
|
|
20
|
+
*/
|
|
21
|
+
export declare function preparedTurn(prepare: TurnWiring['prepare'], turn: NormalizedTurnRequest & HandlerOwned, handler: HandlerTurnParts): Promise<TurnRequest>;
|
|
22
|
+
/**
|
|
23
|
+
* The application's stage handler, then the handler's own when the application's asks for
|
|
24
|
+
* nothing. What the second one would have done waits for the next stage that allows it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function stagesInOrder(app: StageHandler | undefined, own: StageHandler | undefined): StageHandler | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The application's credentials, then the session's. A credential is saved where the slot
|
|
29
|
+
* already lives: a refreshed token back to the application, a sign-in at a gate to the session.
|
|
30
|
+
*/
|
|
31
|
+
export declare function credentialsInOrder(app: ToolCredentialSource | undefined, session: ToolCredentialSource): ToolCredentialSource;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The application's turn wiring (`TurnWiring.prepare`) laid over the turn the handler builds.
|
|
3
|
+
* The handler keeps the fields it owns; the application's stage handler and credentials go
|
|
4
|
+
* first, and the handler's own run when the application's have nothing.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The turn to run: `turn` through the application's `prepare`, with the handler's own fields
|
|
10
|
+
* kept, and its credentials and steering after the application's.
|
|
11
|
+
*/
|
|
12
|
+
export async function preparedTurn(prepare, turn, handler) {
|
|
13
|
+
const app = prepare ? await prepare(turn) : turn;
|
|
14
|
+
const owned = {
|
|
15
|
+
profile: turn.profile,
|
|
16
|
+
...(turn.execution ? { execution: turn.execution } : {}),
|
|
17
|
+
...(turn.pageExecution ? { pageExecution: turn.pageExecution } : {}),
|
|
18
|
+
...(turn.providerState ? { providerState: turn.providerState } : {}),
|
|
19
|
+
...(turn.signal ? { signal: turn.signal } : {}),
|
|
20
|
+
};
|
|
21
|
+
const onStage = stagesInOrder(app.onStage, handler.onStage);
|
|
22
|
+
return {
|
|
23
|
+
...app,
|
|
24
|
+
...owned,
|
|
25
|
+
credentials: credentialsInOrder(app.credentials, handler.credentials),
|
|
26
|
+
...(onStage ? { onStage } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The application's stage handler, then the handler's own when the application's asks for
|
|
31
|
+
* nothing. What the second one would have done waits for the next stage that allows it.
|
|
32
|
+
*/
|
|
33
|
+
export function stagesInOrder(app, own) {
|
|
34
|
+
if (!app || !own)
|
|
35
|
+
return app ?? own;
|
|
36
|
+
return async (ctx) => (await app(ctx)) ?? (await own(ctx));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The application's credentials, then the session's. A credential is saved where the slot
|
|
40
|
+
* already lives: a refreshed token back to the application, a sign-in at a gate to the session.
|
|
41
|
+
*/
|
|
42
|
+
export function credentialsInOrder(app, session) {
|
|
43
|
+
if (!app)
|
|
44
|
+
return session;
|
|
45
|
+
// why: Called as a method: an application's source may keep its secrets on `this`.
|
|
46
|
+
const secrets = app.clientSecret ? app : session;
|
|
47
|
+
return {
|
|
48
|
+
get: async (slot) => (await app.get(slot)) ?? (await session.get(slot)),
|
|
49
|
+
set: async (slot, credential) => (await app.get(slot)) ? await app.set(slot, credential) : await session.set(slot, credential),
|
|
50
|
+
...(secrets.clientSecret
|
|
51
|
+
? { clientSecret: async (clientId) => await secrets.clientSecret?.(clientId) }
|
|
52
|
+
: {}),
|
|
53
|
+
};
|
|
54
|
+
}
|