@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.
Files changed (81) hide show
  1. package/dist/commands/assistant-ops.d.ts.map +1 -1
  2. package/dist/commands/assistant-ops.js +34 -0
  3. package/dist/commands/assistant-ops.js.map +1 -1
  4. package/dist/commands/assistant-surface-ops.d.ts +17 -1
  5. package/dist/commands/assistant-surface-ops.d.ts.map +1 -1
  6. package/dist/commands/assistant-surface-ops.js +38 -2
  7. package/dist/commands/assistant-surface-ops.js.map +1 -1
  8. package/dist/commands/intents-ops.js +1 -1
  9. package/dist/commands/intents-ops.js.map +1 -1
  10. package/dist/dev-local-runtime.d.ts +1 -0
  11. package/dist/dev-local-runtime.d.ts.map +1 -1
  12. package/dist/dev-local-runtime.js +3 -0
  13. package/dist/dev-local-runtime.js.map +1 -1
  14. package/node_modules/@noodle-borg/admission-limits/dist/counter-store.d.ts +20 -0
  15. package/node_modules/@noodle-borg/admission-limits/dist/counter-store.js +12 -0
  16. package/node_modules/@noodle-borg/admission-limits/dist/envelope.d.ts +34 -2
  17. package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +45 -15
  18. package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.d.ts +1 -0
  19. package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.js +14 -1
  20. package/node_modules/@noodle-borg/admission-limits/dist/portable.d.ts +1 -0
  21. package/node_modules/@noodle-borg/admission-limits/dist/portable.js +1 -0
  22. package/node_modules/@noodle-borg/admission-limits/dist/visitor-bucket.d.ts +20 -0
  23. package/node_modules/@noodle-borg/admission-limits/dist/visitor-bucket.js +30 -0
  24. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +1 -1
  25. package/node_modules/@noodle-borg/agent-kit/dist/skill-tool-design-ref.js +4 -0
  26. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  27. package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.d.ts +42 -0
  28. package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.js +45 -0
  29. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.d.ts +2 -2
  30. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +17 -3
  31. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.d.ts +13 -1
  32. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.js +53 -0
  33. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.d.ts +12 -0
  34. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.js +15 -0
  35. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.d.ts +79 -0
  36. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.js +67 -0
  37. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +37 -0
  38. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +37 -0
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +12 -0
  40. package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.d.ts +80 -0
  41. package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.js +186 -0
  42. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +16 -0
  43. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +3 -0
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +3 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.d.ts +14 -1
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.js +23 -6
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.js +22 -0
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +15 -1
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +24 -0
  50. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  51. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +31 -3
  52. package/node_modules/@noodle-borg/authoring/dist/assistant.js +3 -0
  53. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +29 -1
  54. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-observability.js +1 -1
  55. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +12 -0
  56. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +8 -3
  57. package/node_modules/@noodle-borg/module/dist/request-analytics.d.ts +19 -2
  58. package/node_modules/@noodle-borg/module/dist/request-analytics.js +15 -2
  59. package/node_modules/@noodle-borg/observability/dist/assistant-app-tool-usage.js +25 -0
  60. package/node_modules/@noodle-borg/observability/dist/assistant-usage.js +68 -10
  61. package/node_modules/@noodle-borg/observability/dist/index.js +2 -0
  62. package/node_modules/@noodle-borg/observability/dist/request-event-query.js +48 -0
  63. package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +13 -2
  64. package/node_modules/@noodle-borg/observability/dist/request-events.js +3 -0
  65. package/node_modules/@noodle-borg/service/dist/invocation-context.js +1 -53
  66. package/node_modules/@noodle-borg/service/dist/observability-runtime.js +4 -6
  67. package/node_modules/@noodle-borg/service/dist/routes/analytics.js +5 -29
  68. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +57 -29
  69. package/node_modules/@noodle-borg/service/dist/routes/assistant-embeds.js +26 -38
  70. package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-resolution.js +1 -2
  71. package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +1 -4
  72. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-configuration.js +1 -1
  73. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +8 -4
  74. package/node_modules/@noodle-borg/service/dist/routes/assistant-suggestions.js +1 -2
  75. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +64 -49
  76. package/node_modules/@noodle-borg/service/dist/routes/intent-capture.js +0 -12
  77. package/node_modules/@noodle-borg/service/dist/service.js +2 -3
  78. package/node_modules/@noodle-borg/service/package.json +1 -1
  79. package/node_modules/@noodle-borg/transport-http/dist/request-capture.js +2 -0
  80. package/node_modules/@noodleseed/assistant/package.json +1 -1
  81. package/package.json +2 -2
