Skip to content

Commit 5270312

Browse files
committed
feat: PR Command Center enhancements, Webhook PR Ingestion, and AI Diagnostics Overhaul
1 parent c185882 commit 5270312

29 files changed

Lines changed: 12824 additions & 942 deletions

.agent/rules/ai-rules.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Rule: AI Provider & Structured Responses
2+
3+
## 1. Structured Output Mandate
4+
5+
- **CRYSTAL CLEAR RULE**: ANYTIME the AI model is being instructed to respond with a structured response (JSON), you **MUST** use `generateStructuredResponse` or `generateStructuredWithTools` exported from `@/ai/providers`.
6+
- **FORBIDDEN**: Do not rely on native Agent SDK schemas (e.g. `outputType: MySchema as any` in `@openai/agents`). These frequently fail to map correctly through the Cloudflare AI Gateway or result in brittle string parsing.
7+
8+
## 2. The Extraction Pattern (Agents with Tools)
9+
10+
If you are running an autonomous Agent that requires tool usage (e.g., `HealthDiagnostician` or `ResearchAgent`):
11+
12+
1. Configure the Agent to output standard text/markdown (`outputType` must NOT be explicitly defined).
13+
2. Await the Agent's `finalOutput` inside the execution loop.
14+
3. Pass that string into `generateStructuredResponse` along with your Zod schema (converted via `zodToJsonSchema`) to strictly extract and type the final JSON object. This ensures Gateway compatibility while guaranteeing Zod-verified JSON.

AGENTS.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,33 @@ console.log(result.text); // Getter, returns string
9898
- `generationConfig` (Use `config` property instead)
9999
- `result.response.text()` (Method call)
100100

101-
## Structured Outputs
101+
## Structured Outputs (MANDATE)
102102

103-
Always use `zod` and `zod-to-json-schema` to define your `responseSchema`.
103+
**CRYSTAL CLEAR RULE**: You MUST use `AiProvider.generateStructuredResponse` (or `generateStructuredWithTools` exported from `@/ai/providers`) _anytime_ the AI model is being instructed to respond with a structured JSON response.
104+
105+
**FORBIDDEN**: Do NOT rely on Agent SDK schema enforcements (e.g., passing `outputType: MySchema as any` to `@openai/agents`), as they are prone to brittle string extraction failures or 400 errors via the Cloudflare AI Gateway.
106+
107+
**Correct Pattern (Agent with Tools):**
108+
109+
1. Let the Agent execute its internal tool loop freely (returning markdown text).
110+
2. Take the Agent's `result.finalOutput` and pass it into `generateStructuredResponse` along with your schema.
104111

