@noodleseed/assistant 1.1.1 → 1.3.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.
package/README.md CHANGED
@@ -3,10 +3,11 @@
3
3
  Customer-branded embedded assistant surfaces for Noodle Seed deployments.
4
4
 
5
5
  The package exports the canonical `<noodle-assistant>` Web Component, a React wrapper from
6
- `@noodleseed/assistant/react`, and the backend-only `createAssistantSession` helper from
7
- `@noodleseed/assistant/server`. Light, dark, and automatic themes work without configuration; the component
8
- inherits the deployed MCP server's brand kit while slots, methods, events, and semantic CSS variables let a
9
- SaaS developer integrate it without depending on internal DOM selectors.
6
+ `@noodleseed/assistant/react`, a DOM-free client from `@noodleseed/assistant/client`, and the backend-only
7
+ `createAssistantSession` helper from `@noodleseed/assistant/server`. Light, dark, and automatic themes work
8
+ without configuration; the component inherits the deployed MCP server's brand kit while slots, methods,
9
+ events, and semantic CSS variables let a SaaS developer integrate it without depending on internal DOM
10
+ selectors.
10
11
 
11
12
  Never pass an embed client secret, model key, MCP token, or raw application session into the browser
12
13
  component. Exchange the already-authenticated user from the customer backend and return only the short-lived
@@ -14,10 +15,10 @@ assistant session.
14
15
 
15
16
  The model configuration and SaaS integration credentials have different owners:
16
17
 
17
- | Owner | Configuration | Destination |
18
- | --- | --- | --- |
19
- | Noodle deployment | `ASSISTANT_MODEL_BASE_URL`, `ASSISTANT_MODEL`, `ASSISTANT_MODEL_API_KEY` | Managed with `noodle variables set` / `noodle secrets set` |
20
- | Customer backend | `NOODLE_SERVICE_URL`, `NOODLE_ASSISTANT_CLIENT_ID`, `NOODLE_ASSISTANT_CLIENT_SECRET` | Backend environment or secret manager only |
18
+ | Owner | Configuration | Destination |
19
+ | ----------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
20
+ | Noodle deployment | `ASSISTANT_MODEL_BASE_URL`, `ASSISTANT_MODEL`, `ASSISTANT_MODEL_API_KEY` | Managed with `noodle variables set` / `noodle secrets set` |
21
+ | Customer backend | `NOODLE_SERVICE_URL`, `NOODLE_ASSISTANT_CLIENT_ID`, `NOODLE_ASSISTANT_CLIENT_SECRET` | Backend environment or secret manager only |
21
22
 
22
23
  `allowedOrigins` accepts exact origins. Production origins must be HTTPS; plain HTTP is accepted only for
23
24
  loopback development origins such as `http://localhost:3000`. Local MCP authoring remains available without
@@ -40,6 +41,109 @@ example:
40
41
  npm install @noodleseed/assistant
