@theoremjs/web 0.3.4-next.693.g4ab1526 → 0.3.4-next.715.geef22fd
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 +3 -1
- package/client/transcript-schema.js +1 -0
- package/package.json +2 -2
- package/server/checkpoint-ownership.d.ts +1 -1
- package/server/checkpoint-ownership.js +27 -8
- package/server/conversation.d.ts +4 -1
- package/server/conversation.js +3 -1
- package/server/handler.d.ts +10 -0
- package/server/handler.js +15 -4
- package/server/session-boundary.js +2 -2
- package/server/session-store.d.ts +12 -1
- package/server/session-store.js +20 -1
- package/server/tool-operations.d.ts +1 -1
- package/server/tool-operations.js +27 -1
- package/server/turn-wiring.d.ts +1 -1
- package/server/turn-wiring.js +1 -0
package/README.md
CHANGED
|
@@ -418,7 +418,7 @@ Saved outcomes include guarded model read-back, media, provenance, and suspiciou
|
|
|
418
418
|
The handler retains settled operations for 24 hours from settlement by default. Set `operationRetentionMs` to change this window.
|
|
419
419
|
Each saved outcome records its absolute retention deadline. Changing a handler's policy does not shorten that deadline.
|
|
420
420
|
Each issued gate records its absolute expiry. Changing `gateTtlMs` does not extend an issued gate.
|
|
421
|
-
Ready, running, and uncertain operations do not expire.
|
|
421
|
+
Ready, running, background, and uncertain operations do not expire.
|
|
422
422
|
|
|
423
423
|
A session store with a real limit declares its `capacity`: what it holds for one session, in bytes of UTF-8 JSON.
|
|
424
424
|
|
|
@@ -450,6 +450,8 @@ A live session does not restore a dispatched call after an unexpected failure. I
|
|
|
450
450
|
To retain execution claims across connections, supply a durable coordinator through `RunSessionOptions.execution`.
|
|
451
451
|
Browser dispatch uses a host-owned `PageToolExecution` service beside the execution coordinator.
|
|
452
452
|
`createTheoremHandler` supplies both services. Live relays pass the session boundary's `pageExecution` and `toolExecution` to `runSession`.
|
|
453
|
+
|
|
454
|
+
A tool with a `background` setting runs inline until you pass the `background` option to `createTheoremHandler`. The option takes `{ request, sessionId }` and returns a `ToolBackgroundService`, which receives each detached call and its late events. The session keeps the call as `background` until it settles, and a late settlement does not invalidate a checkpoint. Keep the service's values valid after the request ends. A live relay passes its service to `runSession`.
|
|
453
455
|
Forward live page results to `answerPageResult`. Preserve checkpoint ownership through `beforeEvent` when using the Studio bridge.
|
|
454
456
|
The default `PageToolBroker` keeps active result waiters in process memory. Several workers need a shared broker or request affinity.
|
|
455
457
|
Use `pageToolBroker` to supply that broker. It carries results; the atomic session store retains execution authority.
|
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.715.geef22fd",
|
|
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.715.geef22fd"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProviderCheckpoint, type TurnHistoryMessage } from '@theoremjs/agents';
|
|
2
|
-
import type
|
|
2
|
+
import { type TheoremSessionState } from './session-store.ts';
|
|
3
3
|
export declare function checkpointIdentity(checkpoint: ProviderCheckpoint): Promise<string>;
|
|
4
4
|
export declare function checkpointOwnershipKey(identity: string, operations: TheoremSessionState['operations']): Promise<string>;
|
|
5
5
|
export declare function requireOwnedCheckpoint(identities: readonly string[], checkpoint: ProviderCheckpoint | undefined, operations?: TheoremSessionState['operations'], portableHistory?: readonly TurnHistoryMessage[]): Promise<ProviderCheckpoint | undefined>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { labelText, providerCheckpointSchema, TheoremError, toolInvocationHash, } from '@theoremjs/agents';
|
|
2
|
+
import { detachedReply } from "./session-store.js";
|
|
2
3
|
export function checkpointIdentity(checkpoint) {
|
|
3
4
|
return toolInvocationHash(providerCheckpointSchema.parse(checkpoint));
|
|
4
5
|
}
|
|
@@ -7,10 +8,26 @@ export async function checkpointOwnershipKey(identity, operations) {
|
|
|
7
8
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
8
9
|
.map(([id, operation]) => operation.status === 'ready'
|
|
9
10
|
? [id, operation.status, operation.profileId, operation.readyAt]
|
|
10
|
-
: [
|
|
11
|
+
: [
|
|
12
|
+
id,
|
|
13
|
+
// invariant: A late settlement of a detached call leaves this row, and its checkpoint, as it was.
|
|
14
|
+
detachedReply(operation) ? 'background' : operation.status,
|
|
15
|
+
operation.profileId,
|
|
16
|
+
operation.toolId,
|
|
17
|
+
operation.invocationHash,
|
|
18
|
+
]);
|
|
11
19
|
const epoch = await toolInvocationHash(rows);
|
|
12
20
|
return `${identity}:${epoch}`;
|
|
13
21
|
}
|
|
22
|
+
/** What the model read for this call: a detached call's reply, or a settled call's result. */
|
|
23
|
+
function readBackOf(operation) {
|
|
24
|
+
const reply = detachedReply(operation);
|
|
25
|
+
if (reply)
|
|
26
|
+
return reply.event.tool.readBack;
|
|
27
|
+
return operation.status === 'settled' && operation.outcome.effects !== 'unknown'
|
|
28
|
+
? operation.outcome.event.tool.readBack
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
|
14
31
|
export async function requireOwnedCheckpoint(identities, checkpoint, operations = {}, portableHistory) {
|
|
15
32
|
if (checkpoint === undefined)
|
|
16
33
|
return undefined;
|
|
@@ -21,13 +38,15 @@ export async function requireOwnedCheckpoint(identities, checkpoint, operations
|
|
|
21
38
|
const portableSettlements = portableHistory !== undefined &&
|
|
22
39
|
records.length > 0 &&
|
|
23
40
|
identities.some((saved) => saved.startsWith(`${identity}:`)) &&
|
|
24
|
-
records.every((operation) =>
|
|
25
|
-
|
|
26
|
-
operation.
|
|
27
|
-
|
|
28
|
-
message.
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
records.every((operation) => {
|
|
42
|
+
const readBack = readBackOf(operation);
|
|
43
|
+
return (operation.status !== 'ready' &&
|
|
44
|
+
readBack !== undefined &&
|
|
45
|
+
portableHistory.some((message) => message.role === 'tool' &&
|
|
46
|
+
message.tool_call_id === operation.callId &&
|
|
47
|
+
message.name === operation.toolId &&
|
|
48
|
+
message.content === readBack));
|
|
49
|
+
});
|
|
31
50
|
if (!identities.includes(key) &&
|
|
32
51
|
!portableSettlements &&
|
|
33
52
|
!(records.length === 0 && identities.includes(identity))) {
|
package/server/conversation.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type GateReference, type LabelOverrides, type Profile, type ProviderCheckpoint, TheoremError, type TurnEvent, type TurnHistoryMessage, type TurnInput } from '@theoremjs/agents';
|
|
18
18
|
import { type ClientTurnOptions } from '@theoremjs/agents/host';
|
|
19
|
-
import { type ComposerProfileInterface, streamThoughtsEnabled, type TranscriptBlock, type UserTurnDraft, type UserTurnHistoryMedia } from '@theoremjs/agents/interface';
|
|
19
|
+
import { type ComposerProfileInterface, type ReplyCarried, streamThoughtsEnabled, type TranscriptBlock, type UserTurnDraft, type UserTurnHistoryMedia } from '@theoremjs/agents/interface';
|
|
20
20
|
import type { ConversationLine, ConversationView, StatedTurn, TheoremTurnInput } from '../client/transport.ts';
|
|
21
21
|
import { type StoredConversation, type TheoremConversationStore } from './conversation-store.ts';
|
|
22
22
|
/** One walked-away call: its id, and the run that settles it (`invokeTool` with the abandoned resume). */
|
|
@@ -120,6 +120,8 @@ export type ReplyTurn = {
|
|
|
120
120
|
model?: string;
|
|
121
121
|
effort?: string;
|
|
122
122
|
stated?: StatedTurn;
|
|
123
|
+
/** The tools and steps the reply had when it paused, for a reply that goes on. */
|
|
124
|
+
carried?: ReplyCarried;
|
|
123
125
|
};
|
|
124
126
|
/** What a reply's model run takes from the host. */
|
|
125
127
|
export type ReplyRunner = {
|
|
@@ -133,6 +135,7 @@ export type ReplyRunner = {
|
|
|
133
135
|
model?: string;
|
|
134
136
|
effort?: string;
|
|
135
137
|
stated?: StatedTurn;
|
|
138
|
+
carried?: ReplyCarried;
|
|
136
139
|
signal: AbortSignal;
|
|
137
140
|
};
|
|
138
141
|
/** A line of a reply's stream: an event, or the ref of what the store wrote. */
|
package/server/conversation.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { attachmentsRefused, labelText, TheoremError, toErrorEvent, } from '@theoremjs/agents';
|
|
18
18
|
import { sanitizeTurnRequestWithEvents, withPublicLabels } from '@theoremjs/agents/guardrails';
|
|
19
19
|
import { forClient } from '@theoremjs/agents/host';
|
|
20
|
-
import { appendAssistantEventsToHistory, appendUserDraftToHistory, applyTurnEventsToSession, emptyInterfaceTurnSession, foldTurnEvents, gatedToolsFromEvents, historyFromTranscriptBlocks, prepareUserTurn, settlesToolCall, streamThoughtsEnabled, } from '@theoremjs/agents/interface';
|
|
20
|
+
import { appendAssistantEventsToHistory, appendUserDraftToHistory, applyTurnEventsToSession, emptyInterfaceTurnSession, foldTurnEvents, gatedToolsFromEvents, historyFromTranscriptBlocks, prepareUserTurn, replyCarriedFromEvents, settlesToolCall, streamThoughtsEnabled, } from '@theoremjs/agents/interface';
|
|
21
21
|
import { base64ToBytes } from '@theoremjs/agents/kernel';
|
|
22
22
|
import { stampWorked, stoppedReply } from "../client/run-commit.js";
|
|
23
23
|
import { emptyConversation, unknownConversation, } from "./conversation-store.js";
|
|
@@ -236,6 +236,7 @@ async function* runReply(host, opened, reply, runner, input, withMessage) {
|
|
|
236
236
|
model: runner.model,
|
|
237
237
|
effort: runner.effort,
|
|
238
238
|
...(runner.stated ? { stated: runner.stated } : {}),
|
|
239
|
+
...(runner.carried ? { carried: runner.carried } : {}),
|
|
239
240
|
});
|
|
240
241
|
for await (const event of events) {
|
|
241
242
|
reply.events.push(event);
|
|
@@ -492,6 +493,7 @@ export async function* answerReply(args) {
|
|
|
492
493
|
model: runner.model ?? paused.model,
|
|
493
494
|
effort: runner.effort ?? paused.effort,
|
|
494
495
|
...(paused.stated ? { stated: paused.stated } : {}),
|
|
496
|
+
carried: replyCarriedFromEvents(reply.events),
|
|
495
497
|
}, {
|
|
496
498
|
history: replyHistory(reply),
|
|
497
499
|
...(waiting.inputTokens === undefined ? {} : { inputTokens: waiting.inputTokens }),
|
package/server/handler.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { type PageToolBroker } from './page-dispatch.ts';
|
|
|
36
36
|
*/
|
|
37
37
|
import { type Profile, type ProfileDefinition, type ProviderHostOptions, type StageHandler, type ToolGate, type TurnWiring, type z } from '@theoremjs/agents';
|
|
38
38
|
import { type ClientTurnOptions } from '@theoremjs/agents/host';
|
|
39
|
+
import { type ToolBackgroundService } from '@theoremjs/agents/kernel';
|
|
39
40
|
import { type TheoremConversationStore } from './conversation-store.ts';
|
|
40
41
|
import { type TheoremCredentialStore } from './credential-store.ts';
|
|
41
42
|
export { theoremSession, theoremSessionId } from './session-identity.ts';
|
|
@@ -117,6 +118,15 @@ export type TheoremHandlerOptions = {
|
|
|
117
118
|
* answered by then settles as timed out. Unset, it waits until the page answers or the turn stops.
|
|
118
119
|
*/
|
|
119
120
|
pageToolTimeoutMs?: number;
|
|
121
|
+
/**
|
|
122
|
+
* The service for tools with a `background` setting, by session. It receives each detached call
|
|
123
|
+
* and its late events. The session keeps each call's state and result. Unset, every tool runs
|
|
124
|
+
* inline. The value must stay valid after the request ends, until each call settles.
|
|
125
|
+
*/
|
|
126
|
+
background?: (ctx: {
|
|
127
|
+
request: Request;
|
|
128
|
+
sessionId: string;
|
|
129
|
+
}) => ToolBackgroundService;
|
|
120
130
|
/** Check current resource access before disclosing a saved tool result. This callback must be read-only. */
|
|
121
131
|
authorizeToolReplay?: (context: {
|
|
122
132
|
request: Request;
|
package/server/handler.js
CHANGED
|
@@ -370,6 +370,7 @@ async function* invokeAnswered(ctx, request, sessionId, { callId, pending, answe
|
|
|
370
370
|
yield* invokeTool({
|
|
371
371
|
profile: ctx.profile.id,
|
|
372
372
|
execution: sessionToolCoordinator(ctx.sessions, sessionId),
|
|
373
|
+
...backgroundWiring(ctx, request, sessionId),
|
|
373
374
|
pageExecution: sessionPageDispatch(ctx.sessions, sessionId, ctx.pageToolBroker, ctx.options.pageToolTimeoutMs).execution,
|
|
374
375
|
name: pending.name,
|
|
375
376
|
callId,
|
|
@@ -391,6 +392,12 @@ async function* invokeAnswered(ctx, request, sessionId, { callId, pending, answe
|
|
|
391
392
|
host: prepared.host,
|
|
392
393
|
});
|
|
393
394
|
}
|
|
395
|
+
/** The host's service for a call in this session, for a request that runs a tool. */
|
|
396
|
+
function backgroundWiring(ctx, request, sessionId) {
|
|
397
|
+
return ctx.options.background
|
|
398
|
+
? { background: ctx.options.background({ request, sessionId }) }
|
|
399
|
+
: {};
|
|
400
|
+
}
|
|
394
401
|
/** A settled call's answer, streamed again. */
|
|
395
402
|
async function* once(event, labels) {
|
|
396
403
|
yield withPublicLabels(event, labels);
|
|
@@ -409,6 +416,7 @@ async function* chatTurn(ctx, request, session, turn, key, stop) {
|
|
|
409
416
|
const prepared = await preparedTurn(ctx.options.prepare, {
|
|
410
417
|
profile: ctx.profile.id,
|
|
411
418
|
execution: sessionToolCoordinator(ctx.sessions, session.id),
|
|
419
|
+
...backgroundWiring(ctx, request, session.id),
|
|
412
420
|
pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker, ctx.options.pageToolTimeoutMs).execution,
|
|
413
421
|
input: turn.input,
|
|
414
422
|
...(turn.conversationId ? { conversationId: turn.conversationId } : {}),
|
|
@@ -418,6 +426,7 @@ async function* chatTurn(ctx, request, session, turn, key, stop) {
|
|
|
418
426
|
host,
|
|
419
427
|
...(turn.model ? { model: turn.model } : {}),
|
|
420
428
|
...(turn.effort ? { effort: turn.effort } : {}),
|
|
429
|
+
...turn.carried,
|
|
421
430
|
...turn.stated,
|
|
422
431
|
}, {
|
|
423
432
|
credentials: sessionCredentials(ctx, session.id),
|
|
@@ -587,6 +596,7 @@ async function* callEvents(ctx, request, session, body) {
|
|
|
587
596
|
const events = invokeTool({
|
|
588
597
|
profile: ctx.profile.id,
|
|
589
598
|
execution: sessionToolCoordinator(ctx.sessions, session.id),
|
|
599
|
+
...backgroundWiring(ctx, request, session.id),
|
|
590
600
|
pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker, ctx.options.pageToolTimeoutMs).execution,
|
|
591
601
|
name: body.name,
|
|
592
602
|
input: body.input,
|
|
@@ -625,7 +635,8 @@ async function* replayOperation(ctx, request, operation) {
|
|
|
625
635
|
tool.type === 'builtin' ||
|
|
626
636
|
!('tools' in ctx.profile) ||
|
|
627
637
|
!ctx.profile.tools?.allow.includes(operation.toolId) ||
|
|
628
|
-
(
|
|
638
|
+
(operation.status === 'settled' &&
|
|
639
|
+
ctx.options.authorizeToolReplay &&
|
|
629
640
|
!(await ctx.options.authorizeToolReplay({ request, operation }))))
|
|
630
641
|
throw new TheoremError('blocked', labelText('error.blocked'));
|
|
631
642
|
authorizeToolReplay({
|
|
@@ -633,9 +644,9 @@ async function* replayOperation(ctx, request, operation) {
|
|
|
633
644
|
profile: ctx.profile,
|
|
634
645
|
name: operation.toolId,
|
|
635
646
|
resume: undefined,
|
|
636
|
-
outcome: operation.outcome,
|
|
647
|
+
...(operation.status === 'settled' ? { outcome: operation.outcome } : {}),
|
|
637
648
|
});
|
|
638
|
-
yield* once(operation.outcome.event, ctx.profile.labels);
|
|
649
|
+
yield* once(operation.status === 'settled' ? operation.outcome.event : operation.reply.event, ctx.profile.labels);
|
|
639
650
|
}
|
|
640
651
|
/**
|
|
641
652
|
* The user's answer to a paused call. In a kept conversation the reply goes on
|
|
@@ -673,7 +684,7 @@ async function settlingOf(ctx, request, session, body) {
|
|
|
673
684
|
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)
|
|
674
685
|
{ copy: { key: 'tool.execution_uncertain' } });
|
|
675
686
|
}
|
|
676
|
-
if (operation?.status === 'settled' &&
|
|
687
|
+
if ((operation?.status === 'settled' || operation?.status === 'background') &&
|
|
677
688
|
operation.profileId === ctx.profile.id &&
|
|
678
689
|
operation.callId === body.gateId) {
|
|
679
690
|
if (body.input !== undefined &&
|
|
@@ -6,7 +6,7 @@ import { createMemoryPageToolBroker, sessionPageDispatch, } from "./page-dispatc
|
|
|
6
6
|
import { checkRequest } from "./request-check.js";
|
|
7
7
|
import { sessionStateStore } from "./session-authority.js";
|
|
8
8
|
import { theoremSession } from "./session-identity.js";
|
|
9
|
-
import { DEFAULT_OPERATION_RETENTION_MS } from "./session-store.js";
|
|
9
|
+
import { DEFAULT_OPERATION_RETENTION_MS, detachedReply, } from "./session-store.js";
|
|
10
10
|
import { sessionToolCoordinator } from "./tool-operations.js";
|
|
11
11
|
import { liveSessionOpen } from "./turn-input.js";
|
|
12
12
|
export async function createTheoremSessionBoundary(options) {
|
|
@@ -38,7 +38,7 @@ export async function createTheoremSessionBoundary(options) {
|
|
|
38
38
|
const state = await store.read(id);
|
|
39
39
|
const providerState = await requireOwnedCheckpoint(state.providerCheckpoints, prepared.providerState, state.operations);
|
|
40
40
|
if (providerState &&
|
|
41
|
-
Object.values(state.operations).some((operation) => operation.status !== 'settled')) {
|
|
41
|
+
Object.values(state.operations).some((operation) => operation.status !== 'settled' && !detachedReply(operation))) {
|
|
42
42
|
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)
|
|
43
43
|
{ copy: { key: 'tool.execution_uncertain' } });
|
|
44
44
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from '@theoremjs/agents';
|
|
12
12
|
import type { ToolGateAuth } from '@theoremjs/agents/interface';
|
|
13
|
-
import type { AnsweredGate, ToolGate, ToolId, ToolOperationIdentity, ToolOperationOutcome, ToolOperationUncertainty, TurnInput, TurnToolSnapshot } from '@theoremjs/agents/kernel';
|
|
13
|
+
import type { AnsweredGate, ToolGate, ToolId, ToolOperationIdentity, ToolOperationOutcome, ToolOperationReply, ToolOperationUncertainty, TurnInput, TurnToolSnapshot } from '@theoremjs/agents/kernel';
|
|
14
14
|
import { type TheoremSessionCapacity } from './session-limits.ts';
|
|
15
15
|
/** A tool call the kernel paused on a gate, as the server saw it. */
|
|
16
16
|
export type PendingToolGate = {
|
|
@@ -47,19 +47,30 @@ export type SessionToolOperation = {
|
|
|
47
47
|
claimToken: string;
|
|
48
48
|
startedAt: number;
|
|
49
49
|
pageGrant?: SessionPageGrant;
|
|
50
|
+
}) | (ToolOperationIdentity & {
|
|
51
|
+
status: 'background';
|
|
52
|
+
claimToken: string;
|
|
53
|
+
reply: ToolOperationReply;
|
|
54
|
+
startedAt: number;
|
|
55
|
+
detachedAt: number;
|
|
56
|
+
pageGrant?: SessionPageGrant;
|
|
50
57
|
}) | (ToolOperationIdentity & {
|
|
51
58
|
status: 'settled';
|
|
52
59
|
pageGrant?: SessionPageGrant;
|
|
53
60
|
outcome: ToolOperationOutcome;
|
|
61
|
+
reply?: ToolOperationReply;
|
|
54
62
|
settledAt: number;
|
|
55
63
|
retainUntil?: number;
|
|
56
64
|
}) | (ToolOperationIdentity & {
|
|
57
65
|
status: 'uncertain';
|
|
58
66
|
pageGrant?: SessionPageGrant;
|
|
67
|
+
reply?: ToolOperationReply;
|
|
59
68
|
claimToken: string;
|
|
60
69
|
reason: ToolOperationUncertainty;
|
|
61
70
|
detectedAt: number;
|
|
62
71
|
});
|
|
72
|
+
/** The reply the model read for a call that detached, whatever has happened to the call since. */
|
|
73
|
+
export declare function detachedReply(operation: SessionToolOperation): ToolOperationReply | undefined;
|
|
63
74
|
export declare const DEFAULT_OPERATION_RETENTION_MS: number;
|
|
64
75
|
export type TheoremSessionState = {
|
|
65
76
|
version: 3;
|
package/server/session-store.js
CHANGED
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
-
import { jsonObjectSchema, jsonValueSchema, labelText, savedToolInvocationSchema, TheoremError, TOOL_AUTH_TYPES, TOOL_GATE_KINDS, TOOL_PERMISSION, toolOperationOutcomeSchema, turnHistoryMessageSchema, turnToolSnapshotSchema, z, } from '@theoremjs/agents';
|
|
11
|
+
import { jsonObjectSchema, jsonValueSchema, labelText, savedToolInvocationSchema, TheoremError, TOOL_AUTH_TYPES, TOOL_GATE_KINDS, TOOL_PERMISSION, toolOperationOutcomeSchema, toolOperationReplySchema, turnHistoryMessageSchema, turnToolSnapshotSchema, z, } from '@theoremjs/agents';
|
|
12
12
|
import { gateExpired } from '@theoremjs/agents/kernel';
|
|
13
13
|
import { checkedCapacity } from "./session-limits.js";
|
|
14
|
+
/** The reply the model read for a call that detached, whatever has happened to the call since. */
|
|
15
|
+
export function detachedReply(operation) {
|
|
16
|
+
return operation.status === 'background' ||
|
|
17
|
+
operation.status === 'settled' ||
|
|
18
|
+
operation.status === 'uncertain'
|
|
19
|
+
? operation.reply
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
14
22
|
export const DEFAULT_OPERATION_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
15
23
|
const blobSchema = z.strictObject({
|
|
16
24
|
data: z.string(),
|
|
@@ -118,11 +126,21 @@ const operationSchema = z.discriminatedUnion('status', [
|
|
|
118
126
|
claimToken: z.string().min(1),
|
|
119
127
|
startedAt: z.number().finite().nonnegative(),
|
|
120
128
|
}),
|
|
129
|
+
z.strictObject({
|
|
130
|
+
...identityFields,
|
|
131
|
+
status: z.literal('background'),
|
|
132
|
+
pageGrant: pageGrant.optional(),
|
|
133
|
+
claimToken: z.string().min(1),
|
|
134
|
+
reply: toolOperationReplySchema,
|
|
135
|
+
startedAt: z.number().finite().nonnegative(),
|
|
136
|
+
detachedAt: z.number().finite().nonnegative(),
|
|
137
|
+
}),
|
|
121
138
|
z.strictObject({
|
|
122
139
|
...identityFields,
|
|
123
140
|
status: z.literal('settled'),
|
|
124
141
|
pageGrant: pageGrant.optional(),
|
|
125
142
|
outcome: toolOperationOutcomeSchema,
|
|
143
|
+
reply: toolOperationReplySchema.optional(),
|
|
126
144
|
settledAt: z.number().finite().nonnegative(),
|
|
127
145
|
retainUntil: z.number().finite().nonnegative().optional(),
|
|
128
146
|
}),
|
|
@@ -130,6 +148,7 @@ const operationSchema = z.discriminatedUnion('status', [
|
|
|
130
148
|
...identityFields,
|
|
131
149
|
status: z.literal('uncertain'),
|
|
132
150
|
pageGrant: pageGrant.optional(),
|
|
151
|
+
reply: toolOperationReplySchema.optional(),
|
|
133
152
|
claimToken: z.string().min(1),
|
|
134
153
|
reason: z.enum(['execution_interrupted', 'settlement_write_failed', 'worker_lost']),
|
|
135
154
|
detectedAt: z.number().finite().nonnegative(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ToolExecutionCoordinator } from '@theoremjs/agents';
|
|
2
|
-
import type
|
|
2
|
+
import { type TheoremSessionState } from './session-store.ts';
|
|
3
3
|
export declare function sessionToolCoordinator(store: {
|
|
4
4
|
readonly capacity?: {
|
|
5
5
|
maxOutcomeBytes: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TheoremError, } from '@theoremjs/agents';
|
|
2
|
+
import { detachedReply, } from "./session-store.js";
|
|
2
3
|
/** The session's record of a tool operation does not match the call: the user reads `tool.execution_uncertain`. */
|
|
3
4
|
function unavailable(reason) {
|
|
4
5
|
throw new TheoremError('unavailable', reason, { copy: { key: 'tool.execution_uncertain' } });
|
|
@@ -33,7 +34,9 @@ function checkedRecord(state, identity) {
|
|
|
33
34
|
function checkedClaim(state, claim) {
|
|
34
35
|
const record = checkedRecord(state, claim);
|
|
35
36
|
if (!record ||
|
|
36
|
-
(record.status !== 'running' &&
|
|
37
|
+
(record.status !== 'running' &&
|
|
38
|
+
record.status !== 'background' &&
|
|
39
|
+
record.status !== 'uncertain') ||
|
|
37
40
|
record.claimToken !== claim.claimToken)
|
|
38
41
|
unavailable(
|
|
39
42
|
// labels-exempt: internal diagnostic; the user reads tool.execution_uncertain
|
|
@@ -43,6 +46,8 @@ function checkedClaim(state, claim) {
|
|
|
43
46
|
function existingResult(record) {
|
|
44
47
|
if (record?.status === 'settled')
|
|
45
48
|
return { kind: 'settled', outcome: record.outcome };
|
|
49
|
+
if (record?.status === 'background')
|
|
50
|
+
return { kind: 'background', reply: record.reply };
|
|
46
51
|
if (record?.status === 'running')
|
|
47
52
|
return { kind: 'running' };
|
|
48
53
|
if (record?.status === 'uncertain')
|
|
@@ -52,6 +57,7 @@ function existingResult(record) {
|
|
|
52
57
|
function saveOutcome(state, identity, outcome) {
|
|
53
58
|
const before = state.operations[identity.operationId];
|
|
54
59
|
const pageGrant = before && before.status !== 'ready' ? before.pageGrant : undefined;
|
|
60
|
+
const reply = before ? detachedReply(before) : undefined;
|
|
55
61
|
const settledAt = Date.now();
|
|
56
62
|
const { operationId, callId, profileId, toolId, invocationHash, invocation } = identity;
|
|
57
63
|
state.operations[identity.operationId] = {
|
|
@@ -63,6 +69,7 @@ function saveOutcome(state, identity, outcome) {
|
|
|
63
69
|
...(invocation ? { invocation } : {}),
|
|
64
70
|
status: 'settled',
|
|
65
71
|
...(pageGrant ? { pageGrant } : {}),
|
|
72
|
+
...(reply ? { reply } : {}),
|
|
66
73
|
outcome,
|
|
67
74
|
settledAt,
|
|
68
75
|
};
|
|
@@ -93,6 +100,23 @@ export function sessionToolCoordinator(store, sessionId) {
|
|
|
93
100
|
return { kind: 'acquired', claimToken };
|
|
94
101
|
});
|
|
95
102
|
},
|
|
103
|
+
detach(claim, reply) {
|
|
104
|
+
return store.mutate(sessionId, (state) => {
|
|
105
|
+
const record = checkedClaim(state, claim);
|
|
106
|
+
if (record.status !== 'running')
|
|
107
|
+
// labels-exempt: internal diagnostic; the user reads tool.execution_uncertain
|
|
108
|
+
unavailable(`Tool operation ${claim.operationId} is ${record.status}, so it cannot detach`);
|
|
109
|
+
state.operations[claim.operationId] = {
|
|
110
|
+
...claim,
|
|
111
|
+
invocation: record.invocation,
|
|
112
|
+
...(record.pageGrant ? { pageGrant: record.pageGrant } : {}),
|
|
113
|
+
status: 'background',
|
|
114
|
+
reply,
|
|
115
|
+
startedAt: record.startedAt,
|
|
116
|
+
detachedAt: Date.now(),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
},
|
|
96
120
|
settle(claim, outcome) {
|
|
97
121
|
return store.mutate(sessionId, (state) => {
|
|
98
122
|
const record = checkedClaim(state, claim);
|
|
@@ -116,10 +140,12 @@ export function sessionToolCoordinator(store, sessionId) {
|
|
|
116
140
|
markUncertain(claim, reason) {
|
|
117
141
|
return store.mutate(sessionId, (state) => {
|
|
118
142
|
const record = checkedClaim(state, claim);
|
|
143
|
+
const reply = detachedReply(record);
|
|
119
144
|
state.operations[claim.operationId] = {
|
|
120
145
|
...claim,
|
|
121
146
|
invocation: record.invocation,
|
|
122
147
|
...(record.pageGrant ? { pageGrant: record.pageGrant } : {}),
|
|
148
|
+
...(reply ? { reply } : {}),
|
|
123
149
|
status: 'uncertain',
|
|
124
150
|
reason,
|
|
125
151
|
detectedAt: Date.now(),
|
package/server/turn-wiring.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { NormalizedTurnRequest, StageHandler, TurnRequest, TurnWiring } from '@theoremjs/agents';
|
|
9
9
|
import type { ToolCredentialSource } from '@theoremjs/agents/kernel';
|
|
10
10
|
/** The fields a handler sets on every turn, which `prepare` cannot replace. */
|
|
11
|
-
export type HandlerOwned = Pick<TurnRequest, 'profile' | 'execution' | 'pageExecution' | 'providerState' | 'signal'>;
|
|
11
|
+
export type HandlerOwned = Pick<TurnRequest, 'profile' | 'execution' | 'pageExecution' | 'background' | 'providerState' | 'signal'>;
|
|
12
12
|
/** What the handler adds to a turn of its own: the session's credentials and its steering. */
|
|
13
13
|
export type HandlerTurnParts = {
|
|
14
14
|
credentials: ToolCredentialSource;
|
package/server/turn-wiring.js
CHANGED
|
@@ -15,6 +15,7 @@ export async function preparedTurn(prepare, turn, handler) {
|
|
|
15
15
|
profile: turn.profile,
|
|
16
16
|
...(turn.execution ? { execution: turn.execution } : {}),
|
|
17
17
|
...(turn.pageExecution ? { pageExecution: turn.pageExecution } : {}),
|
|
18
|
+
...(turn.background ? { background: turn.background } : {}),
|
|
18
19
|
...(turn.providerState ? { providerState: turn.providerState } : {}),
|
|
19
20
|
...(turn.signal ? { signal: turn.signal } : {}),
|
|
20
21
|
};
|