@noodleseed/one 0.151.0 → 0.152.0

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.
Files changed (64) hide show
  1. package/dist/assistant-embed-scaffold-template.d.ts +1 -0
  2. package/dist/assistant-embed-scaffold-template.d.ts.map +1 -1
  3. package/dist/assistant-embed-scaffold-template.js +80 -12
  4. package/dist/assistant-embed-scaffold-template.js.map +1 -1
  5. package/dist/commands/assistant-embed-ops.d.ts.map +1 -1
  6. package/dist/commands/assistant-embed-ops.js +141 -4
  7. package/dist/commands/assistant-embed-ops.js.map +1 -1
  8. package/dist/commands/assistant-ops.js +2 -1
  9. package/dist/commands/assistant-ops.js.map +1 -1
  10. package/node_modules/@noodle-borg/agent-kit/dist/behavior-skills.js +10 -10
  11. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +6 -5
  12. package/node_modules/@noodle-borg/agent-kit/dist/index.js +13 -1
  13. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +8 -0
  14. package/node_modules/@noodle-borg/agent-kit/dist/skill-content.js +1 -1
  15. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-operations-ref.js +78 -0
  16. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +156 -135
  17. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-renderer-ref.js +109 -0
  18. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  19. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-browser-fields.d.ts +3 -0
  20. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-browser-fields.js +15 -0
  21. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.d.ts +1 -1
  22. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +1 -1
  23. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +19 -11
  24. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +24 -23
  25. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript-events.d.ts +24 -0
  26. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript-events.js +39 -0
  27. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript.d.ts +6 -2
  28. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript.js +11 -0
  29. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-view-availability.d.ts +8 -0
  30. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-view-availability.js +8 -0
  31. package/node_modules/@noodle-borg/assistant-gateway/dist/elevation-store.d.ts +3 -0
  32. package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.d.ts +7 -1
  33. package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.js +1 -0
  34. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-elevation-store.d.ts +1 -0
  35. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-elevation-store.js +1 -0
  36. package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.d.ts +29 -0
  37. package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.js +27 -1
  38. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +1 -0
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +1 -0
  40. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -0
  41. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +2 -0
  42. package/node_modules/@noodle-borg/authoring/dist/server.js +3 -0
  43. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +1 -0
  44. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +4 -1
  45. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +4 -1
  46. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +4 -0
  47. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +20 -1
  48. package/node_modules/@noodle-borg/compiler/dist/state-handles.js +1 -0
  49. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -0
  50. package/node_modules/@noodle-borg/runtime/dist/index.js +1 -0
  51. package/node_modules/@noodle-borg/runtime/dist/state-handle-ownership.d.ts +21 -0
  52. package/node_modules/@noodle-borg/runtime/dist/state-handle-ownership.js +10 -0
  53. package/node_modules/@noodle-borg/runtime/package.json +6 -1
  54. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +8 -3
  55. package/node_modules/@noodle-borg/service/dist/routes/assistant-elevation.js +14 -7
  56. package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-replay.js +4 -2
  57. package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +4 -2
  58. package/node_modules/@noodle-borg/service/dist/routes/assistant-transcript.js +48 -9
  59. package/node_modules/@noodle-borg/service/dist/service.js +3 -0
  60. package/node_modules/@noodle-borg/service/package.json +1 -1
  61. package/node_modules/@noodle-borg/wire-contracts/dist/assistant.d.ts +19 -25
  62. package/node_modules/@noodle-borg/wire-contracts/dist/assistant.js +20 -34
  63. package/node_modules/@noodleseed/assistant/README.md +1 -1
  64. package/package.json +2 -2
