newmark-agent 0.6.3 → 0.6.7

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 (72) hide show
  1. package/config.example.json +13 -3
  2. package/dist/cli-commands.d.ts +1 -1
  3. package/dist/cli-commands.js +53 -2
  4. package/dist/cli-discovery.js +3 -0
  5. package/dist/conversation-utility-host.bundle.cjs +3046 -1038
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +149 -12
  8. package/dist/core/agent.js +843 -176
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +171 -22
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/branchIdentity.d.ts +29 -0
  14. package/dist/core/branchIdentity.js +54 -0
  15. package/dist/core/config.js +27 -11
  16. package/dist/core/continuation/contracts.d.ts +1 -1
  17. package/dist/core/continuation/store.d.ts +119 -1
  18. package/dist/core/continuation/store.js +337 -19
  19. package/dist/core/conversationKernel.d.ts +62 -8
  20. package/dist/core/conversationKernel.js +361 -47
  21. package/dist/core/conversationStateDocument.d.ts +18 -0
  22. package/dist/core/conversationStateDocument.js +88 -0
  23. package/dist/core/conversationVisualBudget.d.ts +74 -0
  24. package/dist/core/conversationVisualBudget.js +153 -0
  25. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  26. package/dist/core/electronUtilityAgentClient.js +32 -8
  27. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  28. package/dist/core/electronUtilityRuntimePool.js +93 -24
  29. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  30. package/dist/core/hostRuntimeHooks.js +17 -0
  31. package/dist/core/jevDecision.d.ts +144 -0
  32. package/dist/core/jevDecision.js +226 -0
  33. package/dist/core/memoryProbe.d.ts +24 -0
  34. package/dist/core/memoryProbe.js +104 -0
  35. package/dist/core/mobilePairing.d.ts +7 -1
  36. package/dist/core/mobilePairing.js +9 -1
  37. package/dist/core/performanceDiagnostics.d.ts +1 -1
  38. package/dist/core/routeDecisionValidator.d.ts +86 -0
  39. package/dist/core/routeDecisionValidator.js +249 -0
  40. package/dist/core/routeEligibility.d.ts +52 -0
  41. package/dist/core/routeEligibility.js +108 -0
  42. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  43. package/dist/core/runtimeMemoryBudget.js +12 -0
  44. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  45. package/dist/core/utilityHostToolRouter.js +2 -0
  46. package/dist/core/visualDownscale.d.ts +6 -0
  47. package/dist/core/visualDownscale.js +137 -0
  48. package/dist/core/wslAgentClient.d.ts +1 -2
  49. package/dist/core/wslAgentClient.js +0 -8
  50. package/dist/core/wslAgentProtocol.d.ts +1 -10
  51. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  52. package/dist/core/wslAgentRuntimePool.js +17 -22
  53. package/dist/llm/provider.d.ts +3 -1
  54. package/dist/llm/provider.js +53 -9
  55. package/dist/main.js +108 -31
  56. package/dist/preload.js +3 -1
  57. package/dist/server.js +30 -18
  58. package/dist/tools/computerUse.d.ts +4 -0
  59. package/dist/tools/computerUse.js +202 -4
  60. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  61. package/dist/tools/computerUsePowerShellHost.js +105 -31
  62. package/dist/tools/index.js +4 -1
  63. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  64. package/dist/tui/src/i18n.js +12 -1
  65. package/dist/tui/src/render.js +9 -2
  66. package/dist/tui/src/settings-schema.js +19 -0
  67. package/dist/tui/src/state.js +69 -1
  68. package/dist/ui/index.html +994 -277
  69. package/dist/ui/lucide-sprite.svg +0 -8
  70. package/dist/wsl-agent-host.bundle.cjs +2851 -1013
  71. package/dist/wsl-agent-host.js +0 -3
  72. package/package.json +38 -9
@@ -1,18 +1,40 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AutoRouter = void 0;
3
+ exports.RouteExecutionController = exports.ROUTE_POLICY_VERSION = void 0;
4
4
  exports.normalizeAutoPreference = normalizeAutoPreference;
5
5
  exports.defaultRoutePolicy = defaultRoutePolicy;
6
6
  exports.classifyTaskClasses = classifyTaskClasses;
7
7
  exports.classifyRouteFailure = classifyRouteFailure;