@@ -0,0 +1,80 @@
1
+ import type { AdmissionEnvelope } from '@noodle-borg/admission-limits/portable';
2
+ import type { AssistantModelRequestPolicy } from './model-request.js';
3
+ import type { SurfaceBudgetBounds } from './surface-budget.js';
4
+ /**
5
+ * Cost breakers for the sponsored managed-assistant beta (ADR 0213 amendment §3).
6
+ *
7
+ * Noodle pays for `noodle-managed` inference and charges nothing for it, so aggregate spend is a
8
+ * platform expense with no customer-side limit standing behind it. Per-surface daily caps bound one
9
+ * surface; they do not bound how many surfaces enrol. This is the missing ceiling, expressed as a
10
+ * ladder rather than a switch: a tenant approaching its allowance gets shorter answers before it gets
11
+ * a closed door, and the door itself is the kill switch that already exists.
12
+ *
13
+ * Two things make it small. Every rung is expressed through `SurfaceBudgetBounds` and
14
+ * `AssistantModelRequestPolicy`, which the resolver already returns and admission already reads live
15
+ * per turn — so degradation applies immediately and reverses on its own when the UTC day rolls, with
16
+ * no unwind path to write. And the unit is one delivered bounded turn, which is the billable unit ADR
17
+ * 0213 §12 already defines.
18
+ */
19
+ /**
20
+ * Billable tokens per charged unit. Prompt and completion are counted together and completion is
21
+ * weighted, because output costs several times input on every provider we have measured.
22
+ */
23
+ export declare const MANAGED_SPEND_UNIT_TOKENS = 4000;
24
+ export interface ManagedSpendPolicy {
25
+ readonly requestPolicy: AssistantModelRequestPolicy;
26
+ readonly admission: SurfaceBudgetBounds;
27
+ }
28
+ /**
29
+ * Which rung a tenant is on.
30
+ *
31
+ * An absent or nonsensical allowance means "unbudgeted", never "exhausted". Failing closed on a typo
32
+ * would take a customer's surface dark for a configuration mistake, and the per-surface daily cap
33
+ * still bounds what that costs.
34
+ */
35
+ export declare function managedSpendRung(unitsUsed: number, allowance: number): number;
36
+ export declare function managedSpendPolicy(rung: number): ManagedSpendPolicy;
37
+ /**
38
+ * Units one turn costs at this rung: its worst case under the rung's own policy, in whole units.
39
+ *
40
+ * Prompt-inclusive on purpose. `maxCompletionTokens` says nothing about how large a prompt the agent
41
+ * loop may resend on each step, and the prompt is where an unbounded turn actually spends.
42
+ */
43
+ export declare function managedSpendUnitWeight(rung: number): number;
44
+ /**
45
+ * The unit ledger for a day's worth of ordinary turns.
46
+ *
47
+ * Units are worst-case, so a turn is charged what its rung's policy *permits* rather than what it
48
+ * happened to use. That is deliberate — the alternative under-charges exactly the adversary the cap
49
+ * exists for — but it means the number is only meaningful denominated in turns. An allowance is
50
+ * therefore configured as "this many sponsored turns a day" and converted here, which is also the
51
+ * form the operator surface reports.
52
+ *
53
+ * The conservatism is uniform within a rung, so it does not distort when the ladder trips; across
54
+ * rungs it is the intended effect, since a degraded turn genuinely costs less and the same allowance
55
+ * stretches further.
56
+ */
57
+ export declare function managedSpendAllowanceForTurns(turnsPerDay: number): number;
58
+ /**
59
+ * Is the ledger telling the truth?
60
+ *
61
+ * A unit weight is a claim about what a turn's own rung permits. If the rung's `requestPolicy` does
62
+ * not actually bound what it says, the claim is fiction and the whole cap is decorative — and the
63
+ * failure is silent, because a cheap-looking ledger is exactly what an unbounded turn produces.
64
+ *
65
+ * So telemetry checks the counter rather than replacing it. Observed billable tokens per delivered
66
+ * turn must stay under what rung 0 permits, since rung 0 is the most permissive rung any turn could
67
+ * have run at. Exceeding it is not a budgeting question; it means an enforcer is not enforcing.
68
+ */
69
+ export declare function managedSpendDrift(observed: {
70
+ readonly deliveredTurns: number;
71
+ readonly promptTokens: number;
72
+ readonly completionTokens: number;
73
+ }): {
74
+ readonly observedPerTurn: number;
75
+ readonly permittedPerTurn: number;
76
+ readonly exceeded: boolean;
77
+ };
78
+ /** Convenience for readers that only care whether a tenant is being held back at all. */
79
+ export declare function isManagedSpendDegraded(envelope: Pick<AdmissionEnvelope, 'turnsPerDay'>): boolean;
80
+ //# sourceMappingURL=managed-spend.d.ts.map
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Cost breakers for the sponsored managed-assistant beta (ADR 0213 amendment §3).
3
+ *
4
+ * Noodle pays for `noodle-managed` inference and charges nothing for it, so aggregate spend is a
5
+ * platform expense with no customer-side limit standing behind it. Per-surface daily caps bound one
6
+ * surface; they do not bound how many surfaces enrol. This is the missing ceiling, expressed as a
7
+ * ladder rather than a switch: a tenant approaching its allowance gets shorter answers before it gets
8
+ * a closed door, and the door itself is the kill switch that already exists.
9
+ *
10
+ * Two things make it small. Every rung is expressed through `SurfaceBudgetBounds` and
11
+ * `AssistantModelRequestPolicy`, which the resolver already returns and admission already reads live
12
+ * per turn — so degradation applies immediately and reverses on its own when the UTC day rolls, with
13
+ * no unwind path to write. And the unit is one delivered bounded turn, which is the billable unit ADR
14
+ * 0213 §12 already defines.
15
+ */
16
+ /**
17
+ * Billable tokens per charged unit. Prompt and completion are counted together and completion is
18
+ * weighted, because output costs several times input on every provider we have measured.
19
+ */
20
+ export const MANAGED_SPEND_UNIT_TOKENS = 4_000;
21
+ const OUTPUT_WEIGHT = 8;
22
+ const BYTES_PER_TOKEN = 4;
23
+ /**
24
+ * The rungs, tightest-claim-first.
25
+ *
26
+ * Rung 0 is not "today's policy": a weight is only honest if the rung's own policy enforces it, and
27
+ * an unbounded prompt makes the worst case forty times the typical turn. Bounding the request keeps
28
+ * the ledger true. These numbers are the starting point and are expected to move once measured P99
29
+ * prompt sizes are read off the live surface — that measurement is a gate on raising the ceilings,
30
+ * not on shipping the breaker.
31
+ */
32
+ const RUNGS = [
33
+ {
34
+ requestPolicy: {
35
+ maxRequestBytes: 32 * 1_024,
36
+ maxCompletionTokens: 1_500,
37
+ maxTokensPerTurn: 3_000,
38
+ maxModelStepsPerTurn: 6,
39
+ maxToolCallsPerTurn: 8,
40
+ },
41
+ admission: {
42
+ defaults: { turnsPerSession: 40, turnsPerDay: 20_000, mintsPerDay: 5_000 },
43
+ ceiling: { turnsPerDay: 20_000, mintsPerDay: 5_000 },
44
+ },
45
+ },
46
+ {
47
+ // Shorter answers. Nothing is announced: telling a stranger on a customer's marketing site that
48
+ // it is on a reduced tier harms the customer and helps no one, and a shorter answer explains
49
+ // itself.
50
+ requestPolicy: {
51
+ maxRequestBytes: 24 * 1_024,
52
+ maxCompletionTokens: 900,
53
+ maxTokensPerTurn: 1_800,
54
+ maxModelStepsPerTurn: 4,
55
+ maxToolCallsPerTurn: 6,
56
+ },
57
+ admission: {
58
+ defaults: { turnsPerSession: 40, turnsPerDay: 20_000, mintsPerDay: 5_000 },
59
+ ceiling: { turnsPerDay: 20_000, mintsPerDay: 5_000 },
60
+ },
61
+ },
62
+ {
63
+ // Shorter conversations. This does not lower a turn's worst case — `maxRequestBytes` already caps
64
+ // that — it lowers the typical prompt by cutting history growth, which stretches the remaining
65
+ // allowance without shortening any individual answer. The visitor meets
66
+ // `session_turn_budget_exhausted`, which the widget already renders as its existing "new
67
+ // conversation" affordance.
68
+ requestPolicy: {
69
+ maxRequestBytes: 16 * 1_024,
70
+ maxCompletionTokens: 700,
71
+ maxTokensPerTurn: 1_400,
72
+ maxModelStepsPerTurn: 3,
73
+ maxToolCallsPerTurn: 4,
74
+ },
75
+ admission: {
76
+ defaults: { turnsPerSession: 8, turnsPerDay: 20_000, mintsPerDay: 5_000 },
77
+ ceiling: { turnsPerDay: 20_000, mintsPerDay: 5_000 },
78
+ },
79
+ },
80
+ {
81
+ // No tools. `projectAssistantGuide` projects the per-turn tool set, so the guide never promises a
82
+ // tool that is not offered; the assistant simply answers from knowledge and its own words.
83
+ requestPolicy: {
84
+ maxRequestBytes: 12 * 1_024,
85
+ maxCompletionTokens: 400,
86
+ maxTokensPerTurn: 800,
87
+ maxModelStepsPerTurn: 1,
88
+ maxToolCallsPerTurn: 0,
89
+ },
90
+ admission: {
91
+ defaults: { turnsPerSession: 8, turnsPerDay: 20_000, mintsPerDay: 5_000 },
92
+ ceiling: { turnsPerDay: 20_000, mintsPerDay: 5_000 },
93
+ },
94
+ },
95
+ {
96
+ // At cap. Deliberately the *existing* kill switch and not a new failure mode: zero ceilings refuse
97
+ // at admission with `daily_turn_budget_exhausted`, a code every published widget already treats as
98
+ // final. A new code would be read as retryable by every widget predating it, and they would hammer
99
+ // the endpoint this cap exists to protect.
100
+ requestPolicy: {
101
+ maxRequestBytes: 12 * 1_024,
102
+ maxCompletionTokens: 400,
103
+ maxTokensPerTurn: 800,
104
+ maxModelStepsPerTurn: 1,
105
+ maxToolCallsPerTurn: 0,
106
+ },
107
+ admission: {
108
+ defaults: { turnsPerSession: 8, turnsPerDay: 0, mintsPerDay: 0 },
109
+ ceiling: { turnsPerDay: 0, mintsPerDay: 0 },
110
+ },
111
+ },
112
+ ];
113
+ /** Fractions of the allowance at which each rung begins. */
114
+ const THRESHOLDS = [0.6, 0.8, 0.95, 1];
115
+ /**
116
+ * Which rung a tenant is on.
117
+ *
118
+ * An absent or nonsensical allowance means "unbudgeted", never "exhausted". Failing closed on a typo
119
+ * would take a customer's surface dark for a configuration mistake, and the per-surface daily cap
120
+ * still bounds what that costs.
121
+ */
122
+ export function managedSpendRung(unitsUsed, allowance) {
123
+ if (!Number.isFinite(allowance) || allowance <= 0)
124
+ return 0;
125
+ const share = Math.max(0, unitsUsed) / allowance;
126
+ return THRESHOLDS.filter((threshold) => share >= threshold).length;
127
+ }
128
+ export function managedSpendPolicy(rung) {
129
+ return RUNGS[Math.min(Math.max(Math.trunc(rung), 0), RUNGS.length - 1)];
130
+ }
131
+ /**
132
+ * Units one turn costs at this rung: its worst case under the rung's own policy, in whole units.
133
+ *
134
+ * Prompt-inclusive on purpose. `maxCompletionTokens` says nothing about how large a prompt the agent
135
+ * loop may resend on each step, and the prompt is where an unbounded turn actually spends.
136
+ */
137
+ export function managedSpendUnitWeight(rung) {
138
+ const { requestPolicy } = managedSpendPolicy(rung);
139
+ const prompt = ((requestPolicy.maxModelStepsPerTurn ?? 1) * (requestPolicy.maxRequestBytes ?? 0)) /
140
+ BYTES_PER_TOKEN;
141
+ const output = OUTPUT_WEIGHT * (requestPolicy.maxTokensPerTurn ?? 0);
142
+ return Math.max(1, Math.ceil((prompt + output) / MANAGED_SPEND_UNIT_TOKENS));
143
+ }
144
+ /**
145
+ * The unit ledger for a day's worth of ordinary turns.
146
+ *
147
+ * Units are worst-case, so a turn is charged what its rung's policy *permits* rather than what it
148
+ * happened to use. That is deliberate — the alternative under-charges exactly the adversary the cap
149
+ * exists for — but it means the number is only meaningful denominated in turns. An allowance is
150
+ * therefore configured as "this many sponsored turns a day" and converted here, which is also the
151
+ * form the operator surface reports.
152
+ *
153
+ * The conservatism is uniform within a rung, so it does not distort when the ladder trips; across
154
+ * rungs it is the intended effect, since a degraded turn genuinely costs less and the same allowance
155
+ * stretches further.
156
+ */
157
+ export function managedSpendAllowanceForTurns(turnsPerDay) {
158
+ if (!Number.isFinite(turnsPerDay) || turnsPerDay <= 0)
159
+ return 0;
160
+ return Math.ceil(turnsPerDay) * managedSpendUnitWeight(0);
161
+ }
162
+ /**
163
+ * Is the ledger telling the truth?
164
+ *
165
+ * A unit weight is a claim about what a turn's own rung permits. If the rung's `requestPolicy` does
166
+ * not actually bound what it says, the claim is fiction and the whole cap is decorative — and the
167
+ * failure is silent, because a cheap-looking ledger is exactly what an unbounded turn produces.
168
+ *
169
+ * So telemetry checks the counter rather than replacing it. Observed billable tokens per delivered
170
+ * turn must stay under what rung 0 permits, since rung 0 is the most permissive rung any turn could
171
+ * have run at. Exceeding it is not a budgeting question; it means an enforcer is not enforcing.
172
+ */
173
+ export function managedSpendDrift(observed) {
174
+ const permittedPerTurn = managedSpendUnitWeight(0) * MANAGED_SPEND_UNIT_TOKENS;
175
+ if (observed.deliveredTurns <= 0) {
176
+ return { observedPerTurn: 0, permittedPerTurn, exceeded: false };
177
+ }
178
+ const billable = observed.promptTokens + OUTPUT_WEIGHT * observed.completionTokens;
179
+ const observedPerTurn = Math.round(billable / observed.deliveredTurns);
180
+ return { observedPerTurn, permittedPerTurn, exceeded: observedPerTurn > permittedPerTurn };
181
+ }
182
+ /** Convenience for readers that only care whether a tenant is being held back at all. */
183
+ export function isManagedSpendDegraded(envelope) {
184
+ return envelope.turnsPerDay === 0;
185
+ }
186
+ //# sourceMappingURL=managed-spend.js.map
@@ -5,6 +5,12 @@ export interface AssistantModelRequestPolicy {
5
5
  readonly maxCompletionTokens?: number;
6
6
  readonly maxTokensPerTurn?: number;
7
7
  readonly maxRequestBytes?: number;
8
+ /**
9
+ * Per-tenant agent-loop bounds. The deployment-wide admission envelope still wins whenever it is
10
+ * lower — these narrow a turn for one tenant, they never widen it for anyone.
11
+ */
12
+ readonly maxModelStepsPerTurn?: number;
13
+ readonly maxToolCallsPerTurn?: number;
8
14
  readonly timeoutMs?: number;
9
15
  readonly maxTurnMs?: number;
10
16
  /** Trusted operator-selected OpenAI-compatible request extensions. */
@@ -26,6 +32,16 @@ interface ResolvedAssistantModelBase {
26
32
  readonly turnsPerDay: number;
27
33
  readonly mintsPerDay: number;
28
34
  };
35
+ /**
36
+ * Platform spend accounting, present only when someone other than the customer is paying. Passed
37
+ * through to `SurfaceBudgetBounds` unchanged, which is why the shape is declared identically
38
+ * here rather than imported — the two contracts stay independent by design.
39
+ */
40
+ readonly spend?: {
41
+ readonly key: string;
42
+ readonly units: number;
43
+ readonly allowance: number;
44
+ };
29
45
  };
