@theoremjs/web 0.3.4-next.679.gcdf1a3a → 0.3.4-next.711.g78e504a

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 CHANGED
@@ -199,6 +199,7 @@ await chat.submit();
199
199
  - `getState()` returns a `ChatState`. It is a new object after each change, and the same object between changes. So you can compare it by reference, as React's `useSyncExternalStore` does.
200
200
  - `subscribe(listener)` calls the listener once for each group of changes, after the code that made them has run. It returns the function that unsubscribes. Until the first subscribe or send, the chat asks nothing of the host. Then it reads a resumed conversation that came without its `conversationView`, and answers a restored page gate as unanswered.
201
201
  - `update(options)` gives the controller your latest options, such as a new `context`, `slots`, or `pageTools`. Each turn reads the options as they are when it is sent. `conversation`, `conversationView`, `initialText`, `initialFiles`, `initialVoice`, and `initialPending` are read only when the controller is created.
202
+ - A live call takes `stated` in its options, and `setStated(stated)` changes it during the call. As with `setSlots`, the call is taken up again with the prompt lines the new value chooses.
202
203
  - Each method is bound to the controller. Pass it as a callback as it is.
203
204
 
204
205
  `state.phase` is `'idle'`, `'streaming'`, or `'gated'`. The methods for the composer depend on it:
