Skip to content

Commit 811179e

Browse files
chore: sync public mirror from internal (#821)
* chore: sync public mirror from internal * chore: sync public mirror from internal * chore: sync public mirror from internal --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 799acd9 commit 811179e

18 files changed

Lines changed: 761 additions & 40 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Automatic Oracle Consultation Policy Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Automatically and audibly recommend or require the read-only Oracle for tasks where independent reasoning is likely to improve outcomes.
6+
7+
**Architecture:** A pure versioned policy evaluates profile, task type, prompt signals, and prior failures. The intelligent router records the decision, and the chat handler injects explicit next-run guidance; a checked-in eval matrix prevents trigger drift.
8+
9+
**Tech Stack:** TypeScript, Vitest, intelligent router, Agent system-prompt additions.
10+
11+
## Global Constraints
12+
13+
- No hidden tool execution; the agent sees and follows an explicit policy directive.
14+
- Oracle remains read-only and uses the profile’s complementary model.
15+
- Low-risk profiles avoid mandatory Oracle cost.
16+
- Policy behavior must be covered by named eval cases before integration.
17+
18+
---
19+
20+
### Task 1: Versioned consultation policy
21+
22+
**Files:** Create `src/agent/oracle-consultation-policy.ts`; test `test/agent/oracle-consultation-policy.test.ts`.
23+
24+
- [x] Write a failing eval matrix for low, medium, high, ultra, architecture/migration, ambiguity, cross-cutting work, and repeated failures.
25+
- [x] Implement minimal deterministic scoring and prompt directive formatting.
26+
- [x] Verify all eval cases pass.
27+
28+
### Task 2: Router and runtime integration
29+
30+
**Files:** Modify intelligent-router types/normalization/service/recorder and `src/server/handlers/chat.ts`; update focused tests.
31+
32+
- [x] Add failing tests for decision recording and next-run prompt injection.
33+
- [x] Thread task summary and prior failure count through routing.
34+
- [x] Queue policy guidance only for recommended/required modes.
35+
- [ ] Run targeted tests, lint, affected tests, commit, open PR after PR 2 merges, address feedback, merge, and verify deployment/mirror workflows.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { AgentProfileLevel } from "./profiles.js";
2+
3+
export const ORACLE_CONSULTATION_POLICY_VERSION =
4+
"evalops.maestro.oracle-consultation.v1";
5+
6+
export type OracleConsultationMode = "available" | "recommended" | "required";
7+
8+
export interface OracleConsultationPolicyInput {
9+
profileLevel: AgentProfileLevel;
10+
taskType: string;
11+
taskSummary?: string;
12+
priorFailures?: number;
13+
}
14+
15+
export interface OracleConsultationDecision {
16+
policyVersion: typeof ORACLE_CONSULTATION_POLICY_VERSION;
17+
evalSuite: "oracle-consultation-policy-v1";
18+
mode: OracleConsultationMode;
19+
reasons: string[];
20+
}
21+
22+
const CONSULTATION_TASK_TYPES = new Set([
23+
"architecture",
24+
"code_review",
25+
"discovery",
26+
"incident_response",
27+
"migration",
28+
"planning",
29+
"security_review",
30+
]);
31+
32+
const UNCERTAINTY_PATTERN =
33+
/\b(?:ambiguous|unclear|uncertain|trade-?offs?|cross[- ]cutting|multiple approaches|data loss|irreversible|root cause unknown)\b/i;
34+
35+
const CONSULTATION_PROMPT_PATTERN =
36+
/\b(?:architect(?:ure|ural|ing)?|migrat(?:e|es|ed|ing|ion)|security|secure|threat model(?:ing)?|auth(?:entication|orization)?|schema (?:change|migration)|backfill)\b/i;
37+
38+
export function recommendOracleConsultation(
39+
input: OracleConsultationPolicyInput,
40+
): OracleConsultationDecision {
41+
const reasons: string[] = [];
42+
const priorFailures = Math.max(0, Math.floor(input.priorFailures ?? 0));
43+
let mode: OracleConsultationMode = "available";
44+
45+
if (input.profileLevel === "ultra") {
46+
mode = "required";
47+
reasons.push("ultra_profile");
48+
} else if (input.profileLevel === "high") {
49+
mode = "recommended";
50+
reasons.push("high_profile");
51+
}
52+
53+
if (CONSULTATION_TASK_TYPES.has(input.taskType.trim().toLowerCase())) {
54+
if (mode === "available") mode = "recommended";
55+
reasons.push("consultation_task_type");
56+
}
57+
58+
if (input.taskSummary && UNCERTAINTY_PATTERN.test(input.taskSummary)) {
59+
if (mode === "available") mode = "recommended";
60+
reasons.push("uncertainty_signal");
61+
}
62+
63+
if (
64+
input.taskSummary &&
65+
CONSULTATION_PROMPT_PATTERN.test(input.taskSummary)
66+
) {
67+
if (mode === "available") mode = "recommended";
68+
reasons.push("consultation_prompt_signal");
69+
}
70+
71+
if (priorFailures >= 2) {
72+
mode = input.profileLevel === "low" ? "recommended" : "required";
73+
reasons.push("repeated_failures");
74+
}
75+
76+
if (reasons.length === 0) reasons.push("oracle_available_on_demand");
77+
return {
78+
policyVersion: ORACLE_CONSULTATION_POLICY_VERSION,
79+
evalSuite: "oracle-consultation-policy-v1",
80+
mode,
81+
reasons,
82+
};
83+
}
84+
85+
export function formatOracleConsultationDirective(
86+
decision: OracleConsultationDecision,
87+
): string {
88+
const instruction =
89+
decision.mode === "required"
90+
? "You MUST consult the read-only Oracle once before committing to the plan or final answer. Incorporate or explicitly rebut its advice."
91+
: decision.mode === "recommended"
92+
? "Consult the read-only Oracle once before committing to the plan or final answer unless the task has become clearly bounded; if you skip it, state the concrete reason."
93+
: "The read-only Oracle is available on demand.";
94+
return [
95+
`Oracle consultation policy (${decision.policyVersion})`,
96+
instruction,
97+
`Triggers: ${decision.reasons.join(", ")}.`,
98+
].join("\n");
99+
}
100+
101+
export function applyOracleConsultationDirective(
102+
agent: { queueNextRunSystemPromptAddition(text: string): void },
103+
decision: OracleConsultationDecision | undefined,
104+
): boolean {
105+
if (!decision || decision.mode === "available") return false;
106+
agent.queueNextRunSystemPromptAddition(
107+
formatOracleConsultationDirective(decision),
108+
);
109+
return true;
110+
}

src/server/handlers/chat-ws.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
* Mirrors the SSE chat flow but streams AgentEvent payloads over WebSocket.
55
*/
66

7+
import { randomBytes, randomUUID } from "node:crypto";
78
import type { IncomingMessage } from "node:http";
89
import type { ComposerChatRequest, ComposerMessage } from "@evalops/contracts";
910
import type { RawData, WebSocket } from "ws";
11+
import { applyOracleConsultationDirective } from "../../agent/oracle-consultation-policy.js";
1012
import { isAssistantMessage } from "../../agent/type-guards.js";
1113
import type {
1214
Attachment as AgentAttachment,
@@ -22,7 +24,13 @@ import {
2224
} from "../../memory/auto-consolidation.js";
2325
import { createAutomaticMemoryExtractionCoordinator } from "../../memory/auto-extraction.js";
2426
import type { RegisteredModel } from "../../models/registry.js";
27+
import {
28+
recordIntelligentRouterChatMetric,
29+
selectIntelligentRouterModel,
30+
} from "../../services/intelligent-router/recorder.js";
2531
import { recordAssistantUsageMetric } from "../../services/usage-analytics/recorder.js";
32+
import { evaluateModelPolicy } from "../../services/workspace-config/policy.js";
33+
import type { WorkspaceConfigRequestContext } from "../../services/workspace-config/types.js";
2634
import { toSessionModelMetadata } from "../../session/manager.js";
2735
import { createRuntimeSessionSummaryUpdater } from "../../session/runtime-summary-updater.js";
2836
import { createLogger } from "../../utils/logger.js";
@@ -37,6 +45,13 @@ import { getAuthSubject } from "../authz.js";
3745
import { getAgentCircuitBreaker } from "../circuit-breaker.js";
3846
import { clientToolService } from "../client-tools-service.js";
3947
import { isHostedSessionManager } from "../hosted-session-manager.js";
48+
import { resolveModelInputForRouting } from "../model-selection.js";
49+
import {
50+
type RequestContext,
51+
getWorkspaceConfigContext,
52+
parseTraceParent,
53+
requestContextStorage,
54+
} from "../request-context.js";
4055
import { serverRequestManager } from "../server-request-manager.js";
4156
import { getRequestHeader } from "../server-utils.js";
4257
import { startSessionWithPolicy } from "../session-initialization.js";
@@ -219,12 +234,15 @@ export function handleChatWebSocket(
219234
ws: WebSocket,
220235
req: IncomingMessage,
221236
context: WebServerContext,
237+
workspaceConfig?: WorkspaceConfigRequestContext,
222238
) {
223239
const {
224240
createAgent,
225241
createBackgroundAgent,
226242
getRegisteredModel,
227243
defaultApprovalMode,
244+
defaultProvider,
245+
defaultModelId,
228246
acquireSse,
229247
releaseSse,
230248
} = context;
@@ -245,6 +263,17 @@ export function handleChatWebSocket(
245263
);
246264
const slimFromQuery = parseBoolean(url.searchParams.get("slim"));
247265
const clientHeaderFromQuery = url.searchParams.get("client")?.trim();
266+
const websocketRequestContext: RequestContext | undefined = workspaceConfig
267+
? {
268+
requestId: randomUUID(),
269+
traceId: parseTraceParent(req.headers.traceparent).traceId,
270+
spanId: randomBytes(8).toString("hex"),
271+
startTime: performance.now(),
272+
method: req.method || "GET",
273+
url: url.pathname,
274+
workspaceConfig,
275+
}
276+
: undefined;
248277

249278
const sendErrorAndClose = (message: string) => {
250279
try {
@@ -271,7 +300,7 @@ export function handleChatWebSocket(
271300
return validatePayload<ChatRequestInput>(parsed, ChatRequestSchema);
272301
};
273302

274-
ws.on("message", async (data) => {
303+
const handleMessage = async (data: RawData) => {
275304
if (requestHandled) {
276305
try {
277306
const size = getRawDataSize(data);
@@ -450,7 +479,55 @@ export function handleChatWebSocket(
450479
}
451480
}
452481

453-
const registeredModel = await getRegisteredModel(chatReq.model);
482+
const routingSelection = selectIntelligentRouterModel({
483+
req,
484+
requestedModel: resolveModelInputForRouting(
485+
chatReq.model,
486+
defaultProvider,
487+
defaultModelId,
488+
),
489+
body: chatReq,
490+
});
491+
let registeredModel: RegisteredModel | undefined;
492+
let lastModelError: unknown;
493+
let selectedModelError: unknown;
494+
let selectedModelPolicyViolation: ReturnType<
495+
typeof evaluateModelPolicy
496+
> | null = null;
497+
for (const [
498+
index,
499+
modelInput,
500+
] of routingSelection.modelInputs.entries()) {
501+
try {
502+
const candidateModel = await getRegisteredModel(modelInput);
503+
const violation = evaluateModelPolicy(
504+
workspaceConfig?.config ?? getWorkspaceConfigContext()?.config,
505+
{
506+
provider: candidateModel.provider,
507+
modelId: candidateModel.id,
508+
},
509+
);
510+
if (violation) {
511+
if (index === 0) selectedModelPolicyViolation = violation;
512+
continue;
513+
}
514+
registeredModel = candidateModel;
515+
break;
516+
} catch (error) {
517+
lastModelError = error;
518+
if (index === 0) selectedModelError = error;
519+
}
520+
}
521+
if (!registeredModel && selectedModelPolicyViolation) {
522+
sendErrorAndClose(selectedModelPolicyViolation.message);
523+
if (sseLease && releaseSse) {
524+
releaseSse(sseLease);
525+
sseLease = null;
526+
}
527+
return;
528+
}
529+
if (!registeredModel) throw selectedModelError ?? lastModelError;
530+
const routeStartedAt = Date.now();
454531

455532
const headerApproval = (() => {
456533
const headerMode = normalizeApprovalMode(
@@ -529,6 +606,10 @@ export function handleChatWebSocket(
529606
: {}),
530607
},
531608
);
609+
applyOracleConsultationDirective(
610+
agent,
611+
routingSelection.decision.oracleConsultation,
612+
);
532613

533614
const historyMessages = incomingMessages.slice(0, -1);
534615
const hydratedHistory = convertComposerMessagesToApp(
@@ -844,6 +925,13 @@ export function handleChatWebSocket(
844925
sessionId: sessionManager.getSessionId(),
845926
subject,
846927
});
928+
recordIntelligentRouterChatMetric({
929+
taskType: routingSelection.taskType,
930+
provider: registeredModel.provider,
931+
model: registeredModel.id,
932+
startedAt: routeStartedAt,
933+
message: event.message,
934+
});
847935
automaticMemoryExtraction.schedule(sessionManager.getSessionFile());
848936
}
849937
sessionManager.updateSnapshot(
@@ -962,5 +1050,14 @@ export function handleChatWebSocket(
9621050
sseLease = null;
9631051
}
9641052
}
1053+
};
1054+
1055+
ws.on("message", (data) => {
1056+
if (websocketRequestContext) {
1057+
return requestContextStorage.run(websocketRequestContext, () =>
1058+
handleMessage(data),
1059+
);
1060+
}
1061+
return handleMessage(data);
9651062
});
9661063
}

src/server/handlers/chat.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import type { IncomingMessage, ServerResponse } from "node:http";
2525
import type { ComposerChatRequest, ComposerMessage } from "@evalops/contracts";
26+
import { applyOracleConsultationDirective } from "../../agent/oracle-consultation-policy.js";
2627
import { isAssistantMessage } from "../../agent/type-guards.js";
2728
import type {
2829
Attachment as AgentAttachment,
@@ -63,6 +64,7 @@ import { getAuthSubject } from "../authz.js";
6364
import { getAgentCircuitBreaker } from "../circuit-breaker.js";
6465
import { clientToolService } from "../client-tools-service.js";
6566
import { isHostedSessionManager } from "../hosted-session-manager.js";
67+
import { resolveModelInputForRouting } from "../model-selection.js";
6668
import { getWorkspaceConfigContext } from "../request-context.js";
6769
import { serverRequestManager } from "../server-request-manager.js";
6870
import {
@@ -131,6 +133,8 @@ export async function handleChat(
131133
createBackgroundAgent,
132134
getRegisteredModel,
133135
defaultApprovalMode,
136+
defaultProvider,
137+
defaultModelId,
134138
acquireSse,
135139
releaseSse,
136140
corsHeaders: cors,
@@ -319,7 +323,11 @@ export async function handleChat(
319323
// it can select a better-scoring model and expose explicit fallbacks.
320324
const routingSelection = selectIntelligentRouterModel({
321325
req,
322-
requestedModel: chatReq.model,
326+
requestedModel: resolveModelInputForRouting(
327+
chatReq.model,
328+
defaultProvider,
329+
defaultModelId,
330+
),
323331
body: chatReq,
324332
});
325333
let registeredModel: RegisteredModel | undefined;
@@ -434,6 +442,10 @@ export async function handleChat(
434442
: {}),
435443
},
436444
);
445+
applyOracleConsultationDirective(
446+
agent,
447+
routingSelection.decision.oracleConsultation,
448+
);
437449

438450
// Hydrate conversation history (all messages except the current user input)
439451
const historyMessages = incomingMessages.slice(0, -1);

src/server/model-selection.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,19 @@ export function determineModelSelection(
130130
};
131131
}
132132

133+
export function resolveModelInputForRouting(
134+
modelInput: string | null | undefined,
135+
defaultProvider: string,
136+
defaultModelId: string,
137+
): string {
138+
const selection = determineModelSelection(
139+
modelInput,
140+
defaultProvider,
141+
defaultModelId,
142+
);
143+
return `${selection.provider}/${selection.modelId}`;
144+
}
145+
133146
export function getRegisteredModelOrThrow(
134147
selection: ModelSelection,
135148
): RegisteredModel {

0 commit comments

Comments
 (0)