41
42
  ```
42
43
 
44
+ ### Author `server.ts`
45
+
46
+ Put customer identity and colors in the server's top-level `branding` option. Put only assistant-specific
47
+ structure and treatment in `embeddedAssistant({ presentation })`:
48
+
49
+ ```ts
50
+ import {
51
+ embeddedAssistant,
52
+ openAICompatible,
53
+ secret,
54
+ server,
55
+ tool,
56
+ variable,
57
+ z,
58
+ } from "@noodleseed/one";
59
+
60
+ export default server(
61
+ "acme_support",
62
+ {
63
+ title: "Acme Support",
64
+ version: "1.0.0",
65
+ branding: {
66
+ name: "Acme Assistant",
67
+ accent: "#5B4CF0",
68
+ surface: "#FFFFFF",
69
+ surfaceDark: "#15131A",
70
+ mark: { uri: "https://assets.acme.example/mark.svg", alt: "Acme" },
71
+ colorScheme: "auto",
72
+ },
73
+ assistant: embeddedAssistant({
74
+ model: openAICompatible({
75
+ baseUrl: variable("ASSISTANT_MODEL_BASE_URL"),
76
+ model: variable("ASSISTANT_MODEL"),
77
+ apiKey: secret("ASSISTANT_MODEL_API_KEY"),
78
+ }),
79
+ allowedOrigins: ["http://localhost:3000", "https://app.acme.example"],
80
+ layout: {
81
+ mode: "floating",
82
+ position: "bottom-right",
83
+ panelWidth: 520,
84
+ panelMinHeight: 540,
85
+ panelMaxHeight: 740,
86
+ edgeOffset: 24,
87
+ },
88
+ behavior: { showTimestamps: true },
89
+ labels: {
90
+ welcomeHeading: "How can Acme help?",
91
+ welcomeMessage: "Fast answers from the tools your team already uses.",
92
+ composerPlaceholder: "Message Acme Support…",
93
+ sessionReady: "Acme support is online",
94
+ },
95
+ presentation: {
96
+ panel: {
97
+ surface: "solid",
98
+ elevation: "dramatic",
99
+ border: "strong",
100
+ radius: 20,
101
+ },
102
+ launcher: {
103
+ icon: "chat",
104
+ size: "lg",
105
+ status: "session",
106
+ effect: "pulse",
107
+ },
108
+ header: {
109
+ mark: "status",
110
+ badge: { text: "Support online", tone: "success", indicator: true },
111
+ },
112
+ composer: {
113
+ leadingIcon: "brand-mark",
114
+ sendIcon: "paper-plane",
115
+ shape: "rounded",
116
+ },
117
+ messages: { userStyle: "accent", assistantStyle: "bubble" },
118
+ },
119
+ }),
120
+ },
121
+ [
122
+ tool("status", {
123
+ description: "Read the support service status.",
124
+ input: z.object({}),
125
+ output: z.object({ status: z.string() }),
126
+ fulfil: () => ({ status: "operational" }),
127
+ }),
128
+ ],
129
+ );
130
+ ```
131
+
132
+ `presentation` is a closed set of semantic primitives for the panel, launcher, header, composer, and
133
+ messages. The Atlas-style product treatment above is the maximum supported customization level: it can
134
+ change geometry, status decoration, controls, and message treatment without replacing the assistant's
135
+ structure. It has no raw HTML, CSS, inline SVG, class-name, or callback field;
136
+ markup-looking text stays text. `presentation.panel.radius` is a bounded panel-specific geometry override,
137
+ not a second color or identity source. An HTTPS or packaged SVG referenced by `branding.logo`, `branding.mark`, or
138
+ `branding.avatar` is a bounded asset, not inline renderer markup.
139
+
140
+ If `presentation` is omitted, the complete Halo baseline remains: glass panel, soft elevation, subtle
141
+ border/motion, medium brand-mark launcher without status or effect, undecorated header, pill composer with
142
+ no leading icon and an arrow-up send icon, and bubble user/plain assistant messages at comfortable width.
143
+ Partial objects merge with those defaults.
144
+
145
+ ### Configure and deploy
146
+
43
147
  Configure the model through Noodle managed config, validate, and deploy the assistant-enabled server before
44
148
  creating the embed client:
45
149
 
@@ -62,7 +166,7 @@ Move those values to the customer backend's secret manager without printing or c
62
166
  route can exchange the current signed-in user like this:
63
167
 
64
168
  ```ts
65
- import { createAssistantSession } from '@noodleseed/assistant/server';
169
+ import { createAssistantSession } from "@noodleseed/assistant/server";
66
170
 
67
171
  export async function POST(request: Request) {
68
172
  const user = await requireCurrentUser(request);
@@ -74,6 +178,7 @@ export async function POST(request: Request) {
74
178
  origin: process.env.PUBLIC_APP_ORIGIN!,
75
179
  user: { id: user.id, email: user.email, roles: user.roles },
76
180
  context,
181
+ preferences: { locale: user.locale, timeZone: user.timeZone },
77
182
  });
78
183
  return Response.json(session);
79
184
  }