@@ -0,0 +1,109 @@
1
+ /** Framework-neutral browser-client and linked-App renderer guidance. */
2
+ export function renderEmbeddedAssistantRendererReference() {
3
+ return [
4
+ '### Framework-neutral DOM client',
5
+ '',
6
+ 'Use the same DOM-free client directly without a component wrapper, and import the isolated App-view entry only when rendering linked Apps. The client keeps the session token in memory, and the element owns only App presentation:',
7
+ '',
8
+ '```html',
9
+ '<div id="assistant-app-views"></div>',
10
+ '```',
11
+ '',
12
+ '```ts',
13
+ 'import { createAssistantClient } from "@noodleseed/assistant/client";',
14
+ 'import "@noodleseed/assistant/app-view";',
15
+ 'import {',
16
+ ' type AssistantViewAvailableDetail,',
17
+ ' type NoodleAppViewElement,',
18
+ '} from "@noodleseed/assistant/app-view";',
19
+ '',
20
+ 'const assistant = createAssistantClient({',
21
+ ' sessionEndpoint: "/api/assistant/session",',
22
+ ' clientContext: () => ({',
23
+ ' locale: navigator.language,',
24
+ ' timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,',
25
+ ' }),',
26
+ '});',
27
+ '',
28
+ 'const appViews = document.querySelector("#assistant-app-views");',
29
+ 'if (!appViews) throw new Error("Missing App views host");',
30
+ 'const mountedViews = new Map<string, NoodleAppViewElement>();',
31
+ 'const readResolvedTheme = (): "light" | "dark" =>',
32
+ ' document.documentElement.classList.contains("dark") ? "dark" : "light";',
33
+ 'let resolvedTheme: "light" | "dark" = readResolvedTheme();',
34
+ 'const syncResolvedTheme = () => {',
35
+ ' resolvedTheme = readResolvedTheme();',
36
+ ' for (const mountedView of mountedViews.values()) mountedView.theme = resolvedTheme;',
37
+ '};',
38
+ 'new MutationObserver(syncResolvedTheme).observe(document.documentElement, {',
39
+ ' attributes: true,',
40
+ ' attributeFilter: ["class"],',
41
+ '});',
42
+ 'const appViewFor = (view: AssistantViewAvailableDetail) => {',
43
+ ' const key = `${view.id}:${view.resourceUri}`;',
44
+ ' let appView = mountedViews.get(key);',
45
+ ' if (!appView) {',
46
+ ' appView = document.createElement("noodle-app-view") as NoodleAppViewElement;',
47
+ ' appView.client = assistant;',
48
+ ' mountedViews.set(key, appView);',
49
+ ' appViews.append(appView);',
50
+ ' }',
51
+ ' appView.theme = resolvedTheme;',
52
+ ' return appView;',
53
+ '};',
54
+ '',
55
+ 'assistant.updateModelContext({',
56
+ " content: [{ type: 'text', text: 'The time-off form is mounted.' }],",
57
+ " structuredContent: { widget: { name: 'time-off', lifecycle: 'mounted' } },",
58
+ '});',
59
+ '',
60
+ 'assistant.subscribeChat((state) => {',
61
+ ' renderUIMessageState(state, {',
62
+ ' respond: (id, response) => assistant.respond(id, response),',
63
+ ' });',
64
+ ' const activeViewKeys = new Set<string>();',
65
+ ' for (const message of state.messages) {',
66
+ ' for (const part of message.parts) {',
67
+ " if (part.type === 'data-view') {",
68
+ ' const key = `${part.data.id}:${part.data.resourceUri}`;',
69
+ ' activeViewKeys.add(key);',
70
+ ' appViewFor(part.data).view = part.data;',
71
+ ' }',
72
+ ' }',
73
+ ' }',
74
+ ' for (const [key, mountedView] of mountedViews) {',
75
+ ' if (!activeViewKeys.has(key)) {',
76
+ ' mountedView.remove();',
77
+ ' mountedViews.delete(key);',
78
+ ' }',
79
+ ' }',
80
+ '});',
81
+ '```',
82
+ '',
83
+ '`subscribeChat` immediately emits a detached `{ messages, status, error? }` snapshot and then emits as `UIMessage.parts` change. Text uses `text`; Noodle confirmations, input requests, sign-in requests, tool results, and linked views use `data-confirmation`, `data-input-request`, `data-sign-in`, `data-tool-result`, and `data-view`. `renderUIMessageState` is application code: it must show a complete schema-aware review or requested form and require an explicit user gesture before calling the supplied `respond`. Never call `respond` while scanning a transcript snapshot. Interaction data moves through pending/submitting/accepted/declined/cancelled. Use raw `subscribe(...)` only for transport/session lifecycle events that are not transcript content.',
84
+ '',
85
+ 'For a chat-first custom host, raw `tool_started` carries the direct invocation call `id` and technical `tool` name before execution. Map known tools through a finite application-owned label table and use a neutral fallback such as "Working"; never mechanically expose an internal identifier as customer copy. Reserve one stable region with `role="status"` and `aria-live="polite"` for thinking, the mapped activity label, and the linked-view skeleton. `view_available` makes the view ready; raw `error` or chat `error` replaces it with `role="alert"`. Mark decorative skeleton shapes `aria-hidden="true"` and disable shimmer/transitions under `@media (prefers-reduced-motion: reduce)`.',
86
+ '',
87
+ '```ts',
88
+ 'const activityByTool: Readonly<Record<string, string>> = {',
89
+ ' list_open_items: "Loading open items",',
90
+ '};',
91
+ 'const viewSlots = new Map([["ui://workspace/current", "current-workspace"]]);',
92
+ '',
93
+ 'assistant.subscribe((event) => {',
94
+ ' if (event.event === "tool_started") {',
95
+ ' showActivity(event.data.id, activityByTool[event.data.tool] ?? "Working");',
96
+ ' }',
97
+ ' if (event.event === "view_available") {',
98
+ ' const key = viewSlots.get(event.data.resourceUri) ?? `${event.data.id}:${event.data.resourceUri}`;',
99
+ ' showReadyView(key, event.data);',
100
+ ' }',
101
+ ' if (event.event === "error") showActivityError(event.data.code);',
102
+ '});',
103
+ '```',
104
+ '',
105
+ '`data-view` means a completed tool has a linked MCP App view. Pass that typed part and the existing client to `<noodle-app-view>` in another component framework or plain DOM, or to its `NoodleAppView` React adapter. It retains one bridge for client + `view.id` + `view.resourceUri` and requests standard App teardown on semantic replacement, disconnect, or App request. That pair is transport identity: different call ids are distinct invocations and must not be deduplicated generically. If the product intentionally owns one current panel for a known resource, declare an application-owned slot map and replace only that slot. Deliberately map the bounded result to an application-trusted native component only when replacing the linked App UI.',
106
+ '',
107
+ ];
108
+ }
109
+ //# sourceMappingURL=skill-embedded-assistant-renderer-ref.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodle-borg/agent-kit",
3
- "version": "0.90.0",
3
+ "version": "0.91.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "engines": {
@@ -0,0 +1,3 @@
1
+ /** Positive allowlist of `server.assistant` fields that may reach the browser. */
2
+ export declare const ASSISTANT_BROWSER_UI_FIELDS: readonly ["theme", "layout", "behavior", "labels", "presentation", "suggestedPrompts", "privacyUrl", "termsUrl", "locale", "direction", "webmcp"];
3
+ //# sourceMappingURL=assistant-browser-fields.d.ts.map
@@ -0,0 +1,15 @@
1
+ /** Positive allowlist of `server.assistant` fields that may reach the browser. */
2
+ export const ASSISTANT_BROWSER_UI_FIELDS = [
3
+ 'theme',
4
+ 'layout',
5
+ 'behavior',
6
+ 'labels',
7
+ 'presentation',
8
+ 'suggestedPrompts',
9
+ 'privacyUrl',
10
+ 'termsUrl',
11
+ 'locale',
12
+ 'direction',
13
+ 'webmcp',
14
+ ];
15
+ //# sourceMappingURL=assistant-browser-fields.js.map
@@ -1,7 +1,7 @@
1
1
  import type { ArtifactServer } from '@noodle-borg/compiler';
