@noodleseed/one 0.148.0 → 0.149.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 +14 -1
- package/dist/commands/assistant-surface-ops.d.ts.map +1 -1
- package/dist/commands/assistant-surface-ops.js +33 -2
- package/dist/commands/assistant-surface-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 +35 -11
- 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-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/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 +71 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.js +59 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +27 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +35 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +6 -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 +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +2 -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 +14 -0
- package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +15 -1
- 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-usage.js +68 -10
- package/node_modules/@noodle-borg/observability/dist/index.js +1 -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/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-model-binding.js +1 -4
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +7 -3
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +14 -8
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ADMISSION_DEFAULTS } from '@noodle-borg/admission-limits/portable';
|
|
2
2
|
import { assistantCoreModelMessages, assistantTurnModelContextMessages, requestAssistantSuggestedPrompts, requestModelCompletion, resolveAssistantContextProviderModelResult, } from '@noodle-borg/assistant-gateway/model-runtime';
|
|
3
|
-
import { assistantGuideModelContext, assistantModelToolOncePerSession, assistantModelToolRequiredWhenVisible, assistantViewAvailableData, dispatchAssistantTool, projectAssistantGuide, publicSurfaceOf, selectAssistantModelTools, toolTouchesDelegatedAuth, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
3
|
+
import { assistantGuideModelContext, assistantModelToolOncePerSession, assistantModelToolRequiredWhenVisible, assistantOmittedToolResult, assistantViewAvailableData, dispatchAssistantTool, projectAssistantGuide, publicSurfaceOf, selectAssistantModelTools, toolTouchesDelegatedAuth, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
4
4
|
import { validateJsonSchemaWithDefaults } from '@noodle-borg/compiler';
|
|
5
5
|
import { guardedFetch } from '@noodle-borg/connector-http';
|
|
6
6
|
import { evaluateToolAuthorization } from '@noodle-borg/protocol';
|
|
@@ -8,6 +8,19 @@ import { interceptForElevation, offersSignIn } from './assistant-elevation.js';
|
|
|
8
8
|
import { assistantKnowledgeModelTools, executeAssistantKnowledgeSearch, findAssistantKnowledgeComponent, KNOWLEDGE_CITATION_GUIDANCE, resolveAssistantKnowledge, } from './assistant-knowledge.js';
|
|
9
9
|
import { resolveAssistantModelBinding } from './assistant-model-binding.js';
|
|
10
10
|
const MAX_MODEL_RESPONSE = 1 << 20;
|
|
11
|
+
/**
|
|
12
|
+
* The agent-loop bounds come from the admission envelope so an operator lowering them changes
|
|
13
|
+
* behaviour. They used to be a constant here that happened to equal the envelope's default, which
|
|
14
|
+
* meant the envelope documented a limit it did not control.
|
|
15
|
+
*/
|
|
16
|
+
function agentBounds(deps, policy) {
|
|
17
|
+
const { modelStepsPerTurn, toolCallsPerTurn } = deps.admissionEnvelope ?? ADMISSION_DEFAULTS;
|
|
18
|
+
// A per-tenant policy narrows a turn; it can never widen one, so the envelope wins when lower.
|
|
19
|
+
return {
|
|
20
|
+
steps: Math.min(modelStepsPerTurn, policy?.maxModelStepsPerTurn ?? modelStepsPerTurn),
|
|
21
|
+
toolCalls: Math.min(toolCallsPerTurn, policy?.maxToolCallsPerTurn ?? toolCallsPerTurn),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
11
24
|
export function createAssistantTurnStats() {
|
|
12
25
|
return {
|
|
13
26
|
modelRequests: 0,
|
|
@@ -21,9 +34,12 @@ export function createAssistantTurnStats() {
|
|
|
21
34
|
}
|
|
22
35
|
/** Run one model/tool loop against the immutable invocation-context snapshot for this turn. */
|
|
23
36
|
export async function runAgentTurn(target, session, message, context, deps, emit, modelContext, pageContext, stats = createAssistantTurnStats(), suggestions = false) {
|
|
37
|
+
// Every terminal failure in this loop is one shape: a code, and the turn ends. Naming it keeps the
|
|
38
|
+
// dozen sites readable and stops a new one inventing a different envelope.
|
|
39
|
+
const fail = (code) => emit({ event: 'error', data: { code } });
|
|
24
40
|
const assistant = target.served.artifact.server.assistant;
|
|
25
41
|
if (!assistant)
|
|
26
|
-
return
|
|
42
|
+
return fail('assistant_unavailable');
|
|
27
43
|
const binding = await resolveAssistantModelBinding(target, session.tenant, session.deploymentId, deps);
|
|
28
44
|
if (!binding) {
|
|
29
45
|
return emit({
|
|
@@ -35,12 +51,18 @@ export async function runAgentTurn(target, session, message, context, deps, emit
|
|
|
35
51
|
},
|
|
36
52
|
});
|
|
37
53
|
}
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
// Zero means offer none, from every source — not "call one and meet `tool_call_budget_exhausted`".
|
|
55
|
+
// A required tool and a knowledge tool each reach the model by their own path, so silencing only
|
|
56
|
+
// the ordinary ones would still advertise a tool the turn can never run.
|
|
57
|
+
const bounds = agentBounds(deps, binding.requestPolicy);
|
|
58
|
+
const toolless = bounds.toolCalls === 0;
|
|
59
|
+
const knowledge = toolless ? undefined : await resolveAssistantKnowledge(target.served);
|
|
60
|
+
let modelTools = toolless
|
|
61
|
+
? []
|
|
62
|
+
: selectTurnModelTools(target, session.caller, message, session.modelToolUses);
|
|
40
63
|
const requiredTools = modelTools.filter(assistantModelToolRequiredWhenVisible);
|
|
41
|
-
if (requiredTools.length > 1)
|
|
42
|
-
return
|
|
43
|
-
}
|
|
64
|
+
if (requiredTools.length > 1)
|
|
65
|
+
return fail('multiple_required_model_tools');
|
|
44
66
|
let requiredTool = requiredTools[0];
|
|
45
67
|
const guideProjection = projectAssistantGuide({
|
|
46
68
|
appPackage: target.served.appPackageSnapshot?.artifact,
|
|
@@ -73,19 +95,17 @@ export async function runAgentTurn(target, session, message, context, deps, emit
|
|
|
73
95
|
...session.history,
|
|
74
96
|
{ role: 'user', content: message },
|
|
75
97
|
];
|
|
76
|
-
const envelope = deps.admissionEnvelope ?? ADMISSION_DEFAULTS;
|
|
77
|
-
const bounds = { steps: envelope.modelStepsPerTurn, toolCalls: envelope.toolCallsPerTurn };
|
|
78
98
|
// Across every step of this turn, not per step: eight tool calls is eight, however the model splits
|
|
79
99
|
// them, or a model that loops one call per step would spend the budget a step at a time.
|
|
80
100
|
let toolCallsThisTurn = 0;
|
|
101
|
+
let omittedToolRecoveries = 0;
|
|
81
102
|
let remainingTokens = binding.requestPolicy?.maxTokensPerTurn;
|
|
82
103
|
const turnSignal = binding.requestPolicy?.maxTurnMs === undefined
|
|
83
104
|
? undefined
|
|
84
105
|
: AbortSignal.timeout(binding.requestPolicy.maxTurnMs);
|
|
85
106
|
for (let step = 0; step < bounds.steps; step += 1) {
|
|
86
|
-
if (remainingTokens !== undefined && remainingTokens <= 0)
|
|
87
|
-
return
|
|
88
|
-
}
|
|
107
|
+
if (remainingTokens !== undefined && remainingTokens <= 0)
|
|
108
|
+
return fail('model_token_budget_exhausted');
|
|
89
109
|
const requestTokenLimit = Math.min(binding.requestPolicy?.maxCompletionTokens ?? Number.MAX_SAFE_INTEGER, remainingTokens ?? Number.MAX_SAFE_INTEGER);
|
|
90
110
|
const requiredToolForStep = requiredTool;
|
|
91
111
|
const stepModelTools = requiredToolForStep === undefined ? modelTools : [requiredToolForStep];
|
|
@@ -106,12 +126,11 @@ export async function runAgentTurn(target, session, message, context, deps, emit
|
|
|
106
126
|
}
|
|
107
127
|
const response = completion.choices[0]?.message;
|
|
108
128
|
if (!response)
|
|
109
|
-
return
|
|
129
|
+
return fail('invalid_model_response');
|
|
110
130
|
if (requiredToolForStep !== undefined &&
|
|
111
131
|
(response.tool_calls?.length !== 1 ||
|
|
112
|
-
response.tool_calls[0]?.function.name !== requiredToolForStep.name))
|
|
113
|
-
return
|
|
114
|
-
}
|
|
132
|
+
response.tool_calls[0]?.function.name !== requiredToolForStep.name))
|
|
133
|
+
return fail('required_model_tool_missing');
|
|
115
134
|
if (!response.tool_calls?.length) {
|
|
116
135
|
const assistantContent = response.content || streamedContent;
|
|
117
136
|
if (assistantContent)
|
|
@@ -141,9 +160,8 @@ export async function runAgentTurn(target, session, message, context, deps, emit
|
|
|
141
160
|
for (const call of response.tool_calls) {
|
|
142
161
|
toolCallsThisTurn += 1;
|
|
143
162
|
stats.toolCalls += 1;
|
|
144
|
-
if (toolCallsThisTurn > bounds.toolCalls)
|
|
145
|
-
return
|
|
146
|
-
}
|
|
163
|
+
if (toolCallsThisTurn > bounds.toolCalls)
|
|
164
|
+
return fail('tool_call_budget_exhausted');
|
|
147
165
|
const knowledgeComponent = findAssistantKnowledgeComponent(knowledge, call.function.name);
|
|
148
166
|
if (knowledge !== undefined && knowledgeComponent !== undefined) {
|
|
149
167
|
let knowledgeArgs;
|
|
@@ -151,7 +169,7 @@ export async function runAgentTurn(target, session, message, context, deps, emit
|
|
|
151
169
|
knowledgeArgs = JSON.parse(call.function.arguments);
|
|
152
170
|
}
|
|
153
171
|
catch {
|
|
154
|
-
return
|
|
172
|
+
return fail('invalid_tool_arguments');
|
|
155
173
|
}
|
|
156
174
|
emit({ event: 'tool_started', data: { id: call.id, tool: call.function.name } });
|
|
157
175
|
messages.push({
|
|
@@ -163,7 +181,19 @@ export async function runAgentTurn(target, session, message, context, deps, emit
|
|
|
163
181
|
}
|
|
164
182
|
const tool = stepModelTools.find((candidate) => candidate.name === call.function.name);
|
|
165
183
|
if (!tool) {
|
|
166
|
-
|
|
184
|
+
// Tell the model rather than ending the turn on a bare error; `assistantOmittedToolResult`
|
|
185
|
+
// carries the reasoning. Once per turn: a model that insists after being told is the
|
|
186
|
+
// protocol violation the error code exists for, and it stops paying for extra steps.
|
|
187
|
+
if (omittedToolRecoveries > 0)
|
|
188
|
+
return fail('invalid_model_tool_call');
|
|
189
|
+
omittedToolRecoveries += 1;
|
|
190
|
+
modelTools = [];
|
|
191
|
+
messages.push({
|
|
192
|
+
role: 'tool',
|
|
193
|
+
tool_call_id: call.id,
|
|
194
|
+
content: assistantOmittedToolResult(call.function.name),
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
167
197
|
}
|
|
168
198
|
// A mixed surface turns "denied" into "sign in first". The intercept runs BEFORE the
|
|
169
199
|
// authorization denial — an ADR 0185-gated tool is exactly what an anonymous visitor signs
|
|
@@ -195,22 +225,20 @@ export async function runAgentTurn(target, session, message, context, deps, emit
|
|
|
195
225
|
});
|
|
196
226
|
continue;
|
|
197
227
|
}
|
|
198
|
-
if (!evaluateToolAuthorization(tool.authorization, session.caller).allow)
|
|
199
|
-
return
|
|
200
|
-
}
|
|
228
|
+
if (!evaluateToolAuthorization(tool.authorization, session.caller).allow)
|
|
229
|
+
return fail('invalid_model_tool_call');
|
|
201
230
|
let args;
|
|
202
231
|
try {
|
|
203
232
|
args = JSON.parse(call.function.arguments);
|
|
204
233
|
}
|
|
205
234
|
catch {
|
|
206
|
-
return
|
|
235
|
+
return fail('invalid_tool_arguments');
|
|
207
236
|
}
|
|
208
237
|
// Validate AND apply schema defaults; the pending record and the execution below both use
|
|
209
238
|
// the coerced copy, so the confirmed call equals the executed call (roadmap S5).
|
|
210
239
|
const coerced = validateJsonSchemaWithDefaults(tool.inputSchema, args);
|
|
211
|
-
if (coerced.issues.length > 0)
|
|
212
|
-
return
|
|
213
|
-
}
|
|
240
|
+
if (coerced.issues.length > 0)
|
|
241
|
+
return fail('invalid_tool_arguments');
|
|
214
242
|
args = coerced.value;
|
|
215
243
|
const dispatch = await dispatchAssistantTool({
|
|
216
244
|
artifact: target.served.artifact,
|
|
@@ -250,7 +278,7 @@ export async function runAgentTurn(target, session, message, context, deps, emit
|
|
|
250
278
|
});
|
|
251
279
|
}
|
|
252
280
|
}
|
|
253
|
-
|
|
281
|
+
fail('step_limit');
|
|
254
282
|
}
|
|
255
283
|
/** Generate an initial prompt set from the same exact surface/context assembly as an ordinary turn. */
|
|
256
284
|
export async function generateInitialAssistantSuggestions(target, session, context, deps, modelContext, pageContext) {
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { ADMISSION_DEFAULTS, clamp } from '@noodle-borg/admission-limits/portable';
|
|
2
|
-
import { publicSurfaceOf, surfaceEnvelope } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
+
import { assistantEmbedOperatorView, parsePublicEmbedBudget, publicSurfaceOf, surfaceEnvelope, } from '@noodle-borg/assistant-gateway/portable';
|
|
3
3
|
import { readJsonBody, sendForbidden, sendJson } from '../http-util.js';
|
|
4
4
|
import { now } from './assistant-route-http.js';
|
|
5
5
|
import { authorizeControlPlane } from './control-plane.js';
|
|
6
|
+
/**
|
|
7
|
+
* The operator's view of a tenant's public surfaces, and the one control over them.
|
|
8
|
+
*
|
|
9
|
+
* `PATCH` sets the daily caps; `--turns-per-day 0` is the kill switch, and it is deliberately the only
|
|
10
|
+
* documented one. Revoking an embed would also stop a surface, but it destroys the paste-once id and
|
|
11
|
+
* every page carrying it, so it stays an exceptional action rather than the routine off-switch.
|
|
12
|
+
*
|
|
13
|
+
* Origins, capabilities, and surfaceMode are read from the *active* deployment rather than the embed
|
|
14
|
+
* row, so what an operator sees here is what a browser will actually be held to at mint time (the row
|
|
15
|
+
* keeps its provisioning-time mode by design — `ensure` never overwrites a live row).
|
|
16
|
+
*/
|
|
6
17
|
export async function handleAssistantEmbeds(req, res, tenant, embedId, deps) {
|
|
7
18
|
const identity = await authorizeControlPlane(req, res, deps.gate, { requireIdentity: false });
|
|
8
19
|
if (identity === false)
|
|
@@ -30,7 +41,7 @@ export async function handleAssistantEmbeds(req, res, tenant, embedId, deps) {
|
|
|
30
41
|
const body = await readJsonBody(req, deps.maxBody);
|
|
31
42
|
if (!body.ok)
|
|
32
43
|
return sendJson(res, body.status, { error: body.error });
|
|
33
|
-
const budget =
|
|
44
|
+
const budget = parsePublicEmbedBudget(body.value);
|
|
34
45
|
if (!budget.ok)
|
|
35
46
|
return sendJson(res, 400, { error: budget.error });
|
|
36
47
|
const updated = await embeds.setBudget(embedId, budget.value, now(deps));
|
|
@@ -46,44 +57,21 @@ export async function handleAssistantEmbeds(req, res, tenant, embedId, deps) {
|
|
|
46
57
|
res.setHeader('Allow', embedId === undefined ? 'GET' : 'PATCH');
|
|
47
58
|
return sendJson(res, 405, { error: 'method not allowed' });
|
|
48
59
|
}
|
|
49
|
-
function parseBudget(body) {
|
|
50
|
-
const { turnsPerDay, mintsPerDay } = body;
|
|
51
|
-
const value = {};
|
|
52
|
-
for (const [name, raw] of [
|
|
53
|
-
['turnsPerDay', turnsPerDay],
|
|
54
|
-
['mintsPerDay', mintsPerDay],
|
|
55
|
-
]) {
|
|
56
|
-
if (raw === undefined)
|
|
57
|
-
continue;
|
|
58
|
-
// Zero is valid and is the point; anything negative or fractional is a mistake worth saying out
|
|
59
|
-
// loud rather than silently flooring, because an operator setting a cap is being deliberate.
|
|
60
|
-
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 0) {
|
|
61
|
-
return { ok: false, error: `"${name}" must be a non-negative integer` };
|
|
62
|
-
}
|
|
63
|
-
value[name] = raw;
|
|
64
|
-
}
|
|
65
|
-
if (Object.keys(value).length === 0) {
|
|
66
|
-
return { ok: false, error: 'set at least one of "turnsPerDay" or "mintsPerDay"' };
|
|
67
|
-
}
|
|
68
|
-
return { ok: true, value };
|
|
69
|
-
}
|
|
70
60
|
async function view(record, tenant, defaults, deps, counters) {
|
|
71
61
|
const target = await deps.registry.getActiveByTenant(tenant);
|
|
72
|
-
const surface = publicSurfaceOf(target?.served.artifact.server.assistant);
|
|
73
|
-
// Clamped even for the pure-default case, so what an operator reads is what admission will enforce.
|
|
74
|
-
const envelope = surfaceEnvelope(clamp(defaults), record);
|
|
75
62
|
const at = now(deps);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
63
|
+
// The same bounds admission resolves, not the bare defaults: a sponsored surface enforces the
|
|
64
|
+
// hosted envelope, and reporting the unsponsored one told an operator a number nothing honours.
|
|
65
|
+
const deploymentId = target?.deploymentId ?? '';
|
|
66
|
+
const bounds = (await deps.managedModelResolver?.resolve({ tenant, deploymentId }))
|
|
67
|
+
?.publicAdmission;
|
|
68
|
+
return assistantEmbedOperatorView({
|
|
69
|
+
record,
|
|
70
|
+
surface: publicSurfaceOf(target?.served.artifact.server.assistant),
|
|
71
|
+
// Clamped even for the pure-default case, so what an operator reads is what admission enforces.
|
|
72
|
+
envelope: surfaceEnvelope(clamp(defaults), record, bounds),
|
|
73
|
+
peek: (key) => counters.peek(key, at),
|
|
74
|
+
...(bounds?.spend === undefined ? {} : { spend: bounds.spend }),
|
|
75
|
+
});
|
|
88
76
|
}
|
|
89
77
|
//# sourceMappingURL=assistant-embeds.js.map
|
|
@@ -5,10 +5,7 @@ export async function resolveAssistantModelBinding(target, tenant, deploymentId,
|
|
|
5
5
|
if (declaration === undefined)
|
|
6
6
|
return undefined;
|
|
7
7
|
if (declaration.kind === 'noodle-managed') {
|
|
8
|
-
return deps.managedModelResolver?.resolve({
|
|
9
|
-
tenant,
|
|
10
|
-
deploymentId,
|
|
11
|
-
});
|
|
8
|
+
return deps.managedModelResolver?.resolve({ tenant, deploymentId });
|
|
12
9
|
}
|
|
13
10
|
const scope = resolveConfigScope(tenant);
|
|
14
11
|
const [variables, secrets] = await Promise.all([
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { ADMISSION_DEFAULTS, clientAddressBucket } from '@noodle-borg/admission-limits/portable';
|
|
3
3
|
import { effectiveAssistantBrowserConfiguration, mintPublicSession, publicEmbedTenant, publicSurfaceOf, } from '@noodle-borg/assistant-gateway/portable';
|
|
4
|
-
import { assistantSessionUsageRequestEvent, captureAssistantUsage, } from '@noodle-borg/observability';
|
|
4
|
+
import { assistantSessionUsageRequestEvent, captureAssistantUsage, captureRefusedAssistantSession, } from '@noodle-borg/observability';
|
|
5
5
|
import { assistantSessionResponseSchema } from '@noodle-borg/wire-contracts';
|
|
6
6
|
import { readJsonBody, sendJson } from '../http-util.js';
|
|
7
7
|
import { applyBrowserCors, assistantSessionEndpoints, now } from './assistant-route-http.js';
|
|
@@ -25,7 +25,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
25
25
|
const body = await readJsonBody(req, deps.maxBody);
|
|
26
26
|
if (!body.ok)
|
|
27
27
|
return sendJson(res, body.status, { error: body.error });
|
|
28
|
-
const { embedId } = body.value;
|
|
28
|
+
const { embedId, visitorId } = body.value;
|
|
29
29
|
// The browser's Origin header is the claim under test. The body never asserts its own origin.
|
|
30
30
|
const origin = typeof req.headers.origin === 'string' ? req.headers.origin : undefined;
|
|
31
31
|
// Admission tier 3. Hashed here so no raw address crosses into the decision, a counter key, or an
|
|
@@ -36,7 +36,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
36
36
|
let configuration;
|
|
37
37
|
let sponsoredBudget;
|
|
38
38
|
let usageSession;
|
|
39
|
-
const result = await mintPublicSession({ embedId, origin, ...(addressBucket ? { addressBucket } : {}) }, deps.admissionEnvelope ?? ADMISSION_DEFAULTS, {
|
|
39
|
+
const result = await mintPublicSession({ embedId, visitorId, origin, ...(addressBucket ? { addressBucket } : {}) }, deps.admissionEnvelope ?? ADMISSION_DEFAULTS, {
|
|
40
40
|
embeds,
|
|
41
41
|
counters,
|
|
42
42
|
resolveActiveSurface: async (embed) => {
|
|
@@ -86,6 +86,10 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
86
86
|
now: () => now(deps),
|
|
87
87
|
});
|
|
88
88
|
if (!result.ok) {
|
|
89
|
+
// A refusal the surface owns is usage. Without it an operator whose visitors are being turned
|
|
90
|
+
// away sees a healthy-looking page; which refusals count is the helper's decision, not this one's.
|
|
91
|
+
const refusedIn = performance.now() - usageStartedAt;
|
|
92
|
+
captureRefusedAssistantSession(deps.captureRequestEvent, result, refusedIn);
|
|
89
93
|
return sendJson(res, result.status, { error: result.message, code: result.code });
|
|
90
94
|
}
|
|
91
95
|
// Only here, with the origin proven against the live surface, may the page read the response.
|
|
@@ -174,6 +174,16 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
174
174
|
captureAssistantUsage(deps.captureRequestEvent, assistantSessionUsageRequestEvent(session.session, performance.now() - usageStartedAt));
|
|
175
175
|
return sendJson(res, 201, sessionBody);
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Both turn entries — a resume and a typed message — refuse identically: the refusal is spent
|
|
179
|
+
* budget, so it is recorded against the surface before the caller is told no. Kept in one place so
|
|
180
|
+
* the two entries cannot drift into recording it differently.
|
|
181
|
+
*/
|
|
182
|
+
function refuseTurn(deps, res, session, refusal, usageStartedAt) {
|
|
183
|
+
const durationMs = performance.now() - usageStartedAt;
|
|
184
|
+
captureAssistantUsage(deps.captureRequestEvent, assistantRefusedTurnUsageRequestEvent(session, refusal.code, durationMs));
|
|
185
|
+
sendJson(res, refusal.status, { error: refusal.message, code: refusal.code });
|
|
186
|
+
}
|
|
177
187
|
export async function handleAssistantTurn(req, res, deps) {
|
|
178
188
|
const usageStartedAt = performance.now();
|
|
179
189
|
const session = await authenticateSession(req, res, deps);
|
|
@@ -201,10 +211,8 @@ export async function handleAssistantTurn(req, res, deps) {
|
|
|
201
211
|
}
|
|
202
212
|
suggestionsRequested = resumeResult.data.suggestions === true;
|
|
203
213
|
const refusal = await refusePublicTurn(deps, session, 'resume', addressBucket);
|
|
204
|
-
if (refusal)
|
|
205
|
-
|
|
206
|
-
return sendJson(res, refusal.status, { error: refusal.message, code: refusal.code });
|
|
207
|
-
}
|
|
214
|
+
if (refusal)
|
|
215
|
+
return refuseTurn(deps, res, session, refusal, usageStartedAt);
|
|
208
216
|
const pending = await deps.store.consumePendingResume(session.id);
|
|
209
217
|
if (!pending) {
|
|
210
218
|
return sendJson(res, 409, {
|
|
@@ -252,10 +260,8 @@ export async function handleAssistantTurn(req, res, deps) {
|
|
|
252
260
|
// Admission before any metered work (ADR 0201 §8): ahead of the deployment read and well ahead
|
|
253
261
|
// of the model, so a refused turn costs the customer nothing.
|
|
254
262
|
const refusal = await refusePublicTurn(deps, session, message, addressBucket);
|
|
255
|
-
if (refusal)
|
|
256
|
-
|
|
257
|
-
return sendJson(res, refusal.status, { error: refusal.message, code: refusal.code });
|
|
258
|
-
}
|
|
263
|
+
if (refusal)
|
|
264
|
+
return refuseTurn(deps, res, session, refusal, usageStartedAt);
|
|
259
265
|
// The visitor typed first: any pending post-sign-in resume is moot, and must not fire later.
|
|
260
266
|
if (session.pendingResume)
|
|
261
267
|
await deps.store.consumePendingResume(session.id);
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
41
41
|
"@noodle-borg/admission-limits": "0.0.0",
|
|
42
|
-
"@noodle-borg/agent-kit": "0.
|
|
42
|
+
"@noodle-borg/agent-kit": "0.88.0",
|
|
43
43
|
"@noodle-borg/app-package": "0.0.0",
|
|
44
44
|
"@noodle-borg/assistant-gateway": "0.0.0",
|
|
45
45
|
"@noodle-borg/auth": "0.0.0",
|
|
@@ -77,6 +77,7 @@ export function emitRequestEvent(input) {
|
|
|
77
77
|
...(clientVersion !== undefined ? { clientVersion } : {}),
|
|
78
78
|
clientFamily: family,
|
|
79
79
|
...(auth.accessMode !== undefined ? { accessMode: auth.accessMode } : {}),
|
|
80
|
+
surface: 'mcp',
|
|
80
81
|
subjectKind: subject !== undefined ? 'authenticated' : 'anonymous',
|
|
81
82
|
method,
|
|
82
83
|
kind: DISCOVERY_METHODS.has(method) ? 'discovery' : 'usage',
|
|
@@ -147,6 +148,7 @@ export function emitDeniedRequestEvent(input) {
|
|
|
147
148
|
sessionSource: 'none',
|
|
148
149
|
clientFamily: clientFamily(undefined, header(req, 'user-agent')),
|
|
149
150
|
...(auth.accessMode !== undefined ? { accessMode: auth.accessMode } : {}),
|
|
151
|
+
surface: 'mcp',
|
|
150
152
|
subjectKind: 'anonymous',
|
|
151
153
|
method,
|
|
152
154
|
kind: 'usage',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/assistant",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Embed the Noodle Seed customer-branded assistant in your web app with managed or framework-owned UI, a framework-neutral MCP App host, a DOM-free client, and a backend session helper. Authoring and deploying the server is @noodleseed/one.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/one",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.149.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Noodle CLI by Noodle Seed — author, run, and deploy declarative MCP servers. Embedding the assistant in your own web app is @noodleseed/assistant.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -236,7 +236,7 @@
|
|
|
236
236
|
"@modelcontextprotocol/client": "2.0.0",
|
|
237
237
|
"@modelcontextprotocol/server": "2.0.0",
|
|
238
238
|
"@noodle-borg/admission-limits": "0.0.0",
|
|
239
|
-
"@noodle-borg/agent-kit": "0.
|
|
239
|
+
"@noodle-borg/agent-kit": "0.88.0",
|
|
240
240
|
"@noodle-borg/app-audit": "0.0.0",
|
|
241
241
|
"@noodle-borg/app-package": "0.0.0",
|
|
242
242
|
"@noodle-borg/assistant-gateway": "0.0.0",
|