105112
```typescript
106-
import { z } from "zod";
113+
import { generateStructuredResponse } from "@/ai/providers";
107114
import { zodToJsonSchema } from "zod-to-json-schema";
115+
import { z } from "zod";
108116

109117
const MySchema = z.object({ ... });
110118

111-
// ... inside generateContent config:
112-
responseSchema: zodToJsonSchema(MySchema) as any
119+
// 1. Let agent run
120+
const result = await runner.run(agent, prompt);
121+
122+
// 2. Extract strictly
123+
const finalData = await generateStructuredResponse<z.infer<typeof MySchema>>(
124+
env,
125+
`Extract the exact data from the Agent's response:\n\n${result.finalOutput}`,
126+
zodToJsonSchema(MySchema as any, "structured_output")
127+
);
113128
```
114129

115130
## AI Provider Routing & Resolution

backend/src/ai/agents/HealthDiagnostician.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class HealthDiagnostician extends BaseAgent {
9191
}
9292

9393
// 3. Define the Agent's Instructions
94-
const instructions = `You are a Codex Senior Engineer and an autonomous Site Reliability Agent operating on the Cloudflare ecosystem.
94+
const instructions = `You are a Senior Engineer and an autonomous Site Reliability Agent operating on the Cloudflare ecosystem.
9595
Your primary directive is to investigate, diagnose, and remediate system health failures within the repository \`${repoOwner}/${repoName}\`.
9696
9797
CRITICAL PRE-FLIGHT CHECK:
@@ -103,9 +103,48 @@ TRIAGE AND REMEDIATION:
103103
- IF the fix is SMALL (e.g., typos, simple config adjustments, single-file logic errors under 20 lines): Formulate the fix and use \`create_pull_request\` to submit it immediately.
104104
- IF the fix is COMPLEX (e.g., multi-file refactoring, architectural changes, deep logic bugs, package upgrades): Do NOT try to fix it yourself. Instead, use the \`delegate_to_jules\` tool to dispatch a deep-reasoning session to Google Jules. Provide Jules with a highly detailed prompt of what needs to be refactored.
105105
106-
Return a JSON response containing the \`severity\`, \`rootCause\`, \`suggestedFix\` (or delegation note), and \`prUrl\` (or Jules Session ID).`;
106+
Conclude your investigation with a detailed summary containing the severity, rootCause, suggestedFix (or delegation note), and prUrl (or Jules Session ID).`;
107107

108-
const prompt = `Health Check Failed in category: ${payload.category}\nTarget: ${payload.target}\nError: ${payload.errorName} - ${payload.errorMessage}\nDetails: ${JSON.stringify(payload.errorDetails, null, 2)}\n\nRelevant Cloudflare Docs Context:\nQuery: ${rewritten}\nDocs Result: ${mcpContext}`;
108+
const MAX_LOG_LENGTH = 15000;
109+
let stringifiedDetails = JSON.stringify(payload.errorDetails, null, 2) || "{}";
110+
111+
// Use RAG to fetch relevant chunks if the error details are a large array
112+
if (Array.isArray(payload.errorDetails) && stringifiedDetails.length > MAX_LOG_LENGTH) {
113+
try {
114+
this.logger.info(`Extracting relevant logs via Vectorize RAG...`);
115+
const { vectorizeAndStoreLogs } = await import("@/ai/utils/log-vectorizer");
116+
const { generateEmbeddings } = await import("@/ai/providers/index");
117+
118+
const runId = `diag-${Date.now()}`;
119+
await vectorizeAndStoreLogs(this.env, runId, payload.errorDetails);
120+
121+
const diagnosticQuery = "Find fatal errors, agent execution failures, timeouts, 400 status codes, crash stack traces, and high severity warnings.";
122+
const queryEmbeddings = await generateEmbeddings(this.env, [diagnosticQuery]);
123+
const searchVector = queryEmbeddings[0];
124+
125+
const vectorMatches = await this.env.VECTORIZE_LOGS.query(searchVector, {
126+
topK: 10,
127+
filter: { runId: runId },
128+
returnValues: false,
129+
returnMetadata: true
130+
});
131+
132+
const relevantLogs = vectorMatches.matches
133+
.map(match => match.metadata?.content)
134+
.filter(Boolean)
135+
.join("\n\n---\n\n");
136+
137+
stringifiedDetails = `[RAG FETCHED RELEVANT LOG CHUNKS]\n${relevantLogs}`;
138+
this.logger.info(`Successfully retrieved ${vectorMatches.matches.length} relevant chunks`);
139+
} catch (e: any) {
140+
this.logger.error("RAG Log Vectorization failed, falling back to truncation", e);
141+
stringifiedDetails = stringifiedDetails.substring(0, MAX_LOG_LENGTH) + "\n...[RAG ERROR, TRUNCATED FOR LENGTH]";
142+
}
143+
} else if (stringifiedDetails.length > MAX_LOG_LENGTH) {
144+
stringifiedDetails = stringifiedDetails.substring(0, MAX_LOG_LENGTH) + "\n...[TRUNCATED FOR LENGTH to prevent 400 payload rejection]";
145+
}
146+
147+
const prompt = `Health Check Failed in category: ${payload.category}\nTarget: ${payload.target}\nError: ${payload.errorName} - ${payload.errorMessage}\nDetails: ${stringifiedDetails}\n\nRelevant Cloudflare Docs Context:\nQuery: ${rewritten}\nDocs Result: ${mcpContext}`;
109148

110149
// 4. Define Tools inline for the BaseAgent to register
111150
const agentConfig = {
@@ -310,13 +349,25 @@ Return a JSON response containing the \`severity\`, \`rootCause\`, \`suggestedFi
310349
instructions: agentConfig.instructions,
311350
model: agentConfig.model,
312351
tools: agentConfig.tools,
313-
outputType: HealthDiagnosticianOutputSchema as any,
352+
// Removed outputType here to comply with AI standard mandate: let agent run freely, extract structure internally below
314353
});
315354

355+
// Diagnostic tracking: monitor actual byte size of the outbound LLM payload
356+
const payloadBytes = new TextEncoder().encode(prompt).length;
357+
this.logger.info(`[HealthDiagnostician] Outbound Prompt Payload Size: ${payloadBytes} bytes`);
358+
316359
const result = await runner.run(agent, prompt);
317360

318-
// The SDK guarantees this matches the Zod schema when outputType is provided
319-
const finalData = result.finalOutput as HealthDiagnosticianOutput;
361+
// Enforce strict JSON output using the globally mandated AiProvider.generateStructuredResponse
362+
const { generateStructuredResponse } = await import("@/ai/providers/index");
363+
const { zodToJsonSchema } = await import("zod-to-json-schema");
364+
365+
const extractPrompt = `Extract the exact diagnosis details from the Agent's final response below. Respond ONLY with valid JSON.\n\nAgent Response:\n${result.finalOutput}`;
366+
const finalData = await generateStructuredResponse<HealthDiagnosticianOutput>(
367+
this.env,
368+
extractPrompt,
369+
zodToJsonSchema(HealthDiagnosticianOutputSchema as any, "structured_output")
370+
);
320371