8
+ exports.createRouteId = createRouteId;
9
+ exports.catalogSnapshotHash = catalogSnapshotHash;
10
+ exports.validationEvidenceIsStale = validationEvidenceIsStale;
11
+ /**
12
+ * Auto Router v2 — shared route contracts, failure classification, operational
13
+ * endpoint health and provider-local recovery planning.
14
+ *
15
+ * This file no longer selects models. Model selection is the exclusive
16
+ * responsibility of the JEV decision engine (`jevDecision.ts`) inside the
17
+ * candidate space produced by the deterministic eligibility guard
18
+ * (`routeEligibility.ts`). What remains here is execution plumbing:
19
+ *
20
+ * - shared route types (`RouteDecision`, `RouteAttempt`, `RoutePolicy`, …)
21
+ * - provider failure classification
22
+ * - endpoint health / circuit breaker (operational protection only)
23
+ * - build-scoped transaction pinning (execution consistency inside one Build)
24
+ * - provider-local recovery planning (retry / equivalent / fallback / JEV alternate)
25
+ *
26
+ * Removed on purpose: the feedback map, EWMA preference learning,
27
+ * `quality_by_task` scoring, cache affinity, switch thresholds, and every
28
+ * weighted cost/reliability/speed utility pass. Nothing in this file may
29
+ * reconstruct a second model selector.
30
+ */
8
31
  const crypto_1 = require("crypto");
32
+ const routeEligibility_1 = require("./routeEligibility");
9
33
  const DAY_MS = 24 * 60 * 60 * 1_000;
10
34
  const VALIDATION_TTL_MS = 7 * DAY_MS;
11
- const AFFINITY_TTL_MS = 5 * 60_000;
12
35
  const HEALTH_WINDOW_MS = 30 * 60_000;
13
36
  const CIRCUIT_COOLDOWN_MS = 60_000;