@@ -82,21 +187,103 @@ export async function POST(request: Request) {
82
187
  Then add the framework-neutral element:
83
188
 
84
189
  ```ts
85
- import '@noodleseed/assistant';
190
+ import "@noodleseed/assistant";
86
191
  ```
87
192
 
88
193
  ```html
89
- <noodle-assistant session-endpoint="/api/assistant/session" theme="auto"></noodle-assistant>
194
+ <noodle-assistant
195
+ session-endpoint="/api/assistant/session"
196
+ theme="auto"
197
+ ></noodle-assistant>
90
198
  ```
91
199
 
92
200
  Or use React:
93
201
 
94
202
  ```tsx
95
- import { NoodleAssistant } from '@noodleseed/assistant/react';
203
+ import { NoodleAssistant } from "@noodleseed/assistant/react";
96
204
 
97
205
  <NoodleAssistant sessionEndpoint="/api/assistant/session" theme="auto" />;
98
206
  ```
99
207
 
208
+ For a customer-owned renderer, subscribe to the same turn and interaction stream without registering a
209
+ custom element or touching browser storage:
210
+
211
+ ```ts
212
+ import { createAssistantClient } from "@noodleseed/assistant/client";
213
+
214
+ const assistant = createAssistantClient({
215
+ sessionEndpoint: "/api/assistant/session",
216
+ clientContext: () => ({
217
+ locale: navigator.language,
218
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
219
+ }),
220
+ });
221
+
222
+ assistant.updateModelContext({
223
+ content: [{ type: "text", text: "The time-off form is mounted." }],
224
+ structuredContent: { widget: { name: "time-off", lifecycle: "mounted" } },
225
+ });
226
+
227
+ const unsubscribe = assistant.subscribe((event) => {
228
+ renderAssistantEvent(event);
229
+ if (event.event === "view_available") {
230
+ renderRegisteredView(event.data.resourceUri, event.data.result);
231
+ }
232
+ });
233
+ await assistant.sendMessage("Book next Thursday and Friday off");
234
+ await assistant.respond("interaction_123", { action: "accept" });
235
+ // Resolve one interaction once; alternatives are { action: 'decline' } or { action: 'cancel' }.
236
+
237
+ unsubscribe();
238
+ ```
239
+
240
+ `clientContext` contains untrusted locale/timezone presentation hints and is evaluated for every turn.
241
+ The client resolves a turn or interaction only after exactly one valid terminal `done` event followed by
242
+ stream EOF. A truncated or malformed stream, duplicate `done`, or any frame after `done` is
243
+ `invalid_response`; it never emits `message_completed` or `interaction_completed` for that response.
244
+ `updateContext` remains the separate untrusted page-context channel used on the next session exchange.
245
+ `updateModelContext({ content, structuredContent })` replaces the complete renderer snapshot attached to each
246
+ later message turn without starting a turn itself; updates do not merge with prior fields. The same method is
247
+ available on `<noodle-assistant>` for cohesive surface snapshots such as mounted, submitted, cancelled, or
248
+ dismissed. Model context is untrusted, per-turn data: it is not conversation history or authorization input,
249
+ and credential-shaped or unbounded updates are rejected.
250
+ Backend `preferences` are the signed-in user's saved locale/time-zone choices and outrank those hints. Only
251
+ message turns retry once after an expired session; the client never auto-retries interaction decisions. A
252
+ `tool_proposed` event carries a complete schema-projected review of the tool input and collected answers. For
253
+ a connector-backed tool it also identifies the exact connector version, operation, and resolved arguments;
254
+ confirmable flows contain at most that one connector operation. Sensitive fields may be redacted, but any
255
+ other truncation or omission fails closed. Accept is bound to that server-held action and claims one
256
+ execution attempt; drift fails closed, while decline/cancel resolve without execution. Normal terminal
257
+ outcomes scrub private arguments and continuations immediately. Only an accepted action still in the
258
+ `executing` state retains them during its one-hour unknown-outcome recovery window; expiry records a bounded
259
+ `interaction_outcome_unknown` result and scrubs the payload. Without downstream idempotency this is not an
260
+ exactly-once business-effect guarantee. A caller may explicitly repeat the same id and decision to retrieve
261
+ the durable stored outcome without another execution attempt.
262
+ At the manifest/runtime boundary, only `confirm: true` enables this gate; omitted or `false` preserves direct
263
+ execution and standard annotations remain hints. TypeScript action helpers likewise require
264
+ `{ confirm: true }`; action/destructive/open-world hints alone never enforce approval. Capable bidirectional
265
+ MCP transports render the same gate as standard form elicitation and fail closed when that exchange is
266
+ unavailable.
267
+ An `input_requested` event carries the message and portable form schema recorded by `ctx.elicit`. After the
268
+ turn stream completes, a headless renderer answers it with
269
+ `respond(id, { action: 'accept', content })`, or stops it with decline/cancel. The Web Component renders the
270
+ same primitive as native controls. Accepted content is schema-validated before the runtime resumes, and the
271
+ server-held continuation is never exposed to browser code. Schema-invalid content returns `arg_invalid` and
272
+ leaves the same input interaction pending so the renderer can submit a corrected answer. Every interactive
273
+ flow collects all elicited input before its first connector operation.
274
+
275
+ A completed widget-linked tool emits typed `view_available` data with its call/interaction id, tool,
276
+ `ui://` resource identity, optional title, and bounded/redacted public result. This is an availability
277
+ signal, not proof of rendering. Map the identity to a component already trusted by your application; never
278
+ fetch the `ui://` URI or inject its resource HTML into your page. The standard element does not render it,
279
+ but forwards the same detail as a DOM event:
280
+
281
+ ```ts
282
+ element.addEventListener("assistant-view-available", (event) => {
283
+ renderRegisteredView(event.detail.resourceUri, event.detail.result);
284
+ });
285
+ ```
286
+
100
287
  Assistant text renders as provider deltas arrive. If a turn finds an expired session, the component calls
101
288
  the same authenticated session endpoint and retries that unprocessed message once. Confirmations never
102
289
  replay across sessions. React applications may observe recovery and structured failures:
@@ -105,15 +292,18 @@ replay across sessions. React applications may observe recovery and structured f
105
292
  <NoodleAssistant
106
293
  sessionEndpoint="/api/assistant/session"
107
294
  onSessionExpired={() => reportAssistantRecovery()}
108
- onError={({ code, status, retryable }) => reportAssistantError({ code, status, retryable })}
109
- />;
295
+ onError={({ code, status, retryable }) =>
296
+ reportAssistantError({ code, status, retryable })
297
+ }
298
+ />
110
299
  ```
111
300
 
112
301
  `createAssistantSession` returns the opaque token, expiry, resolved non-secret configuration, and explicit
113
- `endpoints.turns` / `endpoints.toolConfirmations` URLs. Forward that response unchanged to the component.
302
+ turn/interaction endpoint URLs. Forward that response unchanged to the component or headless client.
114
303
 
115
- The MCP server's top-level `branding` block provides the shared identity and theme for widgets and the
116
- assistant. Set semantic CSS variables only for application-level integration overrides:
304
+ The MCP server's top-level `branding` block is the sole deployment source for identity and colors shared by
305
+ widgets and the assistant. Set public semantic CSS variables only for application-level integration
306
+ overrides:
117
307
 
118
308
  ```css
119
309
  noodle-assistant {
@@ -123,10 +313,19 @@ noodle-assistant {
123
313
  }
124
314
  ```
125
315
 
316
+ For each matching region or token, precedence is host-provided slot content or a public CSS custom property,
317
+ then compiled server `presentation`/`branding`, then Halo defaults. The public slots are `launcher-icon`,
318
+ `header-leading`, `header-actions`, `empty-state`, `composer-leading`, `composer-trailing`, and
319
+ `conversation-footer`; slotted host DOM replaces the renderer fallback for that region. Slots are a trusted
320
+ embedding-page integration API, not a way to put HTML or callbacks in deployment configuration. Internal
321
+ shadow-DOM selectors and classes are not public API.
322
+
126
323
  Server branding controls customer name, themed logo/mark/avatar assets, semantic light/dark colors, density,
127
324
  radius, typography, and automatic theme. `embeddedAssistant(...)` controls floating/inline/drawer layout,
128
325
  position and dimensions, mobile and launcher/header/avatar/timestamp behavior, visible labels, suggested
129
326
  prompts, privacy/terms links, locale, and text direction.
130
327
  Named slots cover launcher/header/composer/footer extensions. Public methods include `open`, `close`,
131
- `toggle`, `focusComposer`, `sendMessage`, `updateContext`, `confirmTool`, and `resetSession`; lifecycle,
132
- message, tool-proposal, completion, context, and error events support application integration.
328
+ `toggle`, `focusComposer`, `sendMessage`, `updateContext`, `updateModelContext`, `respond`, `confirmTool`, and `resetSession`;
329
+ lifecycle, message, tool-proposal, interaction-resolution, view-availability, context, and error events
330
+ support application integration. `confirmTool(id)` remains the compatibility shorthand for
331
+ `respond(id, { action: 'accept' })`.
@@ -1,6 +1,38 @@
1
1
  type AssistantThemeMode = 'auto' | 'light' | 'dark';
2
2
  type AssistantLayoutMode = 'floating' | 'inline' | 'drawer';
3
3
  type AssistantPosition = 'bottom-left' | 'bottom-right';
4
+ type AssistantPresentationTone = 'neutral' | 'success' | 'warning' | 'danger';
5
+ interface AssistantPresentationConfiguration {
6
+ readonly panel?: {
7
+ readonly surface?: 'solid' | 'glass';
8
+ readonly elevation?: 'soft' | 'dramatic';
9
+ readonly border?: 'subtle' | 'strong';
10
+ readonly radius?: number;
11
+ };
12
+ readonly launcher?: {
13
+ readonly icon?: 'brand-mark' | 'chat' | 'none';
14
+ readonly size?: 'md' | 'lg';
15
+ readonly status?: 'none' | 'session';
16
+ readonly effect?: 'none' | 'pulse';
17
+ };
18
+ readonly header?: {
19
+ readonly mark?: 'none' | 'brand-mark' | 'status';
20
+ readonly badge?: {
21
+ readonly text: string;
22
+ readonly tone?: AssistantPresentationTone;
23
+ readonly indicator?: boolean;
24
+ };
25
+ };
26
+ readonly composer?: {
27
+ readonly leadingIcon?: 'none' | 'brand-mark';
28
+ readonly sendIcon?: 'arrow-up' | 'paper-plane';
29
+ readonly shape?: 'rounded' | 'pill';
30
+ };
31
+ readonly messages?: {
32
+ readonly userStyle?: 'bubble' | 'accent';
33
+ readonly assistantStyle?: 'plain' | 'bubble';
34
+ };
35
+ }
4
36
  interface AssistantConfiguration {
5
37
  readonly branding?: {
6
38
  readonly name?: string;
@@ -50,6 +82,7 @@ interface AssistantUiConfiguration {
50
82
  readonly panelWidth?: number;
51
83
  readonly panelMinHeight?: number;
52
84
  readonly panelMaxHeight?: number;
85
+ readonly edgeOffset?: number;
53
86
  readonly zIndex?: number;
54
87
  readonly density?: 'compact' | 'comfortable';
55
88
  readonly mobileFullscreen?: boolean;
@@ -64,6 +97,7 @@ interface AssistantUiConfiguration {
64
97
  readonly showTimestamps?: boolean;
65
98
  };
66
99
  readonly labels?: Partial<AssistantLabels>;
100
+ readonly presentation?: AssistantPresentationConfiguration;
67
101
  readonly suggestedPrompts?: readonly string[];
68
102
  readonly privacyUrl?: string;
69
103
  readonly termsUrl?: string;
@@ -74,6 +108,7 @@ interface AssistantLabels {
74
108
  readonly welcomeHeading: string;
75
109
  readonly welcomeMessage: string;
76
110
  readonly composerPlaceholder: string;
111
+ readonly thinking: string;
77
112
  readonly send: string;
78
113
  readonly stop: string;
79
114
  readonly close: string;
@@ -83,6 +118,10 @@ interface AssistantLabels {
83
118
  readonly retry: string;
84
119
  readonly unavailable: string;
85
120
  readonly sessionExpired: string;
121
+ readonly sessionIdle: string;
122
+ readonly sessionLoading: string;
123
+ readonly sessionReady: string;
124
+ readonly sessionError: string;
86
125
  }
87
126
 
88
127
  export type { AssistantThemeMode as A, AssistantConfiguration as a };
@@ -1,6 +1,38 @@
1
1
  type AssistantThemeMode = 'auto' | 'light' | 'dark';
2
2
  type AssistantLayoutMode = 'floating' | 'inline' | 'drawer';
3
3
  type AssistantPosition = 'bottom-left' | 'bottom-right';
4
+ type AssistantPresentationTone = 'neutral' | 'success' | 'warning' | 'danger';
5
+ interface AssistantPresentationConfiguration {
6
+ readonly panel?: {
7
+ readonly surface?: 'solid' | 'glass';
8
+ readonly elevation?: 'soft' | 'dramatic';
9
+ readonly border?: 'subtle' | 'strong';
10
+ readonly radius?: number;
11
+ };
12
+ readonly launcher?: {
13
+ readonly icon?: 'brand-mark' | 'chat' | 'none';
14
+ readonly size?: 'md' | 'lg';
15
+ readonly status?: 'none' | 'session';
16
+ readonly effect?: 'none' | 'pulse';
17
+ };
18
+ readonly header?: {
19
+ readonly mark?: 'none' | 'brand-mark' | 'status';
20
+ readonly badge?: {
21
+ readonly text: string;
22
+ readonly tone?: AssistantPresentationTone;
23
+ readonly indicator?: boolean;
24
+ };
25
+ };
26
+ readonly composer?: {
27
+ readonly leadingIcon?: 'none' | 'brand-mark';
28
+ readonly sendIcon?: 'arrow-up' | 'paper-plane';
29
+ readonly shape?: 'rounded' | 'pill';
30
+ };
31
+ readonly messages?: {
32
+ readonly userStyle?: 'bubble' | 'accent';
33
+ readonly assistantStyle?: 'plain' | 'bubble';
34
+ };
35
+ }
4
36
  interface AssistantConfiguration {
5
37
  readonly branding?: {
6
38
  readonly name?: string;
@@ -50,6 +82,7 @@ interface AssistantUiConfiguration {
50
82
  readonly panelWidth?: number;
51
83
  readonly panelMinHeight?: number;
52
84
  readonly panelMaxHeight?: number;
85
+ readonly edgeOffset?: number;
53
86
  readonly zIndex?: number;
54
87
  readonly density?: 'compact' | 'comfortable';
55
88
  readonly mobileFullscreen?: boolean;
@@ -64,6 +97,7 @@ interface AssistantUiConfiguration {
64
97
  readonly showTimestamps?: boolean;
65
98
  };
66
99
  readonly labels?: Partial<AssistantLabels>;
100
+ readonly presentation?: AssistantPresentationConfiguration;
67
101
  readonly suggestedPrompts?: readonly string[];
68
102
  readonly privacyUrl?: string;
69
103
  readonly termsUrl?: string;
@@ -74,6 +108,7 @@ interface AssistantLabels {
74
108
  readonly welcomeHeading: string;
75
109
  readonly welcomeMessage: string;
76
110
  readonly composerPlaceholder: string;
111
+ readonly thinking: string;
77
112
  readonly send: string;
78
113
  readonly stop: string;
79
114
  readonly close: string;
@@ -83,6 +118,10 @@ interface AssistantLabels {
83
118
  readonly retry: string;
84
119
  readonly unavailable: string;
85
120
  readonly sessionExpired: string;
121
+ readonly sessionIdle: string;
122
+ readonly sessionLoading: string;
123
+ readonly sessionReady: string;
124
+ readonly sessionError: string;
86
125
  }
87
126
 
88
127
  export type { AssistantThemeMode as A, AssistantConfiguration as a };