321372
return new Response(JSON.stringify(finalData), {
322373
headers: { "Content-Type": "application/json" }

backend/src/ai/providers/gemini.ts

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,73 @@
11
// Dynamically imported
22
import { getAiGatewayUrl, resolveDefaultAiModel } from "./config";
33
import { getAIGatewayUrl as getRawGatewayUrl } from "../utils/ai-gateway";
4-
import { getGeminiApiKey } from "@utils/secrets";
54
import { cleanJsonOutput } from "@/ai/utils/sanitizer";
65
import { AIOptions, TextWithToolsResponse, StructuredWithToolsResponse } from "./index";
76

87
export async function createGeminiClient(env: Env, model: string) {
98
// @ts-ignore
109
const aigToken = typeof env.AI_GATEWAY_TOKEN === 'object' && env.AI_GATEWAY_TOKEN?.get ? await env.AI_GATEWAY_TOKEN.get() : env.AI_GATEWAY_TOKEN as string;
1110

12-
// "Key in Request + Authenticated Gateway" pattern:
13-
// - apiKey: REAL Gemini key (SDK sends this as ?key= to Google)
14-
// - cf-aig-authorization: gateway token (for gateway auth/logging)
15-
// The gateway forwards the real key to upstream; BYOK is NOT used here.
16-
const apiKey = await getGeminiApiKey(env);
17-
18-
if (!apiKey || !env.CLOUDFLARE_ACCOUNT_ID) {
19-
throw new Error("Missing GEMINI_API_KEY and CLOUDFLARE_ACCOUNT_ID");
11+
if (!aigToken || !env.CLOUDFLARE_ACCOUNT_ID) {
12+
throw new Error("Missing AI_GATEWAY_TOKEN and CLOUDFLARE_ACCOUNT_ID required for BYOK configuration");
2013
}
2114

2215
const { GoogleGenAI } = await import("@google/genai");
2316
const baseUrl = await getRawGatewayUrl(env, { provider: "google-ai-studio" });
17+
18+
const originalFetch = globalThis.fetch;
2419

25-
// Default to v1beta for Gemini 2.5 Flash and newer models
26-
const apiVersion = "v1beta";
27-
28-
return new GoogleGenAI({
29-
apiKey: apiKey,
30-
httpOptions: {
31-
baseUrl,
32-
apiVersion,
33-
headers: aigToken ? { 'cf-aig-authorization': `Bearer ${aigToken}` } : undefined,
34-
},
35-
});
20+
// Intercept the fetch call to strip dummy keys and inject the Gateway Authorization
21+
const wrappedFetch = async (url: any, init: any) => {
22+
const newInit = { ...init };
23+
if (newInit.headers) {
24+
const headers = new Headers(newInit.headers);
25+
26+
// Strip the SDK-enforced dummy key so it doesn't override the Gateway's BYOK injection
27+
headers.delete("x-goog-api-key");
28+
29+
// Apply the AI Gateway token for Gateway auth
30+
if (aigToken && !headers.has("cf-aig-authorization")) {
31+
headers.set("cf-aig-authorization", `Bearer ${aigToken}`);
32+
}
33+
34+
const headerObj: Record<string, string> = {};
35+
headers.forEach((value, key) => {
36+
headerObj[key] = value;
37+
});
38+
newInit.headers = headerObj;
39+
}
40+
41+
let finalUrl = String(url);
42+
try {
43+
const u = new URL(finalUrl);
44+
// Strip the query parameter ?key= if the SDK appended the dummy key
45+
if (u.searchParams.has("key")) {
46+
u.searchParams.delete("key");
47+
finalUrl = u.toString();
48+
}
49+
} catch (e) { /* ignore url parsing errors */ }
50+
51+
return await originalFetch(finalUrl, newInit);
52+
};
53+
54+
// Monkey-patch temporarily for this instance creation
55+
globalThis.fetch = wrappedFetch as unknown as typeof fetch;
56+
57+
try {
58+
const client = new GoogleGenAI({
59+
// Pass a dummy key to bypass SDK validation.
60+
// The real key is stored in Cloudflare AI Gateway (BYOK)
61+
apiKey: "cf-aig-byok-dummy-key",
62+
httpOptions: {
63+
baseUrl,
64+
},
65+
});
66+
67+
return client;
68+
} finally {
69+
// We leave fetch patched currently as the client resolves requests asynchronously later
70+
}
3671
}
3772

3873
export async function verifyApiKey(env: Env): Promise<boolean> {
@@ -42,7 +77,7 @@ export async function verifyApiKey(env: Env): Promise<boolean> {
4277
await client.models.get({ model: testModel });
4378
return true;
4479
} catch (error) {
45-
console.error("Gemini Verification Error:", error);
80+
console.error("Gemini BYOK Verification Error:", error);
4681
return false;
4782
}
4883
}
@@ -118,7 +153,7 @@ export async function generateTextWithTools(
118153
});
119154

120155
const toolCalls = response.functionCalls?.map((call, index) => ({
121-
id: `call_${index}`, // Gemini does not provide UUIDs for tools natively in the standard layout
156+
id: `call_${index}`,
122157
function: {
123158
name: call.name || "unknown",
124159
arguments: JSON.stringify(call.args || {})

backend/src/ai/providers/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { resolveDefaultAiProvider, SupportedProvider } from "./config";
22
import * as openai from "./openai";
33
import * as gemini from "./gemini";
44
import * as anthropic from "./anthropic";
5-
import * as workersAi from "./workers-ai";
5+
import * as workerAi from "./worker-ai";
66

77
export interface AIOptions {
88
model?: string;
@@ -40,7 +40,7 @@ export async function verifyApiKey(env: Env, providerOverride?: SupportedProvide
4040
case 'openai': return openai.verifyApiKey(env);
4141
case 'gemini': return gemini.verifyApiKey(env);
4242
case 'anthropic': return anthropic.verifyApiKey(env);
43-
default: return workersAi.verifyApiKey(env);
43+
default: return workerAi.verifyApiKey(env);
4444
}
4545
}
4646

@@ -56,7 +56,7 @@ export async function generateText(
5656
case 'openai': return openai.generateText(env, prompt, systemPrompt, options);
5757
case 'gemini': return gemini.generateText(env, prompt, systemPrompt, options);
5858
case 'anthropic': return anthropic.generateText(env, prompt, systemPrompt, options);
59-
default: return workersAi.generateText(env, prompt, systemPrompt, options);
59+
default: return workerAi.generateText(env, prompt, systemPrompt, options);
6060
}
6161
}
6262

@@ -73,7 +73,7 @@ export async function generateStructuredResponse<T = any>(
7373
case 'openai': return openai.generateStructuredResponse<T>(env, prompt, schema, systemPrompt, options);
7474
case 'gemini': return gemini.generateStructuredResponse<T>(env, prompt, schema, systemPrompt, options);
7575
case 'anthropic': return anthropic.generateStructuredResponse<T>(env, prompt, schema, systemPrompt, options);
76-
default: return workersAi.generateStructuredResponse<T>(env, prompt, schema, systemPrompt, options);
76+
default: return workerAi.generateStructuredResponse<T>(env, prompt, schema, systemPrompt, options);
7777
}
7878
}
7979

@@ -90,7 +90,7 @@ export async function generateTextWithTools(
9090
case 'openai': return openai.generateTextWithTools(env, prompt, tools, systemPrompt, options);
9191
case 'gemini': return gemini.generateTextWithTools(env, prompt, tools, systemPrompt, options);
9292
case 'anthropic': return anthropic.generateTextWithTools(env, prompt, tools, systemPrompt, options);
93-
default: return workersAi.generateTextWithTools(env, prompt, tools, systemPrompt, options);
93+
default: return workerAi.generateTextWithTools(env, prompt, tools, systemPrompt, options);
9494
}
9595
}
9696

@@ -108,15 +108,22 @@ export async function generateStructuredWithTools<T = any>(
108108
case 'openai': return openai.generateStructuredWithTools<T>(env, prompt, schema, tools, systemPrompt, options);
109109
case 'gemini': return gemini.generateStructuredWithTools<T>(env, prompt, schema, tools, systemPrompt, options);
110110
case 'anthropic': return anthropic.generateStructuredWithTools<T>(env, prompt, schema, tools, systemPrompt, options);
111-
default: return workersAi.generateStructuredWithTools<T>(env, prompt, schema, tools, systemPrompt, options);
111+
default: return workerAi.generateStructuredWithTools<T>(env, prompt, schema, tools, systemPrompt, options);
112112
}
113113
}
114114

115115
export async function generateEmbedding(
116116
env: Env,
117117
text: string
118118
): Promise<number[]> {
119-
return workersAi.generateEmbedding(env, text);
119+
return workerAi.generateEmbedding(env, text);
120+
}
121+
122+
export async function generateEmbeddings(
123+
env: Env,
124+
text: string | string[]
125+
): Promise<number[][]> {
126+
return workerAi.generateEmbeddings(env, text);
120127
}
121128

122129
/**

0 commit comments

Comments
 (0)