@harness-engineering/orchestrator 0.11.2 → 0.13.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/index.d.mts +1140 -268
- package/dist/index.d.ts +1140 -268
- package/dist/index.js +2635 -487
- package/dist/index.mjs +2566 -420
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as _harness_engineering_types from '@harness-engineering/types';
|
|
2
|
-
import { Issue, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, IssueRoutingDecision, Result, WorkflowDefinition, BackendDef, RoutingConfig,
|
|
3
|
-
import { IssueTrackerClient, Issue as Issue$1, TrackerConfig, eventSourcing,
|
|
2
|
+
import { Issue, WorkflowExecutionPlan, StageRun, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, IssueRoutingDecision, Result, WorkflowDefinition, BackendDef, RoutingConfig, RoutingDecision, RoutingUseCase, ContainerConfig, SecretConfig, AgentBackend, RoutingRequest, CapabilityTier, WorkspaceConfig, HooksConfig, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, CheckScriptDefinition, OutputRetentionConfig, BackendCapabilityRegistry, RoutingPolicy, ComplexityVerdict, RoutingTelemetry, RoutingStatus, RoutingError, PrivacyClass, BackendCapabilities, AgentConfig, LocalModelStatus, CustomTaskDefinition, MaintenanceConfig, TokenScope, AuthToken, AuthTokenPublic, IndexedFileKind, SessionSearchResult, ReindexStats, SessionSummarizationConfig, SessionSummary, SessionSummaryMeta, SessionsConfig, GatewayEvent, NotificationEnvelope, NotificationDeliveryResult, NotificationSinkConfig, NotificationsConfig } from '@harness-engineering/types';
|
|
3
|
+
import { IssueTrackerClient, Issue as Issue$1, CacheMetricsRecorder, TrackerConfig, eventSourcing, ArchiveHooks, SkillProposal, ProposalGateFinding, SkillKind } from '@harness-engineering/core';
|
|
4
4
|
import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation, AnalysisProvider } from '@harness-engineering/intelligence';
|
|
5
5
|
import { GraphStore } from '@harness-engineering/graph';
|
|
6
6
|
import { execFile } from 'node:child_process';
|
|
7
7
|
import { EventEmitter } from 'node:events';
|
|
8
|
-
import { PoolStateProvider, SchedulerTimerHandle } from '@harness-engineering/local-models';
|
|
8
|
+
import { PoolStateProvider, SchedulerTimerHandle, DiscoverCandidatesOptions, DiscoverCandidatesResult } from '@harness-engineering/local-models';
|
|
9
|
+
export { discoverCandidates } from '@harness-engineering/local-models';
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -44,6 +45,22 @@ interface RunningEntry {
|
|
|
44
45
|
startedAt: string;
|
|
45
46
|
phase: RunAttemptPhase;
|
|
46
47
|
session: LiveSession | null;
|
|
48
|
+
/**
|
|
49
|
+
* AMR Phase 4 (D10/SC16): the tier the AdaptiveRouter resolved this dispatch
|
|
50
|
+
* at (`decision.tierRequired`), captured so a later quality outcome can feed
|
|
51
|
+
* `AdaptiveRouter.recordOutcome(issue.id, lastRoutedTier, ok)`. Absent when
|
|
52
|
+
* AMR is off (no policy) or the dispatch used an override.
|
|
53
|
+
*/
|
|
54
|
+
lastRoutedTier?: _harness_engineering_types.CapabilityTier;
|
|
55
|
+
/** split-routing: the plan this unit is running staged, if any. Engine-owned. */
|
|
56
|
+
workflow?: WorkflowExecutionPlan;
|
|
57
|
+
/** split-routing: the stage index currently executing. Engine-owned. */
|
|
58
|
+
currentStageIndex?: number;
|
|
59
|
+
/**
|
|
60
|
+
* split-routing: per-stage runs. The per-stage session/abort live HERE
|
|
61
|
+
* (C1), never in the issue-level `session` field above.
|
|
62
|
+
*/
|
|
63
|
+
stageRuns?: StageRun[];
|
|
47
64
|
}
|
|
48
65
|
/**
|
|
49
66
|
* Entry in the retry queue.
|
|
@@ -1000,107 +1017,545 @@ declare function discoverSkillCatalog(projectRoot: string): SkillCatalogEntry[];
|
|
|
1000
1017
|
declare function discoverSkillCatalogNames(projectRoot: string): string[];
|
|
1001
1018
|
|
|
1002
1019
|
/**
|
|
1003
|
-
*
|
|
1020
|
+
* split-routing D5/D12/D13 — the result of the doubly-opt-in match.
|
|
1004
1021
|
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1022
|
+
* `workflowFor` is the SINGLE matching authority: it returns BOTH the execution
|
|
1023
|
+
* `plan` AND the matched decl's optional `stageDeadlineMs` (D12) so `dispatchIssue`
|
|
1024
|
+
* never re-runs the prefix+labels+`>= 2` predicate to recover the deadline. One
|
|
1025
|
+
* match, one source of truth — no drift between the predicate and a second matcher.
|
|
1008
1026
|
*/
|
|
1009
|
-
|
|
1010
|
-
|
|
1027
|
+
interface WorkflowMatch {
|
|
1028
|
+
/** The execution plan for the matched >= 2-stage decl. */
|
|
1029
|
+
plan: WorkflowExecutionPlan;
|
|
1030
|
+
/** The matched decl's per-stage deadline override (D12), if declared. */
|
|
1031
|
+
stageDeadlineMs?: number;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* split-routing D5/D13 — the doubly-opt-in gate as a PURE matcher.
|
|
1035
|
+
*
|
|
1036
|
+
* Selects a `WorkflowMatch` for `issue` iff `config.workflows` declares a
|
|
1037
|
+
* matching decl with `stages.length >= 2`. Match grain (v1): an
|
|
1038
|
+
* `identifierPrefix` and/or `labels`; both, when declared, must hold. The FIRST
|
|
1039
|
+
* matching decl wins. The returned `WorkflowMatch` carries the matched decl's
|
|
1040
|
+
* `stageDeadlineMs` (D12) so the caller reads it from HERE rather than
|
|
1041
|
+
* re-matching — this function is the single match authority.
|
|
1042
|
+
*
|
|
1043
|
+
* D13: a matching 1-stage decl returns `undefined` — the unit takes the
|
|
1044
|
+
* single-agent path (a 1-stage "workflow" is not staged). A 0-stage decl never
|
|
1045
|
+
* reaches here (rejected at config validation).
|
|
1046
|
+
*
|
|
1047
|
+
* SC4 — this matcher has ZERO side effects: it never touches the workspace,
|
|
1048
|
+
* claims, routes, logs, or mutates state. When it returns `undefined`
|
|
1049
|
+
* `dispatchIssue` falls through to the byte-identical single-agent path. Keep it
|
|
1050
|
+
* cheap and pure so calling it on every dispatch cannot change non-workflow
|
|
1051
|
+
* behavior.
|
|
1052
|
+
*/
|
|
1053
|
+
declare function workflowFor(issue: Issue, config: WorkflowConfig): WorkflowMatch | undefined;
|
|
1054
|
+
|
|
1055
|
+
interface RoutingDecisionBusFilter {
|
|
1056
|
+
skillName?: string;
|
|
1057
|
+
mode?: string;
|
|
1058
|
+
backendName?: string;
|
|
1059
|
+
limit?: number;
|
|
1060
|
+
}
|
|
1061
|
+
interface RoutingDecisionBusOptions {
|
|
1062
|
+
/** Default 500. Bound on the in-memory ring buffer. */
|
|
1063
|
+
capacity?: number;
|
|
1011
1064
|
/**
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1065
|
+
* Logger for the structured `routing-decision` line (O1) and for
|
|
1066
|
+
* one-off warn() when a subscriber throws (S6). When omitted, the
|
|
1067
|
+
* bus silently swallows subscriber errors (test-mode default).
|
|
1015
1068
|
*/
|
|
1016
|
-
|
|
1069
|
+
logger?: StructuredLogger;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Spec B Phase 4 (D8): in-process bus + ring buffer for
|
|
1073
|
+
* {@link RoutingDecision} events. One emit() per
|
|
1074
|
+
* {@link BackendRouter.resolve} call; subscribers receive the
|
|
1075
|
+
* decision synchronously after the ring buffer is updated.
|
|
1076
|
+
*
|
|
1077
|
+
* Subscriber errors are isolated (caught + logged, never thrown
|
|
1078
|
+
* back to the emitter) so a misbehaving subscriber cannot block a
|
|
1079
|
+
* dispatch. (S6)
|
|
1080
|
+
*
|
|
1081
|
+
* Capacity-bound (default 500) via Array.shift() — acceptable for
|
|
1082
|
+
* v1 (see plan C4); switch to circular indexing if 24h dispatch
|
|
1083
|
+
* volume ever pushes 10K+ records/min.
|
|
1084
|
+
*/
|
|
1085
|
+
declare class RoutingDecisionBus {
|
|
1086
|
+
private readonly ringBuffer;
|
|
1087
|
+
private readonly listeners;
|
|
1088
|
+
private readonly capacity;
|
|
1089
|
+
private readonly logger;
|
|
1090
|
+
constructor(opts?: RoutingDecisionBusOptions);
|
|
1091
|
+
emit(decision: RoutingDecision): void;
|
|
1092
|
+
recent(filter?: RoutingDecisionBusFilter): RoutingDecision[];
|
|
1093
|
+
subscribe(listener: (d: RoutingDecision) => void): () => void;
|
|
1017
1094
|
/**
|
|
1018
|
-
*
|
|
1095
|
+
* Spec B Phase 5 (review-S2 fix): release all subscriber references so
|
|
1096
|
+
* teardown can complete without anchoring closures. Called from
|
|
1097
|
+
* `Orchestrator.stop()` before nulling the bus reference. The bus
|
|
1098
|
+
* remains usable after clear — `subscribe()` works as normal.
|
|
1019
1099
|
*/
|
|
1020
|
-
|
|
1100
|
+
clearListeners(): void;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
interface BackendRouterOptions {
|
|
1104
|
+
backends: Record<string, BackendDef>;
|
|
1105
|
+
routing: RoutingConfig;
|
|
1021
1106
|
/**
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1107
|
+
* Spec B Phase 4 (D8): when present, every resolve() emits its
|
|
1108
|
+
* decision onto the bus. The bus owns the structured log line + ring
|
|
1109
|
+
* buffer; the router stays a pure resolution function.
|
|
1025
1110
|
*/
|
|
1026
|
-
|
|
1111
|
+
decisionBus?: RoutingDecisionBus;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* BackendRouter (Spec B Phase 1)
|
|
1115
|
+
*
|
|
1116
|
+
* Owns the lookup from a {@link RoutingUseCase} (a discriminated query
|
|
1117
|
+
* — tier, intelligence layer, maintenance, chat, isolation, **skill**,
|
|
1118
|
+
* **mode**) to a {@link RoutingDecision} naming a chosen backend and
|
|
1119
|
+
* the full resolution path that produced it.
|
|
1120
|
+
*
|
|
1121
|
+
* Resolution order (D2): invocation override -> per-skill -> per-mode
|
|
1122
|
+
* -> existing per-tier/intelligence/isolation/maintenance/chat ->
|
|
1123
|
+
* `routing.default`. Within each source, fallback chain entries are
|
|
1124
|
+
* tried in declared order; first existing backend wins. Unknown
|
|
1125
|
+
* entries are recorded with `outcome: 'unknown-backend'` and the walk
|
|
1126
|
+
* continues.
|
|
1127
|
+
*
|
|
1128
|
+
* Construction-time validation guarantees every name referenced by
|
|
1129
|
+
* `routing` is present in `backends` so the static-config case can
|
|
1130
|
+
* never produce a runtime exhaustion throw. The runtime throw at the
|
|
1131
|
+
* end of `resolve()` is a safety net for future dynamic-backends
|
|
1132
|
+
* scenarios where a chain entry can become unknown post-construction.
|
|
1133
|
+
*/
|
|
1134
|
+
declare class BackendRouter {
|
|
1135
|
+
private readonly backends;
|
|
1136
|
+
private readonly routing;
|
|
1137
|
+
private readonly decisionBus;
|
|
1138
|
+
constructor(opts: BackendRouterOptions);
|
|
1027
1139
|
/**
|
|
1028
|
-
*
|
|
1029
|
-
* and rewrites the markdown file. Idempotent: if the feature is already
|
|
1030
|
-
* in a terminal state, this is a no-op.
|
|
1140
|
+
* Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
|
|
1031
1141
|
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1142
|
+
* @param useCase the routing query
|
|
1143
|
+
* @param opts.invocationOverride if set and the named backend exists,
|
|
1144
|
+
* beats all other sources (D7 — the `--backend <name>` escape hatch)
|
|
1035
1145
|
*/
|
|
1036
|
-
|
|
1146
|
+
resolve(useCase: RoutingUseCase, opts?: {
|
|
1147
|
+
invocationOverride?: string;
|
|
1148
|
+
}): RoutingDecision;
|
|
1037
1149
|
/**
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1150
|
+
* Returns the {@link BackendDef} reference for the resolved name.
|
|
1151
|
+
* Identity-equal to the entry in `backends` (no copy) so callers
|
|
1152
|
+
* relying on reference equality (SC21) continue to work.
|
|
1041
1153
|
*/
|
|
1042
|
-
|
|
1154
|
+
resolveDefinition(useCase: RoutingUseCase, opts?: {
|
|
1155
|
+
invocationOverride?: string;
|
|
1156
|
+
}): BackendDef;
|
|
1043
1157
|
/**
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1158
|
+
* Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
|
|
1159
|
+
* for callers that need both. Replaces the previous pattern of
|
|
1160
|
+
* `resolveDefinition(useCase) + resolve(useCase)` which produced two
|
|
1161
|
+
* RoutingDecision emissions per dispatch — doubling routing-decision
|
|
1162
|
+
* log volume now that Phase 4 emits.
|
|
1163
|
+
*
|
|
1164
|
+
* Identity-equal `BackendDef` (no copy) so callers relying on
|
|
1165
|
+
* reference equality (SC21) continue to work.
|
|
1046
1166
|
*/
|
|
1047
|
-
|
|
1167
|
+
resolveDecisionAndDef(useCase: RoutingUseCase, opts?: {
|
|
1168
|
+
invocationOverride?: string;
|
|
1169
|
+
}): {
|
|
1170
|
+
decision: RoutingDecision;
|
|
1171
|
+
def: BackendDef;
|
|
1172
|
+
};
|
|
1048
1173
|
/**
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1174
|
+
* The pre-Spec-B resolution helper: returns the configured
|
|
1175
|
+
* {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
|
|
1176
|
+
* use cases (or `undefined` for skill/mode use cases, which are owned
|
|
1177
|
+
* by the per-skill / per-mode steps in {@link resolve}). Returning
|
|
1178
|
+
* `undefined` lets the caller fall through to `routing.default`.
|
|
1053
1179
|
*/
|
|
1054
|
-
private
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1180
|
+
private resolveExistingUseCase;
|
|
1181
|
+
private validateReferences;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Options for `OrchestratorBackendFactory`.
|
|
1186
|
+
*
|
|
1187
|
+
* `sandboxPolicy` and `container`/`secrets` mirror the orchestrator's own
|
|
1188
|
+
* agent-config fields. `getResolverModelFor` is a registration hook the
|
|
1189
|
+
* orchestrator calls to bind each `local`/`pi` backend to its
|
|
1190
|
+
* `LocalModelResolver` (so multi-resolver array-fallback works without
|
|
1191
|
+
* leaking resolver lifetimes into the factory).
|
|
1192
|
+
*/
|
|
1193
|
+
/**
|
|
1194
|
+
* Runtime-feedback callbacks a `local`/`pi` backend fires as turns complete.
|
|
1195
|
+
* See {@link OrchestratorBackendFactoryOptions.getModelUsageHooksFor}.
|
|
1196
|
+
*/
|
|
1197
|
+
interface LocalModelUsageHooks {
|
|
1198
|
+
/** Fired with the resolved model after a successful turn (LRU + breaker clear). */
|
|
1199
|
+
onModelUsed?: (model: string) => void;
|
|
1200
|
+
/** Fired with the resolved model after a failed turn (breaker increment). */
|
|
1201
|
+
onModelFailed?: (model: string) => void;
|
|
1202
|
+
}
|
|
1203
|
+
interface OrchestratorBackendFactoryOptions {
|
|
1204
|
+
backends: Record<string, BackendDef>;
|
|
1205
|
+
routing: RoutingConfig;
|
|
1206
|
+
sandboxPolicy: 'none' | 'docker';
|
|
1207
|
+
container?: ContainerConfig;
|
|
1208
|
+
secrets?: SecretConfig;
|
|
1058
1209
|
/**
|
|
1059
|
-
*
|
|
1210
|
+
* Hook for resolver injection. Invoked per `local`/`pi` backend at
|
|
1211
|
+
* `forUseCase()` time with the backend's name. When the hook returns a
|
|
1212
|
+
* function, the factory rebuilds the local/pi instance using that
|
|
1213
|
+
* function as `getModel` (overriding the head-of-array placeholder
|
|
1214
|
+
* baked into `createBackend`). Returning `undefined` means "no
|
|
1215
|
+
* resolver registered for this name" — the placeholder stays in place.
|
|
1060
1216
|
*
|
|
1061
|
-
*
|
|
1217
|
+
* This indirection keeps the factory ignorant of `LocalModelResolver`'s
|
|
1218
|
+
* existence and lifecycle while still letting it produce backends that
|
|
1219
|
+
* route through the resolver Map.
|
|
1062
1220
|
*/
|
|
1063
|
-
|
|
1221
|
+
getResolverModelFor?: (backendName: string, useCase: RoutingUseCase) => (() => string | null) | undefined;
|
|
1064
1222
|
/**
|
|
1065
|
-
*
|
|
1223
|
+
* Consumption Phase 3 (T11): per-`local`/`pi` backend hook returning the
|
|
1224
|
+
* runtime-feedback callbacks bound to that backend's resolver + pool. The
|
|
1225
|
+
* factory forwards them to the constructed `LocalBackend` so a completed turn
|
|
1226
|
+
* stamps `lastUsedAt` (LRU) + clears the circuit breaker, and a failed turn
|
|
1227
|
+
* feeds the breaker. Returning `undefined` means "no feedback wiring" (the
|
|
1228
|
+
* backend runs exactly as before). Kept parallel to `getResolverModelFor` so
|
|
1229
|
+
* the factory stays ignorant of resolver/pool lifecycles.
|
|
1066
1230
|
*/
|
|
1067
|
-
|
|
1231
|
+
getModelUsageHooksFor?: (backendName: string) => LocalModelUsageHooks | undefined;
|
|
1068
1232
|
/**
|
|
1069
|
-
*
|
|
1233
|
+
* Phase 5: prompt-cache recorder forwarded to Anthropic-capable backends.
|
|
1234
|
+
* Other backends accept-but-ignore. Shared across dispatches so the
|
|
1235
|
+
* `/api/v1/telemetry/cache/stats` endpoint sees the full rolling window.
|
|
1070
1236
|
*/
|
|
1071
|
-
|
|
1237
|
+
cacheMetrics?: CacheMetricsRecorder;
|
|
1238
|
+
/**
|
|
1239
|
+
* Spec B Phase 4 (D8): forwarded to the underlying BackendRouter so
|
|
1240
|
+
* every resolve() during forUseCase / resolveName emits.
|
|
1241
|
+
*/
|
|
1242
|
+
decisionBus?: RoutingDecisionBus;
|
|
1072
1243
|
}
|
|
1073
|
-
|
|
1074
1244
|
/**
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1245
|
+
* High-level factory wrapping `BackendRouter` + `createBackend` plus
|
|
1246
|
+
* orchestrator-side concerns (sandbox wrapping, resolver binding).
|
|
1247
|
+
*
|
|
1248
|
+
* Spec 2 SC22-SC25: every `forUseCase(useCase)` call returns a fresh
|
|
1249
|
+
* `AgentBackend` whose class matches the routed `BackendDef.type`.
|
|
1250
|
+
* `local`/`pi` defs are bound to their per-name resolver before being
|
|
1251
|
+
* returned, and the result is wrapped in `ContainerBackend` when
|
|
1252
|
+
* sandboxPolicy is 'docker'.
|
|
1077
1253
|
*/
|
|
1078
|
-
|
|
1254
|
+
declare class OrchestratorBackendFactory {
|
|
1255
|
+
private readonly router;
|
|
1256
|
+
private readonly opts;
|
|
1257
|
+
constructor(opts: OrchestratorBackendFactoryOptions);
|
|
1079
1258
|
/**
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1259
|
+
* Resolve `useCase` to a backend name, materialize a fresh
|
|
1260
|
+
* `AgentBackend`, optionally rebind its model resolver, and apply
|
|
1261
|
+
* sandbox wrapping. Idempotent across calls (no caching) — the AgentRunner
|
|
1262
|
+
* holds the per-dispatch reference and discards it when the run ends.
|
|
1083
1263
|
*/
|
|
1084
|
-
query(query: string, variables?: Record<string, unknown>): Promise<Result<unknown, Error>>;
|
|
1085
|
-
}
|
|
1086
|
-
interface LinearGraphQLClientOptions {
|
|
1087
1264
|
/**
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1265
|
+
* Resolve `useCase` to its routed backend name, exposing the
|
|
1266
|
+
* router lookup without materializing a backend. Used by callers
|
|
1267
|
+
* (e.g., the orchestrator's dispatch site) that need to label
|
|
1268
|
+
* telemetry with the routed name BEFORE constructing the backend.
|
|
1269
|
+
*
|
|
1270
|
+
* Spec 2 P2-I2: previously the orchestrator labelled `LiveSession`
|
|
1271
|
+
* + `StreamRecorder` with the legacy `agent.backend` field, which
|
|
1272
|
+
* is `undefined` for pure-modern configs. Threading the routed name
|
|
1273
|
+
* through dispatch eliminates that gap.
|
|
1091
1274
|
*/
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
/**
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1275
|
+
resolveName(useCase: RoutingUseCase, opts?: {
|
|
1276
|
+
invocationOverride?: string;
|
|
1277
|
+
}): string;
|
|
1278
|
+
/**
|
|
1279
|
+
* Spec B Phase 1: expose the underlying router for callers that need
|
|
1280
|
+
* it directly (e.g., {@link buildIntelligencePipeline} for the
|
|
1281
|
+
* I1 SEL/PESL comparison fix). Read-only access; consumers must not
|
|
1282
|
+
* mutate router state.
|
|
1283
|
+
*/
|
|
1284
|
+
getRouter(): BackendRouter;
|
|
1285
|
+
forUseCase(useCase: RoutingUseCase, opts?: {
|
|
1286
|
+
invocationOverride?: string;
|
|
1287
|
+
}): AgentBackend;
|
|
1288
|
+
/**
|
|
1289
|
+
* Rebuild a `local`/`pi` backend with a resolver-bound `getModel`,
|
|
1290
|
+
* mirroring `createBackend`'s local/pi branches but substituting the
|
|
1291
|
+
* head-of-array placeholder with the orchestrator-owned resolver.
|
|
1292
|
+
*/
|
|
1293
|
+
private buildLocalLikeWithResolver;
|
|
1294
|
+
/**
|
|
1295
|
+
* Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret
|
|
1296
|
+
* backend per call so each dispatch sees a fresh container handle map
|
|
1297
|
+
* (ContainerBackend keeps its own per-instance Map<sessionId, handle>).
|
|
1298
|
+
*/
|
|
1299
|
+
private wrapInContainer;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Narrow surface the stage-execution engine needs. The Orchestrator will
|
|
1304
|
+
* implement this in Phase 4 when dispatchIssue wires the branch; the engine
|
|
1305
|
+
* must NOT import orchestrator.ts (layer cycle). Phase 1 tests inject a fake.
|
|
1306
|
+
*/
|
|
1307
|
+
interface WorkflowEngineContext {
|
|
1308
|
+
recorder: StreamRecorder;
|
|
1309
|
+
logger: StructuredLogger;
|
|
1310
|
+
/** issueId of the coherence unit — used for recorder keys + terminal transitions. */
|
|
1311
|
+
issueId: string;
|
|
1312
|
+
identifier: string;
|
|
1313
|
+
externalId: string | null;
|
|
1314
|
+
workspacePath: string;
|
|
1315
|
+
/**
|
|
1316
|
+
* D12 (SC7): per-stage wall-clock deadline in ms. If a stage does not finish
|
|
1317
|
+
* within it, the engine fires the per-stage abort (running the runner's
|
|
1318
|
+
* `finally { stopSession }` via `gen.return()`) and treats the stage as
|
|
1319
|
+
* `passed:false` → feeds the D8 retry/terminal path (never an unbounded hang).
|
|
1320
|
+
* Absent ⇒ the engine default (`DEFAULT_STAGE_DEADLINE_MS`). Per-stage/config
|
|
1321
|
+
* override is Phase 4.
|
|
1322
|
+
*/
|
|
1323
|
+
stageDeadlineMs?: number;
|
|
1324
|
+
/** Build a runner for a stage's routed backend. Phase 1: single stubbed backend. */
|
|
1325
|
+
makeRunner(backend: AgentBackend): {
|
|
1326
|
+
runSession: (issue: unknown, ws: string, prompt: string) => AsyncGenerator<AgentEvent, {
|
|
1327
|
+
sessionId: string;
|
|
1328
|
+
success: boolean;
|
|
1329
|
+
usage: {
|
|
1330
|
+
inputTokens: number;
|
|
1331
|
+
outputTokens: number;
|
|
1332
|
+
totalTokens: number;
|
|
1333
|
+
};
|
|
1334
|
+
}, void>;
|
|
1335
|
+
};
|
|
1336
|
+
/** Phase 1 stub: resolve the single backend for a stage (Phase 2 replaces with route()). */
|
|
1337
|
+
resolveStageBackend(step: WorkflowExecutionPlan['stages'][number]): AgentBackend;
|
|
1338
|
+
/**
|
|
1339
|
+
* split-routing 4b: render the REAL per-stage prompt (issue + stage role +
|
|
1340
|
+
* prior-stage outputs), replacing the Phase-1 bare skill-name stub. Bound by
|
|
1341
|
+
* `buildWorkflowContext` via a `PromptRenderer` (never imports orchestrator).
|
|
1342
|
+
* `priorOutputs` maps each prior stage's `produces` label → its captured output.
|
|
1343
|
+
* Optional so fake contexts that don't exercise prompt content fall back to the
|
|
1344
|
+
* skill name (byte-identical to the old stub).
|
|
1345
|
+
*
|
|
1346
|
+
* CONTRACT: must be non-blocking (CPU-bound). It runs BEFORE the per-stage
|
|
1347
|
+
* deadline timer is armed, so an I/O-bound renderer that hangs would stall the
|
|
1348
|
+
* stage with no wall-clock bound. The shipped renderer is synchronous LiquidJS.
|
|
1349
|
+
*/
|
|
1350
|
+
renderStagePrompt?(step: WorkflowExecutionPlan['stages'][number], index: number, priorOutputs: Record<string, string>): string | Promise<string>;
|
|
1351
|
+
/**
|
|
1352
|
+
* split-routing Phase 2: per-stage adaptive router. Present ⇒ each stage is
|
|
1353
|
+
* routed via `route(buildStageRequest(...))`; ABSENT ⇒ identity fallback via
|
|
1354
|
+
* `resolveStageBackend` (no `routing.policy`). Narrow surface only (route +
|
|
1355
|
+
* recordOutcome) so the engine stays off the orchestrator import cycle.
|
|
1356
|
+
*
|
|
1357
|
+
* `route()` returns `{ decision }` only — the engine derives the backend name
|
|
1358
|
+
* from `decision.backendName` (a `BackendDef` carries no `name`; the name is
|
|
1359
|
+
* the router map key), so `def` is deliberately not part of this surface.
|
|
1360
|
+
*/
|
|
1361
|
+
adaptiveRouter?: {
|
|
1362
|
+
route(req: RoutingRequest): Promise<{
|
|
1363
|
+
decision: RoutingDecision;
|
|
1364
|
+
}>;
|
|
1365
|
+
recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void;
|
|
1366
|
+
};
|
|
1367
|
+
/** Terminal success — exactly one running/claimed delete + one lane persist (D6). */
|
|
1368
|
+
emitWorkflowSuccess(unit: string, runs: StageRun[]): Promise<void>;
|
|
1369
|
+
/** Terminal failure/safety-net — exactly one running/claimed delete + one lane persist (D6/I1). */
|
|
1370
|
+
finalizeWorkflowTerminal(unit: string, runs: StageRun[], failingStep?: WorkflowExecutionPlan['stages'][number], err?: unknown): Promise<void>;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* split-routing Phase 4 — the narrow adaptive-router surface the workflow engine
|
|
1375
|
+
* consumes (`route` + `recordOutcome`). Structurally identical to
|
|
1376
|
+
* `WorkflowEngineContext.adaptiveRouter` and to the real `AdaptiveRouter`'s public
|
|
1377
|
+
* methods; typed here so the dep bag never depends on the concrete router class
|
|
1378
|
+
* (and never on `orchestrator.ts`).
|
|
1379
|
+
*/
|
|
1380
|
+
interface WorkflowRouterDep {
|
|
1381
|
+
route(req: RoutingRequest): Promise<{
|
|
1382
|
+
decision: RoutingDecision;
|
|
1383
|
+
}>;
|
|
1384
|
+
recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void;
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* The dependency bag `buildWorkflowContext` composes the real
|
|
1388
|
+
* `WorkflowEngineContext` from. It is passed IN by `dispatchIssue` (Task 8) rather
|
|
1389
|
+
* than read off `this`, so this module imports only same-layer siblings
|
|
1390
|
+
* (`agent/runner`, `agent/orchestrator-backend-factory` types, `core/stream-recorder`)
|
|
1391
|
+
* and NEVER `orchestrator.ts` — the layer-cycle guard the context surface exists
|
|
1392
|
+
* for (concern #3). The two terminal seams are supplied as `settleSuccess` /
|
|
1393
|
+
* `settleTerminal` callbacks the orchestrator binds to its private methods (Task 7).
|
|
1394
|
+
*/
|
|
1395
|
+
interface BuildWorkflowContextDeps {
|
|
1396
|
+
recorder: StreamRecorder;
|
|
1397
|
+
logger: StructuredLogger;
|
|
1398
|
+
/** The dispatch unit; issueId/identifier/externalId flow from here. */
|
|
1399
|
+
issue: Issue;
|
|
1400
|
+
/** The unit's ONE worktree (from the single `ensureWorkspace` in dispatchIssue). */
|
|
1401
|
+
workspacePath: string;
|
|
1402
|
+
/** `this.config.agent.maxTurns` — the per-stage runner turn cap. */
|
|
1403
|
+
maxTurns: number;
|
|
1404
|
+
/**
|
|
1405
|
+
* The orchestrator's backend factory. Used to materialize the real routed
|
|
1406
|
+
* backend for a stage: `forUseCase(useCase, { invocationOverride: name })`
|
|
1407
|
+
* mirrors the AMR dispatch swap (orchestrator.ts:2062-2064). `null` when
|
|
1408
|
+
* migration failed (legacy-only config) — then the identity fallback uses the
|
|
1409
|
+
* name-only `routingDefault` backend.
|
|
1410
|
+
*/
|
|
1411
|
+
backendFactory: OrchestratorBackendFactory | null;
|
|
1412
|
+
/**
|
|
1413
|
+
* The real `AdaptiveRouter` (narrowed) when `routing.policy` is set
|
|
1414
|
+
* (orchestrator.ts:698), else `null` ⇒ the engine takes the identity fallback
|
|
1415
|
+
* (D5). Only `route`/`recordOutcome` are consumed.
|
|
1416
|
+
*/
|
|
1417
|
+
adaptiveRouter: WorkflowRouterDep | null;
|
|
1418
|
+
/** `this.config.agent.routing?.default` — the identity-fallback backend name. */
|
|
1419
|
+
routingDefault: string | undefined;
|
|
1420
|
+
/** D12 override; absent ⇒ engine default DEFAULT_STAGE_DEADLINE_MS. */
|
|
1421
|
+
stageDeadlineMs?: number;
|
|
1422
|
+
/** Terminal-success seam (Task 6/7): bound to `settleWorkflowSuccess`. */
|
|
1423
|
+
settleSuccess: (unit: string, runs: StageRun[]) => Promise<void>;
|
|
1424
|
+
/** Terminal-failure/safety-net seam (Task 6/7): bound to `settleWorkflowTerminal`. */
|
|
1425
|
+
settleTerminal: (unit: string, runs: StageRun[], failingStep?: WorkflowExecutionPlan['stages'][number], err?: unknown) => Promise<void>;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* split-routing Phase 4 — compose the REAL `WorkflowEngineContext` (minus the two
|
|
1429
|
+
* terminal seams, which Task 6 completes) from orchestrator machinery.
|
|
1430
|
+
*
|
|
1431
|
+
* Seam → real code:
|
|
1432
|
+
* - `recorder`/`logger`/`issueId`/`identifier`/`externalId`/`workspacePath` → the deps.
|
|
1433
|
+
* - `stageDeadlineMs` → `deps.stageDeadlineMs` (conditional; engine defaults when absent).
|
|
1434
|
+
* - `makeRunner(backend)` → `new AgentRunner(realBackend, { maxTurns })`. The engine
|
|
1435
|
+
* passes a name-only backend for the routed path (execute-workflow.ts:320); we
|
|
1436
|
+
* re-materialize the real backend from that name via
|
|
1437
|
+
* `backendFactory.forUseCase(useCase, { invocationOverride: backend.name })`
|
|
1438
|
+
* (same swap as orchestrator.ts:2062-2064). Its `runSession` returns a
|
|
1439
|
+
* `TurnResult` (runner.ts:112) — the seam is already typed against it
|
|
1440
|
+
* (carry-forward #9), confirmed self-documenting by the explicit annotation below.
|
|
1441
|
+
* - `resolveStageBackend(step)` → identity fallback: `backendFactory.forUseCase(useCase)`
|
|
1442
|
+
* when the factory exists, else a name-only backend from `routingDefault`.
|
|
1443
|
+
* - `adaptiveRouter` → present iff `deps.adaptiveRouter !== null` (D5).
|
|
1444
|
+
*
|
|
1445
|
+
* The two TERMINAL seams (`emitWorkflowSuccess` / `finalizeWorkflowTerminal`,
|
|
1446
|
+
* SC5) are thin forwarders to the orchestrator's `settleSuccess` / `settleTerminal`
|
|
1447
|
+
* callbacks. They are NOT routed through `emitWorkerExit`/`handleWorkerExit`
|
|
1448
|
+
* (which fire the ISSUE-keyed `finishRecording(attempt)` + `recordAmrOutcome` — a
|
|
1449
|
+
* DOUBLE-fire, since the engine already ran PER-STAGE recorders + per-stage
|
|
1450
|
+
* `recordOutcome`). The reducer-reproduction (running.delete → completed.set →
|
|
1451
|
+
* claimed.delete → cleanWorkspace → persist → emit for success; and the
|
|
1452
|
+
* finalizeRoutingTerminal + needs-human + cleanWorkspace sequence for terminal)
|
|
1453
|
+
* lives in the orchestrator's private methods (Task 7) the callbacks bind to.
|
|
1454
|
+
*/
|
|
1455
|
+
declare function buildWorkflowContext(deps: BuildWorkflowContextDeps): WorkflowEngineContext;
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Adapter for using a markdown roadmap file as an issue tracker.
|
|
1459
|
+
*
|
|
1460
|
+
* This adapter parses a standard Harness roadmap file, extracts features,
|
|
1461
|
+
* and maps them to the internal Issue model using deterministic hashing
|
|
1462
|
+
* for identifiers.
|
|
1463
|
+
*/
|
|
1464
|
+
declare class RoadmapTrackerAdapter implements IssueTrackerClient {
|
|
1465
|
+
private config;
|
|
1466
|
+
/**
|
|
1467
|
+
* Creates a new RoadmapTrackerAdapter.
|
|
1468
|
+
*
|
|
1469
|
+
* @param config - The tracker configuration including the file path
|
|
1470
|
+
*/
|
|
1471
|
+
constructor(config: TrackerConfig);
|
|
1472
|
+
/**
|
|
1473
|
+
* Fetches all issues that are in an "active" state according to the config.
|
|
1474
|
+
*/
|
|
1475
|
+
fetchCandidateIssues(): Promise<Result<Issue$1[], Error>>;
|
|
1476
|
+
/**
|
|
1477
|
+
* Fetches issues that match any of the given state names.
|
|
1478
|
+
*
|
|
1479
|
+
* @param stateNames - List of statuses to filter by
|
|
1480
|
+
*/
|
|
1481
|
+
fetchIssuesByStates(stateNames: string[]): Promise<Result<Issue$1[], Error>>;
|
|
1482
|
+
/**
|
|
1483
|
+
* Transitions a roadmap feature into the first configured terminal state
|
|
1484
|
+
* and rewrites the markdown file. Idempotent: if the feature is already
|
|
1485
|
+
* in a terminal state, this is a no-op.
|
|
1486
|
+
*
|
|
1487
|
+
* The orchestrator calls this after a successful agent exit so the feature
|
|
1488
|
+
* is no longer returned by `fetchCandidateIssues` on the next tick — and,
|
|
1489
|
+
* critically, after an orchestrator restart.
|
|
1490
|
+
*/
|
|
1491
|
+
markIssueComplete(issueId: string): Promise<Result<void, Error>>;
|
|
1492
|
+
/**
|
|
1493
|
+
* Claims an issue by transitioning its status to "in-progress" and
|
|
1494
|
+
* writing the orchestratorId into the assignee field. Idempotent if
|
|
1495
|
+
* already claimed by the same orchestratorId.
|
|
1496
|
+
*/
|
|
1497
|
+
claimIssue(issueId: string, orchestratorId: string): Promise<Result<void, Error>>;
|
|
1498
|
+
/**
|
|
1499
|
+
* Releases a claimed issue by transitioning back to the first active
|
|
1500
|
+
* state and clearing the assignee field.
|
|
1501
|
+
*/
|
|
1502
|
+
releaseIssue(issueId: string): Promise<Result<void, Error>>;
|
|
1503
|
+
/**
|
|
1504
|
+
* Resolve the roadmap store anchored on the configured aggregate file path.
|
|
1505
|
+
* The shard backend is chosen when a sibling `roadmap.d/` exists next to the
|
|
1506
|
+
* file; otherwise the monolith file is read/written whole. Callers guard
|
|
1507
|
+
* `this.config.filePath` before invoking.
|
|
1508
|
+
*/
|
|
1509
|
+
private resolveStore;
|
|
1510
|
+
/** Load the roadmap via the store (sharded or monolith), returning a Result. */
|
|
1511
|
+
private loadRoadmap;
|
|
1512
|
+
private findFeatureById;
|
|
1513
|
+
/**
|
|
1514
|
+
* Fetches full issue details for a list of identifiers.
|
|
1515
|
+
*
|
|
1516
|
+
* @param issueIds - List of issue IDs to fetch
|
|
1517
|
+
*/
|
|
1518
|
+
fetchIssueStatesByIds(issueIds: string[]): Promise<Result<Map<string, Issue$1>, Error>>;
|
|
1519
|
+
/**
|
|
1520
|
+
* Maps a raw RoadmapFeature from the parser to the unified Issue model.
|
|
1521
|
+
*/
|
|
1522
|
+
private mapFeatureToIssue;
|
|
1523
|
+
/**
|
|
1524
|
+
* Generates a deterministic, URL-safe identifier for a feature name.
|
|
1525
|
+
*/
|
|
1526
|
+
private generateId;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Interface for a Linear GraphQL tool extension — a thin authenticated client
|
|
1531
|
+
* over Linear's GraphQL API (https://linear.app/developers/graphql).
|
|
1532
|
+
*/
|
|
1533
|
+
interface LinearGraphQLExtension {
|
|
1534
|
+
/**
|
|
1535
|
+
* Execute a GraphQL operation. Resolves to `Ok(data)` (the `data` object of
|
|
1536
|
+
* the GraphQL envelope) on success, or `Err` on a transport failure, a non-2xx
|
|
1537
|
+
* HTTP status, or a GraphQL `errors` array.
|
|
1538
|
+
*/
|
|
1539
|
+
query(query: string, variables?: Record<string, unknown>): Promise<Result<unknown, Error>>;
|
|
1540
|
+
}
|
|
1541
|
+
interface LinearGraphQLClientOptions {
|
|
1542
|
+
/**
|
|
1543
|
+
* Linear authentication token. A personal API key is sent verbatim in the
|
|
1544
|
+
* `Authorization` header; an OAuth access token must be passed already prefixed
|
|
1545
|
+
* with `Bearer ` (Linear's two supported schemes).
|
|
1546
|
+
*/
|
|
1547
|
+
apiKey: string;
|
|
1548
|
+
/** Override the GraphQL endpoint (e.g. a proxy). Defaults to Linear's API. */
|
|
1549
|
+
endpoint?: string;
|
|
1550
|
+
/** Injectable fetch for tests; defaults to the global `fetch`. */
|
|
1551
|
+
fetchFn?: typeof fetch;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Real Linear GraphQL client. Replaces {@link LinearGraphQLStub}, which only
|
|
1555
|
+
* logged the query and returned an empty object. POSTs the operation to Linear's
|
|
1556
|
+
* GraphQL endpoint with the API key, and normalizes the three failure modes
|
|
1557
|
+
* (transport throw, non-2xx HTTP, GraphQL `errors`) into a single `Err`.
|
|
1558
|
+
*/
|
|
1104
1559
|
declare class LinearGraphQLClient implements LinearGraphQLExtension {
|
|
1105
1560
|
private readonly apiKey;
|
|
1106
1561
|
private readonly endpoint;
|
|
@@ -1123,6 +1578,24 @@ declare class LinearGraphQLStub implements LinearGraphQLExtension {
|
|
|
1123
1578
|
query(query: string, _variables?: Record<string, unknown>): Promise<Result<unknown, Error>>;
|
|
1124
1579
|
}
|
|
1125
1580
|
|
|
1581
|
+
/**
|
|
1582
|
+
* AMR 4c: a single-agent quality-verdict source (ADR 0069). The escalation
|
|
1583
|
+
* mechanism + seam are complete; this supplies the *sound* signal that was
|
|
1584
|
+
* missing — a BASELINE-RELATIVE scan of the diff the agent introduced. Only the
|
|
1585
|
+
* ADDED lines are scanned, so a pre-existing pattern never counts against the
|
|
1586
|
+
* agent. This is sound (not approximate) because every security rule is
|
|
1587
|
+
* single-line, so per-added-line matching yields exactly the introduced findings.
|
|
1588
|
+
*/
|
|
1589
|
+
/** One hunk of added lines from a unified diff, with its true start line. */
|
|
1590
|
+
interface IntroducedHunk {
|
|
1591
|
+
/** Repo-relative path of the changed file. */
|
|
1592
|
+
file: string;
|
|
1593
|
+
/** The added lines (leading `+` stripped), joined with `\n`. */
|
|
1594
|
+
addedContent: string;
|
|
1595
|
+
/** New-file line number the first added line sits at (for accurate findings). */
|
|
1596
|
+
startLine: number;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1126
1599
|
/**
|
|
1127
1600
|
* Structured event emitted when {@link WorkspaceManager.resolveBaseRef}
|
|
1128
1601
|
* falls back past `origin/HEAD` and `origin/main`/`origin/master` to a
|
|
@@ -1162,6 +1635,27 @@ declare class WorkspaceManager {
|
|
|
1162
1635
|
* Resolves the full path for an issue's workspace.
|
|
1163
1636
|
*/
|
|
1164
1637
|
resolvePath(identifier: string): string;
|
|
1638
|
+
/**
|
|
1639
|
+
* AMR 4c: the lines the dispatched agent INTRODUCED in its worktree, as
|
|
1640
|
+
* per-hunk added-line blocks — the sound input for a baseline-relative quality
|
|
1641
|
+
* scan (pre-existing content is excluded by construction). Diffs the working
|
|
1642
|
+
* tree (so both committed AND uncommitted agent work is captured) against the
|
|
1643
|
+
* MERGE-BASE of the worktree HEAD and the base ref — merge-base, not the base
|
|
1644
|
+
* ref itself, so a base branch that advanced mid-dispatch never attributes
|
|
1645
|
+
* other merges to this agent. The seeded handoff overlay (`seedPaths`) is
|
|
1646
|
+
* excluded — those files are not the agent's work.
|
|
1647
|
+
*/
|
|
1648
|
+
getIntroducedDiff(identifier: string): Promise<IntroducedHunk[]>;
|
|
1649
|
+
/**
|
|
1650
|
+
* AMR 4c v2: the SAME introduced change as {@link getIntroducedDiff}, but as the
|
|
1651
|
+
* RAW unified diff text (default context, NOT `--unified=0`) — the input an LLM
|
|
1652
|
+
* spec-satisfaction eval needs to judge whether the diff satisfies the spec.
|
|
1653
|
+
* Merge-base relative for the same reason (a base branch that advanced
|
|
1654
|
+
* mid-dispatch never attributes other merges here), and the seeded handoff
|
|
1655
|
+
* overlay is excluded via git `:(exclude)` pathspecs so the judge never reads
|
|
1656
|
+
* pre-seeded proposal/roadmap content as the agent's work.
|
|
1657
|
+
*/
|
|
1658
|
+
getIntroducedDiffText(identifier: string): Promise<string>;
|
|
1165
1659
|
/**
|
|
1166
1660
|
* Discovers the git repository root from the workspace root directory.
|
|
1167
1661
|
*/
|
|
@@ -1473,133 +1967,235 @@ interface MaintenanceStatus {
|
|
|
1473
1967
|
history: RunResult[];
|
|
1474
1968
|
}
|
|
1475
1969
|
|
|
1476
|
-
interface RoutingDecisionBusFilter {
|
|
1477
|
-
skillName?: string;
|
|
1478
|
-
mode?: string;
|
|
1479
|
-
backendName?: string;
|
|
1480
|
-
limit?: number;
|
|
1481
|
-
}
|
|
1482
|
-
interface RoutingDecisionBusOptions {
|
|
1483
|
-
/** Default 500. Bound on the in-memory ring buffer. */
|
|
1484
|
-
capacity?: number;
|
|
1485
|
-
/**
|
|
1486
|
-
* Logger for the structured `routing-decision` line (O1) and for
|
|
1487
|
-
* one-off warn() when a subscriber throws (S6). When omitted, the
|
|
1488
|
-
* bus silently swallows subscriber errors (test-mode default).
|
|
1489
|
-
*/
|
|
1490
|
-
logger?: StructuredLogger;
|
|
1491
|
-
}
|
|
1492
1970
|
/**
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
* Subscriber errors are isolated (caught + logged, never thrown
|
|
1499
|
-
* back to the emitter) so a misbehaving subscriber cannot block a
|
|
1500
|
-
* dispatch. (S6)
|
|
1501
|
-
*
|
|
1502
|
-
* Capacity-bound (default 500) via Array.shift() — acceptable for
|
|
1503
|
-
* v1 (see plan C4); switch to circular indexing if 24h dispatch
|
|
1504
|
-
* volume ever pushes 10K+ records/min.
|
|
1971
|
+
* D10 vertical escalation. Per-`coherenceUnit` quality-failure counter that
|
|
1972
|
+
* raises the unit's floor tier one step on the Nth consecutive QUALITY failure
|
|
1973
|
+
* (never transport — that is the shipped per-model breaker). Monotonic +
|
|
1974
|
+
* `strong`-capped ⇒ cannot loop or thrash. Naming mirrors
|
|
1975
|
+
* `LocalModelResolver.recordSuccess/recordFailure` (local-model-resolver.ts:460/474).
|
|
1505
1976
|
*/
|
|
1506
|
-
declare class
|
|
1507
|
-
private readonly
|
|
1508
|
-
private readonly
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1977
|
+
declare class EscalationState {
|
|
1978
|
+
private readonly threshold;
|
|
1979
|
+
private readonly units;
|
|
1980
|
+
constructor(threshold?: number);
|
|
1981
|
+
/**
|
|
1982
|
+
* The current escalation floor for a unit — the minimum tier `route()` must
|
|
1983
|
+
* resolve at. Defaults to `'fast'` (no-op floor) for an unknown/absent unit,
|
|
1984
|
+
* so an un-escalated request derives its tier normally.
|
|
1985
|
+
*/
|
|
1986
|
+
floorFor(coherenceUnit?: string): CapabilityTier;
|
|
1987
|
+
/** D10 mechanism flag: has this unit ever climbed a tier? (Phase 6 reads this for Tier-A disqualification.) */
|
|
1988
|
+
isEscalated(coherenceUnit?: string): boolean;
|
|
1989
|
+
/**
|
|
1990
|
+
* AMR observability: the coherence units that have climbed ABOVE the `fast`
|
|
1991
|
+
* floor, with their current floor tier. Operator status only (read-only) — an
|
|
1992
|
+
* empty list means nothing has escalated. Units still at `fast` are omitted.
|
|
1993
|
+
*/
|
|
1994
|
+
climbedUnits(): Array<{
|
|
1995
|
+
coherenceUnit: string;
|
|
1996
|
+
floor: CapabilityTier;
|
|
1997
|
+
}>;
|
|
1515
1998
|
/**
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1999
|
+
* SC16: a QUALITY failure at `tier` increments this unit's counter; on the
|
|
2000
|
+
* Nth (threshold) consecutive failure the floor climbs one step (fast→standard
|
|
2001
|
+
* →strong), the count resets, and `escalated` latches true. `strong` is the
|
|
2002
|
+
* ceiling: a threshold-crossing failure already at `strong` returns
|
|
2003
|
+
* 'exhausted' (router emits routing:escalation-exhausted). `ok` clears the
|
|
2004
|
+
* in-progress count but leaves the raised floor (monotonic per D10).
|
|
2005
|
+
*
|
|
2006
|
+
* `_tier` (the tier the outcome occurred at) is ADVISORY/UNUSED today: the climb
|
|
2007
|
+
* is driven entirely off the unit's own `state.floorTier`, not off this argument.
|
|
2008
|
+
* It is kept in the signature so the call-site contract stays stable for a future
|
|
2009
|
+
* refinement that guards against out-of-order (stale-tier) reports. (Note: this is
|
|
2010
|
+
* NOT positionally mirroring `LocalModelResolver.recordFailure`, which keys on a
|
|
2011
|
+
* model identifier and takes no tier — the earlier comment claiming that shape was
|
|
2012
|
+
* misleading.)
|
|
1520
2013
|
*/
|
|
1521
|
-
|
|
2014
|
+
recordOutcome(coherenceUnit: string, _tier: CapabilityTier, ok: boolean): 'ok' | 'escalated' | 'exhausted';
|
|
1522
2015
|
}
|
|
1523
2016
|
|
|
1524
|
-
interface
|
|
1525
|
-
|
|
1526
|
-
|
|
2017
|
+
interface AdaptiveRouterDeps {
|
|
2018
|
+
router: BackendRouter;
|
|
2019
|
+
registry: BackendCapabilityRegistry;
|
|
2020
|
+
policy: RoutingPolicy;
|
|
2021
|
+
/**
|
|
2022
|
+
* Complexity classifier. Accepts a sync verdict-provider (Phase-3 conservative
|
|
2023
|
+
* stub) OR an async classifier (D4 live cascade); `route()` awaits either shape.
|
|
2024
|
+
* A rejection/throw is caught and degraded to `{moderate, low}` (never blocks
|
|
2025
|
+
* dispatch — Failure modes / D4).
|
|
2026
|
+
*/
|
|
2027
|
+
classify: (req: RoutingRequest) => ComplexityVerdict | Promise<ComplexityVerdict>;
|
|
2028
|
+
/** name → provider type, derived from agent.backends (Task 6). REQUIRED when policy.allowedProviders is set. */
|
|
2029
|
+
providerOf?: (name: string) => BackendDef['type'] | undefined;
|
|
2030
|
+
/** Injected spend snapshot (D8/S1-001). Defaults to 0-spend. */
|
|
2031
|
+
budgetState?: () => {
|
|
2032
|
+
spentUsd: number;
|
|
2033
|
+
};
|
|
1527
2034
|
/**
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
2035
|
+
* D10 vertical escalation state. When present, `route()` uses
|
|
2036
|
+
* `escalation.floorFor(req.coherenceUnit)` as the tier floor (a climbed unit
|
|
2037
|
+
* resolves higher); when absent, the floor is the no-op `'fast'`.
|
|
2038
|
+
*/
|
|
2039
|
+
escalation?: EscalationState;
|
|
2040
|
+
/**
|
|
2041
|
+
* D10 steward-escalation seam. Invoked with the `coherenceUnit` when
|
|
2042
|
+
* `recordOutcome` returns `'exhausted'` (floor already `strong`, re-crossed the
|
|
2043
|
+
* threshold). The orchestrator binds this to `routingDecisionBus`/logger to emit
|
|
2044
|
+
* `routing:escalation-exhausted`.
|
|
2045
|
+
*/
|
|
2046
|
+
onExhausted?: (coherenceUnit: string) => void;
|
|
2047
|
+
/**
|
|
2048
|
+
* SC9: enriched-decision bus. The shipped `resolveDecisionAndDef` emits the
|
|
2049
|
+
* BASE decision inside the D2-frozen router (no `complexity`/`tierRequired`/
|
|
2050
|
+
* `estCostUsd`). When `AdaptiveRouter` owns live dispatch it emits the
|
|
2051
|
+
* ENRICHED decision here so a subscriber actually receives the AMR telemetry.
|
|
2052
|
+
* Absent ⇒ no enriched emit (and when AMR is off the router isn't constructed,
|
|
2053
|
+
* so nothing changes on the bus — SC8/SC17 preserved).
|
|
1531
2054
|
*/
|
|
1532
2055
|
decisionBus?: RoutingDecisionBus;
|
|
1533
2056
|
}
|
|
1534
2057
|
/**
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
* Owns the lookup from a {@link RoutingUseCase} (a discriminated query
|
|
1538
|
-
* — tier, intelligence layer, maintenance, chat, isolation, **skill**,
|
|
1539
|
-
* **mode**) to a {@link RoutingDecision} naming a chosen backend and
|
|
1540
|
-
* the full resolution path that produced it.
|
|
1541
|
-
*
|
|
1542
|
-
* Resolution order (D2): invocation override -> per-skill -> per-mode
|
|
1543
|
-
* -> existing per-tier/intelligence/isolation/maintenance/chat ->
|
|
1544
|
-
* `routing.default`. Within each source, fallback chain entries are
|
|
1545
|
-
* tried in declared order; first existing backend wins. Unknown
|
|
1546
|
-
* entries are recorded with `outcome: 'unknown-backend'` and the walk
|
|
1547
|
-
* continues.
|
|
2058
|
+
* AMR Phase 3 (D2): wraps — never modifies — the shipped {@link BackendRouter}.
|
|
1548
2059
|
*
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
2060
|
+
* `route()` classifies the request, derives the required {@link CapabilityTier}
|
|
2061
|
+
* via `deriveRequiredTier`, picks the cheapest qualifying backend
|
|
2062
|
+
* (`selectCheapestQualifying`), and delegates the actual resolution to
|
|
2063
|
+
* `router.resolveDecisionAndDef` — enriching the returned {@link RoutingDecision}
|
|
2064
|
+
* with `complexity`/`tierRequired`/`estCostUsd` (SC9). The shipped router's
|
|
2065
|
+
* identity/default chain still owns the final backend lookup, so when tier
|
|
2066
|
+
* selection abstains (`undefined`) dispatch falls through unchanged.
|
|
1554
2067
|
*/
|
|
1555
|
-
declare class
|
|
1556
|
-
private readonly
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
2068
|
+
declare class AdaptiveRouter {
|
|
2069
|
+
private readonly deps;
|
|
2070
|
+
/**
|
|
2071
|
+
* D8 live spend accumulator. Monotonic sum of `estCostUsd` over every decision
|
|
2072
|
+
* this router has made. `route()` reads it (via the `budgetState` default)
|
|
2073
|
+
* BEFORE deriving a tier, so `deriveRequiredTier`'s budget clamp actually fires
|
|
2074
|
+
* as spend accrues — previously `budgetState` was an un-wired `{ spentUsd: 0 }`
|
|
2075
|
+
* stub, so the clamp was dead.
|
|
2076
|
+
*
|
|
2077
|
+
* Semantics (deliberately modest — do NOT read this as a hard ceiling):
|
|
2078
|
+
* - **Soft, lagging cap.** The clamp reads spend accrued from PRIOR dispatches;
|
|
2079
|
+
* a burst of concurrent dispatches all read a stale-low total before any of
|
|
2080
|
+
* them accrues, so a burst can overshoot `capUsd` before the clamp engages.
|
|
2081
|
+
* This is a degrade *signal*, not an admission gate.
|
|
2082
|
+
* - **Single-step degrade.** `deriveRequiredTier` lowers the tier by exactly ONE
|
|
2083
|
+
* step under budget pressure and never below the D5 privacy veto floor — it
|
|
2084
|
+
* does not throttle progressively toward `fast`.
|
|
2085
|
+
* - **Monotonic** (NOT the bounded `projectTelemetry` ring-sum): a long run must
|
|
2086
|
+
* not evict early spend and un-clamp. Preserved across `setPolicy` (instance
|
|
2087
|
+
* persists) — note a *lowered* `capUsd` then clamps immediately and, since the
|
|
2088
|
+
* total never decreases, irreversibly for the rest of the run.
|
|
2089
|
+
* An injected `deps.budgetState` overrides this for the clamp (DI/tests).
|
|
2090
|
+
*/
|
|
2091
|
+
private spentUsd;
|
|
2092
|
+
constructor(deps: AdaptiveRouterDeps);
|
|
2093
|
+
/**
|
|
2094
|
+
* Construct an `AdaptiveRouter` from an orchestrator's `agent.backends`
|
|
2095
|
+
* (plus optional LMLM pool). Builds the capability registry via
|
|
2096
|
+
* `buildCapabilityRegistry` and — crucially — derives `providerOf`
|
|
2097
|
+
* UNCONDITIONALLY from `agent.backends` (name → `def.type`).
|
|
2098
|
+
*
|
|
2099
|
+
* Phase-1 finding (capability-registry.ts:60-66): passing an allowlist to
|
|
2100
|
+
* `selectCheapestQualifying` WITHOUT a `providerOf` fail-closes EVERY request
|
|
2101
|
+
* (silent DoS), because every candidate's provider reads back as `undefined`.
|
|
2102
|
+
* Deriving `providerOf` here means enabling the (Phase-5) allowlist branch can
|
|
2103
|
+
* never accidentally deny-all.
|
|
2104
|
+
*/
|
|
2105
|
+
static fromConfig(args: {
|
|
2106
|
+
router: BackendRouter;
|
|
2107
|
+
backends: Record<string, BackendDef>;
|
|
2108
|
+
pool?: PoolStateProvider;
|
|
2109
|
+
policy: RoutingPolicy;
|
|
2110
|
+
classify: (req: RoutingRequest) => ComplexityVerdict | Promise<ComplexityVerdict>;
|
|
2111
|
+
budgetState?: () => {
|
|
2112
|
+
spentUsd: number;
|
|
2113
|
+
};
|
|
2114
|
+
/** D10: injected escalation state; defaults to a fresh one seeded from `policy.escalationThreshold`. */
|
|
2115
|
+
escalation?: EscalationState;
|
|
2116
|
+
/** D10: steward-escalation seam bound by the orchestrator to the decision bus/logger. */
|
|
2117
|
+
onExhausted?: (coherenceUnit: string) => void;
|
|
2118
|
+
/** SC9: enriched-decision bus (the same instance dispatch emits base decisions onto). */
|
|
2119
|
+
decisionBus?: RoutingDecisionBus;
|
|
2120
|
+
}): AdaptiveRouter;
|
|
2121
|
+
/**
|
|
2122
|
+
* AMR Phase 5 (D1): hot-swap the routing policy in place. Every policy field
|
|
2123
|
+
* takes effect on the NEXT dispatch EXCEPT `escalationThreshold` (see note) —
|
|
2124
|
+
* the capability registry and `providerOf` are derived from `agent.backends`,
|
|
2125
|
+
* NOT from `policy`, so a policy edit leaves them untouched; `route()` /
|
|
2126
|
+
* `buildConstraints` / `deriveRequiredTier` all read `this.deps.policy` fresh,
|
|
2127
|
+
* so the swapped-in tier matrix / privacy floor / allowlist / budget all apply.
|
|
2128
|
+
* The live {@link EscalationState} is preserved by reference: a unit's climbed
|
|
2129
|
+
* floor survives the swap (a policy edit must not reset accumulated escalation).
|
|
2130
|
+
* The monotonic spend accumulator also persists — so a swap that LOWERS `capUsd`
|
|
2131
|
+
* clamps immediately and irreversibly for the rest of the run (see `spentUsd`).
|
|
2132
|
+
*
|
|
2133
|
+
* Threshold note: the `EscalationState` was seeded with the ORIGINAL policy's
|
|
2134
|
+
* `escalationThreshold` and is intentionally NOT re-seeded here — re-seeding
|
|
2135
|
+
* would conflate "raise the bar" with "reset progress". Preserving climbed
|
|
2136
|
+
* floors is the invariant (SC1); a live threshold change is out of scope (D1).
|
|
2137
|
+
*/
|
|
2138
|
+
setPolicy(policy: RoutingPolicy): void;
|
|
1560
2139
|
/**
|
|
1561
|
-
*
|
|
2140
|
+
* AMR Phase 5 (D2): project the ENRICHED routing decisions in the bus ring
|
|
2141
|
+
* buffer into the Shuttle telemetry wire shape ({@link RoutingTelemetry}).
|
|
1562
2142
|
*
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
2143
|
+
* Non-destructive — this backs the idempotent `GET /api/v1/routing/telemetry`,
|
|
2144
|
+
* so it reads (never clears) the ring; the ring self-evicts FIFO at capacity
|
|
2145
|
+
* and Shuttle dedups by `decisionTs`. `spentUsd` is therefore a sum over the
|
|
2146
|
+
* RETAINED ring (telemetry, not billing-of-record).
|
|
2147
|
+
*
|
|
2148
|
+
* Filters to ENRICHED decisions only — those `AdaptiveRouter.route()` emitted,
|
|
2149
|
+
* carrying `estCostUsd`+`tierRequired`. The SAME bus also carries the BASE emit
|
|
2150
|
+
* from `BackendRouter.resolveDecisionAndDef` (neither field), so an unfiltered
|
|
2151
|
+
* projection would double-count rows and under-fill `spentUsd`. Returns an
|
|
2152
|
+
* empty payload when no bus is wired or no enriched decisions exist.
|
|
1566
2153
|
*/
|
|
1567
|
-
|
|
1568
|
-
invocationOverride?: string;
|
|
1569
|
-
}): RoutingDecision;
|
|
2154
|
+
projectTelemetry(): RoutingTelemetry;
|
|
1570
2155
|
/**
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
2156
|
+
* D8: the EFFECTIVE spend total the budget clamp reads — the injected
|
|
2157
|
+
* `budgetState` when present, otherwise the router's own monotonic accumulator.
|
|
2158
|
+
* Returning the effective value (not blindly the internal tally) means an
|
|
2159
|
+
* operator reading this always sees the number that actually drove routing.
|
|
1574
2160
|
*/
|
|
1575
|
-
|
|
1576
|
-
invocationOverride?: string;
|
|
1577
|
-
}): BackendDef;
|
|
2161
|
+
getSpentUsd(): number;
|
|
1578
2162
|
/**
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
* log volume now that Phase 4 emits.
|
|
1584
|
-
*
|
|
1585
|
-
* Identity-equal `BackendDef` (no copy) so callers relying on
|
|
1586
|
-
* reference equality (SC21) continue to work.
|
|
2163
|
+
* AMR observability: the live operator status — budget spend-vs-cap (using the
|
|
2164
|
+
* monotonic accumulator that drives the clamp, NOT the telemetry ring sum),
|
|
2165
|
+
* the coherence units that have climbed their escalation floor, and the active
|
|
2166
|
+
* provider allowlist. Called only on a live router, so `active` is always true.
|
|
1587
2167
|
*/
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
}): {
|
|
2168
|
+
getStatus(): RoutingStatus;
|
|
2169
|
+
route(req: RoutingRequest): Promise<{
|
|
1591
2170
|
decision: RoutingDecision;
|
|
1592
2171
|
def: BackendDef;
|
|
1593
|
-
}
|
|
2172
|
+
}>;
|
|
1594
2173
|
/**
|
|
1595
|
-
*
|
|
1596
|
-
* {
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
* `undefined` lets the caller fall through to `routing.default`.
|
|
2174
|
+
* D4 fail-safe: await the (possibly async) classifier; if it rejects or throws,
|
|
2175
|
+
* degrade to a conservative `{ level:'moderate', confidence:'low' }` verdict.
|
|
2176
|
+
* Classification NEVER blocks dispatch — a classifier failure/timeout must not
|
|
2177
|
+
* propagate out of `route()`.
|
|
1600
2178
|
*/
|
|
1601
|
-
private
|
|
1602
|
-
|
|
2179
|
+
private classifySafe;
|
|
2180
|
+
/**
|
|
2181
|
+
* D10 outcome feedback (mirrors LocalModelResolver.recordSuccess/recordFailure).
|
|
2182
|
+
* Only QUALITY failures are reported here; transport/inference errors go to the
|
|
2183
|
+
* shipped per-model breaker and must NOT reach this path (they never
|
|
2184
|
+
* double-count). A quality failure that re-crosses the threshold while the floor
|
|
2185
|
+
* is already `strong` returns `'exhausted'` from EscalationState, at which point
|
|
2186
|
+
* the injected `onExhausted` seam emits `routing:escalation-exhausted` for
|
|
2187
|
+
* steward escalation. No-op when no EscalationState is injected.
|
|
2188
|
+
*/
|
|
2189
|
+
recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void;
|
|
2190
|
+
private selectTarget;
|
|
2191
|
+
/**
|
|
2192
|
+
* Translate the request's declared constraints into {@link SelectConstraints}.
|
|
2193
|
+
* `policy.privacyFloor`, the provider allowlist (AMR Phase 5, D3), and the
|
|
2194
|
+
* per-request capability needs are threaded in. `providerOf` is derived
|
|
2195
|
+
* unconditionally in `fromConfig`, so the allowlist can never silently
|
|
2196
|
+
* fail-close every request (Phase-1 finding, capability-registry.ts:60-66).
|
|
2197
|
+
*/
|
|
2198
|
+
private buildConstraints;
|
|
1603
2199
|
}
|
|
1604
2200
|
|
|
1605
2201
|
/**
|
|
@@ -1668,6 +2264,13 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1668
2264
|
* construction time. Eliminating this fallback is autopilot Phase 4+.
|
|
1669
2265
|
*/
|
|
1670
2266
|
private backendFactory;
|
|
2267
|
+
/**
|
|
2268
|
+
* AMR Phase 3 (D11): opt-in adaptive router. Constructed ONLY when
|
|
2269
|
+
* `agent.routing.policy` is present and non-empty; `null` otherwise so
|
|
2270
|
+
* dispatch stays byte-identical on the shipped `BackendRouter`
|
|
2271
|
+
* (SC8/SC17/SC19). Exposed for tests via {@link getAdaptiveRouter}.
|
|
2272
|
+
*/
|
|
2273
|
+
private adaptiveRouter;
|
|
1671
2274
|
/**
|
|
1672
2275
|
* Spec B Phase 4 (D8): per-orchestrator in-process bus for
|
|
1673
2276
|
* `RoutingDecision` events. Constructed alongside backendFactory when
|
|
@@ -1698,6 +2301,13 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1698
2301
|
* so this map is the single source of truth post-migration.
|
|
1699
2302
|
*/
|
|
1700
2303
|
private localResolvers;
|
|
2304
|
+
/**
|
|
2305
|
+
* Consumption Phase 1 (T2): bus listener that debounce-refreshes every local
|
|
2306
|
+
* resolver when a `local-models:pool` mutation fires, so a just-installed or
|
|
2307
|
+
* swapped model becomes usable within the refresh window instead of waiting up
|
|
2308
|
+
* to `probeIntervalMs` for the next poll. Held for removal in {@link stop}.
|
|
2309
|
+
*/
|
|
2310
|
+
private poolRefreshListener;
|
|
1701
2311
|
/** Phase 4 (D5): pool-state port shared by all local/pi resolvers. Null when LMLM disabled. */
|
|
1702
2312
|
private poolStateProvider;
|
|
1703
2313
|
private poolStateStore;
|
|
@@ -1735,6 +2345,10 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1735
2345
|
* see the Phase 2 candidate-parser gap noted on `startRefreshScheduler`.
|
|
1736
2346
|
*/
|
|
1737
2347
|
private modelRecommender;
|
|
2348
|
+
/** Live HF candidate discovery (injectable for tests so startup makes no network calls). */
|
|
2349
|
+
private readonly discoverCandidatesFn;
|
|
2350
|
+
/** Snapshot of the last candidate seeding, surfaced to the refresh route. */
|
|
2351
|
+
private candidateSourceState;
|
|
1738
2352
|
/** Test seam: injected timer/clock for the scheduler so no real 24h timer runs. */
|
|
1739
2353
|
private readonly schedulerTimerOverride;
|
|
1740
2354
|
/**
|
|
@@ -1756,6 +2370,15 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1756
2370
|
*/
|
|
1757
2371
|
private localModelStatusUnsubscribes;
|
|
1758
2372
|
private pipeline;
|
|
2373
|
+
/**
|
|
2374
|
+
* AMR live-classifier provider (final-review finding #2). The complexity
|
|
2375
|
+
* cascade may spend a fast-tier LLM tie-break; it borrows the SEL-layer
|
|
2376
|
+
* AnalysisProvider. Built lazily on first classify (the AdaptiveRouter is
|
|
2377
|
+
* constructed before start(), so this cannot be eager); `null` means "no
|
|
2378
|
+
* provider — cascade stays fully offline / static-only". `undefined` means
|
|
2379
|
+
* "not yet resolved".
|
|
2380
|
+
*/
|
|
2381
|
+
private complexityProvider;
|
|
1759
2382
|
private analysisArchive;
|
|
1760
2383
|
private graphStore;
|
|
1761
2384
|
private claimManager;
|
|
@@ -1813,6 +2436,8 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1813
2436
|
now?: () => number;
|
|
1814
2437
|
random?: () => number;
|
|
1815
2438
|
};
|
|
2439
|
+
/** Live-candidate-discovery seam: tests inject a fake so startup makes no HF calls. */
|
|
2440
|
+
discoverCandidates?: (opts: DiscoverCandidatesOptions) => Promise<DiscoverCandidatesResult>;
|
|
1816
2441
|
});
|
|
1817
2442
|
/**
|
|
1818
2443
|
* Phase 5: construct the OTLP/HTTP trace exporter and wire telemetry fanout.
|
|
@@ -1868,6 +2493,19 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1868
2493
|
*/
|
|
1869
2494
|
private initMaintenance;
|
|
1870
2495
|
private createIntelligencePipeline;
|
|
2496
|
+
/**
|
|
2497
|
+
* AMR live-classifier provider resolution (final-review finding #2). The
|
|
2498
|
+
* complexity cascade's OPTIONAL fast-tier tie-break borrows the SEL-layer
|
|
2499
|
+
* AnalysisProvider (the same one intelligence enrichment uses). Resolved and
|
|
2500
|
+
* memoized on first classify because the AdaptiveRouter is constructed BEFORE
|
|
2501
|
+
* start(), so the provider cannot be resolved eagerly.
|
|
2502
|
+
*
|
|
2503
|
+
* Returns `undefined` when no provider is available (intelligence disabled, no
|
|
2504
|
+
* backendFactory, or the layer resolves to nothing) — the cascade then stays
|
|
2505
|
+
* fully offline and returns the static verdict (never throws). A build failure
|
|
2506
|
+
* degrades the same way: static-only, never blocks dispatch (D4).
|
|
2507
|
+
*/
|
|
2508
|
+
private resolveComplexityProvider;
|
|
1871
2509
|
/**
|
|
1872
2510
|
* Lazily initializes the ClaimManager if it hasn't been created yet.
|
|
1873
2511
|
* Called from both start() and asyncTick() to avoid duplicating the init block.
|
|
@@ -1954,10 +2592,136 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1954
2592
|
private processAgentEvent;
|
|
1955
2593
|
private awaitRateLimitClearance;
|
|
1956
2594
|
private runAgentInBackgroundTask;
|
|
2595
|
+
/**
|
|
2596
|
+
* AMR 4c (ADR 0069): the sound single-agent quality-verdict feeder. On a normal
|
|
2597
|
+
* exit, when AMR is active, run a BASELINE-RELATIVE security scan of the lines
|
|
2598
|
+
* the agent INTRODUCED (only added lines — a pre-existing pattern never counts);
|
|
2599
|
+
* a NEW error-severity finding → `'quality-fail'`, which climbs the coherence
|
|
2600
|
+
* unit's escalation floor. Success stays `neutral` (returns `undefined`) — a
|
|
2601
|
+
* premature `'quality-pass'` would clear accumulating failures (ADR 0069).
|
|
2602
|
+
*
|
|
2603
|
+
* Fully guarded: any error (git/scan/parse) → `undefined` → `neutral`, so this
|
|
2604
|
+
* NEVER breaks completion. No-op (zero cost) when AMR is off, keeping the
|
|
2605
|
+
* dispatch path byte-identical. Staged workflows use their own per-stage gate
|
|
2606
|
+
* feeder; this is the single-agent equivalent.
|
|
2607
|
+
*/
|
|
2608
|
+
private deriveSingleAgentQualityVerdict;
|
|
2609
|
+
/**
|
|
2610
|
+
* AMR 4c v2: opt-in LLM spec-satisfaction verdict. Gated on
|
|
2611
|
+
* `routing.policy.acceptanceEval.enabled` (HEAVY — one model call + latency per
|
|
2612
|
+
* exit, so separate from the always-on cheap security scan), a present spec, and
|
|
2613
|
+
* an available analysis provider. Runs the shared `OutcomeEvaluator` over the
|
|
2614
|
+
* introduced diff vs the spec's judgment section, reusing the SEL-layer provider
|
|
2615
|
+
* the live classifier already builds; a BLOCKING verdict (high-confidence
|
|
2616
|
+
* NOT_SATISFIED) → `'quality-fail'`. Conservative + fully guarded: no
|
|
2617
|
+
* spec / no provider / empty diff / any error → `undefined` (neutral). The mapper
|
|
2618
|
+
* only ever emits the negative, so success can never become a premature
|
|
2619
|
+
* `'quality-pass'`. An absent GraphStore falls back to an ephemeral one — the
|
|
2620
|
+
* evaluator's `execution_outcome` persistence is best-effort and never blocks.
|
|
2621
|
+
*/
|
|
2622
|
+
private deriveAcceptanceEvalVerdict;
|
|
1957
2623
|
/**
|
|
1958
2624
|
* Informs the state machine that an agent worker has exited.
|
|
1959
2625
|
*/
|
|
1960
2626
|
private emitWorkerExit;
|
|
2627
|
+
/**
|
|
2628
|
+
* AMR Phase 4 (D10/SC16): feed a dispatch outcome into vertical escalation.
|
|
2629
|
+
* No-op unless an AdaptiveRouter is live (policy present) AND this dispatch was
|
|
2630
|
+
* AMR-routed (a `lastRoutedTier` was captured). Transport outcomes never reach
|
|
2631
|
+
* `recordOutcome` — the shipped per-model breaker owns those, so the two signals
|
|
2632
|
+
* never double-count. The coherence unit is the issue id (D6 issue-grain pinning).
|
|
2633
|
+
*/
|
|
2634
|
+
private recordAmrOutcome;
|
|
2635
|
+
/**
|
|
2636
|
+
* AMR steward-escalation seam (D10, findings item 1 + 2). Queues a `needs-human`
|
|
2637
|
+
* interaction for a coherence unit whose routing hard-failed — either the vertical
|
|
2638
|
+
* escalation exhausted the `strong` ceiling (`escalation-exhausted`) or the
|
|
2639
|
+
* fail-closed selector left no compliant backend (`privacy-no-match`). Both ride
|
|
2640
|
+
* the SAME `needs-human` mechanism as every other escalation. The `RoutingError`
|
|
2641
|
+
* code disambiguates the two on the steward's channel. The coherence unit is the
|
|
2642
|
+
* issue id (D6 issue-grain pinning); title/description are recovered from running
|
|
2643
|
+
* state when still present. Fire-and-forget + `.catch` — a queue write must never
|
|
2644
|
+
* block or throw out of the dispatch/outcome path.
|
|
2645
|
+
*/
|
|
2646
|
+
private escalateRoutingToHuman;
|
|
2647
|
+
/**
|
|
2648
|
+
* AMR dispatch-boundary routing-failure handler (finding #3 + live-wiring
|
|
2649
|
+
* review blocker). When `AdaptiveRouter.route()` throws a fail-closed
|
|
2650
|
+
* `PrivacyNoMatch` (`RoutingError` code `'privacy-no-match'`) at dispatch, that
|
|
2651
|
+
* distinct signal MUST NOT be swallowed by the generic transport/dispatch-error
|
|
2652
|
+
* path (S4-001): it is not a runner/transport failure, so it must never be
|
|
2653
|
+
* recorded as one or feed the vertical escalation breaker. Instead it emits a
|
|
2654
|
+
* DISTINCT `routing:no-tier-match` steward escalation (needs-human, same
|
|
2655
|
+
* mechanism as `onExhausted`) carrying the coherence unit + reason.
|
|
2656
|
+
*
|
|
2657
|
+
* It is ALSO deterministic — the `privacyFloor`/allowlist that emptied the
|
|
2658
|
+
* candidate set is config-driven, so re-dispatch would throw the SAME
|
|
2659
|
+
* `PrivacyNoMatch`. Therefore this path is TERMINAL: it drives the unit to the
|
|
2660
|
+
* `canceled` lane and removes it from `running`/`claimed` directly, rather than
|
|
2661
|
+
* routing through `emitWorkerExit('error')` (whose state-machine error branch
|
|
2662
|
+
* enqueues a retry whenever the retry budget is not yet exhausted — which would
|
|
2663
|
+
* re-dispatch, re-fail closed, and re-escalate up to `maxRetries` times). No
|
|
2664
|
+
* retry is scheduled, no transport outcome is recorded, and exactly one
|
|
2665
|
+
* needs-human escalation is queued. Fail-closed is preserved — `route()` already
|
|
2666
|
+
* refused to pick a non-compliant backend, and returning `true` here stops the
|
|
2667
|
+
* caller from falling through to any further routing.
|
|
2668
|
+
*
|
|
2669
|
+
* Returns `true` when the boundary CLAIMED the error (`privacy-no-match` or the
|
|
2670
|
+
* hard-cap `budget-exhausted`), so the caller returns without ANY
|
|
2671
|
+
* `emitWorkerExit`. Returns `false` for any other error (including
|
|
2672
|
+
* `escalation-exhausted`, which the `onExhausted` seam owns) so the generic
|
|
2673
|
+
* dispatch-error path runs unchanged.
|
|
2674
|
+
*/
|
|
2675
|
+
private handleRoutingFailure;
|
|
2676
|
+
/**
|
|
2677
|
+
* AMR live-wiring review blocker: terminally retire a unit whose dispatch failed
|
|
2678
|
+
* closed (`privacy-no-match`). Mirrors the terminal side of a worker exit —
|
|
2679
|
+
* remove the unit from `running` and release its `claimed` slot — then persist
|
|
2680
|
+
* the terminal `canceled` lane (`abandon`), matching how retry-exhausted
|
|
2681
|
+
* escalations settle. Crucially it does NOT run the state-machine `worker_exit`
|
|
2682
|
+
* reducer, so no `scheduleRetry` effect is emitted. Best-effort lane persistence
|
|
2683
|
+
* (`persistLaneSafe` never throws). No transport/escalation outcome is recorded —
|
|
2684
|
+
* that stays the sole job of the single `routing:no-tier-match` escalation already
|
|
2685
|
+
* queued by `escalateRoutingToHuman`.
|
|
2686
|
+
*/
|
|
2687
|
+
private finalizeRoutingTerminal;
|
|
2688
|
+
/**
|
|
2689
|
+
* split-routing Phase 4 (D6/SC5) — terminal SUCCESS settle for a workflow unit.
|
|
2690
|
+
* The real `WorkflowEngineContext.emitWorkflowSuccess` forwards here (bound via
|
|
2691
|
+
* the context's `settleSuccess` dep in `dispatchIssue`). It reproduces the
|
|
2692
|
+
* `worker_exit`/`reason==='normal'` reducer BY HAND (state-machine.ts:457,467-474):
|
|
2693
|
+
* `running.delete` → `completed.set(now)` → `claimed.delete` → `cleanWorkspace`
|
|
2694
|
+
* effect → then persists the terminal `success` lane and emits one state change.
|
|
2695
|
+
*
|
|
2696
|
+
* It deliberately does NOT route through `emitWorkerExit`/`handleWorkerExit`
|
|
2697
|
+
* (completion/handler.ts): that fires the ISSUE-keyed `finishRecording(issueId,
|
|
2698
|
+
* attempt)` + `recordAmrOutcome`, but the engine already ran PER-STAGE recorders
|
|
2699
|
+
* (`stageAttemptKey(index, attempt)`) and per-stage `recordOutcome`. Going through
|
|
2700
|
+
* the worker-exit path would (a) finish a recording never started at the
|
|
2701
|
+
* issue-attempt key and (b) double-feed the escalation state. This is the ONE
|
|
2702
|
+
* hand-reproduced reducer sequence in Phase 4; the `worker_exit` reducer itself
|
|
2703
|
+
* stays untouched (Task 12 pins it) so the two remain in sync.
|
|
2704
|
+
*
|
|
2705
|
+
* `runs` are the per-stage records (best-effort telemetry; the per-stage cost is
|
|
2706
|
+
* already attributable via the recorders). Never throws — a success settle must
|
|
2707
|
+
* complete the single terminal transition (D6).
|
|
2708
|
+
*/
|
|
2709
|
+
private settleWorkflowSuccess;
|
|
2710
|
+
/**
|
|
2711
|
+
* split-routing Phase 4 (D6/I1/SC5) — terminal FAILURE/safety-net settle for a
|
|
2712
|
+
* workflow unit. The real context's `finalizeWorkflowTerminal` forwards here
|
|
2713
|
+
* (bound via `settleTerminal`). Composed from the `finalizeRoutingTerminal`
|
|
2714
|
+
* pattern (`running.delete` + `claimed.delete` + `persistLaneSafe('abandon')`,
|
|
2715
|
+
* orchestrator.ts:2388-2394) PLUS a single `needs-human` escalation
|
|
2716
|
+
* (escalateRoutingToHuman-style queue push, :2301-2316) PLUS `cleanWorkspace`
|
|
2717
|
+
* (S5). It must NEVER rethrow — the engine's `catch` calls it on the I1 safety
|
|
2718
|
+
* net, so a throw here would defeat the single-exit guarantee.
|
|
2719
|
+
*
|
|
2720
|
+
* It is NOT a verbatim `finalizeRoutingTerminal` call (that lacks the needs-human
|
|
2721
|
+
* + cleanWorkspace the Phase-3 terminal contract pinned). Exactly one needs-human
|
|
2722
|
+
* per terminal transition.
|
|
2723
|
+
*/
|
|
2724
|
+
private settleWorkflowTerminal;
|
|
1961
2725
|
/**
|
|
1962
2726
|
* Hermes Phase 3: wire in-process notification sinks against the
|
|
1963
2727
|
* orchestrator's event bus (`this`). A misconfigured sink (unknown kind,
|
|
@@ -1993,6 +2757,14 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1993
2757
|
* `store.load()`.
|
|
1994
2758
|
*/
|
|
1995
2759
|
private initModelPool;
|
|
2760
|
+
/**
|
|
2761
|
+
* Resume installs interrupted by a restart. A proposal left `installing` had
|
|
2762
|
+
* its background `ollama pull` cut short when the orchestrator went down; the
|
|
2763
|
+
* pull is idempotent (ollama resumes from cached blobs), so we re-drive it.
|
|
2764
|
+
* Fire-and-forget with its own error isolation — a resumed multi-GB download
|
|
2765
|
+
* must not block startup, and a re-drive failure only logs.
|
|
2766
|
+
*/
|
|
2767
|
+
private redriveInterruptedInstalls;
|
|
1996
2768
|
/**
|
|
1997
2769
|
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
1998
2770
|
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
@@ -2015,6 +2787,17 @@ declare class Orchestrator extends EventEmitter {
|
|
|
2015
2787
|
* breadth is the only piece deferred to the Phase 2 recommender.
|
|
2016
2788
|
*/
|
|
2017
2789
|
private startRefreshScheduler;
|
|
2790
|
+
/** (Re)build the recommender over `candidates` and record the seeding source. */
|
|
2791
|
+
private seedRecommender;
|
|
2792
|
+
/**
|
|
2793
|
+
* Refresh ranking candidates live from HuggingFace, merge the curated
|
|
2794
|
+
* `ollamaName`/`family` tags from the frozen snapshot (so results stay
|
|
2795
|
+
* installable — decision A), and re-seed the recommender. Fail-closed: on any
|
|
2796
|
+
* error or an empty installable result, the current candidates stand. Runs a
|
|
2797
|
+
* `forceRefresh` tick so recommendations + proposals reflect the fresh set.
|
|
2798
|
+
* Used by both the startup background refresh and the operator "Refresh" button.
|
|
2799
|
+
*/
|
|
2800
|
+
private refreshCandidatesLive;
|
|
2018
2801
|
/** Resolve the hardware profile for a refresh tick (operator override wins). */
|
|
2019
2802
|
private detectLmlmHardware;
|
|
2020
2803
|
/** Map the on-disk model-proposal queue to F7 dedup pairs (open→pending, rejected→rejected). */
|
|
@@ -2041,6 +2824,51 @@ declare class Orchestrator extends EventEmitter {
|
|
|
2041
2824
|
* single-backend config bypassed agent.backends synthesis.
|
|
2042
2825
|
*/
|
|
2043
2826
|
getRoutingDecisionBus(): RoutingDecisionBus | null;
|
|
2827
|
+
/**
|
|
2828
|
+
* AMR Phase 3 (D11): the opt-in adaptive router, or `null` when no
|
|
2829
|
+
* `routing.policy` is configured (the default-off path). Exposed for the
|
|
2830
|
+
* SC8/SC17/SC19 default-off proof: `null` here means dispatch stays on the
|
|
2831
|
+
* shipped `BackendRouter`, byte-identical, with no classify()/telemetry.
|
|
2832
|
+
*/
|
|
2833
|
+
getAdaptiveRouter(): AdaptiveRouter | null;
|
|
2834
|
+
/**
|
|
2835
|
+
* AMR Phase 3 (D11) / Phase 5 (D1): construct the opt-in AdaptiveRouter for a
|
|
2836
|
+
* policy. Extracted from the constructor so runtime ingestion
|
|
2837
|
+
* (`ingestRoutingPolicy`) builds a router IDENTICAL to the constructor's —
|
|
2838
|
+
* same live classify seam, strong-cap escalation-exhaustion hard-fail-to-human
|
|
2839
|
+
* (D10), and enriched-decision bus (SC9). Precondition: the routing subsystem
|
|
2840
|
+
* exists (`backendFactory` + `agent.backends` present); callers guard.
|
|
2841
|
+
*/
|
|
2842
|
+
private buildAdaptiveRouter;
|
|
2843
|
+
/**
|
|
2844
|
+
* AMR Phase 5 (D1/D5): ingest a routing policy pushed at runtime by the
|
|
2845
|
+
* Shuttle control plane (`PUT /api/v1/routing/policy`). Hot-swaps the live
|
|
2846
|
+
* router:
|
|
2847
|
+
* - empty policy (`{}` / no activating fields) → `adaptiveRouter = null`
|
|
2848
|
+
* (default-off restored, D5 — byte-identical dispatch resumes);
|
|
2849
|
+
* - an existing router → `setPolicy` (preserves the accumulated
|
|
2850
|
+
* `EscalationState` climbed floors — a policy edit must not reset them);
|
|
2851
|
+
* - no router yet → construct one from the pushed policy.
|
|
2852
|
+
*
|
|
2853
|
+
* The field-swap is atomic between `await`s (single-threaded): a dispatch that
|
|
2854
|
+
* already captured the router finishes on it; the next dispatch sees the new
|
|
2855
|
+
* policy. No-op-safe when the routing subsystem is absent (`backendFactory`
|
|
2856
|
+
* null) — the caller (`PUT` handler) reports 503 in that case, so this path is
|
|
2857
|
+
* reached only when routing is available.
|
|
2858
|
+
*/
|
|
2859
|
+
ingestRoutingPolicy(policy: RoutingPolicy): void;
|
|
2860
|
+
/**
|
|
2861
|
+
* AMR Phase 5 (D2): project the live router's enriched decision ring into the
|
|
2862
|
+
* Shuttle telemetry wire shape (`GET /api/v1/routing/telemetry`). Returns an
|
|
2863
|
+
* empty payload when routing is off (no router) — a safe, idempotent read.
|
|
2864
|
+
*/
|
|
2865
|
+
getRoutingTelemetry(): RoutingTelemetry;
|
|
2866
|
+
/**
|
|
2867
|
+
* AMR observability: the live operator status (budget spend-vs-cap, escalated
|
|
2868
|
+
* units, allowlist), or an inactive payload when AMR is off. Backs
|
|
2869
|
+
* `GET /api/v1/routing/status`.
|
|
2870
|
+
*/
|
|
2871
|
+
getRoutingStatus(): RoutingStatus;
|
|
2044
2872
|
/**
|
|
2045
2873
|
* Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
|
|
2046
2874
|
* dispatch path uses the factory-owned router directly; observability
|
|
@@ -2079,102 +2907,70 @@ declare function launchTUI(orchestrator: Orchestrator): {
|
|
|
2079
2907
|
};
|
|
2080
2908
|
|
|
2081
2909
|
/**
|
|
2082
|
-
*
|
|
2910
|
+
* Fail-closed signal: privacy floor / allowlist emptied the candidate set (S4-001).
|
|
2911
|
+
* Distinguishable from a tier/cost-only exclusion, which returns `undefined`.
|
|
2083
2912
|
*
|
|
2084
|
-
*
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2913
|
+
* Finding #4 — ONE typed error family: `PrivacyNoMatch` stays orchestrator-local
|
|
2914
|
+
* (it is thrown deep in the selector, a layer below the exported types package) but
|
|
2915
|
+
* EXTENDS the exported `RoutingError`, carrying its canonical `'privacy-no-match'`
|
|
2916
|
+
* code. The dispatch boundary can therefore catch it as either `PrivacyNoMatch`
|
|
2917
|
+
* (ergonomic `instanceof`) or `RoutingError` (code-narrowed), and the sibling
|
|
2918
|
+
* `escalation-exhausted` code is emitted via `RoutingError` too — no divergent
|
|
2919
|
+
* surfaces, no dead exported error type. `code` narrows to the literal here.
|
|
2089
2920
|
*/
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
/**
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
* baked into `createBackend`). Returning `undefined` means "no
|
|
2102
|
-
* resolver registered for this name" — the placeholder stays in place.
|
|
2103
|
-
*
|
|
2104
|
-
* This indirection keeps the factory ignorant of `LocalModelResolver`'s
|
|
2105
|
-
* existence and lifecycle while still letting it produce backends that
|
|
2106
|
-
* route through the resolver Map.
|
|
2107
|
-
*/
|
|
2108
|
-
getResolverModelFor?: (backendName: string) => (() => string | null) | undefined;
|
|
2109
|
-
/**
|
|
2110
|
-
* Phase 5: prompt-cache recorder forwarded to Anthropic-capable backends.
|
|
2111
|
-
* Other backends accept-but-ignore. Shared across dispatches so the
|
|
2112
|
-
* `/api/v1/telemetry/cache/stats` endpoint sees the full rolling window.
|
|
2113
|
-
*/
|
|
2114
|
-
cacheMetrics?: CacheMetricsRecorder;
|
|
2115
|
-
/**
|
|
2116
|
-
* Spec B Phase 4 (D8): forwarded to the underlying BackendRouter so
|
|
2117
|
-
* every resolve() during forUseCase / resolveName emits.
|
|
2118
|
-
*/
|
|
2119
|
-
decisionBus?: RoutingDecisionBus;
|
|
2921
|
+
declare class PrivacyNoMatch extends RoutingError {
|
|
2922
|
+
readonly code: "privacy-no-match";
|
|
2923
|
+
constructor(message: string);
|
|
2924
|
+
}
|
|
2925
|
+
interface SelectConstraints {
|
|
2926
|
+
privacyFloor?: PrivacyClass;
|
|
2927
|
+
/** Present ⇒ only these providers allowed. Absent ⇒ all allowed. Empty array ⇒ none. */
|
|
2928
|
+
allowed?: BackendDef['type'][];
|
|
2929
|
+
needsVision?: boolean;
|
|
2930
|
+
needsToolUse?: boolean;
|
|
2931
|
+
minContextTokens?: number;
|
|
2120
2932
|
}
|
|
2121
2933
|
/**
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2934
|
+
* D1 core: filter the registry to backends with tier ≥ requiredTier, privacyClass
|
|
2935
|
+
* at least as strong as the floor, provider in the allowlist, and capabilities ⊇
|
|
2936
|
+
* required (vision/toolUse/minContextTokens); sort by costPer1kTokens ascending;
|
|
2937
|
+
* return the cheapest.
|
|
2124
2938
|
*
|
|
2125
|
-
*
|
|
2126
|
-
*
|
|
2127
|
-
*
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2939
|
+
* Fail-closed (S4-001): if a privacy-floor OR allowlist constraint empties the set,
|
|
2940
|
+
* throw PrivacyNoMatch (the item must surface to the steward — never fall through
|
|
2941
|
+
* to identity routing at a non-compliant backend). A tier/cost-only exclusion is
|
|
2942
|
+
* best-effort: return undefined so the caller can fall back to the shipped router's
|
|
2943
|
+
* identity/default chain. No `if (local)` anywhere.
|
|
2130
2944
|
*/
|
|
2131
|
-
declare
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
*/
|
|
2161
|
-
getRouter(): BackendRouter;
|
|
2162
|
-
forUseCase(useCase: RoutingUseCase, opts?: {
|
|
2163
|
-
invocationOverride?: string;
|
|
2164
|
-
}): AgentBackend;
|
|
2165
|
-
/**
|
|
2166
|
-
* Rebuild a `local`/`pi` backend with a resolver-bound `getModel`,
|
|
2167
|
-
* mirroring `createBackend`'s local/pi branches but substituting the
|
|
2168
|
-
* head-of-array placeholder with the orchestrator-owned resolver.
|
|
2169
|
-
*/
|
|
2170
|
-
private buildLocalLikeWithResolver;
|
|
2171
|
-
/**
|
|
2172
|
-
* Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret
|
|
2173
|
-
* backend per call so each dispatch sees a fresh container handle map
|
|
2174
|
-
* (ContainerBackend keeps its own per-instance Map<sessionId, handle>).
|
|
2175
|
-
*/
|
|
2176
|
-
private wrapInContainer;
|
|
2177
|
-
}
|
|
2945
|
+
declare function selectCheapestQualifying(registry: BackendCapabilityRegistry, requiredTier: CapabilityTier, constraints: SelectConstraints,
|
|
2946
|
+
/**
|
|
2947
|
+
* Provider lookup by name, required to enforce a `constraints.allowed` allowlist.
|
|
2948
|
+
* If `allowed` is set but `providerOf` is absent (or returns `undefined` for an
|
|
2949
|
+
* entry), that entry cannot be admitted → excluded; an allowlist with no
|
|
2950
|
+
* `providerOf` therefore excludes ALL candidates and fails closed
|
|
2951
|
+
* (`PrivacyNoMatch`). Phase 3 (`AdaptiveRouter`) MUST derive this from
|
|
2952
|
+
* `agent.backends` whenever it sets an allowlist. Absent `allowed` ⇒ unused.
|
|
2953
|
+
*/
|
|
2954
|
+
providerOf?: (name: string) => BackendDef['type'] | undefined): {
|
|
2955
|
+
name: string;
|
|
2956
|
+
capabilities: BackendCapabilities;
|
|
2957
|
+
} | undefined;
|
|
2958
|
+
/** Default capability block derived for an LMLM pool candidate that carries no
|
|
2959
|
+
* explicit capabilities. On-device ⇒ strongest privacy, zero marginal cost.
|
|
2960
|
+
* Seed values (tunable in later phases); a candidate is thus visible to tier
|
|
2961
|
+
* selection (spec "Failure modes": a backend with NO capabilities is invisible,
|
|
2962
|
+
* but a pool candidate is always given a derived block so it can win on cost). */
|
|
2963
|
+
declare function defaultPoolCapabilities(): BackendCapabilities;
|
|
2964
|
+
/**
|
|
2965
|
+
* Build the tier-selection registry (name → capabilities) from configured
|
|
2966
|
+
* `agent.backends` (their `capabilities` blocks, when present) merged with LMLM
|
|
2967
|
+
* pool candidates (each given a derived on-device/zero-cost block). A configured
|
|
2968
|
+
* backend WITHOUT a `capabilities` block is omitted — invisible to tier selection,
|
|
2969
|
+
* reachable only via identity routing (spec "Failure modes"). No LMLM code changes.
|
|
2970
|
+
*/
|
|
2971
|
+
declare function buildCapabilityRegistry(backends: Record<string, BackendDef>, pool?: PoolStateProvider): BackendCapabilityRegistry;
|
|
2972
|
+
|
|
2973
|
+
declare function estimateCost(def: BackendDef, _req: RoutingRequest): number;
|
|
2178
2974
|
|
|
2179
2975
|
/**
|
|
2180
2976
|
* Result of running `migrateAgentConfig`.
|
|
@@ -2451,6 +3247,26 @@ interface LocalModelResolverOptions {
|
|
|
2451
3247
|
poolState?: PoolStateProvider;
|
|
2452
3248
|
/** Probe cadence in ms; default 30_000, minimum 1_000. */
|
|
2453
3249
|
probeIntervalMs?: number;
|
|
3250
|
+
/** Debounce window for event-driven {@link LocalModelResolver.refresh}; default 250ms. */
|
|
3251
|
+
refreshDebounceMs?: number;
|
|
3252
|
+
/**
|
|
3253
|
+
* Consumption Phase 5 (T19/T20): called with the newly-resolved model name
|
|
3254
|
+
* whenever the resolver's composite selection changes (and is non-null), so
|
|
3255
|
+
* the orchestrator can warm it into VRAM before the next dispatch. Naturally
|
|
3256
|
+
* debounced — it fires only on an actual selection change, not every probe.
|
|
3257
|
+
* Best-effort: a throwing hook is swallowed. Injected in tests; wired to
|
|
3258
|
+
* {@link defaultWarmModel} in production.
|
|
3259
|
+
*/
|
|
3260
|
+
warmModel?: (ollamaName: string) => void;
|
|
3261
|
+
/**
|
|
3262
|
+
* Consumption Phase 3 (T10): consecutive inference failures before a model is
|
|
3263
|
+
* deprioritized by the circuit breaker. Default 3.
|
|
3264
|
+
*/
|
|
3265
|
+
breakerThreshold?: number;
|
|
3266
|
+
/** Cooldown (ms) after which a tripped model is eligible again. Default 60_000. */
|
|
3267
|
+
breakerCooldownMs?: number;
|
|
3268
|
+
/** Injectable clock (ms since epoch) for the circuit-breaker cooldown; default `Date.now`. Test seam. */
|
|
3269
|
+
now?: () => number;
|
|
2454
3270
|
/**
|
|
2455
3271
|
* Per-request timeout for the default fetch implementation, in ms.
|
|
2456
3272
|
* Default: 5_000. Ignored when a custom `fetchModels` is provided
|
|
@@ -2482,8 +3298,21 @@ declare class LocalModelResolver {
|
|
|
2482
3298
|
private readonly poolState?;
|
|
2483
3299
|
private readonly probeIntervalMs;
|
|
2484
3300
|
private readonly fetchModels;
|
|
3301
|
+
private readonly warmModel?;
|
|
2485
3302
|
private readonly logger;
|
|
2486
3303
|
private timer;
|
|
3304
|
+
/** Debounce handle for event-driven {@link refresh}; coalesces a burst into one probe. */
|
|
3305
|
+
private refreshTimer;
|
|
3306
|
+
/** Test seam for the refresh debounce delay (0 in tests → next-tick probe). */
|
|
3307
|
+
private readonly refreshDebounceMs;
|
|
3308
|
+
/** Consumption Phase 3 (T10): circuit-breaker config + per-model failure/trip state. */
|
|
3309
|
+
private readonly breakerThreshold;
|
|
3310
|
+
private readonly breakerCooldownMs;
|
|
3311
|
+
private readonly now;
|
|
3312
|
+
/** Consecutive inference failures per model since its last success. */
|
|
3313
|
+
private consecutiveFailures;
|
|
3314
|
+
/** Epoch-ms at which a tripped model becomes eligible again (cooldown expiry). */
|
|
3315
|
+
private trippedUntil;
|
|
2487
3316
|
private listeners;
|
|
2488
3317
|
/**
|
|
2489
3318
|
* Tracks an in-flight probe so concurrent invocations (interval tick while a
|
|
@@ -2502,7 +3331,15 @@ declare class LocalModelResolver {
|
|
|
2502
3331
|
private warnings;
|
|
2503
3332
|
private available;
|
|
2504
3333
|
constructor(opts: LocalModelResolverOptions);
|
|
2505
|
-
|
|
3334
|
+
/**
|
|
3335
|
+
* The model to dispatch to. With no `useCase`, returns the cached composite
|
|
3336
|
+
* resolution from the last probe (byte-identical to the pre-Phase-4 resolver).
|
|
3337
|
+
* With a `useCase`, orders the (pool-derived) candidates by that use-case's
|
|
3338
|
+
* task profile and picks the best loaded, breaker-healthy one — so a
|
|
3339
|
+
* coding-tagged dispatch prefers the coding specialist. A `general`-mapped
|
|
3340
|
+
* use-case returns the cached composite resolution unchanged.
|
|
3341
|
+
*/
|
|
3342
|
+
resolveModel(useCase?: RoutingUseCase): string | null;
|
|
2506
3343
|
getStatus(): LocalModelStatus;
|
|
2507
3344
|
/**
|
|
2508
3345
|
* Effective candidate list. With a poolState port present the list derives
|
|
@@ -2513,6 +3350,41 @@ declare class LocalModelResolver {
|
|
|
2513
3350
|
onStatusChange(handler: (status: LocalModelStatus) => void): () => void;
|
|
2514
3351
|
probe(): Promise<LocalModelStatus>;
|
|
2515
3352
|
private runProbe;
|
|
3353
|
+
/** Best-effort warm-hook invocation — a throwing warm never breaks a probe. */
|
|
3354
|
+
private warm;
|
|
3355
|
+
/**
|
|
3356
|
+
* Consumption Phase 3 (T10): pick the resolved model from the candidates that
|
|
3357
|
+
* are actually loaded, preferring ones whose circuit breaker is not tripped.
|
|
3358
|
+
* Candidate order (score-desc from the pool) is preserved within each tier, so
|
|
3359
|
+
* a tripped top pick sinks below a healthy lower pick but still resolves as a
|
|
3360
|
+
* last resort when every loaded candidate is tripped (better a flaky model than
|
|
3361
|
+
* none). Returns null when no candidate is loaded.
|
|
3362
|
+
*/
|
|
3363
|
+
private selectMatch;
|
|
3364
|
+
/**
|
|
3365
|
+
* Whether `model`'s circuit breaker is currently tripped. Lazily clears the
|
|
3366
|
+
* trip once its cooldown has elapsed so the model becomes eligible again on the
|
|
3367
|
+
* next resolution without needing a separate timer.
|
|
3368
|
+
*/
|
|
3369
|
+
private isTripped;
|
|
3370
|
+
/**
|
|
3371
|
+
* Record a successful inference on `model`: clears its failure count and any
|
|
3372
|
+
* active trip so a recovered model is immediately re-preferred.
|
|
3373
|
+
*/
|
|
3374
|
+
recordSuccess(model: string): void;
|
|
3375
|
+
/**
|
|
3376
|
+
* Record a failed inference on `model`. On the Nth consecutive failure the
|
|
3377
|
+
* breaker trips (deprioritizing the model for `breakerCooldownMs`) and a
|
|
3378
|
+
* re-probe is scheduled so the resolver rolls to a healthy alternative.
|
|
3379
|
+
*/
|
|
3380
|
+
recordFailure(model: string): void;
|
|
3381
|
+
/**
|
|
3382
|
+
* Event-driven refresh: schedule a debounced re-probe. Called when a
|
|
3383
|
+
* `local-models:pool` mutation fires so a just-installed/swapped model becomes
|
|
3384
|
+
* usable within a debounce window instead of waiting up to `probeIntervalMs`.
|
|
3385
|
+
* A burst of frames coalesces into one probe (the timer is only armed once).
|
|
3386
|
+
*/
|
|
3387
|
+
refresh(): void;
|
|
2516
3388
|
start(): Promise<void>;
|
|
2517
3389
|
stop(): void;
|
|
2518
3390
|
private snapshotForDiff;
|
|
@@ -3663,4 +4535,4 @@ declare function emitProposalCreated(bus: EventEmitter, proposal: SkillProposal)
|
|
|
3663
4535
|
declare function emitProposalApproved(bus: EventEmitter, proposal: SkillProposal): void;
|
|
3664
4536
|
declare function emitProposalRejected(bus: EventEmitter, proposal: SkillProposal): void;
|
|
3665
4537
|
|
|
3666
|
-
export { type AgentDispatchResult, type AgentDispatcher, type AgentDispatcherDeps, type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BUILT_IN_TASKS, BackendDefSchema, type BackendResolver, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, type CheckCommandResult, type CheckCommandRunner, type CheckFailureClassification, type CheckFailureKind, CheckScriptRunner, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CommandExecResult, type CommandExecutor, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileAsyncFn, type ExecFileError, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, type HarnessSpawn, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, LinearGraphQLClient, type LinearGraphQLClientOptions, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAINTENANCE_CHECK_MAX_BUFFER, MAINTENANCE_CHECK_TIMEOUT_MS, MAX_ATTEMPTS, MaintenanceReporter, type MaintenanceReporterOptions, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PRLifecycleManager, type PendingInteraction, type PersistedOutputEntry, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunHarnessCheckOptions, type RunMode, type RunOrigin, type RunResult, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, TaskRunner, type TaskRunnerOptions, type TaskSelectionFilter, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, calculateRetryDelay, canDispatch, classifyCheckExecutionFailure, computeRateLimitDelay, createAgentDispatcher, createBackend, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, deriveSeedPaths, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, explicitFindingsCount, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isCheckTimeoutError, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, makeBackendResolver, migrateAgentConfig, normalizeFts5Query, normalizeHarnessCommand, normalizeLocalModel, openSearchIndex, promote, reconcile, recoverFindingsCount, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, routingWarnings, runGate, runHarnessCheck, savePublishedIndex, searchIndexPath, selectCandidates, selectTasks, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateCustomTasks, validateWorkflowConfig, wireNotificationSinks, wrapAsEnvelope };
|
|
4538
|
+
export { AdaptiveRouter, type AdaptiveRouterDeps, type AgentDispatchResult, type AgentDispatcher, type AgentDispatcherDeps, type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BUILT_IN_TASKS, BackendDefSchema, type BackendResolver, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, type BuildWorkflowContextDeps, type CheckCommandResult, type CheckCommandRunner, type CheckFailureClassification, type CheckFailureKind, CheckScriptRunner, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CommandExecResult, type CommandExecutor, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, type DispatchEffect, type EmitLogEffect, type EscalateEffect, EscalationState, type ExecFileAsyncFn, type ExecFileError, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, type HarnessSpawn, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, LinearGraphQLClient, type LinearGraphQLClientOptions, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAINTENANCE_CHECK_MAX_BUFFER, MAINTENANCE_CHECK_TIMEOUT_MS, MAX_ATTEMPTS, MaintenanceReporter, type MaintenanceReporterOptions, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PRLifecycleManager, type PendingInteraction, type PersistedOutputEntry, PrivacyNoMatch, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunHarnessCheckOptions, type RunMode, type RunOrigin, type RunResult, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SelectConstraints, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, TaskRunner, type TaskRunnerOptions, type TaskSelectionFilter, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, WebhookQueue, type WorkerExitEvent, WorkflowLoader, type WorkflowRouterDep, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, buildCapabilityRegistry, buildWorkflowContext, calculateRetryDelay, canDispatch, classifyCheckExecutionFailure, computeRateLimitDelay, createAgentDispatcher, createBackend, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, defaultPoolCapabilities, deriveSeedPaths, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, estimateCost, explicitFindingsCount, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isCheckTimeoutError, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, makeBackendResolver, migrateAgentConfig, normalizeFts5Query, normalizeHarnessCommand, normalizeLocalModel, openSearchIndex, promote, reconcile, recoverFindingsCount, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, routingWarnings, runGate, runHarnessCheck, savePublishedIndex, searchIndexPath, selectCandidates, selectCheapestQualifying, selectTasks, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateCustomTasks, validateWorkflowConfig, wireNotificationSinks, workflowFor, wrapAsEnvelope };
|