Skip to content

Commit 7dc4419

Browse files
chore: sync public mirror from internal
1 parent 799acd9 commit 7dc4419

15 files changed

Lines changed: 681 additions & 39 deletions

File tree

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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
export function recommendOracleConsultation(
36+
input: OracleConsultationPolicyInput,
37+
): OracleConsultationDecision {
38+
const reasons: string[] = [];
39+
const priorFailures = Math.max(0, Math.floor(input.priorFailures ?? 0));
40+
let mode: OracleConsultationMode = "available";
41+
42+
if (input.profileLevel === "ultra") {
43+
mode = "required";
44+
reasons.push("ultra_profile");
45+
} else if (input.profileLevel === "high") {
46+
mode = "recommended";
47+
reasons.push("high_profile");
48+
}
49+
50+
if (CONSULTATION_TASK_TYPES.has(input.taskType.trim().toLowerCase())) {
51+
if (mode === "available") mode = "recommended";
52+
reasons.push("consultation_task_type");
53+
}
54+
55+
if (input.taskSummary && UNCERTAINTY_PATTERN.test(input.taskSummary)) {
56+
if (mode === "available") mode = "recommended";
57+
reasons.push("uncertainty_signal");
58+
}
59+
60+
if (priorFailures >= 2) {
61+
mode = input.profileLevel === "low" ? "recommended" : "required";
62+
reasons.push("repeated_failures");
63+
}
64+
65+
if (reasons.length === 0) reasons.push("oracle_available_on_demand");
66+
return {
67+
policyVersion: ORACLE_CONSULTATION_POLICY_VERSION,
68+
evalSuite: "oracle-consultation-policy-v1",
69+
mode,
70+
reasons,
71+
};
72+
}
73+
74+
export function formatOracleConsultationDirective(
75+
decision: OracleConsultationDecision,
76+
): string {
77+
const instruction =
78+
decision.mode === "required"
79+
? "You MUST consult the read-only Oracle once before committing to the plan or final answer. Incorporate or explicitly rebut its advice."
80+
: decision.mode === "recommended"
81+
? "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."
82+
: "The read-only Oracle is available on demand.";
83+
return [
84+
`Oracle consultation policy (${decision.policyVersion})`,
85+
instruction,
86+
`Triggers: ${decision.reasons.join(", ")}.`,
87+
].join("\n");
88+
}
89+
90+
export function applyOracleConsultationDirective(
91+
agent: { queueNextRunSystemPromptAddition(text: string): void },
92+
decision: OracleConsultationDecision | undefined,
93+
): boolean {
94+
if (!decision || decision.mode === "available") return false;
95+
agent.queueNextRunSystemPromptAddition(
96+
formatOracleConsultationDirective(decision),
97+
);
98+
return true;
99+
}

src/server/handlers/chat-ws.ts

Lines changed: 92 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,12 @@ 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 {
49+
type RequestContext,
50+
getWorkspaceConfigContext,
51+
parseTraceParent,
52+
requestContextStorage,
53+
} from "../request-context.js";
4054
import { serverRequestManager } from "../server-request-manager.js";
4155
import { getRequestHeader } from "../server-utils.js";
4256
import { startSessionWithPolicy } from "../session-initialization.js";
@@ -219,6 +233,7 @@ export function handleChatWebSocket(
219233
ws: WebSocket,
220234
req: IncomingMessage,
221235
context: WebServerContext,
236+
workspaceConfig?: WorkspaceConfigRequestContext,
222237
) {
223238
const {
224239
createAgent,
@@ -245,6 +260,17 @@ export function handleChatWebSocket(
245260
);
246261
const slimFromQuery = parseBoolean(url.searchParams.get("slim"));
247262
const clientHeaderFromQuery = url.searchParams.get("client")?.trim();
263+
const websocketRequestContext: RequestContext | undefined = workspaceConfig
264+
? {
265+
requestId: randomUUID(),
266+
traceId: parseTraceParent(req.headers.traceparent).traceId,
267+
spanId: randomBytes(8).toString("hex"),
268+
startTime: performance.now(),
269+
method: req.method || "GET",
270+
url: url.pathname,
271+
workspaceConfig,
272+
}
273+
: undefined;
248274

249275
const sendErrorAndClose = (message: string) => {
250276
try {
@@ -271,7 +297,7 @@ export function handleChatWebSocket(
271297
return validatePayload<ChatRequestInput>(parsed, ChatRequestSchema);
272298
};
273299

274-
ws.on("message", async (data) => {
300+
const handleMessage = async (data: RawData) => {
275301
if (requestHandled) {
276302
try {
277303
const size = getRawDataSize(data);
@@ -450,7 +476,51 @@ export function handleChatWebSocket(
450476
}
451477
}
452478

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

455525
const headerApproval = (() => {
456526
const headerMode = normalizeApprovalMode(
@@ -529,6 +599,10 @@ export function handleChatWebSocket(
529599
: {}),
530600
},
531601
);
602+
applyOracleConsultationDirective(
603+
agent,
604+
routingSelection.decision.oracleConsultation,
605+
);
532606

533607
const historyMessages = incomingMessages.slice(0, -1);
534608
const hydratedHistory = convertComposerMessagesToApp(
@@ -844,6 +918,13 @@ export function handleChatWebSocket(
844918
sessionId: sessionManager.getSessionId(),
845919
subject,
846920
});
921+
recordIntelligentRouterChatMetric({
922+
taskType: routingSelection.taskType,
923+
provider: registeredModel.provider,
924+
model: registeredModel.id,
925+
startedAt: routeStartedAt,
926+
message: event.message,
927+
});
847928
automaticMemoryExtraction.schedule(sessionManager.getSessionFile());
848929
}
849930
sessionManager.updateSnapshot(
@@ -962,5 +1043,14 @@ export function handleChatWebSocket(
9621043
sseLease = null;
9631044
}
9641045
}
1046+
};
1047+
1048+
ws.on("message", (data) => {
1049+
if (websocketRequestContext) {
1050+
return requestContextStorage.run(websocketRequestContext, () =>
1051+
handleMessage(data),
1052+
);
1053+
}
1054+
return handleMessage(data);
9651055
});
9661056
}

src/server/handlers/chat.ts

Lines changed: 5 additions & 0 deletions
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,
@@ -434,6 +435,10 @@ export async function handleChat(
434435
: {}),
435436
},
436437
);
438+
applyOracleConsultationDirective(
439+
agent,
440+
routingSelection.decision.oracleConsultation,
441+
);
437442

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

src/services/intelligent-router/normalize.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,28 @@ export function normalizeRoutingRequest(
182182
const availableModels = normalizeAvailableModels(
183183
requestInput.availableModels,
184184
);
185+
const taskSummary =
186+
cleanOptionalString(requestInput.taskSummary) ??
187+
cleanOptionalString(requestInput.task_summary);
188+
const priorFailures = Math.max(
189+
0,
190+
Math.floor(
191+
parseFiniteNumber(
192+
requestInput.priorFailures ?? requestInput.prior_failures,
193+
"priorFailures",
194+
) ?? 0,
195+
),
196+
);
185197
return {
186198
taskType,
187199
profileLevel,
188200
strategy: parseRoutingStrategy(requestInput.strategy),
189201
unavailableModels,
190202
availableModels:
191203
availableModels.length > 0 ? availableModels : defaultAvailableModels,
204+
priorFailures,
192205
...(modelHint ? { modelHint } : {}),
206+
...(taskSummary ? { taskSummary } : {}),
193207
};
194208
}
195209

0 commit comments

Comments
 (0)