@@ -261,6 +262,7 @@ const transport = withPageToolDispatch(
261
262
  | `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. A call held at a gate runs on, once answered, with the `host`, `onStage` and `credentials` you give for that call's turn. |
262
263
  | `onStage` | The server's own stage handler, on every call the handler runs, a host profile's included: after your app's (`prepare`), before steering. For a rule the server adds to whatever the app says, such as a question before each tool that writes. |
263
264
  | `pageToolBroker` | Delivers results to active page dispatches. Default: bounded process memory. |
265
+ | `pageStatesTurn` | Let the page say which prompt lines a turn gets, through `stated` on a turn or a live call. Set it only for a page you trust as yourself, such as a builder's own tool. Without it, a request with `stated` is refused. |
264
266
  | `conversationStore` | Where the conversation is kept. Required, except for a stateless profile, which must not have one. See [Keep the conversation](#keep-the-conversation). |
265
267
  | `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`. |
266
268
  | `credentialStore` | Where tool credentials are kept, by session. Default: process memory. |
@@ -416,7 +418,7 @@ Saved outcomes include guarded model read-back, media, provenance, and suspiciou
416
418
  The handler retains settled operations for 24 hours from settlement by default. Set `operationRetentionMs` to change this window.
417
419
  Each saved outcome records its absolute retention deadline. Changing a handler's policy does not shorten that deadline.
418
420
  Each issued gate records its absolute expiry. Changing `gateTtlMs` does not extend an issued gate.
419
- Ready, running, and uncertain operations do not expire.
421
+ Ready, running, background, and uncertain operations do not expire.
420
422
 
421
423
  A session store with a real limit declares its `capacity`: what it holds for one session, in bytes of UTF-8 JSON.
422
424
 
@@ -448,6 +450,8 @@ A live session does not restore a dispatched call after an unexpected failure. I
448
450
  To retain execution claims across connections, supply a durable coordinator through `RunSessionOptions.execution`.
449
451
  Browser dispatch uses a host-owned `PageToolExecution` service beside the execution coordinator.
450
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`.
451
455
  Forward live page results to `answerPageResult`. Preserve checkpoint ownership through `beforeEvent` when using the Studio bridge.
452
456
  The default `PageToolBroker` keeps active result waiters in process memory. Several workers need a shared broker or request affinity.
453
457
  Use `pageToolBroker` to supply that broker. It carries results; the atomic session store retains execution authority.
package/client/chat.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type AttachmentValidationIssue, type ComposerMenuAction, type ComposerP
2
2
  import { type ClientFailure } from './failure.ts';
3
3
  import { type PageTools } from './live/live-page-tool.ts';
4
4
  import type { AnsweringGate, ToolDecisionAction } from './tool-resume.ts';
5
- import { type ConversationView, type TheoremTransport, type TurnEventSink } from './transport.ts';
5
+ import { type ConversationView, type StatedTurn, type TheoremTransport, type TurnEventSink } from './transport.ts';
6
6
  export type ChatOptions = {
7
7
  transport: TheoremTransport;
8
8
  /** The profile's composer interface, as `transport.describe()` returns it. `null` while it loads. */
@@ -42,6 +42,8 @@ export type ChatOptions = {
42
42
  * `pickSlot`), starting at its first allowed value.
43
43
  */
44
44
  slots?: Record<string, string>;
45
+ /** Which prompt lines each turn gets, for a host that lets its page choose (`pageStatesTurn`). */
46
+ stated?: StatedTurn;
45
47
  /** Told each time the visitor picks a slot, with every slot's value. */
46
48
  onSlotsChange?: (slots: Record<string, string>) => void;
47
49
  /** What the page wants the agent to know: any JSON. The latest value goes with every turn. */
package/client/chat.js CHANGED
@@ -653,12 +653,13 @@ class Chat {
653
653
  };
654
654
  }
655
655
  #withValues(request) {
656
- const { iface, slots: fixed, context } = this.#options;
656
+ const { iface, slots: fixed, context, stated } = this.#options;
657
657
  const slots = slotsOf(iface, fixed, this.#picked).values;
658
- if (slots === undefined && context === undefined)
658
+ if (slots === undefined && context === undefined && stated === undefined)
659
659
  return request;
660
660
  return {
661
661
  ...request,
662
+ ...(stated === undefined ? {} : { stated }),
662
663
  input: {
663
664
  ...request.input,
664
665
  ...(slots === undefined ? {} : { slots }),
package/client/index.d.ts CHANGED
@@ -33,5 +33,5 @@ export { hasLead, layoutResult, type ResultChart, type ResultFigure, type Result
33
33
  export type { AnsweringGate, ToolDecisionAction, ToolGateResolution, } from './tool-resume.ts';
34
34
  export { continueGatedToolInvocation, type PersonGateResolution } from './tool-resume.ts';
35
35
  export { createTraceFeed, recordedFeed, type TraceFeed } from './trace-feed.ts';
36
- export { CHAT_LINES, type ChatLine, type ChatLineSink, type ClientTurnEvent, type ConversationLine, type ConversationView, createHttpTransport, fetchConversation, fetchRewound, fetchStarted, type HostErrorBody, type HttpOptions, type HttpTransportOptions, hostError, isTheoremStreamError, type MalformedEvent, postJson, postNdjson, readNdjsonStream, type TheoremConversationRequest, type TheoremInvokeRequest, type TheoremReplay, type TheoremRewindRequest, type TheoremRewound, type TheoremStartRequest, type TheoremSteerRequest, type TheoremStopRequest, TheoremStreamError, type TheoremTransport, type TheoremTurnInput, type TheoremTurnRequest, type TurnEventSink, type UnsupportedEvent, type WireLines, } from './transport.ts';
36
+ export { CHAT_LINES, type ChatLine, type ChatLineSink, type ClientTurnEvent, type ConversationLine, type ConversationView, createHttpTransport, fetchConversation, fetchRewound, fetchStarted, type HostErrorBody, type HttpOptions, type HttpTransportOptions, hostError, isTheoremStreamError, type MalformedEvent, postJson, postNdjson, readNdjsonStream, type StatedTurn, type TheoremConversationRequest, type TheoremInvokeRequest, type TheoremReplay, type TheoremRewindRequest, type TheoremRewound, type TheoremStartRequest, type TheoremSteerRequest, type TheoremStopRequest, TheoremStreamError, type TheoremTransport, type TheoremTurnInput, type TheoremTurnRequest, type TurnEventSink, type UnsupportedEvent, type WireLines, } from './transport.ts';
37
37
  export { buildInvokeRequest, buildTurnRequest, foldAssistantTurn, issuedOperationId, type RunScope, type WalkAway, } from './turn-client.ts';
@@ -1,3 +1,4 @@
1
+ import type { StatedTurn } from '../transport.ts';
1
2
  /** The page's result for a call: the tool's output schema checks it, then the model reads it. */
2
3
  export type PageToolAnswer = {
3
4
  output: unknown;
@@ -16,6 +17,8 @@ export type PageTools = Record<string, PageTool>;
16
17
  export type LiveCallOptions = {
17
18
  /** The value chosen for each of the profile's `inputs.slots`, read as the call starts. */
18
19
  slots?: Record<string, string>;
20
+ /** Which prompt lines the call gets, for a host that lets its page choose (`pageStatesTurn`). */
21
+ stated?: StatedTurn;
19
22
  /**
20
23
  * What the page tells the agent: any JSON. It goes with the call's opening, and
21
24
  * again whenever it changes, as background the agent reads without replying.
@@ -15,7 +15,7 @@ import { type SessionEventOf, type TraceRecord } from '@theoremjs/agents';
15
15
  import type { PageTools } from './live/live-page-tool.ts';
16
16
  import type { LiveConnectPhase, LiveSessionStatus } from './live/live-state.ts';
17
17
  import type { LiveGateReply, LiveToolGatePrompt } from './live/live-tool.ts';
18
- import { type ClientTurnEvent } from './transport.ts';
18
+ import { type ClientTurnEvent, type StatedTurn } from './transport.ts';
19
19
  export type { LiveConnection, LiveSocket } from './live-messages.ts';
20
20
  import type { LiveSocket } from './live-messages.ts';
21
21
  export interface LiveClientOptions {
@@ -24,6 +24,8 @@ export interface LiveClientOptions {
24
24
  openMessage?: Record<string, unknown>;
25
25
  /** The value chosen for each of the profile's `inputs.slots`, fixed for the call. */
26
26
  slots?: Record<string, string>;
27
+ /** Which prompt lines the call gets, for a host that lets its page choose (`pageStatesTurn`). */
28
+ stated?: StatedTurn;
27
29
  /** What the page tells the agent as the call opens; `setContext` replaces it during the call. */
28
30
  context?: unknown;
29
31
  relayUrl?: string;
@@ -181,6 +183,9 @@ export declare class LiveSessionClient {
181
183
  * handle; with none, the next call has them.
182
184
  */
183
185
  setSlots(slots: Record<string, string> | undefined): void;
186
+ /** The call's prompt lines, as `setSlots` takes its slots. */
187
+ setStated(stated: StatedTurn | undefined): void;
188
+ private takeUpAgain;
184
189
  /**
185
190
  * Replace the page's context package. In a call it reaches the agent as
186
191
  * background, with no reply; before one, it goes with the call's opening.
@@ -228,11 +228,12 @@ export class LiveSessionClient {
228
228
  }
229
229
  /** The first message: the call's values, and the handle and time away when it takes a dropped call up again. */
230
230
  openMessage() {
231
- const { slots, openMessage: host } = this.options;
231
+ const { slots, stated, openMessage: host } = this.options;
232
232
  return {
233
233
  type: 'open',
234
234
  ...(this.history.length ? { history: structuredClone(this.history) } : {}),
235
235
  ...(slots ? { slots } : {}),
236
+ ...(stated ? { stated } : {}),
236
237
  ...(this.context === undefined ? {} : { context: this.context }),
237
238
  ...(this.providerState
238
239
  ? {
@@ -771,6 +772,16 @@ export class LiveSessionClient {
771
772
  if (JSON.stringify(slots) === JSON.stringify(this.options.slots))
772
773
  return;
773
774
  this.options = { ...this.options, slots };
775
+ this.takeUpAgain();
776
+ }
777
+ /** The call's prompt lines, as `setSlots` takes its slots. */
778
+ setStated(stated) {
779
+ if (JSON.stringify(stated) === JSON.stringify(this.options.stated))
780
+ return;
781
+ this.options = { ...this.options, stated };
782
+ this.takeUpAgain();
783
+ }
784
+ takeUpAgain() {
774
785
  if (this.providerState && !this.opening && this.ws?.readyState === WebSocket.OPEN) {
775
786
  this.handleDrop();
776
787
  }
@@ -7,7 +7,7 @@ import { type ProviderCheckpoint } from '@theoremjs/agents';
7
7
  * @module
8
8
  */
9
9
  import { type GateDecision, type ToolGate, type TraceRecord, type TurnEvent, type TurnHistoryMessage, z } from '@theoremjs/agents';
10
- import { type HostErrorBody } from './transport.ts';
10
+ import { type HostErrorBody, type StatedTurn } from './transport.ts';
11
11
  import { type MalformedEvent, type UnsupportedEvent } from './wire-line.ts';
12
12
  /**
13
13
  * The live client's first message, before the relay opens the session: the
@@ -20,6 +20,8 @@ export type LiveOpenMessage = {
20
20
  slots?: Record<string, string>;
21
21
  /** What the page tells the agent as the call opens: the client's context package. */
22
22
  context?: unknown;
23
+ /** Which prompt lines the call gets, for a host that lets its page choose (`pageStatesTurn`). */
24
+ stated?: StatedTurn;
23
25
  /** A call that dropped, taken up again: the provider's handle, and how long the caller was away. */
24
26
  resume?: {
25
27
  providerState: ProviderCheckpoint;
@@ -7,13 +7,14 @@ import { providerCheckpointSchema } from '@theoremjs/agents';
7
7
  * @module
8
8
  */
9
9
  import { GATE_DECISIONS, TURN_EVENT_SCHEMAS, toolGateSchema, traceRecordSchema, turnHistoryMessageSchema, z, } from '@theoremjs/agents';
10
- import { hostErrorBodySchema } from "./transport.js";
10
+ import { hostErrorBodySchema, statedTurnSchema, } from "./transport.js";
11
11
  import { readWireLine, readWireValue, } from "./wire-line.js";
12
12
  const liveOpenMessage = z.object({
13
13
  type: z.literal('open'),
14
14
  history: z.array(turnHistoryMessageSchema).optional(),
15
15
  slots: z.record(z.string(), z.string()).optional(),
16
16
  context: z.unknown().optional(),
17
+ stated: statedTurnSchema.optional(),
17
18
  resume: z
18
19
  .object({ providerState: providerCheckpointSchema, awayMs: z.number().nonnegative() })
19
20
  .optional(),
@@ -13,6 +13,7 @@ function eventPart(probe) {
13
13
  const TOOL_CALL_STATE_PHASES = [
14
14
  'running',
15
15
  'gate',
16
+ 'background',
16
17
  'complete',
17
18
  'error',
18
19
  'cancel',
@@ -49,6 +49,13 @@ export type TheoremReplay = {
49
49
  };
50
50
  /** For a host that reads the studio's client-held state. */
51
51
  export declare const theoremReplaySchema: z.ZodType<TheoremReplay>;
52
+ /** Which prompt lines a turn gets: the path it says it came by, and its `identity.conditions`. */
53
+ export type StatedTurn = {
54
+ path?: string;
55
+ conditions?: Record<string, string>;
56
+ };
57
+ /** A stated turn off the wire. */
58
+ export declare const statedTurnSchema: z.ZodType<StatedTurn>;
52
59
  /**
53
60
  * One message. The host resolves the profile and owns the conversation: the
54
61
  * client sends only the message and the ref of the conversation it continues.
@@ -63,6 +70,8 @@ export type TheoremTurnRequest = {
63
70
  conversation?: string;
64
71
  model?: string;
65
72
  effort?: string;
73
+ /** For a host that lets its page choose (`pageStatesTurn`); any other host refuses it. */
74
+ stated?: StatedTurn;
66
75
  /** Client-generated id so mid-turn steers can find this turn's inbox. */
67
76
  turnId?: string;
68
77
  /** invariant: Both issued IDs identify each paused action that this message abandons. */
@@ -39,11 +39,19 @@ true;
39
39
  export const theoremReplaySchema = theoremReplay;
40
40
  /** An id the client made: trimmed, never blank. */
41
41
  const clientId = z.string().trim().min(1);
42
+ const statedTurn = z.object({
43
+ path: z.string().optional(),
44
+ conditions: z.record(z.string(), z.string()).optional(),
45
+ });
46
+ true;
47
+ /** A stated turn off the wire. */
48
+ export const statedTurnSchema = statedTurn;
42
49
  const theoremTurnRequest = z.object({
43
50
  input: theoremTurnInput,
44
51
  conversation: z.string().min(1).optional(),
45
52
  model: z.string().optional(),
46
53
  effort: z.string().optional(),
54
+ stated: statedTurn.optional(),
47
55
  turnId: clientId.optional(),
48
56
  abandon: z.array(gateReferenceSchema).min(1).optional(),
49
57
  replay: theoremReplay.pick({ sessionPermissions: true }).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/web",
3
- "version": "0.3.4-next.679.gcdf1a3a",
3
+ "version": "0.3.4-next.711.g78e504a",
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.679.gcdf1a3a"
28
+ "@theoremjs/agents": "^0.3.4-next.711.g78e504a"
29
29
  }
30
30
  }
@@ -1,5 +1,5 @@
1
1
  import { type ProviderCheckpoint, type TurnHistoryMessage } from '@theoremjs/agents';
2
- import type { TheoremSessionState } from './session-store.ts';
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
- : [id, operation.status, operation.profileId, operation.toolId, operation.invocationHash]);
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) => operation.status === 'settled' &&
25
- operation.outcome.effects !== 'unknown' &&
26
- operation.outcome.event.tool.readBack !== undefined &&
27
- portableHistory.some((message) => message.role === 'tool' &&
28
- message.tool_call_id === operation.callId &&
29
- message.name === operation.toolId &&
30
- message.content === operation.outcome.event.tool.readBack));
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))) {
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { type ProviderCheckpoint, TheoremError, type TurnEvent, type TurnHistoryMessage, z } from '@theoremjs/agents';
13
13
  import type { TranscriptBlock } from '@theoremjs/agents/interface';
14
+ import { type StatedTurn } from '../client/transport.ts';
14
15
  import { type MemorySessionStoreOptions } from './session-store.ts';
15
16
  /** A reply paused on gates: its events so far, and the history it was given. */
16
17
  export type PausedReply = {
@@ -23,9 +24,10 @@ export type PausedReply = {
23
24
  workedMs: number;
24
25
  /** When the reply last paused (epoch ms). */
25
26
  endedAt: number;
26
- /** The model and effort the reply ran with, for the run that goes on after its gates. */
27
+ /** The model, effort, and stated turn the reply ran with, for the run that goes on after its gates. */
27
28
  model?: string;
28
29
  effort?: string;
30
+ stated?: StatedTurn;
29
31
  };
30
32
  /** One conversation as the host stores it. */
31
33
  export type StoredConversation = {
@@ -12,6 +12,7 @@
12
12
  import { labelText, providerCheckpointSchema, TheoremError, turnEventSchema, turnHistoryMessageSchema, z, } from '@theoremjs/agents';
13
13
  import { openSecret, sealSecret } from '@theoremjs/agents/kernel';
14
14
  import { transcriptBlockSchema } from "../client/transcript-schema.js";
15
+ import { statedTurnSchema } from "../client/transport.js";
15
16
  import { memoryStoreLimits } from "./session-store.js";
16
17
  const pausedReply = z.object({
17
18
  events: z.array(turnEventSchema),
@@ -21,6 +22,7 @@ const pausedReply = z.object({
21
22
  endedAt: z.number(),
22
23
  model: z.string().optional(),
23
24
  effort: z.string().optional(),
25
+ stated: statedTurnSchema.optional(),
24
26
  });
25
27
  true;
26
28
  const storedConversation = z.object({
@@ -14,10 +14,10 @@
14
14
  *
15
15
  * @module
16
16
  */
17
- import { type GateReference, type LabelOverrides, type Profile, type ProviderCheckpoint, TheoremError, type TurnEvent, type TurnHistoryMessage, type TurnInput } from '@theoremjs/agents';
17
+ import { type GateReference, type LabelOverrides, type Profile, type ProviderCheckpoint, TheoremError, type ToolId, type TurnEvent, type TurnHistoryMessage, type TurnInput } from '@theoremjs/agents';
18
18
  import { type ClientTurnOptions } from '@theoremjs/agents/host';
19
19
  import { type ComposerProfileInterface, streamThoughtsEnabled, type TranscriptBlock, type UserTurnDraft, type UserTurnHistoryMedia } from '@theoremjs/agents/interface';
20
- import type { ConversationLine, ConversationView, TheoremTurnInput } from '../client/transport.ts';
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). */
23
23
  export type WalkedAwayCall = {
@@ -119,6 +119,9 @@ export type ReplyTurn = {
119
119
  providerState?: ProviderCheckpoint;
120
120
  model?: string;
121
121
  effort?: string;
122
+ stated?: StatedTurn;
123
+ /** The tools the reply had when it paused, for a reply that goes on. */
124
+ promoted?: ToolId[];
122
125
  };
123
126
  /** What a reply's model run takes from the host. */
124
127
  export type ReplyRunner = {
@@ -131,6 +134,8 @@ export type ReplyRunner = {
131
134
  compact(history: TurnHistoryMessage[], tokens: number, model: string | undefined): Promise<TurnHistoryMessage[] | undefined>;
132
135
  model?: string;
133
136
  effort?: string;
137
+ stated?: StatedTurn;
138
+ promoted?: ToolId[];
134
139
  signal: AbortSignal;
135
140
  };
136
141
  /** A line of a reply's stream: an event, or the ref of what the store wrote. */
@@ -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, replyToolIdsFromEvents, 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";
@@ -192,6 +192,7 @@ function pausedOn(conversation, reply, runner) {
192
192
  endedAt: Date.now(),
193
193
  ...(runner.model ? { model: runner.model } : {}),
194
194
  ...(runner.effort ? { effort: runner.effort } : {}),
195
+ ...(runner.stated ? { stated: runner.stated } : {}),
195
196
  };
196
197
  return { ...conversation, ...metered(conversation, reply.events), paused };
197
198
  }
@@ -234,6 +235,8 @@ async function* runReply(host, opened, reply, runner, input, withMessage) {
234
235
  providerState: conversation.providerState,
235
236
  model: runner.model,
236
237
  effort: runner.effort,
238
+ ...(runner.stated ? { stated: runner.stated } : {}),
239
+ ...(runner.promoted ? { promoted: runner.promoted } : {}),
237
240
  });
238
241
  for await (const event of events) {
239
242
  reply.events.push(event);
@@ -485,7 +488,13 @@ export async function* answerReply(args) {
485
488
  yield error;
486
489
  return;
487
490
  }
488
- yield* runReply(host, { ...opened, conversation: waiting }, reply, { ...runner, model: runner.model ?? paused.model, effort: runner.effort ?? paused.effort }, {
491
+ yield* runReply(host, { ...opened, conversation: waiting }, reply, {
492
+ ...runner,
493
+ model: runner.model ?? paused.model,
494
+ effort: runner.effort ?? paused.effort,
495
+ ...(paused.stated ? { stated: paused.stated } : {}),
496
+ promoted: replyToolIdsFromEvents(reply.events),
497
+ }, {
489
498
  history: replyHistory(reply),
490
499
  ...(waiting.inputTokens === undefined ? {} : { inputTokens: waiting.inputTokens }),
491
500
  ...(waiting.historyTokens === undefined ? {} : { historyTokens: waiting.historyTokens }),
@@ -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';
@@ -73,6 +74,11 @@ export type TheoremHandlerOptions = {
73
74
  * answered, with the `host`, `onStage` and `credentials` it gives for that call's turn.
74
75
  */
75
76
  prepare?: TurnWiring['prepare'];
77
+ /**
78
+ * Let the page say which prompt lines a turn gets (`stated`), for a page the host trusts as it
79
+ * trusts itself: the studio's. Unset refuses them, so a public page never chooses its own lines.
80
+ */
81
+ pageStatesTurn?: boolean;
76
82
  /**
77
83
  * The server's own stage handler, on every call this handler runs, a host's included: after
78
84
  * the application's, before steering. For a rule the server adds to whatever the application
@@ -112,6 +118,15 @@ export type TheoremHandlerOptions = {
112
118
  * answered by then settles as timed out. Unset, it waits until the page answers or the turn stops.
113
119
  */
114
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;
115
130
  /** Check current resource access before disclosing a saved tool result. This callback must be read-only. */
116
131
  authorizeToolReplay?: (context: {
117
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,8 @@ 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.promoted ? { promoted: turn.promoted } : {}),
430
+ ...turn.stated,
421
431
  }, {
422
432
  credentials: sessionCredentials(ctx, session.id),
423
433
  ...(onStage ? { onStage } : {}),
@@ -454,6 +464,7 @@ function chatRunner(ctx, request, session, generation) {
454
464
  },
455
465
  ...(generation.model ? { model: generation.model } : {}),
456
466
  ...(generation.effort ? { effort: generation.effort } : {}),
467
+ ...(generation.stated ? { stated: generation.stated } : {}),
457
468
  signal,
458
469
  };
459
470
  }
@@ -472,6 +483,9 @@ function conversationHost(ctx, session) {
472
483
  /** The person's message: a new reply, run against the conversation the host keeps. */
473
484
  async function turn(ctx, chat, request, session) {
474
485
  const body = await readBody(request, theoremTurnRequestSchema, ctx.options.maxRequestBytes);
486
+ if (body.stated && !ctx.options.pageStatesTurn) {
487
+ throw new TheoremError('request', labelText('error.request'));
488
+ }
475
489
  const message = postedMessage(chatInterface(chat), body.input, await ctx.options.context?.(request));
476
490
  const runner = chatRunner(ctx, request, session, body);
477
491
  const host = conversationHost(ctx, session);
@@ -481,7 +495,12 @@ async function turn(ctx, chat, request, session) {
481
495
  // labels-exempt: internal diagnostic; the user reads error.request
482
496
  if (body.abandon)
483
497
  throw statelessRefusal('walk away from');
484
- return eventStream(ctx, request, () => runner.run({ input: message.input, model: body.model, effort: body.effort }));
498
+ return eventStream(ctx, request, () => runner.run({
499
+ input: message.input,
500
+ model: body.model,
501
+ effort: body.effort,
502
+ ...(body.stated ? { stated: body.stated } : {}),
503
+ }));
485
504
  }
486
505
  const opened = await openConversation(host, body.conversation, {
487
506
  kind: 'message',
@@ -577,6 +596,7 @@ async function* callEvents(ctx, request, session, body) {
577
596
  const events = invokeTool({
578
597
  profile: ctx.profile.id,
579
598
  execution: sessionToolCoordinator(ctx.sessions, session.id),
599
+ ...backgroundWiring(ctx, request, session.id),
580
600
  pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker, ctx.options.pageToolTimeoutMs).execution,
581
601
  name: body.name,
582
602
  input: body.input,
@@ -615,7 +635,8 @@ async function* replayOperation(ctx, request, operation) {
615
635
  tool.type === 'builtin' ||
616
636
  !('tools' in ctx.profile) ||
617
637
  !ctx.profile.tools?.allow.includes(operation.toolId) ||
618
- (ctx.options.authorizeToolReplay &&
638
+ (operation.status === 'settled' &&
639
+ ctx.options.authorizeToolReplay &&
619
640
  !(await ctx.options.authorizeToolReplay({ request, operation }))))
620
641
  throw new TheoremError('blocked', labelText('error.blocked'));
621
642
  authorizeToolReplay({
@@ -623,9 +644,9 @@ async function* replayOperation(ctx, request, operation) {
623
644
  profile: ctx.profile,
624
645
  name: operation.toolId,
625
646
  resume: undefined,
626
- outcome: operation.outcome,
647
+ ...(operation.status === 'settled' ? { outcome: operation.outcome } : {}),
627
648
  });
628
- yield* once(operation.outcome.event, ctx.profile.labels);
649
+ yield* once(operation.status === 'settled' ? operation.outcome.event : operation.reply.event, ctx.profile.labels);
629
650
  }
630
651
  /**
631
652
  * The user's answer to a paused call. In a kept conversation the reply goes on
@@ -663,7 +684,7 @@ async function settlingOf(ctx, request, session, body) {
663
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)
664
685
  { copy: { key: 'tool.execution_uncertain' } });
665
686
  }
666
- if (operation?.status === 'settled' &&
687
+ if ((operation?.status === 'settled' || operation?.status === 'background') &&
667
688
  operation.profileId === ctx.profile.id &&
668
689
  operation.callId === body.gateId) {
669
690
  if (body.input !== undefined &&
package/server/mod.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export { theoremHostCallRequestSchema } from '../client/host-transport.ts';
9
9
  export type { LiveClientMessage, LiveOpenMessage } from '../client/live-messages.ts';
10
- export { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremReplaySchema, theoremRewindRequestSchema, theoremStartRequestSchema, theoremSteerRequestSchema, theoremStopRequestSchema, theoremTurnRequestSchema, } from '../client/transport.ts';
10
+ export { type StatedTurn, theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremReplaySchema, theoremRewindRequestSchema, theoremStartRequestSchema, theoremSteerRequestSchema, theoremStopRequestSchema, theoremTurnRequestSchema, } from '../client/transport.ts';
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';
@@ -8,11 +8,13 @@ export interface SessionBoundaryOptions {
8
8
  sessionStore: TheoremSessionStore;
9
9
  gateTtlMs?: number;
10
10
  operationRetentionMs?: number;
11
+ /** As `TheoremHandlerOptions.pageStatesTurn`: refuses a live call's `stated` unless set. */
12
+ pageStatesTurn?: boolean;
11
13
  pageToolBroker?: PageToolBroker;
12
14
  /** How long the page has to answer a tool call it runs, in milliseconds. Unset, it waits. */
13
15
  pageToolTimeoutMs?: number;
14
16
  }
15
- export type PreparedLiveOpen = Pick<SessionRequest, 'history' | 'slots' | 'context' | 'providerState' | 'awayMs'>;
17
+ export type PreparedLiveOpen = Pick<SessionRequest, 'history' | 'slots' | 'context' | 'providerState' | 'awayMs' | 'path' | 'conditions'>;
16
18
  export interface TheoremSessionBoundary {
17
19
  sessionId: string;
18
20
  setCookie?: string;
@@ -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) {
@@ -31,11 +31,14 @@ export async function createTheoremSessionBoundary(options) {
31
31
  toolExecution: sessionToolCoordinator(store, id),
32
32
  async prepareLiveOpen(raw, server) {
33
33
  const open = checkRequest(liveOpenMessageSchema, raw, 'live open');
34
+ if (open.stated && !options.pageStatesTurn) {
35
+ throw new TheoremError('request', labelText('error.request'));
36
+ }
34
37
  const prepared = liveSessionOpen(open, server);
35
38
  const state = await store.read(id);
36
39
  const providerState = await requireOwnedCheckpoint(state.providerCheckpoints, prepared.providerState, state.operations);
37
40
  if (providerState &&
38
- Object.values(state.operations).some((operation) => operation.status !== 'settled')) {
41
+ Object.values(state.operations).some((operation) => operation.status !== 'settled' && !detachedReply(operation))) {
39
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)
40
43
  { copy: { key: 'tool.execution_uncertain' } });
41
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;
@@ -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 { TheoremSessionState } from './session-store.ts';
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' && record.status !== 'uncertain') ||
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(),
@@ -7,4 +7,4 @@ export declare function kernelTurnInput(input: TheoremTurnInput, server?: unknow
7
7
  * What a live call's first message asks of the session, as `runSession` takes
8
8
  * it: the page's context is the client's, and `server` is the host's own.
9
9
  */
10
- export declare function liveSessionOpen(open: LiveOpenMessage, server?: unknown): Pick<SessionRequest, 'slots' | 'context' | 'providerState' | 'awayMs' | 'history'>;
10
+ export declare function liveSessionOpen(open: LiveOpenMessage, server?: unknown): Pick<SessionRequest, 'slots' | 'context' | 'providerState' | 'awayMs' | 'history' | 'path' | 'conditions'>;
@@ -22,6 +22,7 @@ export function liveSessionOpen(open, server) {
22
22
  ...(open.history ? { history: open.history } : {}),
23
23
  ...(open.slots ? { slots: open.slots } : {}),
24
24
  ...(context ? { context } : {}),
25
+ ...open.stated,
25
26
  ...(open.resume
26
27
  ? { providerState: open.resume.providerState, awayMs: open.resume.awayMs }
27
28
  : {}),
@@ -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;
@@ -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
  };