2
2
  import { type AssistantAppearanceOverride } from './assistant-appearance.js';
3
3
  import type { AssistantAppearanceSettingsStore } from './assistant-appearance-store.js';
4
- import { ASSISTANT_BROWSER_UI_FIELDS } from './assistant-store.js';
4
+ import { ASSISTANT_BROWSER_UI_FIELDS } from './assistant-browser-fields.js';
5
5
  import type { TenantRef } from './tenant-ref.js';
6
6
  /**
7
7
  * What the browser is allowed to see.
@@ -1,5 +1,5 @@
1
1
  import { resolveAssistantAppearanceConfiguration, } from './assistant-appearance.js';
2
- import { ASSISTANT_BROWSER_UI_FIELDS } from './assistant-store.js';
2
+ import { ASSISTANT_BROWSER_UI_FIELDS } from './assistant-browser-fields.js';
3
3
  /**
4
4
  * What the browser is allowed to see.
5
5
  *
@@ -1,20 +1,14 @@
1
1
  import type { ArtifactServer } from '@noodle-borg/compiler';
2
2
  import type { CallerIdentity } from '@noodle-borg/runtime';
3
+ import type { ASSISTANT_BROWSER_UI_FIELDS } from './assistant-browser-fields.js';
3
4
  import type { AssistantContextPreferences } from './assistant-context.js';
4
5
  import type { AssistantCustomerRouting } from './assistant-customer-routing.js';
5
- import { type AssistantInteractionClaimResult, type AssistantInteractionCompletionResult, type AssistantInteractionCreateInput, type AssistantInteractionRecord, type AssistantInteractionScope, type AssistantInteractionTransitionInput, type AssistantInteractionTransitionResult, type AssistantPendingConfirmationInteractionRecord, type AssistantPendingInputInteractionRecord } from './assistant-interaction-state.js';
6
+ import { type AssistantInteractionClaimResult, type AssistantInteractionCompletionResult, type AssistantInteractionCreateInput, type AssistantInteractionRecord, type AssistantInteractionScope, type AssistantInteractionTransitionInput, type AssistantInteractionTransitionResult, type AssistantPendingConfirmationInteractionRecord, type AssistantPendingInputInteractionRecord, type AssistantPendingInteractionRecord } from './assistant-interaction-state.js';
7
+ import type { AssistantRecoverableView } from './assistant-view-availability.js';
6
8
  import type { TenantRef } from './tenant-ref.js';
7
- export { ASSISTANT_INTERACTION_EXECUTING_GRACE_MS, ASSISTANT_INTERACTION_OUTCOME_RETENTION_MS, type AssistantConfirmationInteractionRecord, AssistantInteractionCapacityError, type AssistantInteractionClaimResult, type AssistantInteractionCompletion, type AssistantInteractionCompletionResult, type AssistantInteractionCreateInput, type AssistantInteractionKind, type AssistantInteractionPublicArray, type AssistantInteractionPublicObject, type AssistantInteractionPublicOutcome, type AssistantInteractionPublicValue, type AssistantInteractionRecord, type AssistantInteractionScope, type AssistantInteractionStatus, type AssistantInteractionTransitionInput, type AssistantInteractionTransitionNext, type AssistantInteractionTransitionResult, type AssistantPendingConfirmationInteractionRecord, type AssistantPendingInputInteractionRecord, DEFAULT_MAX_PENDING_INTERACTIONS_PER_SESSION, } from './assistant-interaction-state.js';
9
+ export { ASSISTANT_INTERACTION_EXECUTING_GRACE_MS, ASSISTANT_INTERACTION_OUTCOME_RETENTION_MS, type AssistantConfirmationInteractionRecord, AssistantInteractionCapacityError, type AssistantInteractionClaimResult, type AssistantInteractionCompletion, type AssistantInteractionCompletionResult, type AssistantInteractionCreateInput, type AssistantInteractionKind, type AssistantInteractionPublicArray, type AssistantInteractionPublicObject, type AssistantInteractionPublicOutcome, type AssistantInteractionPublicValue, type AssistantInteractionRecord, type AssistantInteractionScope, type AssistantInteractionStatus, type AssistantInteractionTransitionInput, type AssistantInteractionTransitionNext, type AssistantInteractionTransitionResult, type AssistantPendingConfirmationInteractionRecord, type AssistantPendingInputInteractionRecord, type AssistantPendingInteractionRecord, DEFAULT_MAX_PENDING_INTERACTIONS_PER_SESSION, } from './assistant-interaction-state.js';
8
10
  export declare const ASSISTANT_SESSION_IDLE_MS: number;
9
- /**
10
- * The only `server.assistant` fields that may reach a browser.
11
- *
12
- * It lives here, beside the record it types, so the allowlist and the shape it produces cannot drift
13
- * apart: the projection in `packages/service` iterates this same list. A denylist here would be a type
14
- * that lies — it once admitted `sessionClaims` key names, and would now admit the public surface's
15
- * capability allowlist — so growth in the manifest must not be able to grow the browser payload.
16
- */
17
- export declare const ASSISTANT_BROWSER_UI_FIELDS: readonly ["theme", "layout", "behavior", "labels", "presentation", "suggestedPrompts", "privacyUrl", "termsUrl", "locale", "direction", "webmcp"];
11
+ export { ASSISTANT_BROWSER_UI_FIELDS } from './assistant-browser-fields.js';
18
12
  export interface AssistantClientRecord {
19
13
  readonly id: string;
20
14
  readonly name: string;
@@ -79,6 +73,8 @@ export interface AssistantSessionRecord {
79
73
  readonly initialSuggestions?: AssistantInitialSuggestionsState;
80
74
  /** Latest validated follow-up suggestions, replayed with the visible transcript. */
81
75
  readonly latestSuggestions?: AssistantSuggestedPrompts;
76
+ /** Latest renderer descriptor; HTML is re-resolved from the current surface artifact on recovery. */
77
+ readonly latestView?: AssistantRecoverableView;
82
78
  }
83
79
  export interface AssistantSuggestedPrompts {
84
80
  readonly phase: 'initial' | 'follow_up';
@@ -165,6 +161,7 @@ export interface AssistantStore {
165
161
  completeInitialSuggestions(id: string, prompts: readonly string[]): Promise<boolean>;
166
162
  failInitialSuggestions(id: string): Promise<boolean>;
167
163
  replaceLatestSuggestions(id: string, suggestions: AssistantSuggestedPrompts | undefined): Promise<boolean>;
164
+ replaceLatestView(id: string, view: AssistantRecoverableView | undefined): Promise<boolean>;
168
165
  /** Atomically reserve one once-per-session model tool use. */
169
166
  claimModelToolUse(id: string, tool: string): Promise<boolean>;
170
167
  /** Release a reservation only when execution failed before a usable result or interaction existed. */
@@ -221,6 +218,11 @@ export interface AssistantStore {
221
218
  }): Promise<AssistantInteractionCompletionResult>;
222
219
  transitionInteraction(input: AssistantInteractionTransitionInput): Promise<AssistantInteractionTransitionResult>;
223
220
  getInteraction(input: AssistantInteractionScope): Promise<AssistantInteractionRecord | undefined>;
221
+ findPendingInteraction(input: {
222
+ readonly sessionId: string;
223
+ readonly deploymentId: string;
224
+ readonly now: Date;
225
+ }): Promise<AssistantPendingInteractionRecord | undefined>;
224
226
  /** @deprecated Temporary compatibility for the accept-only route while it moves to claim/complete. */
225
227
  consumeInteraction(id: string, sessionId: string, deploymentId: string, now: Date): Promise<(import('./assistant-interaction-state.js').AssistantConfirmationInteractionRecord & {
226
228
  readonly status: 'executing';
@@ -269,6 +271,7 @@ export declare class InMemoryAssistantStore implements AssistantStore {
269
271
  completeInitialSuggestions(id: string, prompts: readonly string[]): Promise<boolean>;
270
272
  failInitialSuggestions(id: string): Promise<boolean>;
271
273
  replaceLatestSuggestions(id: string, suggestions: AssistantSuggestedPrompts | undefined): Promise<boolean>;
274
+ replaceLatestView(id: string, view: AssistantRecoverableView | undefined): Promise<boolean>;
272
275
  claimModelToolUse(id: string, tool: string): Promise<boolean>;
273
276
  releaseModelToolUse(id: string, tool: string): Promise<boolean>;
274
277
  getSession(token: string, now: Date): Promise<AssistantSessionRecord | undefined>;
@@ -285,6 +288,11 @@ export declare class InMemoryAssistantStore implements AssistantStore {
285
288
  }): Promise<AssistantInteractionCompletionResult>;
286
289
  transitionInteraction(input: AssistantInteractionTransitionInput): Promise<AssistantInteractionTransitionResult>;
287
290
  getInteraction(input: AssistantInteractionScope): Promise<AssistantInteractionRecord | undefined>;
291
+ findPendingInteraction(input: {
292
+ readonly sessionId: string;
293
+ readonly deploymentId: string;
294
+ readonly now: Date;
295
+ }): Promise<AssistantPendingInteractionRecord | undefined>;
288
296
  consumeInteraction(id: string, sessionId: string, deploymentId: string, now: Date): Promise<(import('./assistant-interaction-state.js').AssistantConfirmationInteractionRecord & {
289
297
  readonly status: 'executing';
290
298
  }) | undefined>;
@@ -3,29 +3,7 @@ import { ADMISSION_DEFAULTS } from '@noodle-borg/admission-limits/portable';
3
3
  import { AssistantInteractionCapacityError, cloneInteraction, completedInteraction, createPendingInteraction, DEFAULT_MAX_PENDING_INTERACTIONS_PER_SESSION, executingInteraction, expireStrandedExecutingInteraction, isPrunableInteraction, isTerminalInteraction, shouldExpireStrandedExecutingInteraction, transitionedInteractions, } from './assistant-interaction-state.js';
4
4
  export { ASSISTANT_INTERACTION_EXECUTING_GRACE_MS, ASSISTANT_INTERACTION_OUTCOME_RETENTION_MS, AssistantInteractionCapacityError, DEFAULT_MAX_PENDING_INTERACTIONS_PER_SESSION, } from './assistant-interaction-state.js';
5
5
  export const ASSISTANT_SESSION_IDLE_MS = 30 * 60 * 1000;
6
- /**
7
- * The only `server.assistant` fields that may reach a browser.
8
- *
9
- * It lives here, beside the record it types, so the allowlist and the shape it produces cannot drift
10
- * apart: the projection in `packages/service` iterates this same list. A denylist here would be a type
11
- * that lies — it once admitted `sessionClaims` key names, and would now admit the public surface's
12
- * capability allowlist — so growth in the manifest must not be able to grow the browser payload.
13
- */
14
- export const ASSISTANT_BROWSER_UI_FIELDS = [
15
- 'theme',
16
- 'layout',
17
- 'behavior',
18
- 'labels',
19
- 'presentation',
20
- 'suggestedPrompts',
21
- 'privacyUrl',
22
- 'termsUrl',
23
- 'locale',
24
- 'direction',
25
- // Not appearance: the browser must know whether this deployment opted into projecting its tools to
26
- // a page agent (ADR 0220). It travels the allowlist like everything else the page may see.
27
- 'webmcp',
28
- ];
6
+ export { ASSISTANT_BROWSER_UI_FIELDS } from './assistant-browser-fields.js';
29
7
  /** Latest prompt messages retained for model continuity; turn admission is tracked separately. */
30
8
  export const ASSISTANT_HISTORY_MAX_MESSAGES = 40;
31
9
  export class InMemoryAssistantStore {
@@ -199,6 +177,19 @@ export class InMemoryAssistantStore {
199
177
  }
200
178
  return true;
201
179
  }
180
+ async replaceLatestView(id, view) {
181
+ const session = this.#sessions.get(id);
182
+ if (!session)
183
+ return false;
184
+ const next = {
185
+ ...session,
186
+ ...(view === undefined ? {} : { latestView: structuredClone(view) }),
187
+ };
188
+ if (view === undefined)
189
+ delete next.latestView;
190
+ this.#sessions.set(id, next);
191
+ return true;
192
+ }
202
193
  async claimModelToolUse(id, tool) {
203
194
  const session = this.#sessions.get(id);
204
195
  if (!session || session.modelToolUses.includes(tool))
@@ -319,6 +310,16 @@ export class InMemoryAssistantStore {
319
310
  const interaction = this.#scopedInteraction(input);
320
311
  return interaction ? cloneInteraction(interaction) : undefined;
321
312
  }
313
+ async findPendingInteraction(input) {
314
+ this.#pruneInteractions(input.now);
315
+ const interaction = [...this.#interactions.values()]
316
+ .filter((candidate) => candidate.sessionId === input.sessionId &&
317
+ candidate.deploymentId === input.deploymentId &&
318
+ candidate.status === 'pending' &&
319
+ Date.parse(candidate.expiresAt) > input.now.getTime())
320
+ .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
321
+ return interaction ? cloneInteraction(interaction) : undefined;
322
+ }
322
323
  async consumeInteraction(id, sessionId, deploymentId, now) {
323
324
  const result = this.#claimInteraction({ id, sessionId, deploymentId, now }, 'confirmation');
324
325
  return result.disposition === 'claimed' && result.interaction.kind === 'confirmation'
@@ -0,0 +1,24 @@
1
+ import type { AssistantTranscriptEntry } from './assistant-transcript.js';
2
+ import type { AssistantViewAvailableData } from './assistant-view-availability.js';
3
+ export interface AssistantTranscriptEvent {
4
+ readonly event: string;
5
+ readonly data: Readonly<Record<string, unknown>>;
6
+ }
7
+ export interface AssistantRecoveredInteraction {
8
+ readonly event: 'tool_proposed' | 'input_requested';
9
+ readonly data: Readonly<Record<string, unknown>>;
10
+ }
11
+ /**
12
+ * Project retained visual state through the ordinary assistant event protocol. The browser therefore
13
+ * has one parser and one rendering path for live and recovered turns; this function owns only ordering.
14
+ */
15
+ export declare function assistantTranscriptEvents(input: {
16
+ readonly entries: readonly AssistantTranscriptEntry[];
17
+ readonly latestView?: AssistantViewAvailableData;
18
+ readonly pendingInteraction?: AssistantRecoveredInteraction;
19
+ readonly suggestions?: {
20
+ readonly phase: 'initial' | 'follow_up';
21
+ readonly prompts: readonly string[];
22
+ };
23
+ }): readonly AssistantTranscriptEvent[];
24
+ //# sourceMappingURL=assistant-transcript-events.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Project retained visual state through the ordinary assistant event protocol. The browser therefore
3
+ * has one parser and one rendering path for live and recovered turns; this function owns only ordering.
4
+ */
5
+ export function assistantTranscriptEvents(input) {
6
+ const events = [];
7
+ let open = false;
8
+ let awaitingAssistant = false;
9
+ for (const entry of input.entries) {
10
+ if (entry.role === 'user') {
11
+ if (open)
12
+ events.push({ event: 'message_completed', data: {} });
13
+ events.push({ event: 'message_started', data: { message: entry.text } });
14
+ open = true;
15
+ awaitingAssistant = true;
16
+ continue;
17
+ }
18
+ if (!open || !awaitingAssistant) {
19
+ if (open)
20
+ events.push({ event: 'message_completed', data: {} });
21
+ events.push({ event: 'resume_started', data: { tool: '' } });
22
+ open = true;
23
+ }
24
+ events.push({ event: 'content', data: { delta: entry.text } });
25
+ awaitingAssistant = false;
26
+ }
27
+ if (open)
28
+ events.push({ event: 'message_completed', data: {} });
29
+ if (input.latestView)
30
+ events.push({ event: 'view_available', data: { ...input.latestView } });
31
+ if (input.pendingInteraction)
32
+ events.push(input.pendingInteraction);
33
+ if (input.suggestions?.phase === 'follow_up') {
34
+ events.push({ event: 'suggested_prompts', data: { ...input.suggestions } });
35
+ }
36
+ events.push({ event: 'done', data: {} });
37
+ return events;
38
+ }
39
+ //# sourceMappingURL=assistant-transcript-events.js.map
@@ -1,4 +1,3 @@
1
- import type { AssistantHistoryMessage } from './assistant-store.js';
2
1
  /**
3
2
  * The bounded visible-transcript projection (ADR 0141/0201 amendments 2026-08-26): what a widget may
4
3
  * repaint after a full-page navigation — the sign-in redirect above all — from the session's own
@@ -14,5 +13,10 @@ export interface AssistantTranscriptEntry {
14
13
  readonly role: 'user' | 'assistant';
15
14
  readonly text: string;
16
15
  }
17
- export declare function visibleTranscript(history: readonly AssistantHistoryMessage[]): readonly AssistantTranscriptEntry[];
16
+ export interface AssistantTranscriptHistoryEntry {
17
+ readonly role: 'user' | 'assistant';
18
+ readonly content: string;
19
+ readonly kind?: 'visible' | 'narration';
20
+ }
21
+ export declare function visibleTranscript(history: readonly AssistantTranscriptHistoryEntry[]): readonly AssistantTranscriptEntry[];
18
22
  //# sourceMappingURL=assistant-transcript.d.ts.map
@@ -1,3 +1,14 @@
1
+ /**
2
+ * The bounded visible-transcript projection (ADR 0141/0201 amendments 2026-08-26): what a widget may
3
+ * repaint after a full-page navigation — the sign-in redirect above all — from the session's own
4
+ * short-lived history. Transport-free like every gateway decision; `packages/service` adapts.
5
+ *
6
+ * Only rows explicitly tagged `visible` project. Everything else fails closed out of the transcript:
7
+ * `narration` rows are model-facing scaffolding (resolution summaries carrying tool output,
8
+ * `[platform]` resume prompts the visitor never saw), and an untagged legacy row cannot prove which
9
+ * it was. This is a read-only projection over state that already exists and already expires with the
10
+ * session — it is not a durable transcript, and nothing here may extend a retention window.
11
+ */
1
12
  export function visibleTranscript(history) {
2
13
  return history
3
14
  .filter((entry) => entry.kind === 'visible')
@@ -14,6 +14,14 @@ export interface AssistantViewAvailableData {
14
14
  readonly allowedOpenDomains?: readonly string[];
15
15
  readonly replayed?: true;
16
16
  }
17
+ /** Persisted renderer descriptor: bounded public data only, never HTML or a resource body. */
18
+ export interface AssistantRecoverableView {
19
+ readonly id: string;
20
+ readonly tool: string;
21
+ readonly result: unknown;
22
+ readonly arguments?: unknown;
23
+ }
24
+ export declare function recoverableAssistantView(view: Pick<AssistantViewAvailableData, 'id' | 'tool' | 'result' | 'arguments'>): AssistantRecoverableView;
17
25
  /** A declared widget the completed tool could not surface — reported so the drop is never silent. */
18
26
  export interface AssistantViewUnresolved {
19
27
  readonly tool: string;
@@ -1,5 +1,13 @@
1
1
  import { injectWidgetBridge } from '@noodle-borg/protocol';
2
2
  const MCP_APP_MIME_TYPE = 'text/html;profile=mcp-app';
3
+ export function recoverableAssistantView(view) {
4
+ return {
5
+ id: view.id,
6
+ tool: view.tool,
7
+ result: structuredClone(view.result),
8
+ ...(view.arguments === undefined ? {} : { arguments: structuredClone(view.arguments) }),
9
+ };
10
+ }
3
11
  /**
4
12
  * Resolve a self-contained, statically compiled MCP App document for the embedded host. The linked
5
13
  * resource must be present in the same compiled artifact and carry the MCP App MIME type; arbitrary
@@ -19,6 +19,8 @@ export interface AssistantElevationRecord {
19
19
  readonly tenant: TenantRef;
20
20
  /** The capability the visitor was refused, carried so the audit says what they signed in for. */
21
21
  readonly tool: string;
22
+ /** Canonical server-side snapshot of handles eligible when this ticket was issued. */
23
+ readonly claimableStateHandles: readonly string[];
22
24
  readonly continuationHash: string;
23
25
  readonly createdAt: string;
24
26
  readonly expiresAt: string;
@@ -48,6 +50,7 @@ export interface AssistantElevationStore {
48
50
  readonly sessionId: string;
49
51
  readonly tenant: TenantRef;
50
52
  readonly tool: string;
53
+ readonly claimableStateHandles?: readonly string[];
51
54
  readonly now: Date;
52
55
  }): Promise<{
53
56
  readonly elevation: AssistantElevationRecord;
@@ -66,10 +66,16 @@ export type ElevationResult = {
66
66
  readonly token: string;
67
67
  /** What the visitor was refused before signing in, for the audit trail. */
68
68
  readonly tool: string;
69
+ /** Whether one-shot auto-resume was actually armed after pending-interaction suppression. */
70
+ readonly resumeArmed: boolean;
69
71
  } | {
70
72
  readonly ok: false;
71
73
  readonly status: 403 | 409;
72
- readonly code: 'elevation_ticket_invalid' | 'elevation_ticket_expired' | 'elevation_tenant_mismatch' | 'elevation_session_unavailable' | 'elevation_already_signed_in';
74
+ readonly code: 'elevation_ticket_invalid' | 'elevation_ticket_expired' | 'elevation_tenant_mismatch' | 'elevation_session_unavailable' | 'elevation_already_signed_in' | 'elevation_state_conflict';
73
75
  };
76
+ /** One commit boundary for ticket spend, state adoption, session rebind, and audit. */
77
+ export interface AssistantElevationCoordinator {
78
+ complete(request: ElevationRequest): Promise<ElevationResult>;
79
+ }
74
80
  export declare function completeElevation(request: ElevationRequest, ports: ElevationPorts): Promise<ElevationResult>;
75
81
  //# sourceMappingURL=elevation.d.ts.map
@@ -45,6 +45,7 @@ export async function completeElevation(request, ports) {
45
45
  session: elevated.session,
46
46
  token: elevated.token,
47
47
  tool: claimed.elevation.tool,
48
+ resumeArmed: request.resume === true,
48
49
  };
49
50
  }
50
51
  //# sourceMappingURL=elevation.js.map
@@ -7,6 +7,7 @@ export declare class InMemoryAssistantElevationStore implements AssistantElevati
7
7
  readonly sessionId: string;
8
8
  readonly tenant: TenantRef;
9
9
  readonly tool: string;
10
+ readonly claimableStateHandles?: readonly string[];
10
11
  readonly now: Date;
11
12
  }): Promise<{
12
13
  readonly elevation: AssistantElevationRecord;
@@ -15,6 +15,7 @@ export class InMemoryAssistantElevationStore {
15
15
  sessionId: input.sessionId,
16
16
  tenant: input.tenant,
17
17
  tool: input.tool,
18
+ claimableStateHandles: [...(input.claimableStateHandles ?? [])].sort(),
18
19
  continuationHash: elevationDigest(continuation),
19
20
  createdAt: input.now.toISOString(),
20
21
  expiresAt: new Date(input.now.getTime() + ASSISTANT_ELEVATION_TTL_MS).toISOString(),
@@ -55,6 +55,34 @@ export declare function managedSpendUnitWeight(rung: number): number;
55
55
  * stretches further.
56
56
  */
57
57
  export declare function managedSpendAllowanceForTurns(turnsPerDay: number): number;
58
+ /**
59
+ * Output tokens a provider bills for one completion.
60
+ *
61
+ * Reasoning tokens are output. Every provider we serve bills "thinking" at the output rate, and the
62
+ * production surface measured them at 5.4x the visible completion — so counting `completionTokens`
63
+ * alone enforces a per-turn ceiling on roughly a sixth of what the turn costs, and reports the same
64
+ * shortfall to the drift check whose entire job is noticing a rung that does not bound what it
65
+ * claims. One definition, used by the enforcer and by the telemetry that audits the enforcer,
66
+ * because two definitions is how the blind spot got here.
67
+ *
68
+ * An absent count means the provider reported none, never "unknown": a model with no reasoning
69
+ * phase omits the field, and must read exactly as it did before.
70
+ */
71
+ export declare function billedOutputTokens(usage: {
72
+ readonly completionTokens: number;
73
+ readonly reasoningTokens?: number;
74
+ }): number;
75
+ /**
76
+ * What remains of a turn's token budget after one completion.
77
+ *
78
+ * The fallback when a provider reports no usage at all is the whole request limit rather than zero:
79
+ * a step whose cost is unknown must be assumed to have spent everything it was allowed, or a
80
+ * provider that omits the block would make the turn unbounded.
81
+ */
82
+ export declare function remainingTurnTokens(remaining: number, usage: {
83
+ readonly completionTokens: number;
84
+ readonly reasoningTokens?: number;
85
+ } | undefined, requestLimit: number): number;
58
86
  /**
59
87
  * Is the ledger telling the truth?
60
88
  *
@@ -70,6 +98,7 @@ export declare function managedSpendDrift(observed: {
70
98
  readonly deliveredTurns: number;
71
99
  readonly promptTokens: number;
72
100
  readonly completionTokens: number;
101
+ readonly reasoningTokens?: number;
73
102
  }): {
74
103
  readonly observedPerTurn: number;
75
104
  readonly permittedPerTurn: number;
@@ -159,6 +159,32 @@ export function managedSpendAllowanceForTurns(turnsPerDay) {
159
159
  return 0;
160
160
  return Math.ceil(turnsPerDay) * managedSpendUnitWeight(0);
161
161
  }
162
+ /**
163
+ * Output tokens a provider bills for one completion.
164
+ *
165
+ * Reasoning tokens are output. Every provider we serve bills "thinking" at the output rate, and the
166
+ * production surface measured them at 5.4x the visible completion — so counting `completionTokens`
167
+ * alone enforces a per-turn ceiling on roughly a sixth of what the turn costs, and reports the same
168
+ * shortfall to the drift check whose entire job is noticing a rung that does not bound what it
169
+ * claims. One definition, used by the enforcer and by the telemetry that audits the enforcer,
170
+ * because two definitions is how the blind spot got here.
171
+ *
172
+ * An absent count means the provider reported none, never "unknown": a model with no reasoning
173
+ * phase omits the field, and must read exactly as it did before.
174
+ */
175
+ export function billedOutputTokens(usage) {
176
+ return usage.completionTokens + (usage.reasoningTokens ?? 0);
177
+ }
178
+ /**
179
+ * What remains of a turn's token budget after one completion.
180
+ *
181
+ * The fallback when a provider reports no usage at all is the whole request limit rather than zero:
182
+ * a step whose cost is unknown must be assumed to have spent everything it was allowed, or a
183
+ * provider that omits the block would make the turn unbounded.
184
+ */
185
+ export function remainingTurnTokens(remaining, usage, requestLimit) {
186
+ return Math.max(0, remaining - (usage === undefined ? requestLimit : billedOutputTokens(usage)));
187
+ }
162
188
  /**
163
189
  * Is the ledger telling the truth?
164
190
  *
@@ -175,7 +201,7 @@ export function managedSpendDrift(observed) {
175
201
  if (observed.deliveredTurns <= 0) {
176
202
  return { observedPerTurn: 0, permittedPerTurn, exceeded: false };
177
203
  }
178
- const billable = observed.promptTokens + OUTPUT_WEIGHT * observed.completionTokens;
204
+ const billable = observed.promptTokens + OUTPUT_WEIGHT * billedOutputTokens(observed);
179
205
  const observedPerTurn = Math.round(billable / observed.deliveredTurns);
180
206
  return { observedPerTurn, permittedPerTurn, exceeded: observedPerTurn > permittedPerTurn };
181
207
  }
@@ -15,6 +15,7 @@ export * from './assistant-presentation.js';
15
15
  export * from './assistant-sensitive-values.js';
16
16
  export * from './assistant-store.js';
17
17
  export * from './assistant-transcript.js';
18
+ export * from './assistant-transcript-events.js';
18
19
  export * from './assistant-view-availability.js';
19
20
  export * from './elevation.js';
20
21
  export * from './elevation-store.js';
@@ -15,6 +15,7 @@ export * from './assistant-presentation.js';
15
15
  export * from './assistant-sensitive-values.js';
16
16
  export * from './assistant-store.js';
17
17
  export * from './assistant-transcript.js';
18
+ export * from './assistant-transcript-events.js';
18
19
  export * from './assistant-view-availability.js';
19
20
  export * from './elevation.js';
20
21
  export * from './elevation-store.js';
@@ -39,6 +39,7 @@
39
39
  "@noodle-borg/compiler": "0.0.0",
40
40
  "@noodle-borg/connector-defs": "0.0.0",
41
41
  "@noodle-borg/module": "0.0.0",
42
+ "@noodle-borg/module-audit": "0.0.0",
42
43
  "@noodle-borg/protocol": "0.0.0",
43
44
  "@noodle-borg/runtime": "0.0.0",
44
45
  "@noodleseed/assistant": "1.31.0",
@@ -19,6 +19,8 @@ export interface StateHandleOptions {
19
19
  readonly version: string;
20
20
  readonly scope?: StateHandleScope;
21
21
  readonly ttlSeconds?: number;
22
+ /** Move this caller-scoped, finite-lived state to the authenticated caller during ticket elevation. */
23
+ readonly claimOnAuthentication?: true;
22
24
  }
23
25
  export interface ServerOptions {
24
26
  readonly title: string;
@@ -231,6 +231,9 @@ class ServerBuilder {
231
231
  version: handle.version,
232
232
  ...(handle.scope !== undefined ? { scope: handle.scope } : {}),
233
233
  ...(handle.ttlSeconds !== undefined ? { ttlSeconds: handle.ttlSeconds } : {}),
234
+ ...(handle.claimOnAuthentication === true
235
+ ? { claimOnAuthentication: true }
236
+ : {}),
234
237
  },
235
238
  ])),
236
239
  },