@noodleseed/one 0.148.0 → 0.150.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/dist/commands/assistant-ops.d.ts.map +1 -1
- package/dist/commands/assistant-ops.js +34 -0
- package/dist/commands/assistant-ops.js.map +1 -1
- package/dist/commands/assistant-surface-ops.d.ts +17 -1
- package/dist/commands/assistant-surface-ops.d.ts.map +1 -1
- package/dist/commands/assistant-surface-ops.js +38 -2
- package/dist/commands/assistant-surface-ops.js.map +1 -1
- package/dist/commands/intents-ops.js +1 -1
- package/dist/commands/intents-ops.js.map +1 -1
- package/dist/dev-local-runtime.d.ts +1 -0
- package/dist/dev-local-runtime.d.ts.map +1 -1
- package/dist/dev-local-runtime.js +3 -0
- package/dist/dev-local-runtime.js.map +1 -1
- package/node_modules/@noodle-borg/admission-limits/dist/counter-store.d.ts +20 -0
- package/node_modules/@noodle-borg/admission-limits/dist/counter-store.js +12 -0
- package/node_modules/@noodle-borg/admission-limits/dist/envelope.d.ts +34 -2
- package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +45 -15
- package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.d.ts +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.js +14 -1
- package/node_modules/@noodle-borg/admission-limits/dist/portable.d.ts +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/portable.js +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/visitor-bucket.d.ts +20 -0
- package/node_modules/@noodle-borg/admission-limits/dist/visitor-bucket.js +30 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-tool-design-ref.js +4 -0
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.d.ts +42 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.js +45 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.d.ts +2 -2
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +17 -3
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.d.ts +13 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.js +53 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.d.ts +12 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.js +15 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.d.ts +79 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.js +67 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +37 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +37 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +12 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.d.ts +80 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.js +186 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +16 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +3 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +3 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.d.ts +14 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.js +23 -6
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.js +22 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +15 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +24 -0
- package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +31 -3
- package/node_modules/@noodle-borg/authoring/dist/assistant.js +3 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +29 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-observability.js +1 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +12 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +8 -3
- package/node_modules/@noodle-borg/module/dist/request-analytics.d.ts +19 -2
- package/node_modules/@noodle-borg/module/dist/request-analytics.js +15 -2
- package/node_modules/@noodle-borg/observability/dist/assistant-app-tool-usage.js +25 -0
- package/node_modules/@noodle-borg/observability/dist/assistant-usage.js +68 -10
- package/node_modules/@noodle-borg/observability/dist/index.js +2 -0
- package/node_modules/@noodle-borg/observability/dist/request-event-query.js +48 -0
- package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +13 -2
- package/node_modules/@noodle-borg/observability/dist/request-events.js +3 -0
- package/node_modules/@noodle-borg/service/dist/invocation-context.js +1 -53
- package/node_modules/@noodle-borg/service/dist/observability-runtime.js +4 -6
- package/node_modules/@noodle-borg/service/dist/routes/analytics.js +5 -29
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +57 -29
- package/node_modules/@noodle-borg/service/dist/routes/assistant-embeds.js +26 -38
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-resolution.js +1 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +1 -4
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-configuration.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +8 -4
- package/node_modules/@noodle-borg/service/dist/routes/assistant-suggestions.js +1 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +64 -49
- package/node_modules/@noodle-borg/service/dist/routes/intent-capture.js +0 -12
- package/node_modules/@noodle-borg/service/dist/service.js +2 -3
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/request-capture.js +2 -0
- package/node_modules/@noodleseed/assistant/package.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/** Longest identifier accepted. A stranger supplies this string; the hash input has to be bounded. */
|
|
3
|
+
const MAX_VISITOR_ID_CHARS = 128;
|
|
4
|
+
/**
|
|
5
|
+
* Turn a browser-supplied visitor identifier into a counter bucket.
|
|
6
|
+
*
|
|
7
|
+
* **This is a fairness key, never an abuse bound.** Anyone can clear their storage or forge a fresh
|
|
8
|
+
* value, so nothing may be authorized by it and no limit may depend on it holding still. The
|
|
9
|
+
* per-address tier stays underneath for exactly that reason. What it buys is the case the address
|
|
10
|
+
* tier gets wrong: a hundred people behind one corporate NAT or a mobile carrier's CGNAT share a
|
|
11
|
+
* single address, and without this they race each other for one visitor's allowance — the eleventh
|
|
12
|
+
* person sees "assistant is unavailable right now" and cannot tell it from a broken product.
|
|
13
|
+
*
|
|
14
|
+
* Hashed on the way out for the same reason an address is: the raw value is the browser's, and the
|
|
15
|
+
* package rule is that nothing a client supplies reaches a counter key, a log line, or an audit
|
|
16
|
+
* payload. Bucketing needs only "is this the same visitor as before", which a digest answers exactly
|
|
17
|
+
* as well.
|
|
18
|
+
*
|
|
19
|
+
* Returns `undefined` for anything it cannot treat as an identifier, so the caller falls back to
|
|
20
|
+
* address-only fairness rather than inventing a bucket that every unparseable request would share.
|
|
21
|
+
*/
|
|
22
|
+
export function visitorBucket(id) {
|
|
23
|
+
if (typeof id !== 'string')
|
|
24
|
+
return undefined;
|
|
25
|
+
const trimmed = id.trim();
|
|
26
|
+
if (trimmed.length === 0 || trimmed.length > MAX_VISITOR_ID_CHARS)
|
|
27
|
+
return undefined;
|
|
28
|
+
return `vis_${createHash('sha256').update(trimmed).digest('hex').slice(0, 32)}`;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=visitor-bucket.js.map
|
|
@@ -168,7 +168,7 @@ export function renderEmbeddedAssistantReference() {
|
|
|
168
168
|
'',
|
|
169
169
|
'Omitted UI fields retain the complete managed baseline: a bottom-center frosted prompt pill, 970px outer desktop shell with 20px side padding, 85vh/1025px height bounds, 24px panel with built-in `#F8F8F8` light and `#0C0A09` dark surfaces, bottom prompt chips and pill composer, plain assistant messages, 85%-wide user bubbles, Noodle Seed attribution, and mobile fullscreen. The pill morphs into an input before opening; `launcher.style: "bubble"` opens directly, while `panel.surface: "glass"` remains an explicit translucent alternative. `theme: "auto"` follows the host page and `"invert"` selects its opposite. `suggestedPrompts` is the exact initial set only: pass `[]` for no initial chips, or omit it so the active model generates context-aware initial prompts. After the first message, follow-up prompts are always regenerated from the complete authorized conversation context and are never copied into transcript history. The only attribution is the Noodle Seed row, removed by `behavior.showPoweredBy: false`; the baseline carries no third-party promotion. For exact application-owned color roles, pass the typed React `appearance={{ light: { panel: { surface, text, border }, composer: {...}, confirmation: {...}, primaryButton: {...} }, dark: {...} }}` prop or assign the same object to `element.appearance`. CSS custom properties inherit through the assistant host, so those values may reuse existing application tokens such as `surface: "var(--app-surface)"` without copying literals. The appearance surface covers canvas, panel, header, messages, composer, suggestions, confirmation, buttons, launcher, code, and the MCP App frame; the package README publishes the complete role-to-`--ns-assistant-*` map. Exact parseable literal colors are preserved and low contrast emits `assistant-appearance-warning`; contrast for unresolved CSS references remains host-owned. Precedence is host appearance object, host slots/public variables, saved environment operator override, deployed semantic presentation, then defaults. Prefer reusable `server.ts` defaults; use the Console Assistant tab or `noodle assistant appearance show|apply|reset` for environment-owned changes that should reach existing embeds without a redeploy.',
|
|
170
170
|
'',
|
|
171
|
-
"Set `webmcp: { enabled: true }` on the assistant,
|
|
171
|
+
"Set `webmcp: { enabled: true }` on the assistant to let a browser agent reach this session's tools through the page's WebMCP API, and set it on an individual access surface to override that default in either direction — a marketing surface can opt in while a signed-in one opts out, or the reverse. Off unless set, and inert in browsers without `document.modelContext`. It governs discovery: whether the embed registers the tools this session already projects, narrowed to those that are both app-callable and model-visible. Every call executes over the same apps-bridge path the assistant's own calls take, so a browser agent gets the session's authority and nothing more, and a `confirm: true` tool still stops for a human in the panel rather than being accepted on the agent's behalf. It is not a second authorization boundary — the session is the only one. Bridge calls spend their own per-session and per-day budgets instead of model turns, and the surface's daily kill switch stops them too. Prefer this over hand-registering page-local tools that borrow the visitor's session: those carry no scoped authority, policy, or audit trail.",
|
|
172
172
|
'',
|
|
173
173
|
'Give every business action a portable `tool(..., { title: "Complete task", description: "This will mark the task complete for everyone.", input: z.object({ task: z.string().meta({ title: "Task" }) }) })` title. The standard confirmation uses the tool title/description plus schema field `title`, `description`, and `format`; it shows Confirm and Don\'t proceed and keeps technical action details secondary. `behavior.showConfirmationDetails` defaults to `true`; set it to `false` to remove only the built-in card\'s Additional details disclosure and connector mechanics. The business review and decisions remain, `confirm: true` still suspends until acceptance, and headless/BYO `data-confirmation` stays unchanged. Do not put JSON or implementation names in business-facing copy.',
|
|
174
174
|
'',
|
|
@@ -93,6 +93,10 @@ export function renderToolDesignReference() {
|
|
|
93
93
|
'',
|
|
94
94
|
"`noodle check` warns above 20 model-visible tools. That is a documented heuristic, not a host limit: no host publishes a hard number, and the real threshold depends on how distinct your descriptions are. Collapse variants that differ only by a filter into one intent-shaped tool with a typed enum, and mark widget-only helpers `visibility: ['app']` so they stay callable from the app surface without entering the model's list.",
|
|
95
95
|
'',
|
|
96
|
+
'**Two tools over one data source must differ by name, not only by description.** Repeated live testing has shown a model picking the broader-sounding neighbour for a question the narrower tool owns, even when every description, server instruction and workflow prompt named the right one — the public name is read before the finer guidance is weighed. When adjacent tools form a fallback chain, give them mutually exclusive model-visible names *and* mutually exclusive entry conditions, project one capability rather than two overlapping ones, and keep the lower-level primitive available to direct MCP clients instead of to the model. Prove it with repeated fresh-session live cases: one successful route does not show a stochastic choice is stable, and unit tests cannot show it at all.',
|
|
97
|
+
'',
|
|
98
|
+
'**Repetition and eligibility belong to the runtime, not the prompt.** A rolling model will render a "once per conversation" card three times under mild pressure and will reach for a conversion action on a question that merely sounds adjacent. When a tool must fire at most once, or only on an explicit ask, express that as a runtime policy the deployment enforces rather than as a sentence the model is asked to honour.',
|
|
99
|
+
'',
|
|
96
100
|
'## Provision context deliberately',
|
|
97
101
|
'',
|
|
98
102
|
"Most “the model guessed wrong” bugs are missing context, not a missing tool. Every invocation already carries a server-authoritative instant plus locale and time zone, so never ask the model for today's date. Set `server(..., { context: { defaults: { locale, timeZone } } })` for ambient defaults, and mark one zero-input tool `contextProvider: true` when the model needs portable application context such as workspace, plan, or permissions. `references/authoring-workflow.md` owns the full invocation-context contract.",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RuntimeArtifact } from '@noodle-borg/compiler';
|
|
2
|
+
import type { ExecuteDeps } from '@noodle-borg/runtime';
|
|
3
|
+
import { dispatchAssistantTool } from './assistant-interactive.js';
|
|
4
|
+
import type { AssistantSessionRecord } from './assistant-store.js';
|
|
5
|
+
/**
|
|
6
|
+
* What one apps-bridge tool call decided, before anything turns it into an HTTP answer.
|
|
7
|
+
*
|
|
8
|
+
* Projection, authorization, argument validation, and dispatch are all this package's already —
|
|
9
|
+
* `dispatchAssistantTool` lives beside this — so the decision belongs here and only the status code
|
|
10
|
+
* belongs to the route. Keeping them apart is also what lets every outcome be recorded in one
|
|
11
|
+
* place: the call used to leave no trace at all, which made `webmcp` a surface label with nothing
|
|
12
|
+
* to put it on.
|
|
13
|
+
*/
|
|
14
|
+
export type AssistantAppToolCallResult = {
|
|
15
|
+
readonly kind: 'not-found';
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'forbidden';
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'invalid-arguments';
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'interaction';
|
|
22
|
+
readonly event: string;
|
|
23
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'error';
|
|
26
|
+
readonly code: string;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'output';
|
|
29
|
+
readonly output: unknown;
|
|
30
|
+
};
|
|
31
|
+
export interface AssistantAppToolCallInput {
|
|
32
|
+
readonly artifact: RuntimeArtifact;
|
|
33
|
+
readonly deps: ExecuteDeps;
|
|
34
|
+
readonly session: AssistantSessionRecord;
|
|
35
|
+
readonly toolName: string;
|
|
36
|
+
readonly arguments?: unknown;
|
|
37
|
+
readonly now: () => Date;
|
|
38
|
+
readonly store: Parameters<typeof dispatchAssistantTool>[0]['store'];
|
|
39
|
+
readonly audit: Parameters<typeof dispatchAssistantTool>[0]['audit'];
|
|
40
|
+
}
|
|
41
|
+
export declare function executeAssistantAppToolCall(input: AssistantAppToolCallInput): Promise<AssistantAppToolCallResult>;
|
|
42
|
+
//# sourceMappingURL=app-tool-call.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { validateJsonSchemaWithDefaults } from '@noodle-borg/compiler';
|
|
2
|
+
import { evaluateToolAuthorization } from '@noodle-borg/protocol';
|
|
3
|
+
import { resolveInvocationContextSnapshot } from './assistant-context.js';
|
|
4
|
+
import { withAssistantSessionExecutionAuthority } from './assistant-customer-routing.js';
|
|
5
|
+
import { dispatchAssistantTool } from './assistant-interactive.js';
|
|
6
|
+
export async function executeAssistantAppToolCall(input) {
|
|
7
|
+
const { artifact, session } = input;
|
|
8
|
+
const tool = artifact.tools.find((candidate) => candidate.name === input.toolName);
|
|
9
|
+
// The same gate `tools/list` projected with: a tool an app may not see is a tool it may not call.
|
|
10
|
+
if (!tool || tool._meta?.ui?.visibility?.includes('app') === false)
|
|
11
|
+
return { kind: 'not-found' };
|
|
12
|
+
if (!evaluateToolAuthorization(tool.authorization, session.caller).allow) {
|
|
13
|
+
return { kind: 'forbidden' };
|
|
14
|
+
}
|
|
15
|
+
const executeDeps = withAssistantSessionExecutionAuthority(input.deps, artifact, session);
|
|
16
|
+
const context = await resolveInvocationContextSnapshot({
|
|
17
|
+
artifact,
|
|
18
|
+
executeDeps,
|
|
19
|
+
caller: session.caller,
|
|
20
|
+
instant: input.now(),
|
|
21
|
+
...(session.preferences ? { applicationPreference: session.preferences } : {}),
|
|
22
|
+
});
|
|
23
|
+
const validated = validateJsonSchemaWithDefaults(tool.inputSchema, input.arguments ?? {});
|
|
24
|
+
if (validated.issues.length > 0)
|
|
25
|
+
return { kind: 'invalid-arguments' };
|
|
26
|
+
const dispatched = await dispatchAssistantTool({
|
|
27
|
+
artifact,
|
|
28
|
+
tool,
|
|
29
|
+
arguments: validated.value,
|
|
30
|
+
executeDeps,
|
|
31
|
+
caller: session.caller,
|
|
32
|
+
context,
|
|
33
|
+
session,
|
|
34
|
+
store: input.store,
|
|
35
|
+
audit: input.audit,
|
|
36
|
+
now: input.now,
|
|
37
|
+
});
|
|
38
|
+
if (dispatched.kind !== 'event')
|
|
39
|
+
return { kind: 'output', output: dispatched.output };
|
|
40
|
+
if (dispatched.event === 'tool_proposed' || dispatched.event === 'input_requested') {
|
|
41
|
+
return { kind: 'interaction', event: dispatched.event, data: dispatched.data };
|
|
42
|
+
}
|
|
43
|
+
return { kind: 'error', code: String(dispatched.data.code ?? dispatched.event) };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=app-tool-call.js.map
|
|
@@ -16,9 +16,9 @@ import type { TenantRef } from './tenant-ref.js';
|
|
|
16
16
|
* manifest cannot silently grow the browser payload. That list is owned by the gateway, beside the
|
|
17
17
|
* record type it produces, so this projection and the type it satisfies cannot disagree.
|
|
18
18
|
*/
|
|
19
|
-
export declare function assistantBrowserConfiguration(server: ArtifactServer): AssistantAppearanceOverride | undefined;
|
|
19
|
+
export declare function assistantBrowserConfiguration(server: ArtifactServer, boundSurface?: 'public' | 'authenticated'): AssistantAppearanceOverride | undefined;
|
|
20
20
|
/** Resolve environment operator state above the immutable deployment projection. */
|
|
21
|
-
export declare function effectiveAssistantBrowserConfiguration(server: ArtifactServer, tenant: TenantRef, store: AssistantAppearanceSettingsStore | undefined): Promise<{
|
|
21
|
+
export declare function effectiveAssistantBrowserConfiguration(server: ArtifactServer, tenant: TenantRef, store: AssistantAppearanceSettingsStore | undefined, boundSurface?: 'public' | 'authenticated'): Promise<{
|
|
22
22
|
effective: AssistantAppearanceOverride | undefined;
|
|
23
23
|
fallback: "halo";
|
|
24
24
|
provenance: import("./assistant-appearance.js").AssistantAppearanceProvenance;
|
|
@@ -13,7 +13,7 @@ import { ASSISTANT_BROWSER_UI_FIELDS } from './assistant-store.js';
|
|
|
13
13
|
* manifest cannot silently grow the browser payload. That list is owned by the gateway, beside the
|
|
14
14
|
* record type it produces, so this projection and the type it satisfies cannot disagree.
|
|
15
15
|
*/
|
|
16
|
-
export function assistantBrowserConfiguration(server) {
|
|
16
|
+
export function assistantBrowserConfiguration(server, boundSurface) {
|
|
17
17
|
const assistant = server.assistant;
|
|
18
18
|
const ui = {};
|
|
19
19
|
for (const field of ASSISTANT_BROWSER_UI_FIELDS) {
|
|
@@ -21,6 +21,15 @@ export function assistantBrowserConfiguration(server) {
|
|
|
21
21
|
if (value !== undefined)
|
|
22
22
|
ui[field] = value;
|
|
23
23
|
}
|
|
24
|
+
// `webmcp` is the one field the session's own surface may answer differently (ADR 0220, amended):
|
|
25
|
+
// it decides whether this embed registers its tools with `document.modelContext`, and a deployment
|
|
26
|
+
// serving a marketing page and a signed-in app has two honest answers to that. Read from the bound
|
|
27
|
+
// surface only — a caller that cannot name one, including a pre-surfaces deployment, gets the
|
|
28
|
+
// assistant's own value rather than an arbitrary surface's. Nothing else is read here, so
|
|
29
|
+
// consulting the surface cannot widen what crosses to the browser.
|
|
30
|
+
const surfaceWebmcp = surfaceOfMode(assistant, boundSurface)?.webmcp;
|
|
31
|
+
if (surfaceWebmcp !== undefined)
|
|
32
|
+
ui.webmcp = surfaceWebmcp;
|
|
24
33
|
const configuration = {
|
|
25
34
|
...(server.branding ? { branding: server.branding } : {}),
|
|
26
35
|
...(Object.keys(ui).length > 0 ? { assistant: ui } : {}),
|
|
@@ -29,9 +38,14 @@ export function assistantBrowserConfiguration(server) {
|
|
|
29
38
|
? configuration
|
|
30
39
|
: undefined;
|
|
31
40
|
}
|
|
41
|
+
function surfaceOfMode(assistant, mode) {
|
|
42
|
+
if (mode === undefined || !Array.isArray(assistant?.surfaces))
|
|
43
|
+
return undefined;
|
|
44
|
+
return assistant.surfaces.find((surface) => surface.mode === mode);
|
|
45
|
+
}
|
|
32
46
|
/** Resolve environment operator state above the immutable deployment projection. */
|
|
33
|
-
export async function effectiveAssistantBrowserConfiguration(server, tenant, store) {
|
|
34
|
-
const developer = assistantBrowserConfiguration(server);
|
|
47
|
+
export async function effectiveAssistantBrowserConfiguration(server, tenant, store, boundSurface) {
|
|
48
|
+
const developer = assistantBrowserConfiguration(server, boundSurface);
|
|
35
49
|
const record = await store?.get(tenant);
|
|
36
50
|
return {
|
|
37
51
|
developer,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RuntimeArtifact } from '@noodle-borg/compiler';
|
|
2
|
+
import { type CallerIdentity, type ExecuteDeps, type InvocationContext, type InvocationLocationContext } from '@noodle-borg/runtime';
|
|
2
3
|
export interface AssistantContextPreferences {
|
|
3
4
|
readonly locale?: string | undefined;
|
|
4
5
|
readonly timeZone?: string | undefined;
|
|
@@ -23,5 +24,16 @@ export declare function invocationContextSystemMessage(context: InvocationContex
|
|
|
23
24
|
export declare function parseAssistantContextPreferences(value: unknown): AssistantContextPreferencesResult;
|
|
24
25
|
export declare function isAssistantVerifiedClaims(value: unknown): value is Readonly<Record<string, string | number | boolean | null>>;
|
|
25
26
|
export declare function isAssistantPageContext(value: unknown): value is Readonly<Record<string, string | number | boolean | null>>;
|
|
27
|
+
interface ResolveAssistantInvocationContextInput {
|
|
28
|
+
readonly artifact: RuntimeArtifact;
|
|
29
|
+
readonly executeDeps: ExecuteDeps;
|
|
30
|
+
readonly caller?: CallerIdentity;
|
|
31
|
+
readonly instant: Date;
|
|
32
|
+
readonly applicationPreference?: AssistantContextPreferences;
|
|
33
|
+
readonly clientHint?: AssistantContextPreferences;
|
|
34
|
+
readonly clientLocationHint?: Omit<InvocationLocationContext, 'source'>;
|
|
35
|
+
}
|
|
36
|
+
/** Resolve one immutable snapshot, including a schema-validated read-only ambient provider. */
|
|
37
|
+
export declare function resolveInvocationContextSnapshot(input: ResolveAssistantInvocationContextInput): Promise<InvocationContext>;
|
|
26
38
|
export {};
|
|
27
39
|
//# sourceMappingURL=assistant-context.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { executeAmbientContext, } from '@noodle-borg/runtime';
|
|
2
|
+
import { toolTouchesDelegatedAuth } from './assistant-delegated-projection.js';
|
|
1
3
|
const PLATFORM_DEFAULTS = { locale: 'en-US', timeZone: 'UTC' };
|
|
2
4
|
/** Resolve presentation preferences independently from server-authoritative time. */
|
|
3
5
|
export function resolveInvocationContext(input) {
|
|
@@ -144,4 +146,55 @@ function offsetFromPart(value) {
|
|
|
144
146
|
throw new RangeError(`unsupported timezone offset ${value ?? 'unknown'}`);
|
|
145
147
|
return match[1];
|
|
146
148
|
}
|
|
149
|
+
/** Resolve one immutable snapshot, including a schema-validated read-only ambient provider. */
|
|
150
|
+
export async function resolveInvocationContextSnapshot(input) {
|
|
151
|
+
const callerPreference = {
|
|
152
|
+
...(input.caller?.locale ? { locale: input.caller.locale } : {}),
|
|
153
|
+
...(input.caller?.timeZone ? { timeZone: input.caller.timeZone } : {}),
|
|
154
|
+
...(input.applicationPreference ?? {}),
|
|
155
|
+
};
|
|
156
|
+
const clientHint = {
|
|
157
|
+
...(input.clientLocationHint?.timeZone === undefined
|
|
158
|
+
? {}
|
|
159
|
+
: { timeZone: input.clientLocationHint.timeZone }),
|
|
160
|
+
...(input.clientHint ?? {}),
|
|
161
|
+
};
|
|
162
|
+
const temporal = resolveInvocationContext({
|
|
163
|
+
instant: input.instant,
|
|
164
|
+
...(Object.keys(callerPreference).length > 0
|
|
165
|
+
? { applicationPreference: callerPreference }
|
|
166
|
+
: {}),
|
|
167
|
+
...(Object.keys(clientHint).length === 0 ? {} : { clientHint }),
|
|
168
|
+
...(input.artifact.server.context?.defaults
|
|
169
|
+
? { defaults: input.artifact.server.context.defaults }
|
|
170
|
+
: {}),
|
|
171
|
+
});
|
|
172
|
+
const context = {
|
|
173
|
+
...temporal,
|
|
174
|
+
...(input.clientLocationHint === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: { location: { ...input.clientLocationHint, source: 'client-hint' } }),
|
|
177
|
+
};
|
|
178
|
+
const ambient = input.artifact.server.context?.ambient;
|
|
179
|
+
if (!ambient)
|
|
180
|
+
return context;
|
|
181
|
+
// An anonymous principal can never satisfy a delegated-auth fulfilment: the broker exchange
|
|
182
|
+
// is guaranteed to fail, so skip the doomed round trip instead of paying it every turn.
|
|
183
|
+
if (input.caller?.identityKind === 'anonymous') {
|
|
184
|
+
const delegatedKeys = input.executeDeps.broker.assistantDelegatedAuthKeys?.();
|
|
185
|
+
if (delegatedKeys !== undefined &&
|
|
186
|
+
delegatedKeys.size > 0 &&
|
|
187
|
+
toolTouchesDelegatedAuth(ambient, delegatedKeys)) {
|
|
188
|
+
return { ...context, ambientStatus: 'unavailable' };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const result = await executeAmbientContext(input.artifact, {
|
|
192
|
+
...input.executeDeps,
|
|
193
|
+
...(input.caller ? { caller: input.caller } : {}),
|
|
194
|
+
context,
|
|
195
|
+
});
|
|
196
|
+
if (!result.ok)
|
|
197
|
+
return { ...context, ambientStatus: 'unavailable' };
|
|
198
|
+
return { ...context, ambientStatus: 'available', ambient: result.output };
|
|
199
|
+
}
|
|
147
200
|
//# sourceMappingURL=assistant-context.js.map
|
|
@@ -32,6 +32,18 @@ export declare function selectAssistantModelTools(artifact: RuntimeArtifact, cal
|
|
|
32
32
|
readonly latestMessage?: string;
|
|
33
33
|
readonly usedToolNames?: readonly string[];
|
|
34
34
|
}): readonly ArtifactTool[];
|
|
35
|
+
/**
|
|
36
|
+
* What to tell a model that called a tool this turn does not offer.
|
|
37
|
+
*
|
|
38
|
+
* The alternative — ending the turn on `invalid_model_tool_call` — is a security-correct rejection
|
|
39
|
+
* with a dishonest failure mode: the widget renders it as "temporarily unavailable" beside a
|
|
40
|
+
* Reconnect button that mints a whole new session, so a visitor who asked for something reasonable
|
|
41
|
+
* gets silence and the surface pays a session for it. A `role:'tool'` result costs one model step
|
|
42
|
+
* and gets the visitor a real answer instead. It names the tool because the model supplied that
|
|
43
|
+
* name in the first place, and says nothing about why the tool is absent, which is the part a
|
|
44
|
+
* visitor is not entitled to.
|
|
45
|
+
*/
|
|
46
|
+
export declare function assistantOmittedToolResult(name: string): string;
|
|
35
47
|
/** Whether an already-selected tool requires the first model step to call it. */
|
|
36
48
|
export declare function assistantModelToolRequiredWhenVisible(tool: ArtifactTool): boolean;
|
|
37
49
|
/** Whether a successful model-selected call consumes this tool for the assistant session. */
|
|
@@ -31,6 +31,21 @@ export function selectAssistantModelTools(artifact, caller, options = {}) {
|
|
|
31
31
|
hasValidSessionVisibility(tool, options.usedToolNames) &&
|
|
32
32
|
matchesLatestMessageConstraint(tool, options.latestMessage));
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* What to tell a model that called a tool this turn does not offer.
|
|
36
|
+
*
|
|
37
|
+
* The alternative — ending the turn on `invalid_model_tool_call` — is a security-correct rejection
|
|
38
|
+
* with a dishonest failure mode: the widget renders it as "temporarily unavailable" beside a
|
|
39
|
+
* Reconnect button that mints a whole new session, so a visitor who asked for something reasonable
|
|
40
|
+
* gets silence and the surface pays a session for it. A `role:'tool'` result costs one model step
|
|
41
|
+
* and gets the visitor a real answer instead. It names the tool because the model supplied that
|
|
42
|
+
* name in the first place, and says nothing about why the tool is absent, which is the part a
|
|
43
|
+
* visitor is not entitled to.
|
|
44
|
+
*/
|
|
45
|
+
export function assistantOmittedToolResult(name) {
|
|
46
|
+
return (`The tool "${name}" is not available in this conversation. Do not call it again. ` +
|
|
47
|
+
'Answer the person directly in your own words, and say plainly if you cannot help with this.');
|
|
48
|
+
}
|
|
34
49
|
/** Whether an already-selected tool requires the first model step to call it. */
|
|
35
50
|
export function assistantModelToolRequiredWhenVisible(tool) {
|
|
36
51
|
return tool.annotations?.[REQUIRED_WHEN_VISIBLE] === true;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AdmissionEnvelope } from '@noodle-borg/admission-limits/portable';
|
|
2
|
+
import type { PublicEmbedRecord } from './embed-store.js';
|
|
3
|
+
import type { PublicSurface } from './public-surface.js';
|
|
4
|
+
/**
|
|
5
|
+
* The operator's view of one public surface: what it is, and what it may spend.
|
|
6
|
+
*
|
|
7
|
+
* Origins, capabilities, and surfaceMode come from the *active* deployment rather than the embed
|
|
8
|
+
* row, so what an operator reads here is what a browser will actually be held to at mint time. The
|
|
9
|
+
* row keeps its provisioning-time mode by design — `ensure` never overwrites a live row.
|
|
10
|
+
*/
|
|
11
|
+
export interface AssistantEmbedOperatorView {
|
|
12
|
+
readonly embedId: string;
|
|
13
|
+
readonly surfaceMode: string;
|
|
14
|
+
readonly origins: readonly string[];
|
|
15
|
+
readonly capabilities: readonly string[];
|
|
16
|
+
readonly turnsPerDay: number;
|
|
17
|
+
readonly mintsPerDay: number;
|
|
18
|
+
/**
|
|
19
|
+
* What a browser agent may spend on this surface through the WebMCP bridge. Reported beside the
|
|
20
|
+
* turn caps because "is my budget about to run out" is the same question, and a bridge call spends
|
|
21
|
+
* neither a turn nor a mint — an operator reading only those two would see a quiet day.
|
|
22
|
+
*/
|
|
23
|
+
readonly bridgeCallsPerSession: number;
|
|
24
|
+
readonly bridgeCallsPerDay: number;
|
|
25
|
+
/** True when the caps are the deployment defaults rather than something an operator chose. */
|
|
26
|
+
readonly budgetIsDefault: boolean;
|
|
27
|
+
readonly turnsToday: number;
|
|
28
|
+
readonly mintsToday: number;
|
|
29
|
+
readonly bridgeCallsToday: number;
|
|
30
|
+
readonly createdAt: string;
|
|
31
|
+
/**
|
|
32
|
+
* Present only when Noodle is funding this surface's model. It exists so an operator can tell
|
|
33
|
+
* platform-imposed degradation from a bug in their own app, which is the one thing they cannot
|
|
34
|
+
* work out from the caps above — those look normal right up to the moment the ladder closes them.
|
|
35
|
+
*/
|
|
36
|
+
readonly managedSpend?: AssistantManagedSpendView;
|
|
37
|
+
}
|
|
38
|
+
export interface AssistantManagedSpendView {
|
|
39
|
+
/** `normal`, `approaching`, `near`, `limited`, or `closed`. */
|
|
40
|
+
readonly state: string;
|
|
41
|
+
/**
|
|
42
|
+
* Turns left today at the current rung, not a percentage. The spend consume is all-or-nothing, so
|
|
43
|
+
* a costly turn is refused while a cheap one still passes at the tail of the day — a percentage
|
|
44
|
+
* would look like it stopped early.
|
|
45
|
+
*/
|
|
46
|
+
readonly turnsRemaining: number;
|
|
47
|
+
/** One plain sentence about what a visitor experiences right now. */
|
|
48
|
+
readonly visitors: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Project a tenant's spend into what an operator needs to read.
|
|
52
|
+
*
|
|
53
|
+
* Takes the raw peek rather than a rung so the caller owns only the read: which rung that is, and
|
|
54
|
+
* what a turn costs there, are the ladder's business and stay in one place.
|
|
55
|
+
*/
|
|
56
|
+
export declare function assistantManagedSpendView(input: {
|
|
57
|
+
readonly spend: {
|
|
58
|
+
readonly allowance: number;
|
|
59
|
+
};
|
|
60
|
+
readonly unitsUsed: number;
|
|
61
|
+
}): AssistantManagedSpendView;
|
|
62
|
+
/**
|
|
63
|
+
* Project one embed row into that view. Pure on purpose: the caller owns the reads (active
|
|
64
|
+
* deployment, counter peeks) and this owns the shape, so the projection stays testable without a
|
|
65
|
+
* registry or a counter store.
|
|
66
|
+
*/
|
|
67
|
+
export declare function assistantEmbedOperatorView(input: {
|
|
68
|
+
readonly record: PublicEmbedRecord;
|
|
69
|
+
readonly surface: PublicSurface | undefined;
|
|
70
|
+
readonly envelope: AdmissionEnvelope;
|
|
71
|
+
/** Usage without consuming. The keys are this package's own convention, so it forms them here. */
|
|
72
|
+
readonly peek: (key: string) => Promise<number>;
|
|
73
|
+
/** Present only when Noodle funds this surface's model. */
|
|
74
|
+
readonly spend?: {
|
|
75
|
+
readonly key: string;
|
|
76
|
+
readonly allowance: number;
|
|
77
|
+
};
|
|
78
|
+
}): Promise<AssistantEmbedOperatorView>;
|
|
79
|
+
//# sourceMappingURL=embed-operator-view.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { managedSpendRung, managedSpendUnitWeight } from './managed-spend.js';
|
|
2
|
+
const SPEND_STATES = [
|
|
3
|
+
{ state: 'normal', visitors: 'Full answers, full tools.' },
|
|
4
|
+
{ state: 'approaching', visitors: 'Shorter answers. Nothing else changes.' },
|
|
5
|
+
{
|
|
6
|
+
state: 'near',
|
|
7
|
+
visitors: 'Shorter answers, and conversations restart after 8 turns.',
|
|
8
|
+
},
|
|
9
|
+
{ state: 'limited', visitors: 'Short answers from knowledge only; tools are paused.' },
|
|
10
|
+
{
|
|
11
|
+
state: 'closed',
|
|
12
|
+
visitors: 'Paused until 00:00 UTC. Connect your own model key to keep serving.',
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Project a tenant's spend into what an operator needs to read.
|
|
17
|
+
*
|
|
18
|
+
* Takes the raw peek rather than a rung so the caller owns only the read: which rung that is, and
|
|
19
|
+
* what a turn costs there, are the ladder's business and stay in one place.
|
|
20
|
+
*/
|
|
21
|
+
export function assistantManagedSpendView(input) {
|
|
22
|
+
const rungAt = managedSpendRung(input.unitsUsed, input.spend.allowance);
|
|
23
|
+
const perTurn = managedSpendUnitWeight(rungAt);
|
|
24
|
+
const remaining = Math.max(0, input.spend.allowance - input.unitsUsed);
|
|
25
|
+
const rung = Math.min(rungAt, SPEND_STATES.length - 1);
|
|
26
|
+
return {
|
|
27
|
+
...SPEND_STATES[rung],
|
|
28
|
+
turnsRemaining: perTurn > 0 ? Math.floor(remaining / perTurn) : 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Project one embed row into that view. Pure on purpose: the caller owns the reads (active
|
|
33
|
+
* deployment, counter peeks) and this owns the shape, so the projection stays testable without a
|
|
34
|
+
* registry or a counter store.
|
|
35
|
+
*/
|
|
36
|
+
export async function assistantEmbedOperatorView(input) {
|
|
37
|
+
const { record, surface, envelope, spend } = input;
|
|
38
|
+
return {
|
|
39
|
+
embedId: record.embedId,
|
|
40
|
+
surfaceMode: surface?.mode ?? record.surfaceMode,
|
|
41
|
+
origins: surface?.origins ?? [],
|
|
42
|
+
capabilities: surface?.capabilities.map((entry) => entry.name) ?? [],
|
|
43
|
+
turnsPerDay: envelope.turnsPerDay,
|
|
44
|
+
mintsPerDay: envelope.mintsPerDay,
|
|
45
|
+
bridgeCallsPerSession: envelope.bridgeToolCallsPerSession,
|
|
46
|
+
bridgeCallsPerDay: envelope.bridgeToolCallsPerDay,
|
|
47
|
+
budgetIsDefault: record.turnsPerDay === undefined &&
|
|
48
|
+
record.mintsPerDay === undefined &&
|
|
49
|
+
record.bridgeToolCallsPerSession === undefined &&
|
|
50
|
+
record.bridgeToolCallsPerDay === undefined,
|
|
51
|
+
turnsToday: await input.peek(`turns:${record.embedId}`),
|
|
52
|
+
mintsToday: await input.peek(`mints:${record.embedId}`),
|
|
53
|
+
// Keyed as `admitBridgeToolCall` writes it, so the number an operator reads is the one the
|
|
54
|
+
// gate consumed rather than a parallel count that can drift from it.
|
|
55
|
+
bridgeCallsToday: await input.peek(`bridge:${record.embedId}`),
|
|
56
|
+
createdAt: record.createdAt.toISOString(),
|
|
57
|
+
...(spend === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: {
|
|
60
|
+
managedSpend: assistantManagedSpendView({
|
|
61
|
+
spend,
|
|
62
|
+
unitsUsed: await input.peek(spend.key),
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=embed-operator-view.js.map
|
|
@@ -31,6 +31,21 @@ export interface PublicEmbedRecord {
|
|
|
31
31
|
*/
|
|
32
32
|
readonly turnsPerDay?: number;
|
|
33
33
|
readonly mintsPerDay?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Operator overrides for the per-address abuse bounds. Rarely needed — tier 4 handles fairness and
|
|
36
|
+
* these only have to stop a single machine — but an operator who knows their own traffic shape
|
|
37
|
+
* (an internal tool behind one proxy, say) can lower them, and lowering is all anyone may do.
|
|
38
|
+
*/
|
|
39
|
+
readonly mintsPerAddressHour?: number;
|
|
40
|
+
readonly turnsPerAddressHour?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Operator overrides for this surface's WebMCP bridge budgets. The shipped defaults are
|
|
43
|
+
* proportionate rather than measured (ADR 0220), so an operator who can see their own agent
|
|
44
|
+
* traffic needs to move them without waiting for a release. `0` is the kill switch here too,
|
|
45
|
+
* though zeroing `turnsPerDay` already stops bridge calls with it.
|
|
46
|
+
*/
|
|
47
|
+
readonly bridgeToolCallsPerSession?: number;
|
|
48
|
+
readonly bridgeToolCallsPerDay?: number;
|
|
34
49
|
}
|
|
35
50
|
/** Project the non-secret embed coordinate onto the shared assistant tenant shape. */
|
|
36
51
|
export declare function publicEmbedTenant(embed: PublicEmbedRecord): TenantRef;
|
|
@@ -38,6 +53,10 @@ export declare function publicEmbedTenant(embed: PublicEmbedRecord): TenantRef;
|
|
|
38
53
|
export interface PublicEmbedBudget {
|
|
39
54
|
readonly turnsPerDay?: number;
|
|
40
55
|
readonly mintsPerDay?: number;
|
|
56
|
+
readonly mintsPerAddressHour?: number;
|
|
57
|
+
readonly turnsPerAddressHour?: number;
|
|
58
|
+
readonly bridgeToolCallsPerSession?: number;
|
|
59
|
+
readonly bridgeToolCallsPerDay?: number;
|
|
41
60
|
}
|
|
42
61
|
export interface PublicEmbedStore {
|
|
43
62
|
/**
|
|
@@ -73,4 +92,22 @@ export interface PublicEmbedStore {
|
|
|
73
92
|
export declare const PUBLIC_EMBED_ID_PREFIX = "pub_";
|
|
74
93
|
export declare function isPublicEmbedId(value: string): boolean;
|
|
75
94
|
import type { TenantRef } from './tenant-ref.js';
|
|
95
|
+
/**
|
|
96
|
+
* Validate an operator's requested caps.
|
|
97
|
+
*
|
|
98
|
+
* Lives with the budget type rather than with the route that receives it: the rules are about what a
|
|
99
|
+
* cap may be, not about HTTP, and a second caller must not be able to reach the store with a shape
|
|
100
|
+
* this would have rejected.
|
|
101
|
+
*/
|
|
102
|
+
export type BudgetField = 'turnsPerDay' | 'mintsPerDay' | 'mintsPerAddressHour' | 'turnsPerAddressHour' | 'bridgeToolCallsPerSession' | 'bridgeToolCallsPerDay';
|
|
103
|
+
type BudgetValue = Partial<Record<BudgetField, number>>;
|
|
104
|
+
type ParsedBudget = {
|
|
105
|
+
readonly ok: true;
|
|
106
|
+
readonly value: BudgetValue;
|
|
107
|
+
} | {
|
|
108
|
+
readonly ok: false;
|
|
109
|
+
readonly error: string;
|
|
110
|
+
};
|
|
111
|
+
export declare function parsePublicEmbedBudget(body: unknown): ParsedBudget;
|
|
112
|
+
export {};
|
|
76
113
|
//# sourceMappingURL=embed-store.d.ts.map
|
|
@@ -23,4 +23,41 @@ export const PUBLIC_EMBED_ID_PREFIX = 'pub_';
|
|
|
23
23
|
export function isPublicEmbedId(value) {
|
|
24
24
|
return new RegExp(`^${PUBLIC_EMBED_ID_PREFIX}[0-9a-z]{20,32}$`).test(value);
|
|
25
25
|
}
|
|
26
|
+
const BUDGET_FIELDS = [
|
|
27
|
+
'turnsPerDay',
|
|
28
|
+
'mintsPerDay',
|
|
29
|
+
'mintsPerAddressHour',
|
|
30
|
+
'turnsPerAddressHour',
|
|
31
|
+
'bridgeToolCallsPerSession',
|
|
32
|
+
'bridgeToolCallsPerDay',
|
|
33
|
+
];
|
|
34
|
+
export function parsePublicEmbedBudget(body) {
|
|
35
|
+
// A JSON body of `null` is an object to `typeof` and reads as a crash to a property access, so it
|
|
36
|
+
// is checked by name: an operator sending nonsense deserves the 400 this returns, not the 500 an
|
|
37
|
+
// uncaught `TypeError` would become.
|
|
38
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
39
|
+
return { ok: false, error: 'body must be a JSON object' };
|
|
40
|
+
}
|
|
41
|
+
const supplied = body;
|
|
42
|
+
const value = {};
|
|
43
|
+
for (const [name, raw] of BUDGET_FIELDS.map((field) => [field, supplied[field]])) {
|
|
44
|
+
if (raw === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
// Zero is valid and is the point; anything negative or fractional is a mistake worth saying out
|
|
47
|
+
// loud rather than silently flooring, because an operator setting a cap is being deliberate.
|
|
48
|
+
// `isSafeInteger` rather than `isInteger`: past 2^53 a JSON number has already lost the value
|
|
49
|
+
// the operator typed, and it reaches a `::bigint` column that would store the rounded one.
|
|
50
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0) {
|
|
51
|
+
return { ok: false, error: `"${name}" must be a non-negative integer` };
|
|
52
|
+
}
|
|
53
|
+
value[name] = raw;
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(value).length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `set at least one of ${BUDGET_FIELDS.map((f) => `"${f}"`).join(', ')}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, value };
|
|
62
|
+
}
|
|
26
63
|
//# sourceMappingURL=embed-store.js.map
|
|
@@ -39,6 +39,18 @@ export class InMemoryPublicEmbedStore {
|
|
|
39
39
|
...record,
|
|
40
40
|
...(budget.turnsPerDay !== undefined ? { turnsPerDay: budget.turnsPerDay } : {}),
|
|
41
41
|
...(budget.mintsPerDay !== undefined ? { mintsPerDay: budget.mintsPerDay } : {}),
|
|
42
|
+
...(budget.mintsPerAddressHour !== undefined
|
|
43
|
+
? { mintsPerAddressHour: budget.mintsPerAddressHour }
|
|
44
|
+
: {}),
|
|
45
|
+
...(budget.turnsPerAddressHour !== undefined
|
|
46
|
+
? { turnsPerAddressHour: budget.turnsPerAddressHour }
|
|
47
|
+
: {}),
|
|
48
|
+
...(budget.bridgeToolCallsPerSession !== undefined
|
|
49
|
+
? { bridgeToolCallsPerSession: budget.bridgeToolCallsPerSession }
|
|
50
|
+
: {}),
|
|
51
|
+
...(budget.bridgeToolCallsPerDay !== undefined
|
|
52
|
+
? { bridgeToolCallsPerDay: budget.bridgeToolCallsPerDay }
|
|
53
|
+
: {}),
|
|
42
54
|
};
|
|
43
55
|
this.#byId.set(embedId, updated);
|
|
44
56
|
return updated;
|