diff --git a/src/backend/src/ai/agents/pr-manager/PrManagerAgent.ts b/src/backend/src/ai/agents/pr-manager/PrManagerAgent.ts index f60da992..18ae543f 100644 --- a/src/backend/src/ai/agents/pr-manager/PrManagerAgent.ts +++ b/src/backend/src/ai/agents/pr-manager/PrManagerAgent.ts @@ -6,6 +6,9 @@ import { Agent, run } from '@openai/agents'; import { setupOpenAIAgentClient, getJulesClient } from '../../providers'; import { Octokit } from '@octokit/rest'; import { getAgentByName } from 'agents'; +import { getAgentDb, migrateAgentDb } from '@/db/schemas/agents/stateful'; +import { prManagerJobs } from '@/db/schemas/agents/events'; +import { desc } from 'drizzle-orm'; function safeParseJson(output: string) { let clean = output.trim(); @@ -46,8 +49,8 @@ export class PrManagerAgent extends runtime.Agent { return new Response('OK'); } if (url.pathname === '/api/jobs') { - await this.onStart(); - const results = this.sql.prepare("SELECT * FROM pr_manager_jobs ORDER BY created_at DESC LIMIT 50").all(); + const db = getAgentDb(this.ctx.storage); + const results = await db.select().from(prManagerJobs).orderBy(desc(prManagerJobs.createdAt)).limit(50); return new Response(JSON.stringify(results), { headers: { 'Content-Type': 'application/json' } }); @@ -56,24 +59,15 @@ export class PrManagerAgent extends runtime.Agent { } async onStart() { - // DO SQLite state management init - this.sql.prepare(` - CREATE TABLE IF NOT EXISTS pr_manager_jobs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - owner TEXT NOT NULL, - repo TEXT NOT NULL, - pull_number INTEGER NOT NULL, - status TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `).run(); + await this.ctx.blockConcurrencyWhile(async () => { + migrateAgentDb(this.ctx.storage); + }); } async scheduled() { console.log('[PrManagerAgent] Running scheduled PR scan...'); - // Ensure agent state is initialized - await this.onStart(); + const db = getAgentDb(this.ctx.storage); + // Use env defaults if not provided explicitly in Wrangler const owner = this.env.TEST_REPO_OWNER || 'cloudflare'; const repo = this.env.TEST_REPO_NAME || 'core-github-api'; @@ -191,24 +185,24 @@ Output a JSON object with: if (!allResolved) { console.log(`[PrManagerAgent] Low confidence in resolving PR #${pr.number}. Adding comment.`); await octokit.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: "I am unable to confidently resolve these conflicts automatically. Manual intervention is required." }); - this.sql.prepare('INSERT INTO pr_manager_jobs (owner, repo, pull_number, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').bind(owner, repo, pr.number, 'conflict_commented', Date.now(), Date.now()).run(); + await db.insert(prManagerJobs).values({ owner, repo, pullNumber: pr.number, status: 'conflict_commented', createdAt: Date.now(), updatedAt: Date.now() }); await execInSandbox('git merge --abort'); } else { console.log(`[PrManagerAgent] High confidence in resolving PR #${pr.number}. Pushing merge commit...`); await execInSandbox('git add .'); await execInSandbox('git commit -m "Auto-resolved merge conflicts"'); await execInSandbox(`git push origin ${headRef}`); - this.sql.prepare('INSERT INTO pr_manager_jobs (owner, repo, pull_number, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').bind(owner, repo, pr.number, 'conflict_resolved', Date.now(), Date.now()).run(); + await db.insert(prManagerJobs).values({ owner, repo, pullNumber: pr.number, status: 'conflict_resolved', createdAt: Date.now(), updatedAt: Date.now() }); } } else { // Merge succeeded cleanly? Should not happen if mergeable_state === 'dirty', but handle just in case console.log(`[PrManagerAgent] Merge surprisingly succeeded without conflicts for PR #${pr.number}`); await execInSandbox(`git push origin ${headRef}`); - this.sql.prepare('INSERT INTO pr_manager_jobs (owner, repo, pull_number, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').bind(owner, repo, pr.number, 'conflict_resolved', Date.now(), Date.now()).run(); + await db.insert(prManagerJobs).values({ owner, repo, pullNumber: pr.number, status: 'conflict_resolved', createdAt: Date.now(), updatedAt: Date.now() }); } } catch (err) { console.error(`[PrManagerAgent] Failed to merge PR #${pr.number}:`, err); - this.sql.prepare('INSERT INTO pr_manager_jobs (owner, repo, pull_number, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').bind(owner, repo, pr.number, 'conflict_failed', Date.now(), Date.now()).run(); + await db.insert(prManagerJobs).values({ owner, repo, pullNumber: pr.number, status: 'conflict_failed', createdAt: Date.now(), updatedAt: Date.now() }); } finally { // Let Sandbox terminate normally, no explicit cleanup needed here unless requested. } diff --git a/src/backend/src/db/schemas/agents/events.ts b/src/backend/src/db/schemas/agents/events.ts index 03feabf3..aec4b5d4 100644 --- a/src/backend/src/db/schemas/agents/events.ts +++ b/src/backend/src/db/schemas/agents/events.ts @@ -7,6 +7,7 @@ import { sqliteTable, text, + integer, sqliteTableCreator, check, index, @@ -56,6 +57,25 @@ export const automationRuns = sqliteTable( }) ); +// env.DB +// ── pr_manager_jobs ───────────────────────────────────────────── +export const prManagerJobs = sqliteTable( + "pr_manager_jobs", + { + id: integer("id").primaryKey({ autoIncrement: true }), + owner: text("owner").notNull(), + repo: text("repo").notNull(), + pullNumber: integer("pull_number").notNull(), + status: text("status").notNull(), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), + }, + (table) => ({ + createdAtIdx: index("idx_pr_manager_jobs_created_at").on(table.createdAt), + }) +); + + // Agent activities timeline table // ── agent_activities ─────────────────────────── export const agentActivities = sqliteTable( diff --git a/src/backend/src/db/schemas/agents/stateful.ts b/src/backend/src/db/schemas/agents/stateful.ts index ae91a138..1f645290 100644 --- a/src/backend/src/db/schemas/agents/stateful.ts +++ b/src/backend/src/db/schemas/agents/stateful.ts @@ -71,6 +71,17 @@ export function migrateAgentDb(storage: DurableObjectStorage): void { CONSTRAINT status_check CHECK(status IN ('pending','active','completed','failed')) ); CREATE INDEX IF NOT EXISTS idx_agent_activities_op ON agent_activities (operation_id); + + CREATE TABLE IF NOT EXISTS pr_manager_jobs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + owner TEXT NOT NULL, + repo TEXT NOT NULL, + pull_number INTEGER NOT NULL, + status TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_pr_manager_jobs_created_at ON pr_manager_jobs (created_at); `); }