14
- const FEEDBACK_WINDOW_MS = 30 * DAY_MS;
15
- const FEEDBACK_HALF_LIFE_MS = 30 * DAY_MS;
37
+ exports.ROUTE_POLICY_VERSION = 'newmark-auto-v2';
16
38
  function finite(value) {
17
39
  const number = Number(value);
18
40
  return Number.isFinite(number) ? number : undefined;
@@ -23,32 +45,17 @@ function clamp(value, min = 0, max = 1) {
23
45
  function deploymentKey(deployment) {
24
46
  return `${deployment.providerId}\u0000${deployment.modelId}\u0000${deployment.logicalModelGroupId || ''}`;
25
47
  }
26
- function sameDeployment(left, right) {
27
- return left.providerId === right.providerId && left.modelId === right.modelId;
28
- }
29
- function inSubset(deployment, subset) {
30
- if (!subset?.length)
31
- return true;
32
- return subset.some(item => sameDeployment(item, deployment));
33
- }
34
- function inScope(deployment, scope) {
35
- return scope.kind === 'global' || deployment.providerId === scope.providerId;
36
- }
37
48
  function normalizeAutoPreference(value) {
38
- switch (String(value || '').toLowerCase()) {
39
- case 'performance':
40
- case 'quality': return 'quality';
41
- case 'cheap_save':
42
- case 'cost': return 'cost';
43
- case 'speed': return 'speed';
44
- default: return 'balanced';
45
- }
49
+ const normalized = String(value || '').trim().toLowerCase();
50
+ if (normalized === 'conservative' || normalized === 'balanced' || normalized === 'aggressive')
51
+ return normalized;
52
+ // Previous values selected fixed quality/cost/speed objectives. They do not
53
+ // have a reliable one-to-one mapping to a model decision tendency.
54
+ return 'balanced';
46
55
  }
47
56
  function defaultRoutePolicy(mode = 'balanced') {
48
- const maxQualityLoss = mode === 'quality' ? 0 : mode === 'balanced' ? 0.02 : mode === 'cost' ? 0.06 : 0.04;
49
57
  return {
50
58
  mode,
51
- maxQualityLoss,
52
59
  allowPreview: false,
53
60
  privacy: 'default',
54
61
  requiredCapabilities: [],
@@ -134,132 +141,76 @@ function classifyRouteFailure(error) {
134
141
  }
135
142
  return { type: 'execution_error', retryable: false, switchAllowed: false, statusCode };
136
143
  }
137
- class AutoRouter {
144
+ function createRouteId() {
145
+ return (0, crypto_1.randomUUID)();
146
+ }
147
+ function catalogSnapshotHash(candidates) {
148
+ const snapshot = candidates.map(candidate => ({
149
+ deployment: candidate.deployment,
150
+ enabled: candidate.enabled,
151
+ unavailableReason: candidate.unavailableReason,
152
+ validation: candidate.validation,
153
+ capabilities: [...candidate.capabilities].sort(),
154
+ maxContextTokens: candidate.maxContextTokens,
155
+ preview: candidate.preview,
156
+ privacy: [...candidate.privacy].sort(),
157
+ dataRegions: [...(candidate.dataRegions || [])].sort(),
158
+ supportedProtocolParameters: [...(candidate.supportedProtocolParameters || [])].sort(),
159
+ expectedInputCostUsdPerM: candidate.expectedInputCostUsdPerM,
160
+ expectedOutputCostUsdPerM: candidate.expectedOutputCostUsdPerM,
161
+ latencyMs: candidate.latencyMs,
162
+ reliability: candidate.reliability,
163
+ toolValidity: candidate.toolValidity,
164
+ throughput: candidate.throughput,
165
+ circuit: candidate.circuit,
166
+ preference: candidate.preference,
167
+ fallbackOnly: !!candidate.fallbackOnly,
168
+ })).sort((left, right) => deploymentKey(left.deployment).localeCompare(deploymentKey(right.deployment)));
169
+ return (0, crypto_1.createHash)('sha256').update(JSON.stringify(snapshot)).digest('hex');
170
+ }
171
+ /**
172
+ * Operational execution controller.
173
+ *
174
+ * It owns endpoint health, the circuit breaker, build-scoped transaction pins
175
+ * and the provider-local recovery ladder. It never ranks models and never
176
+ * learns a preference; health observations only decide whether an endpoint is
177
+ * attempted right now.
178
+ */
179
+ class RouteExecutionController {
138
180
  now;
139
181
  policyVersion;
140
- affinityTtlMs;
141
- switchThreshold;
142
182
  transactionPins = new Map();
143
- affinities = new Map();
144
183
  endpointHealth = new Map();
145
- feedback = new Map();
146
184
  constructor(options = {}) {
147
185
  this.now = options.now || Date.now;
148
- this.policyVersion = options.policyVersion || 'newmark-auto-v1';
149
- this.affinityTtlMs = Math.max(0, options.affinityTtlMs ?? AFFINITY_TTL_MS);
150
- this.switchThreshold = Math.max(0, options.switchThreshold ?? 0.15);
186
+ this.policyVersion = options.policyVersion || exports.ROUTE_POLICY_VERSION;
151
187
  }
152
- route(selection, policy, candidates, request) {
153
- const now = this.now();
154
- const taskClasses = classifyTaskClasses(request.taskText, [...policy.requiredCapabilities, ...request.requiredCapabilities], request.estimatedInputTokens);
155
- const decision = {
156
- routeId: (0, crypto_1.randomUUID)(),
157
- requestedSelection: cloneSelection(selection),
158
- policyVersion: this.policyVersion,
159
- catalogSnapshotHash: catalogHash(candidates),
160
- taskClasses,
161
- excludedCandidates: [],
162
- rankedCandidates: [],
163
- attempts: [],
164
- finalStatus: 'no_candidate',
165
- retryBudgetMs: request.batch ? 15_000 : 5_000,
166
- };
167
- if (selection.kind === 'fixed') {
168
- const fixed = candidates.find(item => sameDeployment(item.deployment, selection.deployment));
169
- if (!fixed || fixed.enabled === false) {
170
- decision.finalStatus = 'fixed_unavailable';
171
- return decision;
172
- }
173
- const ranked = this.rankCandidate(fixed, taskClasses, policy, request, false, now);
174
- decision.rankedCandidates = [ranked];
175
- decision.resolvedDeployment = { ...fixed.deployment };
176
- decision.attempts = [{ deployment: { ...fixed.deployment }, kind: 'initial', status: 'planned' }];
177
- decision.finalStatus = 'resolved';
178
- this.claimEndpointAttempt(fixed.deployment);
179
- return decision;
180
- }
181
- const eligible = [];
182
- for (const candidate of candidates) {
183
- const reasons = [];
184
- if (!candidate.enabled)
185
- reasons.push('disabled');
186
- if (!inScope(candidate.deployment, selection.scope))
187
- reasons.push('outside_scope');
188
- if (!inSubset(candidate.deployment, selection.subset))
189
- reasons.push('outside_subset');
190
- if (candidate.fallbackOnly)
191
- reasons.push('fallback_only');
192
- if (candidate.preview && !policy.allowPreview)
193
- reasons.push('preview_disallowed');
194
- if (request.estimatedInputTokens + request.expectedOutputTokens > Math.max(0, candidate.maxContextTokens || 0))
195
- reasons.push('context_too_small');
196
- if (policy.privacy !== 'default' && !candidate.privacy.includes(policy.privacy))
197
- reasons.push(`privacy:${policy.privacy}`);
198
- if (policy.dataRegion) {
199
- const requiredRegion = policy.dataRegion.toLowerCase();
200
- const regions = new Set((candidate.dataRegions || []).map(item => String(item).toLowerCase()));
201
- if (!regions.has(requiredRegion))
202
- reasons.push(`data_region:${policy.dataRegion}`);
203
- }
204
- const supportedParameters = new Set((candidate.supportedProtocolParameters || []).map(item => String(item).toLowerCase()));
205
- for (const parameter of policy.requiredProtocolParameters || []) {
206
- const requiredParameter = String(parameter).toLowerCase();
207
- if (requiredParameter && !supportedParameters.has(requiredParameter))
208
- reasons.push(`protocol_parameter:${requiredParameter}`);
209
- }
210
- const expectedCost = expectedRequestCost(candidate, request);
211
- if (policy.maxExpectedCostUsd !== undefined && (expectedCost === undefined || expectedCost > policy.maxExpectedCostUsd)) {
212
- reasons.push(expectedCost === undefined ? 'unknown_cost' : 'budget_exceeded');
213
- }
214
- if (reasons.length)
215
- decision.excludedCandidates.push({ deployment: { ...candidate.deployment }, reasons: [...new Set(reasons)] });
216
- else
217
- eligible.push(candidate);
218
- }
219
- if (!eligible.length)
220
- return decision;
221
- const affinityKey = this.affinityKey(request.affinityKey, selection.scope, policy.mode);
222
- let affinity = this.affinities.get(affinityKey);
223
- if (affinity && affinity.expiresAt <= now) {
224
- this.affinities.delete(affinityKey);
225
- affinity = undefined;
226
- }
227
- const affinityDeployment = affinity?.deployment;
228
- const transactionPin = this.transactionPins.get(request.transactionId);
229
- if (transactionPin) {
230
- const pinned = eligible.find(item => sameDeployment(item.deployment, transactionPin));
231
- if (pinned) {
232
- decision.rankedCandidates = this.rankEligible(eligible, taskClasses, policy, request, now, affinityDeployment);
233
- decision.resolvedDeployment = { ...pinned.deployment };
234
- decision.pinReason = 'transaction';
235
- decision.attempts = [{ deployment: { ...pinned.deployment }, kind: 'initial', status: 'planned' }];
236
- decision.finalStatus = 'resolved';
237
- this.claimEndpointAttempt(pinned.deployment);
238
- return decision;
239
- }
240
- this.transactionPins.delete(request.transactionId);
241
- }
242
- const ranked = this.rankEligible(eligible, taskClasses, policy, request, now, affinityDeployment);
243
- decision.rankedCandidates = ranked;
244
- let selected = ranked[0];
245
- if (affinity) {
246
- const incumbent = ranked.find(item => sameDeployment(item.deployment, affinity.deployment));
247
- if (incumbent && selected.utility - incumbent.utility < this.switchThreshold) {
248
- selected = incumbent;
249
- decision.pinReason = 'cache_affinity';
250
- }
251
- }
252
- decision.resolvedDeployment = { ...selected.deployment };
253
- decision.attempts = [{ deployment: { ...selected.deployment }, kind: 'initial', status: 'planned' }];
254
- decision.finalStatus = 'resolved';
255
- this.claimEndpointAttempt(selected.deployment);
256
- this.transactionPins.set(request.transactionId, { ...selected.deployment });
257
- this.affinities.set(affinityKey, { deployment: { ...selected.deployment }, expiresAt: now + this.affinityTtlMs });
258
- return decision;
188
+ version() {
189
+ return this.policyVersion;
190
+ }
191
+ /** Execution-consistency pin for one Build / route transaction. */
192
+ pinTransaction(transactionId, deployment) {
193
+ if (!transactionId || !deployment.providerId || !deployment.modelId)
194
+ return;
195
+ this.transactionPins.set(transactionId, { ...deployment });
196
+ }
197
+ transactionPin(transactionId) {
198
+ const pinned = this.transactionPins.get(transactionId);
199
+ return pinned ? { ...pinned } : undefined;
259
200
  }
260
201
  endTransaction(transactionId) {
261
202
  this.transactionPins.delete(transactionId);
262
203
  }
204
+ /**
205
+ * Provider-local recovery ladder.
206
+ *
207
+ * Order of preference: retry the same deployment (when the failure is
208
+ * retryable inside the retry budget), then an equivalent deployment of the
209
+ * same logical model group, then the alternates the decision engine already
210
+ * validated (in the order it produced them), then explicit fallback-only
211
+ * models. Recovery never crosses the failed provider boundary and never
212
+ * re-runs model selection.
213
+ */
263
214
  planAttempts(decision, candidates, failure) {
264
215
  const current = decision.resolvedDeployment;
265
216
  if (!current || !failure.error.switchAllowed || failure.streamCommitted || failure.sideEffectCommitted)
@@ -270,7 +221,7 @@ class AutoRouter {
270
221
  const attempts = [];
271
222
  const retryDelayMs = failure.error.retryAfterMs ?? 250;
272
223
  const alreadyRetriedCurrent = decision.attempts.some(attempt => attempt.kind === 'retry_same_deployment'
273
- && sameDeployment(attempt.deployment, current));
224
+ && (0, routeEligibility_1.sameDeploymentRef)(attempt.deployment, current));
274
225
  if (failure.error.retryable && !alreadyRetriedCurrent && retryDelayMs <= (decision.retryBudgetMs ?? 5_000)) {
275
226
  attempts.push({
276
227
  deployment: { ...current },
@@ -287,11 +238,9 @@ class AutoRouter {
287
238
  const selection = decision.requestedSelection;
288
239
  if (selection.kind === 'fixed')
289
240
  return attempts.slice(0, remainingAttempts);
290
- const scope = selection.kind === 'auto' ? selection.scope : { kind: 'provider', providerId: current.providerId };
291
241
  const subset = selection.kind === 'auto' ? selection.subset : undefined;
292
242
  const currentGroup = current.logicalModelGroupId;
293
243
  const currentProviderId = current.providerId;
294
- const now = this.now();
295
244
  const attemptedDeployments = decision.attempts.map(attempt => attempt.deployment);
296
245
  // Fallback is a recovery operation, not a new global routing decision.
297
246
  // It must never cross the provider boundary, even when the original Auto
@@ -299,26 +248,33 @@ class AutoRouter {
299
248
  // switching credentials/endpoints (for example provider A/model X to
300
249
  // provider B/model X).
301
250
  const eligible = candidates.filter(candidate => candidate.enabled
251
+ && !candidate.unavailableReason
302
252
  && candidate.deployment.providerId === currentProviderId
303
- && inScope(candidate.deployment, scope)
304
- && inSubset(candidate.deployment, subset)
305
- && !sameDeployment(candidate.deployment, current)
306
- && !attemptedDeployments.some(attempted => sameDeployment(candidate.deployment, attempted))
253
+ && (0, routeEligibility_1.inAutoScope)(candidate.deployment, { kind: 'provider', providerId: currentProviderId })
254
+ && (0, routeEligibility_1.inAutoSubset)(candidate.deployment, subset)
255
+ && !(0, routeEligibility_1.sameDeploymentRef)(candidate.deployment, current)
256
+ && !attemptedDeployments.some(attempted => (0, routeEligibility_1.sameDeploymentRef)(candidate.deployment, attempted))
307
257
  && this.passedInitialHardFilters(decision, candidate));
308
258
  const equivalent = currentGroup
309
259
  ? eligible.find(candidate => candidate.deployment.logicalModelGroupId === currentGroup && !candidate.fallbackOnly)
310
260
  : undefined;
311
261
  const fallback = eligible.find(candidate => candidate.fallbackOnly);
312
- const rankedAlternates = decision.rankedCandidates
313
- .map(ranked => eligible.find(candidate => sameDeployment(candidate.deployment, ranked.deployment)))
314
- .filter((candidate) => !!candidate && candidate !== equivalent && candidate !== fallback && !candidate.fallbackOnly);
262
+ const orderedAlternates = [];
263
+ for (const alternate of decision.alternatives) {
264
+ const match = eligible.find(candidate => (0, routeEligibility_1.sameDeploymentRef)(candidate.deployment, alternate));
265
+ if (!match || match === equivalent || match === fallback || match.fallbackOnly)
266
+ continue;
267
+ if (orderedAlternates.some(existing => (0, routeEligibility_1.sameDeploymentRef)(existing.deployment, match.deployment)))
268
+ continue;
269
+ orderedAlternates.push(match);
270
+ }
315
271
  for (const candidate of eligible) {
316
272
  if (candidate === equivalent || candidate === fallback || candidate.fallbackOnly
317
- || rankedAlternates.some(existing => sameDeployment(existing.deployment, candidate.deployment)))
273
+ || orderedAlternates.some(existing => (0, routeEligibility_1.sameDeploymentRef)(existing.deployment, candidate.deployment)))
318
274
  continue;
319
- rankedAlternates.push(candidate);
275
+ orderedAlternates.push(candidate);
320
276
  }
321
- for (const next of [equivalent, fallback, ...rankedAlternates]) {
277
+ for (const next of [equivalent, ...orderedAlternates, fallback]) {
322
278
  if (!next || attempts.length >= 2)
323
279
  continue;
324
280
  attempts.push({
@@ -396,96 +352,6 @@ class AutoRouter {
396
352
  circuit: this.circuitState(deployment, now, false),
397
353
  };
398
354
  }
399
- recordFeedback(event) {
400
- const at = event.at ?? this.now();
401
- const safe = {
402
- deployment: { ...event.deployment },
403
- taskClass: event.taskClass,
404
- score: clamp(Number(event.score) || 0, -1, 1),
405
- source: event.source,
406
- at,
407
- };
408
- const key = feedbackKey(safe.deployment, safe.taskClass);
409
- const events = (this.feedback.get(key) || []).filter(item => at - Number(item.at || 0) <= FEEDBACK_WINDOW_MS);
410
- events.push(safe);
411
- this.feedback.set(key, events);
412
- return events.length >= 3;
413
- }
414
- clearLearnedPreferences() {
415
- this.feedback.clear();
416
- }
417
- learnedPreference(deployment, taskClasses) {
418
- const now = this.now();
419
- const values = [];
420
- for (const taskClass of taskClasses) {
421
- const events = (this.feedback.get(feedbackKey(deployment, taskClass)) || [])
422
- .filter(event => now - Number(event.at || 0) <= FEEDBACK_WINDOW_MS)
423
- .sort((left, right) => Number(left.at || 0) - Number(right.at || 0));
424
- if (events.length < 3)
425
- continue;
426
- let ewma = events[0].score;
427
- let previousAt = Number(events[0].at || now);
428
- for (const event of events.slice(1)) {
429
- const eventAt = Number(event.at || previousAt);
430
- const betweenDecay = Math.pow(0.5, Math.max(0, eventAt - previousAt) / FEEDBACK_HALF_LIFE_MS);
431
- ewma = 0.2 * event.score + 0.8 * ewma * betweenDecay;
432
- previousAt = eventAt;
433
- }
434
- const newestAt = Number(events[events.length - 1].at || now);
435
- const decay = Math.pow(0.5, Math.max(0, now - newestAt) / FEEDBACK_HALF_LIFE_MS);
436
- values.push(clamp(ewma * decay, -1, 1));
437
- }
438
- return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
439
- }
440
- rankEligible(candidates, taskClasses, policy, request, now, affinityDeployment) {
441
- const qualities = candidates.map(candidate => ({ candidate, quality: qualityScore(candidate, taskClasses) }));
442
- const bestQuality = Math.max(...qualities.map(item => item.quality));
443
- const band = qualities.filter(item => bestQuality - item.quality <= policy.maxQualityLoss + Number.EPSILON);
444
- return band.map(item => this.rankCandidate(item.candidate, taskClasses, policy, request, !!affinityDeployment && sameDeployment(item.candidate.deployment, affinityDeployment), now, item.quality))
445
- .sort((left, right) => right.utility - left.utility || right.quality - left.quality || deploymentKey(left.deployment).localeCompare(deploymentKey(right.deployment)));
446
- }
447
- rankCandidate(candidate, taskClasses, policy, request, cache, _now, quality = qualityScore(candidate, taskClasses)) {
448
- const expectedCostUsd = expectedRequestCost(candidate, request);
449
- const cost = expectedCostUsd === undefined ? 0 : 1 / (1 + Math.max(0, expectedCostUsd) * 1_000);
450
- const endpointReliability = candidate.reliability === undefined ? 0.5 : clamp(candidate.reliability);
451
- const toolRelevant = taskClasses.some(taskClass => taskClass === 'tool_use' || taskClass === 'computer_use');
452
- const reliability = toolRelevant && candidate.toolValidity !== undefined
453
- ? (endpointReliability + clamp(candidate.toolValidity)) / 2
454
- : endpointReliability;
455
- const latency = candidate.latencyMs === undefined ? 0.5 : 1 / (1 + Math.max(0, candidate.latencyMs) / 1_000);
456
- const throughput = candidate.throughput === undefined ? 0.5 : Math.max(0, candidate.throughput) / (Math.max(0, candidate.throughput) + 40);
457
- const speed = (latency + throughput) / 2;
458
- const learned = this.learnedPreference(candidate.deployment, taskClasses);
459
- const configuredPreference = clamp(Number(candidate.preference) || 0, -1, 1);
460
- const effectivePreference = configuredPreference === 0 ? learned : configuredPreference;
461
- const preference = clamp((effectivePreference + 1) / 2);
462
- const components = { cost, reliability, speed, cache: cache ? 1 : 0, preference };
463
- let utility;
464
- switch (policy.mode) {
465
- case 'quality':
466
- utility = quality * 0.55 + reliability * 0.25 + speed * 0.15 + cost * 0.05;
467
- break;
468
- case 'cost':
469
- utility = cost * 0.65 + reliability * 0.15 + speed * 0.10 + components.cache * 0.05 + preference * 0.05;
470
- break;
471
- case 'speed':
472
- utility = speed * 0.60 + reliability * 0.20 + cost * 0.10 + components.cache * 0.05 + preference * 0.05;
473
- break;
474
- default:
475
- utility = cost * 0.40 + reliability * 0.25 + speed * 0.20 + components.cache * 0.10 + preference * 0.05;
476
- break;
477
- }
478
- return {
479
- deployment: { ...candidate.deployment },
480
- quality,
481
- utility: clamp(utility),
482
- expectedCostUsd,
483
- components,
484
- };
485
- }
486
- affinityKey(key, scope, mode) {
487
- return `${key}\u0000${scope.kind === 'global' ? 'global' : `provider:${scope.providerId}`}\u0000${mode}`;
488
- }
489
355
  healthFor(deployment, now) {
490
356
  const key = deploymentKey(deployment);
491
357
  let health = this.endpointHealth.get(key);
@@ -497,12 +363,10 @@ class AutoRouter {
497
363
  return health;
498
364
  }
499
365
  passedInitialHardFilters(decision, candidate) {
500
- const exclusion = decision.excludedCandidates.find(entry => sameDeployment(entry.deployment, candidate.deployment));
366
+ const exclusion = decision.excludedCandidates.find(entry => (0, routeEligibility_1.sameDeploymentRef)(entry.deployment, candidate.deployment));
501
367
  if (!exclusion)
502
368
  return true;
503
- return !!candidate.fallbackOnly
504
- && exclusion.reasons.length > 0
505
- && exclusion.reasons.every(reason => reason === 'fallback_only');
369
+ return !!candidate.fallbackOnly && (0, routeEligibility_1.excludedOnlyForFallbackPool)(exclusion);
506
370
  }
507
371
  circuitState(deployment, now, claimHalfOpen = true) {
508
372
  const health = this.healthFor(deployment, now);
@@ -519,61 +383,12 @@ class AutoRouter {
519
383
  return 'closed';
520
384
  }
521
385
  }
522
- exports.AutoRouter = AutoRouter;
523
- function cloneSelection(selection) {
524
- if (selection.kind === 'fixed')
525
- return { kind: 'fixed', deployment: { ...selection.deployment } };
526
- return {
527
- kind: 'auto',
528
- scope: selection.scope.kind === 'global' ? { kind: 'global' } : { kind: 'provider', providerId: selection.scope.providerId },
529
- policyId: selection.policyId,
530
- subset: selection.subset?.map(item => ({ ...item })),
531
- };
532
- }
533
- function qualityScore(candidate, taskClasses) {
534
- const scores = taskClasses.map(taskClass => {
535
- const metric = candidate.qualityByTask?.[taskClass];
536
- if (!metric)
537
- return 0.5;
538
- const attempts = Math.max(0, Math.floor(Number(metric.attempts) || 0));
539
- const successes = clamp(Number(metric.successes) || 0, 0, attempts);
540
- return (successes + 2) / (attempts + 4);
541
- });
542
- return scores.reduce((sum, value) => sum + value, 0) / Math.max(1, scores.length);
543
- }
544
- function expectedRequestCost(candidate, request) {
545
- const input = finite(candidate.expectedInputCostUsdPerM);
546
- const output = finite(candidate.expectedOutputCostUsdPerM);
547
- if (input === undefined || output === undefined)
548
- return undefined;
549
- return input * Math.max(0, request.estimatedInputTokens) / 1_000_000
550
- + output * Math.max(0, request.expectedOutputTokens) / 1_000_000;
551
- }
552
- function catalogHash(candidates) {
553
- const snapshot = candidates.map(candidate => ({
554
- deployment: candidate.deployment,
555
- enabled: candidate.enabled,
556
- validation: candidate.validation,
557
- capabilities: [...candidate.capabilities].sort(),
558
- maxContextTokens: candidate.maxContextTokens,
559
- preview: candidate.preview,
560
- privacy: [...candidate.privacy].sort(),
561
- dataRegions: [...(candidate.dataRegions || [])].sort(),
562
- supportedProtocolParameters: [...(candidate.supportedProtocolParameters || [])].sort(),
563
- expectedInputCostUsdPerM: candidate.expectedInputCostUsdPerM,
564
- expectedOutputCostUsdPerM: candidate.expectedOutputCostUsdPerM,
565
- latencyMs: candidate.latencyMs,
566
- reliability: candidate.reliability,
567
- toolValidity: candidate.toolValidity,
568
- throughput: candidate.throughput,
569
- qualityByTask: candidate.qualityByTask,
570
- preference: candidate.preference,
571
- fallbackOnly: !!candidate.fallbackOnly,
572
- })).sort((left, right) => deploymentKey(left.deployment).localeCompare(deploymentKey(right.deployment)));
573
- return (0, crypto_1.createHash)('sha256').update(JSON.stringify(snapshot)).digest('hex');
574
- }
575
- function feedbackKey(deployment, taskClass) {
576
- return `${deploymentKey(deployment)}\u0000${taskClass}`;
386
+ exports.RouteExecutionController = RouteExecutionController;
387
+ function validationEvidenceIsStale(checkedAt, now = Date.now()) {
388
+ const checked = Date.parse(String(checkedAt || ''));
389
+ if (!Number.isFinite(checked))
390
+ return true;
391
+ return now - checked > VALIDATION_TTL_MS;
577
392
  }
578
393
  function trimHealth(health, now) {
579
394
  health.events = health.events.filter(event => now - event.at <= HEALTH_WINDOW_MS).slice(-100);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Branch conversation identity (dev-0.6.4).
3
+ *
4
+ * A conversation owns a tree of branch nodes. Every node is its own full
5
+ * conversation: the provider-side session sequence id, the folded-history
6
+ * archive and every local transcript cache are scoped by the node id, so
7
+ * sibling pages can never read each other's remote session or cached history.
8
+ *
9
+ * The user's pagination edits and the experimental branch-communication
10
+ * branches are created by the same `Agent.branchConversation` transaction, so
11
+ * both kinds of branch must share this single identity function. Never derive
12
+ * the identity from "the branch that happens to be active" at request time.
13
+ */
14
+ export declare const BRANCH_IDENTITY_SEPARATOR = "::branch:";
15
+ /** The full conversation id of one branch node: `<conversationId>::branch:<branchNodeId>`. */
16
+ export declare function branchConversationIdentity(conversationId: string, branchNodeId: string): string;
17
+ /** True when a value already carries a branch node identity. */
18
+ export declare function isBranchConversationIdentity(value: string): boolean;
19
+ /**
20
+ * Workspace-wide cache scope for one branch. Used by caches that must never be
21
+ * shared across sibling branches (compression history, provider payloads).
22
+ */
23
+ export declare function branchCacheScopeKey(workspaceId: string, conversationId: string, branchNodeId: string): string;
24
+ /**
25
+ * A branch may never reuse the identity of another node. Callers that mint a
26
+ * new page assert this before the node becomes reachable.
27
+ */
28
+ export declare function assertFreshBranchIdentity(conversationId: string, branchNodeId: string, existing: Iterable<string>): void;
29
+ //# sourceMappingURL=branchIdentity.d.ts.map
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * Branch conversation identity (dev-0.6.4).
4
+ *
5
+ * A conversation owns a tree of branch nodes. Every node is its own full
6
+ * conversation: the provider-side session sequence id, the folded-history
7
+ * archive and every local transcript cache are scoped by the node id, so
8
+ * sibling pages can never read each other's remote session or cached history.
9
+ *
10
+ * The user's pagination edits and the experimental branch-communication
11
+ * branches are created by the same `Agent.branchConversation` transaction, so
12
+ * both kinds of branch must share this single identity function. Never derive
13
+ * the identity from "the branch that happens to be active" at request time.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.BRANCH_IDENTITY_SEPARATOR = void 0;
17
+ exports.branchConversationIdentity = branchConversationIdentity;
18
+ exports.isBranchConversationIdentity = isBranchConversationIdentity;
19
+ exports.branchCacheScopeKey = branchCacheScopeKey;
20
+ exports.assertFreshBranchIdentity = assertFreshBranchIdentity;
21
+ exports.BRANCH_IDENTITY_SEPARATOR = '::branch:';
22
+ /** The full conversation id of one branch node: `<conversationId>::branch:<branchNodeId>`. */
23
+ function branchConversationIdentity(conversationId, branchNodeId) {
24
+ const conversation = String(conversationId || '').trim() || 'default';
25
+ const branch = String(branchNodeId || '').trim();
26
+ return branch ? `${conversation}${exports.BRANCH_IDENTITY_SEPARATOR}${branch}` : conversation;
27
+ }
28
+ /** True when a value already carries a branch node identity. */
29
+ function isBranchConversationIdentity(value) {
30
+ return String(value || '').includes(exports.BRANCH_IDENTITY_SEPARATOR);
31
+ }
32
+ /**
33
+ * Workspace-wide cache scope for one branch. Used by caches that must never be
34
+ * shared across sibling branches (compression history, provider payloads).
35
+ */
36
+ function branchCacheScopeKey(workspaceId, conversationId, branchNodeId) {
37
+ const workspace = String(workspaceId || '').trim() || 'none';
38
+ return `${workspace}::${branchConversationIdentity(conversationId, branchNodeId)}`;
39
+ }
40
+ /**
41
+ * A branch may never reuse the identity of another node. Callers that mint a
42
+ * new page assert this before the node becomes reachable.
43
+ */
44
+ function assertFreshBranchIdentity(conversationId, branchNodeId, existing) {
45
+ const branch = String(branchNodeId || '').trim();
46
+ if (!branch)
47
+ throw new Error('Branch conversation identity requires a branch node id.');
48
+ for (const known of existing) {
49
+ if (String(known || '') === branch) {
50
+ throw new Error(`Branch conversation identity ${branchConversationIdentity(conversationId, branch)} is already in use.`);
51
+ }
52
+ }
53
+ }
54
+ //# sourceMappingURL=branchIdentity.js.map