30
46
  }
31
47
  export type ResolvedAssistantModel = ResolvedAssistantModelBase & ({
@@ -1,4 +1,5 @@
1
1
  /** Assistant gateway contracts and in-memory adapters safe for the published CLI runtime. */
2
+ export * from './app-tool-call.js';
2
3
  export * from './artifact-projection.js';
3
4
  export * from './assistant-appearance.js';
4
5
  export * from './assistant-appearance-store.js';
@@ -17,11 +18,13 @@ export * from './assistant-transcript.js';
17
18
  export * from './assistant-view-availability.js';
18
19
  export * from './elevation.js';
19
20
  export * from './elevation-store.js';
21
+ export * from './embed-operator-view.js';
20
22
  export * from './embed-script.js';
21
23
  export * from './embed-store.js';
22
24
  export * from './in-memory-assistant-appearance-store.js';
23
25
  export * from './in-memory-elevation-store.js';
24
26
  export * from './in-memory-embed-store.js';
27
+ export * from './managed-spend.js';
25
28
  export * from './public-configuration.js';
26
29
  export * from './public-session.js';
27
30
  export * from './public-surface.js';
@@ -1,4 +1,5 @@
1
1
  /** Assistant gateway contracts and in-memory adapters safe for the published CLI runtime. */
2
+ export * from './app-tool-call.js';
2
3
  export * from './artifact-projection.js';
3
4
  export * from './assistant-appearance.js';
4
5
  export * from './assistant-appearance-store.js';
@@ -17,11 +18,13 @@ export * from './assistant-transcript.js';
17
18
  export * from './assistant-view-availability.js';
18
19
  export * from './elevation.js';
19
20
  export * from './elevation-store.js';
21
+ export * from './embed-operator-view.js';
20
22
  export * from './embed-script.js';
21
23
  export * from './embed-store.js';
22
24
  export * from './in-memory-assistant-appearance-store.js';
23
25
  export * from './in-memory-elevation-store.js';
24
26
  export * from './in-memory-embed-store.js';
27
+ export * from './managed-spend.js';
25
28
  export * from './public-configuration.js';
26
29
  export * from './public-session.js';
27
30
  export * from './public-surface.js';
@@ -1,4 +1,4 @@
1
- import type { AdmissionEnvelope, DailyCounterStore } from '@noodle-borg/admission-limits/portable';
1
+ import { type AdmissionEnvelope, type DailyCounterStore } from '@noodle-borg/admission-limits/portable';
2
2
  import { type PublicEmbedRecord, type PublicEmbedStore } from './embed-store.js';
3
3
  import type { PublicSurface } from './public-surface.js';
4
4
  import { type SurfaceBudgetBounds } from './surface-budget.js';
@@ -43,6 +43,13 @@ export interface PublicSessionRequest {
43
43
  * be parsed, in which case the surface tier alone applies: fairness degrades, solvency does not.
44
44
  */
45
45
  readonly addressBucket?: string | undefined;
46
+ /**
47
+ * The browser's own visitor identifier (admission tier 4), unvalidated exactly like `embedId`:
48
+ * it arrives from the request body, so the decision owns its parse rather than the transport.
49
+ * Client-supplied and therefore rotatable, so it bounds fairness and never abuse — the address
50
+ * tier underneath is the one that cannot be rotated.
51
+ */
52
+ readonly visitorId?: unknown;
46
53
  }
47
54
  export type PublicSessionResult = {
48
55
  readonly ok: true;
@@ -54,6 +61,12 @@ export type PublicSessionResult = {
54
61
  readonly status: number;
55
62
  readonly code: string;
56
63
  readonly message: string;
64
+ /**
65
+ * The surface this refusal belongs to, once it is known. Present for every refusal an operator
66
+ * needs to see — out of budget, address at its ceiling, origin off the live allowlist — and
67
+ * absent for the ones that cannot be attributed to a tenant at all, which are prober noise.
68
+ */
69
+ readonly embed?: PublicEmbedRecord;
57
70
  };
58
71
  export declare function mintPublicSession(request: PublicSessionRequest, envelope: AdmissionEnvelope, ports: PublicSessionPorts): Promise<PublicSessionResult>;
59
72
  //# sourceMappingURL=public-session.d.ts.map
@@ -1,7 +1,8 @@
1
+ import { visitorBucket, } from '@noodle-borg/admission-limits/portable';
1
2
  import { isPublicEmbedId } from './embed-store.js';
2
3
  import { surfaceEnvelope } from './surface-budget.js';
3
- function refuse(status, code, message) {
4
- return { ok: false, status, code, message };
4
+ function refuse(status, code, message, embed) {
5
+ return { ok: false, status, code, message, ...(embed === undefined ? {} : { embed }) };
5
6
  }
6
7
  export async function mintPublicSession(request, envelope, ports) {
7
8
  if (typeof request.embedId !== 'string' || !isPublicEmbedId(request.embedId)) {
@@ -22,17 +23,33 @@ export async function mintPublicSession(request, envelope, ports) {
22
23
  return refuse(403, 'embed_not_found', 'embed is not available');
23
24
  const surface = await ports.resolveActiveSurface(embed);
24
25
  if (surface === undefined) {
25
- return refuse(409, 'surface_unavailable', 'assistant deployment is unavailable');
26
+ return refuse(409, 'surface_unavailable', 'assistant deployment is unavailable', embed);
26
27
  }
27
28
  // Origins come from the live artifact, so `noodle deploy` alone updates a pasted snippet.
28
29
  if (!surface.origins.includes(request.origin)) {
29
- return refuse(403, 'origin_not_allowed', 'origin is not allowed');
30
+ return refuse(403, 'origin_not_allowed', 'origin is not allowed', embed);
30
31
  }
31
32
  // The surface's own budget, which an operator may have lowered — or set to zero, which is the kill
32
33
  // switch. Resolved after the embed is known and before anything is spent.
33
34
  const budgetBounds = await ports.resolveBudgetBounds?.(embed);
34
35
  const surfaceLimits = surfaceEnvelope(envelope, embed, budgetBounds);
35
36
  const now = ports.now();
37
+ // Tier 4 before tier 3, which is before solvency. A visitor at their own hourly ceiling is refused
38
+ // without spending any of the shared address allowance, so one person behind a corporate NAT cannot
39
+ // consume the allowance the other three hundred are also using. An abuser who rotates the
40
+ // identifier simply never meets this tier and lands on the address bound below, which is the point:
41
+ // this is fairness, that is abuse control, and they are deliberately not the same limit.
42
+ const visitor = visitorBucket(request.visitorId);
43
+ if (visitor !== undefined) {
44
+ const perVisitor = await ports.counters.consume({
45
+ key: `mints:vis:${embed.embedId}:${visitor}`,
46
+ limit: surfaceLimits.mintsPerVisitorHour,
47
+ window: 'hour',
48
+ }, now);
49
+ if (!perVisitor.allowed) {
50
+ return refuse(429, 'visitor_session_budget_exhausted', 'assistant is unavailable right now', embed);
51
+ }
52
+ }
36
53
  // Fairness before solvency: an address at its hourly ceiling is refused without spending any of the
37
54
  // surface's day, so one visitor cannot burn the customer's budget on refusals.
38
55
  if (typeof request.addressBucket === 'string') {
@@ -42,14 +59,14 @@ export async function mintPublicSession(request, envelope, ports) {
42
59
  window: 'hour',
43
60
  }, now);
44
61
  if (!perAddress.allowed) {
45
- return refuse(429, 'address_session_budget_exhausted', 'assistant is unavailable right now');
62
+ return refuse(429, 'address_session_budget_exhausted', 'assistant is unavailable right now', embed);
46
63
  }
47
64
  }
48
65
  const mints = await ports.counters.consume({ key: `mints:${embed.embedId}`, limit: surfaceLimits.mintsPerDay }, now);
49
66
  if (!mints.allowed) {
50
67
  // Distinct from a hard error: the widget renders this calmly, and an operator sees a surface that
51
68
  // is switched off (limit 0) or spent, not a broken embed.
52
- return refuse(429, 'daily_session_budget_exhausted', 'assistant is unavailable right now');
69
+ return refuse(429, 'daily_session_budget_exhausted', 'assistant is unavailable right now', embed);
53
70
  }
54
71
  const session = await ports.createSession({
55
72
  embed,
@@ -125,6 +125,28 @@ export async function admitPublicTurn(request, defaults, ports) {
125
125
  message: 'assistant is unavailable right now',
126
126
  };
127
127
  }
128
+ // Platform spend last, when someone other than the customer is paying. Every counter here is
129
+ // all-or-nothing with no refund, so whichever runs first is charged for turns the ones after it
130
+ // refuse — and a session ending at its length limit is ordinary traffic, many times a day, while
131
+ // spend exhaustion is rare and for one day only. Charging the sponsor for every visitor's last
132
+ // turn is the worse of the two leaks, so this is the counter that goes last.
133
+ //
134
+ // It reports the surface's own exhaustion code on purpose: every published widget already treats
135
+ // `daily_turn_budget_exhausted` as final, and a new code would be read as retryable by all of them
136
+ // — precisely the load this ceiling exists to refuse. At the ladder's closing rung the surface
137
+ // ceiling is already zero, so that refusal arrives above; this is the backstop between rungs.
138
+ if (budgetBounds?.spend !== undefined) {
139
+ const { key, units, allowance } = budgetBounds.spend;
140
+ const spend = await ports.counters.consume({ key, limit: allowance, amount: units }, ports.now());
141
+ if (!spend.allowed) {
142
+ return {
143
+ ok: false,
144
+ status: 429,
145
+ code: 'daily_turn_budget_exhausted',
146
+ message: 'assistant is unavailable right now',
147
+ };
148
+ }
149
+ }
128
150
  return { ok: true, turnCount: session.turnCount };
129
151
  }
130
152
  export async function admitBridgeToolCall(request, defaults, ports) {
@@ -4,6 +4,20 @@ import type { PublicEmbedRecord } from './embed-store.js';
4
4
  export interface SurfaceBudgetBounds {
5
5
  readonly defaults: Pick<AdmissionEnvelope, 'turnsPerSession' | 'turnsPerDay' | 'mintsPerDay'>;
6
6
  readonly ceiling: Pick<AdmissionEnvelope, 'turnsPerDay' | 'mintsPerDay'>;
7
+ /**
8
+ * Platform spend accounting for this turn, present only when someone other than the customer is
9
+ * paying. Absent means customer-funded, and nothing is charged.
10
+ *
11
+ * `units` is what this turn costs at the tenant's current rung and `allowance` is its ceiling for
12
+ * the day; the ladder that produces both lives in `managed-spend.ts`. The ceilings above remain the
13
+ * primary mechanism — this counter is the exact backstop for the window in which a cached rung
14
+ * read still says "plenty left".
15
+ */
16
+ readonly spend?: {
17
+ readonly key: string;
18
+ readonly units: number;
19
+ readonly allowance: number;
20
+ };
7
21
  }
8
22
  /**
9
23
  * One surface's daily budget, resolved from the operator's override and the deployment's defaults.
@@ -17,5 +31,5 @@ export interface SurfaceBudgetBounds {
17
31
  * operator any more than by app code, so an over-eager `--turns-per-day 999999999` degrades to the
18
32
  * ceiling instead of becoming the ceiling. Zero survives clamping and is the kill switch.
19
33
  */
20
- export declare function surfaceEnvelope(base: AdmissionEnvelope, embed: Pick<PublicEmbedRecord, 'turnsPerDay' | 'mintsPerDay'>, bounds?: SurfaceBudgetBounds): AdmissionEnvelope;
34
+ export declare function surfaceEnvelope(base: AdmissionEnvelope, embed: Pick<PublicEmbedRecord, 'turnsPerDay' | 'mintsPerDay' | 'mintsPerAddressHour' | 'turnsPerAddressHour' | 'bridgeToolCallsPerSession' | 'bridgeToolCallsPerDay'>, bounds?: SurfaceBudgetBounds): AdmissionEnvelope;
21
35
  //# sourceMappingURL=surface-budget.d.ts.map
@@ -17,6 +17,30 @@ export function surfaceEnvelope(base, embed, bounds) {
17
17
  ...effectiveBase,
18
18
  ...(embed.turnsPerDay !== undefined ? { turnsPerDay: embed.turnsPerDay } : {}),
19
19
  ...(embed.mintsPerDay !== undefined ? { mintsPerDay: embed.mintsPerDay } : {}),
20
+ // The per-address bounds carry no hosted ceiling, because they govern abuse rather than spend —
21
+ // so the deployed default is what caps them, and an operator may only lower it. A surface that
22
+ // could raise its own abuse bound toward the structural maximum would be turning the bound off
23
+ // for everyone behind one address, which is not a budget an operator gets to spend.
24
+ ...(embed.mintsPerAddressHour !== undefined
25
+ ? {
26
+ mintsPerAddressHour: Math.min(embed.mintsPerAddressHour, effectiveBase.mintsPerAddressHour),
27
+ }
28
+ : {}),
29
+ ...(embed.turnsPerAddressHour !== undefined
30
+ ? {
31
+ turnsPerAddressHour: Math.min(embed.turnsPerAddressHour, effectiveBase.turnsPerAddressHour),
32
+ }
33
+ : {}),
34
+ // The bridge bounds are the operator's to spend in either direction, like the daily caps and
35
+ // unlike the per-address ones: they govern this surface's own agent traffic against its own
36
+ // connectors, not a bound shared with everyone behind one address. `clamp` still holds them
37
+ // under the structural maximum.
38
+ ...(embed.bridgeToolCallsPerSession !== undefined
39
+ ? { bridgeToolCallsPerSession: embed.bridgeToolCallsPerSession }
40
+ : {}),
41
+ ...(embed.bridgeToolCallsPerDay !== undefined
42
+ ? { bridgeToolCallsPerDay: embed.bridgeToolCallsPerDay }
43
+ : {}),
20
44
  });
21
45
  if (bounds === undefined)
22
46
  return requested;
@@ -41,7 +41,7 @@
41
41
  "@noodle-borg/module": "0.0.0",
42
42
  "@noodle-borg/protocol": "0.0.0",
43
43
  "@noodle-borg/runtime": "0.0.0",
44
- "@noodleseed/assistant": "1.28.0",
44
+ "@noodleseed/assistant": "1.30.0",
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "devDependencies": {
@@ -168,12 +168,20 @@ export interface AuthenticatedWebsiteAccess {
168
168
  /** Allowlist of verified session claims; undeclared claims are dropped at session exchange. */
169
169
  readonly sessionClaims?: Readonly<Record<string, SessionClaimDeclaration>>;
170
170
  readonly capabilities?: readonly CapabilityRef[];
171
+ /** @see AuthenticatedWebsiteInput.webmcp */
172
+ readonly webmcp?: {
173
+ readonly enabled?: boolean;
174
+ };
171
175
  }
172
176
  export interface PublicWebsiteAccess {
173
177
  readonly mode: 'public' | 'mixed';
174
178
  readonly origins: readonly string[];
175
179
  readonly capabilities: readonly CapabilityRef[];
176
180
  readonly instructions?: string;
181
+ /** @see AuthenticatedWebsiteInput.webmcp */
182
+ readonly webmcp?: {
183
+ readonly enabled?: boolean;
184
+ };
177
185
  }
178
186
  export type AssistantAccess = AuthenticatedWebsiteAccess | PublicWebsiteAccess;
179
187
  export interface AuthenticatedWebsiteInput {
@@ -183,6 +191,19 @@ export interface AuthenticatedWebsiteInput {
183
191
  readonly sessionClaims?: Readonly<Record<string, SessionClaimDeclaration>>;
184
192
  /** Optional narrowing; omitted means the signed-in surface projects the whole server. */
185
193
  readonly capabilities?: readonly CapabilityRef[];
194
+ /**
195
+ * Override this assistant's WebMCP opt-in for sessions minted on this surface, in either
196
+ * direction (ADR 0220, amended). Omitted inherits the assistant's value.
197
+ *
198
+ * It governs **discovery**: whether the embed registers this session's tools with
199
+ * `document.modelContext`, and so whether a browser agent learns they exist. It is not a second
200
+ * authorization boundary — every call the bridge makes already carries exactly this session's
201
+ * authority. A deployment serving a marketing page and a signed-in app usually has two honest
202
+ * answers, which one switch cannot express.
203
+ */
204
+ readonly webmcp?: {
205
+ readonly enabled?: boolean;
206
+ };
186
207
  }
187
208
  export interface PublicWebsiteInput {
188
209
  readonly origins: readonly (string | ConfigRef)[];
@@ -201,6 +222,10 @@ export interface PublicWebsiteInput {
201
222
  * never operates one — and its backend completes the exchange.
202
223
  */
203
224
  readonly signIn?: boolean;
225
+ /** @see AuthenticatedWebsiteInput.webmcp */
226
+ readonly webmcp?: {
227
+ readonly enabled?: boolean;
228
+ };
204
229
  }
205
230
  export declare function authenticatedWebsite(input: AuthenticatedWebsiteInput): AuthenticatedWebsiteAccess;
206
231
  export declare function publicWebsite(input: PublicWebsiteInput): PublicWebsiteAccess;
@@ -213,8 +238,7 @@ export interface EmbeddedAssistantOptions extends AssistantUiOptions {
213
238
  * page's WebMCP API. Off unless set. Calls carry exactly the embed session's authority and take the
214
239
  * same authorization, limits, confirmation, and audit path as the assistant's own — see ADR 0220.
215
240
  *
216
- * Deployment-wide on purpose: it decides which callers may reach the server's tools, which is
217
- * surface authority rather than presentation, so it is not refinable per surface.
241
+ * The default for every surface, which any surface may override in either direction.
218
242
  */
219
243
  readonly webmcp?: {
220
244
  readonly enabled?: boolean;
@@ -227,6 +251,10 @@ export interface AssistantSurfaceConfig {
227
251
  readonly instructions?: string;
228
252
  readonly capabilities?: readonly AssistantCapability[];
229
253
  readonly sessionClaims?: Readonly<Record<string, SessionClaimDeclaration>>;
254
+ /** @see AuthenticatedWebsiteInput.webmcp */
255
+ readonly webmcp?: {
256
+ readonly enabled?: boolean;
257
+ };
230
258
  }
231
259
  export interface EmbeddedAssistantConfig extends AssistantUiOptions {
232
260
  readonly model: AssistantModel;
@@ -238,7 +266,7 @@ export interface EmbeddedAssistantConfig extends AssistantUiOptions {
238
266
  readonly allowedOrigins: readonly string[];
239
267
  /** Mirrored from the authenticated surface, for the same reason. */
240
268
  readonly sessionClaims?: Readonly<Record<string, SessionClaimDeclaration>>;
241
- /** @see EmbeddedAssistantOptions.webmcp — deployment-wide, never per surface. */
269
+ /** @see EmbeddedAssistantOptions.webmcp — the default each surface may override. */
242
270
  readonly webmcp?: {
243
271
  readonly enabled?: boolean;
244
272
  };
@@ -6,6 +6,7 @@ export function authenticatedWebsite(input) {
6
6
  ...(input.instructions === undefined ? {} : { instructions: input.instructions }),
7
7
  ...(input.sessionClaims ? { sessionClaims: structuredClone(input.sessionClaims) } : {}),
8
8
  ...(input.capabilities ? { capabilities: [...input.capabilities] } : {}),
9
+ ...(input.webmcp === undefined ? {} : { webmcp: { ...input.webmcp } }),
9
10
  };
10
11
  }
11
12
  export function publicWebsite(input) {
@@ -14,6 +15,7 @@ export function publicWebsite(input) {
14
15
  origins: serializeOrigins(input.origins, 'assistant.access.origins'),
15
16
  capabilities: [...input.capabilities],
16
17
  ...(input.instructions === undefined ? {} : { instructions: input.instructions }),
18
+ ...(input.webmcp === undefined ? {} : { webmcp: { ...input.webmcp } }),
17
19
  };
18
20
  }
19
21
  function serializeOrigins(origins, path) {
@@ -56,6 +58,7 @@ export function embeddedAssistant(input) {
56
58
  ...(surface.mode === 'authenticated' && surface.sessionClaims
57
59
  ? { sessionClaims: structuredClone(surface.sessionClaims) }
58
60
  : {}),
61
+ ...(surface.webmcp === undefined ? {} : { webmcp: { ...surface.webmcp } }),
59
62
  })),
60
63
  allowedOrigins: surfaces.flatMap((surface) => [...surface.origins]),
61
64
  ...structuredClone(ui),