@theoremjs/web 0.3.4-next.528.g44c76f9 → 0.3.4-next.540.g67b727f
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 -1
- package/package.json +2 -2
- package/server/conversation-store.js +1 -1
- package/server/handler.js +6 -7
- package/server/page-dispatch.js +23 -15
- package/server/session-boundary.js +2 -3
- package/server/single-action.js +4 -4
package/README.md
CHANGED
|
@@ -282,7 +282,7 @@ The session store keeps gates, permissions, and operations. The two stores are s
|
|
|
282
282
|
| `createSealedConversationStore({ keys, keyVersion })` | Nowhere on the server. The ref is the conversation itself, encrypted and signed with your key, and bound to the session. The browser cannot read it or change it. |
|
|
283
283
|
| Your own `TheoremConversationStore` | Your database. |
|
|
284
284
|
|
|
285
|
-
A ref that the store cannot open fails with `session.conversation_unknown
|
|
285
|
+
A ref that the store cannot open fails with `session.conversation_unknown`, kind `not_found` (HTTP 404). This includes a ref from another session. A route or method the handler does not serve also answers 404 with kind `not_found`.
|
|
286
286
|
|
|
287
287
|
The sealed store:
|
|
288
288
|
|
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.540.g67b727f",
|
|
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.540.g67b727f"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -41,7 +41,7 @@ export function emptyConversation() {
|
|
|
41
41
|
}
|
|
42
42
|
/** Refuses a ref the store does not know or cannot open: the browser reads `session.conversation_unknown`. */
|
|
43
43
|
export function unknownConversation(cause) {
|
|
44
|
-
return new TheoremError('
|
|
44
|
+
return new TheoremError('not_found', labelText('session.conversation_unknown'), {
|
|
45
45
|
copy: { key: 'session.conversation_unknown' },
|
|
46
46
|
...(cause === undefined ? {} : { cause }),
|
|
47
47
|
});
|
package/server/handler.js
CHANGED
|
@@ -34,7 +34,7 @@ import { createMemoryPageToolBroker, pageToolResultSchema, sessionPageDispatch,
|
|
|
34
34
|
* @module
|
|
35
35
|
*/
|
|
36
36
|
import { authorizeToolReplay, compactHistory, defaultKernelScope, defineProfile, invokeTool, jsonObjectSchema, labelText, registerProfile, runTurn, TheoremError, toolInvocationHash, } from '@theoremjs/agents';
|
|
37
|
-
import { caughtStatus, clientError, forClient,
|
|
37
|
+
import { caughtStatus, clientError, forClient, HTTP_NOT_FOUND, } from '@theoremjs/agents/host';
|
|
38
38
|
import { clientProfileInterface, gatedToolsFromEvents, interfaceFromProfile, promotedToolIdsFromEvents, toolSnapshotFromEvents, } from '@theoremjs/agents/interface';
|
|
39
39
|
import { answerGatedCall, resolveGateTtlMs, } from '@theoremjs/agents/kernel';
|
|
40
40
|
import { hostInterface, theoremHostCallRequestSchema, } from "../client/host-transport.js";
|
|
@@ -589,9 +589,9 @@ function answerRequest(body) {
|
|
|
589
589
|
}
|
|
590
590
|
/** A route this profile doesn't serve. */
|
|
591
591
|
function refused(ctx) {
|
|
592
|
-
return jsonResponse(
|
|
593
|
-
error: labelText('error.
|
|
594
|
-
errorKind: '
|
|
592
|
+
return jsonResponse(HTTP_NOT_FOUND, {
|
|
593
|
+
error: labelText('error.not_found', {}, ctx.profile.labels),
|
|
594
|
+
errorKind: 'not_found',
|
|
595
595
|
});
|
|
596
596
|
}
|
|
597
597
|
async function* replayOperation(ctx, request, operation) {
|
|
@@ -646,9 +646,8 @@ async function settlingOf(ctx, request, session, body) {
|
|
|
646
646
|
operation.callId === body.gateId &&
|
|
647
647
|
operation.profileId === ctx.profile.id &&
|
|
648
648
|
(operation.status === 'running' || operation.status === 'uncertain')) {
|
|
649
|
-
throw new TheoremError('
|
|
650
|
-
|
|
651
|
-
});
|
|
649
|
+
throw new TheoremError('conflict', `The tool operation is ${operation.status}, so it does not run again until the host settles it`, // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
650
|
+
{ copy: { key: 'tool.execution_uncertain' } });
|
|
652
651
|
}
|
|
653
652
|
if (operation?.status === 'settled' &&
|
|
654
653
|
operation.profileId === ctx.profile.id &&
|
package/server/page-dispatch.js
CHANGED
|
@@ -9,10 +9,9 @@ export const pageToolResultSchema = z.strictObject({
|
|
|
9
9
|
z.strictObject({ timedOut: z.literal(true) }),
|
|
10
10
|
]),
|
|
11
11
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
12
|
+
/** The user reads that the action is unresolved; the builder reads why, under the kind of what happened. */
|
|
13
|
+
function refused(kind, reason) {
|
|
14
|
+
throw new TheoremError(kind, reason, { copy: { key: 'tool.execution_uncertain' } });
|
|
16
15
|
}
|
|
17
16
|
export function createMemoryPageToolBroker(maxPending = 128) {
|
|
18
17
|
if (!Number.isSafeInteger(maxPending) || maxPending <= 0)
|
|
@@ -22,8 +21,12 @@ export function createMemoryPageToolBroker(maxPending = 128) {
|
|
|
22
21
|
return {
|
|
23
22
|
reserve(sessionId, grantId, signal) {
|
|
24
23
|
const id = key(sessionId, grantId);
|
|
25
|
-
if (signal?.aborted
|
|
26
|
-
|
|
24
|
+
if (signal?.aborted)
|
|
25
|
+
refused('cancelled', 'The page dispatch was stopped before it was sent'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
26
|
+
if (waiting.has(id))
|
|
27
|
+
refused('internal', 'The page broker already waits for this grant'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
28
|
+
if (waiting.size >= maxPending)
|
|
29
|
+
refused('rate_limit', `The page broker already waits for its maxPending of ${maxPending} answers`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
27
30
|
let finish = () => { };
|
|
28
31
|
const result = new Promise((resolve) => {
|
|
29
32
|
finish = resolve;
|
|
@@ -44,7 +47,8 @@ export function createMemoryPageToolBroker(maxPending = 128) {
|
|
|
44
47
|
deliver(sessionId, grantId, page) {
|
|
45
48
|
const finish = waiting.get(key(sessionId, grantId));
|
|
46
49
|
if (!finish)
|
|
47
|
-
return Promise.reject(new TheoremError('
|
|
50
|
+
return Promise.reject(new TheoremError('conflict', 'No dispatch in this process waits for the page answer to this grant', // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
51
|
+
{ copy: { key: 'tool.execution_uncertain' } }));
|
|
48
52
|
finish(page);
|
|
49
53
|
return Promise.resolve();
|
|
50
54
|
},
|
|
@@ -57,7 +61,7 @@ export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000
|
|
|
57
61
|
execution: {
|
|
58
62
|
async dispatch(claim, signal) {
|
|
59
63
|
if (!claim.invocation)
|
|
60
|
-
|
|
64
|
+
refused('internal', 'The page dispatch got a claim with no invocation'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
61
65
|
const grantId = crypto.randomUUID();
|
|
62
66
|
const expiresAt = Date.now() + timeoutMs;
|
|
63
67
|
const pending = broker.reserve(sessionId, grantId, signal);
|
|
@@ -68,7 +72,7 @@ export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000
|
|
|
68
72
|
record.claimToken !== claim.claimToken ||
|
|
69
73
|
record.invocationHash !== claim.invocationHash ||
|
|
70
74
|
record.pageGrant)
|
|
71
|
-
|
|
75
|
+
refused('internal', 'The session store holds no running claim for this page dispatch');
|
|
72
76
|
record.pageGrant = { grantId, expiresAt };
|
|
73
77
|
});
|
|
74
78
|
}
|
|
@@ -79,7 +83,9 @@ export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000
|
|
|
79
83
|
const remaining = expiresAt - Date.now();
|
|
80
84
|
if (remaining <= 0 || signal?.aborted) {
|
|
81
85
|
pending.cancel();
|
|
82
|
-
|
|
86
|
+
if (signal?.aborted)
|
|
87
|
+
refused('cancelled', 'The page dispatch was stopped before it was sent'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
88
|
+
refused('timeout', 'The page dispatch used its timeoutMs before it was sent'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
83
89
|
}
|
|
84
90
|
let expire = () => { };
|
|
85
91
|
const expired = new Promise((resolve) => {
|
|
@@ -106,16 +112,18 @@ export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000
|
|
|
106
112
|
record.status === 'ready' ||
|
|
107
113
|
record.callId !== result.callId ||
|
|
108
114
|
record.pageGrant?.grantId !== result.grantId)
|
|
109
|
-
|
|
115
|
+
refused('request', 'The page answered a grant this session did not issue for that call'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
110
116
|
if (record.pageGrant.resultHash !== undefined) {
|
|
111
117
|
if (record.pageGrant.resultHash !== hash)
|
|
112
|
-
|
|
118
|
+
refused('request', 'The page answered this grant twice with different results'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
113
119
|
if (record.status === 'uncertain')
|
|
114
|
-
|
|
120
|
+
refused('request', 'The page answered again for a call that is uncertain'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
115
121
|
return { duplicate: true, settled: record.status === 'settled' };
|
|
116
122
|
}
|
|
117
|
-
if (record.status !== 'running'
|
|
118
|
-
|
|
123
|
+
if (record.status !== 'running')
|
|
124
|
+
refused('request', `The page answered a call that is ${record.status}, not running`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
125
|
+
if (Date.now() >= record.pageGrant.expiresAt)
|
|
126
|
+
refused('timeout', 'The page answered after its grant expired'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
119
127
|
record.pageGrant.resultHash = hash;
|
|
120
128
|
return { duplicate: false, settled: false };
|
|
121
129
|
});
|
|
@@ -38,9 +38,8 @@ export async function createTheoremSessionBoundary(options) {
|
|
|
38
38
|
const providerState = await requireOwnedCheckpoint(state.providerCheckpoints, prepared.providerState, state.operations);
|
|
39
39
|
if (providerState &&
|
|
40
40
|
Object.values(state.operations).some((operation) => operation.status !== 'settled')) {
|
|
41
|
-
throw new TheoremError('
|
|
42
|
-
|
|
43
|
-
});
|
|
41
|
+
throw new TheoremError('conflict', 'The session holds a tool operation that is not settled, so its provider state does not resume', // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
42
|
+
{ copy: { key: 'tool.execution_uncertain' } });
|
|
44
43
|
}
|
|
45
44
|
return {
|
|
46
45
|
...prepared,
|
package/server/single-action.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module
|
|
7
7
|
*/
|
|
8
8
|
import { labelText } from '@theoremjs/agents';
|
|
9
|
-
import { caughtStatus, clientError,
|
|
9
|
+
import { caughtStatus, clientError, HTTP_NOT_FOUND, } from '@theoremjs/agents/host';
|
|
10
10
|
import { jsonResponse } from "./json-response.js";
|
|
11
11
|
export function singleActionHandler({ action, iface, labels, clientEvents, onError }, run) {
|
|
12
12
|
const isAction = (request) => new URL(request.url).pathname.split('/').filter(Boolean).at(-1) === action;
|
|
@@ -15,9 +15,9 @@ export function singleActionHandler({ action, iface, labels, clientEvents, onErr
|
|
|
15
15
|
if (request.method === 'GET' && !isAction(request))
|
|
16
16
|
return jsonResponse(200, { interface: iface });
|
|
17
17
|
if (request.method !== 'POST' || !isAction(request)) {
|
|
18
|
-
return jsonResponse(
|
|
19
|
-
error: labelText('error.
|
|
20
|
-
errorKind: '
|
|
18
|
+
return jsonResponse(HTTP_NOT_FOUND, {
|
|
19
|
+
error: labelText('error.not_found', {}, labels),
|
|
20
|
+
errorKind: 'not_found',
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
return jsonResponse(200, { result: await run(request) });
|