@theoremjs/web 0.3.4-next.715.geef22fd → 0.3.4-next.721.g7ff150a

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
@@ -131,8 +131,13 @@ const boundary = await createTheoremSessionBoundary({
131
131
  });
132
132
  const open = parseLiveOpenMessage(firstFrame);
133
133
  const session = await runSession(
134
- { profile, ...await boundary.prepareLiveOpen(open, hostContext) },
135
- { vault: hostKeyVault, execution: boundary.toolExecution, pageExecution: boundary.pageExecution },
134
+ {
135
+ profile,
136
+ ...await boundary.prepareLiveOpen(open, hostContext),
137
+ execution: boundary.toolExecution,
138
+ pageExecution: boundary.pageExecution,
139
+ },
140
+ { vault: hostKeyVault },
136
141
  );
137
142
  for await (const event of session.events()) {
138
143
  if ((event.type === 'done' || event.type === 'provider_checkpoint') && event.providerState) {
@@ -447,11 +452,11 @@ These claims do not provide exactly-once effects at the destination. Use the ope
447
452
  Concurrent pending attempts can reach preparation before one claims the body.
448
453
  Permission checks and pre-body hooks must be read-only or independently idempotent. The kernel cannot prove hook purity.
449
454
  A live session does not restore a dispatched call after an unexpected failure. It retains an uncertain call on that connection.
450
- To retain execution claims across connections, supply a durable coordinator through `RunSessionOptions.execution`.
455
+ To retain execution claims across connections, supply a durable coordinator as `execution` on `SessionRequest`.
451
456
  Browser dispatch uses a host-owned `PageToolExecution` service beside the execution coordinator.
452
- `createTheoremHandler` supplies both services. Live relays pass the session boundary's `pageExecution` and `toolExecution` to `runSession`.
457
+ `createTheoremHandler` supplies both services. Live relays put the session boundary's `pageExecution` and `toolExecution` on `SessionRequest`.
453
458
 
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`.
459
+ 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. A conversation read shows the call as it stands: running, then its result, or an error for a marked call. An open page reads it on its next turn or reload. When the session has dropped a settled operation, the block reads an error with the code `outcome_not_retained`. On a durable store, a worker that dies leaves the operation as `background`, so your sweep marks it `uncertain` with `worker_lost` through `compareAndSwap`. Keep the service's values valid after the request ends. A live relay passes its service to `runSession`.
455
460
  Forward live page results to `answerPageResult`. Preserve checkpoint ownership through `beforeEvent` when using the Studio bridge.
456
461
  The default `PageToolBroker` keeps active result waiters in process memory. Several workers need a shared broker or request affinity.
457
462
  Use `pageToolBroker` to supply that broker. It carries results; the atomic session store retains execution authority.
@@ -19,6 +19,10 @@ export type LiveCallOptions = {
19
19
  slots?: Record<string, string>;
20
20
  /** Which prompt lines the call gets, for a host that lets its page choose (`pageStatesTurn`). */
21
21
  stated?: StatedTurn;
22
+ /** The model picked for the call, read as the call starts, when the profile allows it. */
23
+ model?: string;
24
+ /** That model's effort, read as the call starts, when the binding allows it. */
25
+ effort?: string;
22
26
  /**
23
27
  * What the page tells the agent: any JSON. It goes with the call's opening, and
24
28
  * again whenever it changes, as background the agent reads without replying.
@@ -26,6 +26,10 @@ export interface LiveClientOptions {
26
26
  slots?: Record<string, string>;
27
27
  /** Which prompt lines the call gets, for a host that lets its page choose (`pageStatesTurn`). */
28
28
  stated?: StatedTurn;
29
+ /** The model picked for the call, when the profile allows it. */
30
+ model?: string;
31
+ /** That model's effort, when the binding allows it. */
32
+ effort?: string;
29
33
  /** What the page tells the agent as the call opens; `setContext` replaces it during the call. */
30
34
  context?: unknown;
31
35
  relayUrl?: string;
@@ -228,12 +228,14 @@ 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, stated, openMessage: host } = this.options;
231
+ const { slots, stated, model, effort, 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
236
  ...(stated ? { stated } : {}),
237
+ ...(model ? { model } : {}),
238
+ ...(effort ? { effort } : {}),
237
239
  ...(this.context === undefined ? {} : { context: this.context }),
238
240
  ...(this.providerState
239
241
  ? {
@@ -22,6 +22,10 @@ export type LiveOpenMessage = {
22
22
  context?: unknown;
23
23
  /** Which prompt lines the call gets, for a host that lets its page choose (`pageStatesTurn`). */
24
24
  stated?: StatedTurn;
25
+ /** The model picked for the call, when the profile allows it. */
26
+ model?: string;
27
+ /** That model's effort, when the binding allows it. */
28
+ effort?: string;
25
29
  /** A call that dropped, taken up again: the provider's handle, and how long the caller was away. */
26
30
  resume?: {
27
31
  providerState: ProviderCheckpoint;
@@ -15,6 +15,8 @@ const liveOpenMessage = z.object({
15
15
  slots: z.record(z.string(), z.string()).optional(),
16
16
  context: z.unknown().optional(),
17
17
  stated: statedTurnSchema.optional(),
18
+ model: z.string().optional(),
19
+ effort: z.string().optional(),
18
20
  resume: z
19
21
  .object({ providerState: providerCheckpointSchema, awayMs: z.number().nonnegative() })
20
22
  .optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/web",
3
- "version": "0.3.4-next.715.geef22fd",
3
+ "version": "0.3.4-next.721.g7ff150a",
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.715.geef22fd"
28
+ "@theoremjs/agents": "^0.3.4-next.721.g7ff150a"
29
29
  }
30
30
  }
package/server/handler.js CHANGED
@@ -42,7 +42,7 @@ import { answerGatedCall, resolveGateTtlMs, } from '@theoremjs/agents/kernel';
42
42
  import { hostInterface, theoremHostCallRequestSchema, } from "../client/host-transport.js";
43
43
  import { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremRewindRequestSchema, theoremSteerRequestSchema, theoremStopRequestSchema, theoremTurnRequestSchema, } from "../client/transport.js";
44
44
  import { answerReply, conversationView, messageReply, openConversation, postedMessage, readerConversationHost, requiredOperationId, rewindConversation, statelessRefusal, waitingCalls, } from "./conversation.js";
45
- import { unknownConversation } from "./conversation-store.js";
45
+ import { unknownConversation, } from "./conversation-store.js";
46
46
  import { createMemoryCredentialStore, } from "./credential-store.js";
47
47
  import { sessionStateStore, } from "./session-authority.js";
48
48
  import { theoremSession } from "./session-identity.js";
@@ -519,7 +519,57 @@ async function conversation(ctx, request, session) {
519
519
  if (!host)
520
520
  throw statelessRefusal('read');
521
521
  const opened = await openConversation(host, body.conversation, { kind: 'read' });
522
- return jsonResponse(200, conversationView(host, opened.conversation));
522
+ return jsonResponse(200, await currentView(ctx, session, host, opened.conversation));
523
+ }
524
+ /**
525
+ * why: The transcript keeps a detached call as the turn left it. The session holds what became of
526
+ * it, so a call that has ended does not read as running.
527
+ */
528
+ async function currentView(ctx, session, host, conversation) {
529
+ const view = conversationView(host, conversation);
530
+ if (!view.transcript.some(isDetachedBlock))
531
+ return view;
532
+ const { operations } = await ctx.sessions.read(session.id);
533
+ return {
534
+ ...view,
535
+ transcript: view.transcript.map((block) => isDetachedBlock(block) ? lateBlock(block, operations, host, ctx.profile) : block),
536
+ };
537
+ }
538
+ function isDetachedBlock(block) {
539
+ return block.kind === 'tool' && block.tool.state?.phase === 'background';
540
+ }
541
+ /** A detached call's block as its operation stands. One the session no longer holds has no known end. */
542
+ function lateBlock(block, operations, host, profile) {
543
+ const { tool } = block;
544
+ const id = tool.operationId;
545
+ const operation = id && Object.hasOwn(operations, id) ? operations[id] : undefined;
546
+ if (operation?.status === 'running' || operation?.status === 'background')
547
+ return block;
548
+ if (operation?.status === 'settled') {
549
+ const settled = host.forReader(operation.outcome.event);
550
+ if (settled.type !== 'tool')
551
+ return block;
552
+ const { tool: state } = settled;
553
+ if (!('phase' in state) || (state.phase !== 'complete' && state.phase !== 'error'))
554
+ return block;
555
+ return { ...block, tool: { ...tool, state, endedAt: operation.settledAt } };
556
+ }
557
+ const message = labelText('tool.execution_uncertain', {}, profile.labels);
558
+ const failure = {
559
+ code: operation?.status === 'uncertain' ? operation.reason : 'outcome_not_retained',
560
+ kind: 'internal',
561
+ message,
562
+ };
563
+ const { name, callId } = tool;
564
+ const at = operation?.status === 'uncertain' ? operation.detectedAt : (tool.state?.at ?? Date.now());
565
+ return {
566
+ ...block,
567
+ tool: {
568
+ ...tool,
569
+ state: { name, callId, at, phase: 'error', failure, readBack: message },
570
+ endedAt: at,
571
+ },
572
+ };
523
573
  }
524
574
  /** The conversation cut after one block; a reply that waits on gates is walked away from first. */
525
575
  async function rewind(ctx, request, session) {
@@ -542,7 +592,7 @@ async function rewind(ctx, request, session) {
542
592
  throw new TheoremError('internal', labelText('error.internal'));
543
593
  const rewound = {
544
594
  conversation: next.ref,
545
- view: conversationView(host, next.conversation),
595
+ view: await currentView(ctx, session, host, next.conversation),
546
596
  };
547
597
  return jsonResponse(200, rewound);
548
598
  }
@@ -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' | 'path' | 'conditions'>;
10
+ export declare function liveSessionOpen(open: LiveOpenMessage, server?: unknown): Pick<SessionRequest, 'slots' | 'context' | 'providerState' | 'awayMs' | 'history' | 'path' | 'conditions' | 'model' | 'effort'>;
@@ -23,6 +23,8 @@ export function liveSessionOpen(open, server) {
23
23
  ...(open.slots ? { slots: open.slots } : {}),
24
24
  ...(context ? { context } : {}),
25
25
  ...open.stated,
26
+ ...(open.model ? { model: open.model } : {}),
27
+ ...(open.effort ? { effort: open.effort } : {}),
26
28
  ...(open.resume
27
29
  ? { providerState: open.resume.providerState, awayMs: open.resume.awayMs }
28
30
  : {}),