Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/api/routers/agentConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server';
import { eq } from 'drizzle-orm';
import { z } from 'zod';
import { validateTemplate } from '../../agents/prompts/index.js';
import { CLAUDE_CODE_MODELS } from '../../backends/claude-code/models.js';
import { getDb } from '../../db/client.js';
import { loadPartials } from '../../db/repositories/partialsRepository.js';
import {
Expand All @@ -11,7 +12,7 @@ import {
updateAgentConfig,
} from '../../db/repositories/settingsRepository.js';
import { agentConfigs, projects } from '../../db/schema/index.js';
import { protectedProcedure, router } from '../trpc.js';
import { protectedProcedure, publicProcedure, router } from '../trpc.js';

async function validatePromptIfPresent(prompt: string | null | undefined) {
if (!prompt) return;
Expand All @@ -26,6 +27,10 @@ async function validatePromptIfPresent(prompt: string | null | undefined) {
}

export const agentConfigsRouter = router({
claudeCodeModels: publicProcedure.query(() => {
return CLAUDE_CODE_MODELS;
}),

list: protectedProcedure
.input(z.object({ projectId: z.string().optional() }).optional())
.query(async ({ ctx, input }) => {
Expand Down
43 changes: 37 additions & 6 deletions src/backends/claude-code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
SDKStatusMessage,
SDKSystemMessage,
} from '@anthropic-ai/claude-agent-sdk';
import { logger } from '../../utils/logging.js';
import type {
AgentBackend,
AgentBackendInput,
Expand All @@ -16,6 +17,7 @@ import type {
ToolManifest,
} from '../types.js';
import { buildHooks } from './hooks.js';
import { CLAUDE_CODE_MODEL_IDS, DEFAULT_CLAUDE_CODE_MODEL } from './models.js';

/**
* Build prompt guidance for CASCADE-specific CLI tools.
Expand Down Expand Up @@ -83,10 +85,15 @@ export function buildSystemPrompt(systemPrompt: string, tools: ToolManifest[]):
* The Claude Code SDK expects Anthropic model IDs.
*/
export function resolveClaudeModel(cascadeModel: string): string {
if (CLAUDE_CODE_MODEL_IDS.includes(cascadeModel)) return cascadeModel;
if (cascadeModel.startsWith('claude-')) return cascadeModel;
if (cascadeModel.startsWith('anthropic:')) return cascadeModel.replace('anthropic:', '');
// Fallback for non-Claude models configured in CASCADE
return 'claude-sonnet-4-5-20250929';
// Non-Claude model configured for Claude Code backend — warn and fall back
logger.warn('Non-Claude model configured for Claude Code backend, falling back to default', {
configured: cascadeModel,
fallback: DEFAULT_CLAUDE_CODE_MODEL,
});
return DEFAULT_CLAUDE_CODE_MODEL;
}

/**
Expand Down Expand Up @@ -289,6 +296,28 @@ function buildResult(
return { success, output, cost, error, prUrl };
}

/**
* Process a task_notification system message: log and report completed tasks.
*/
function processTaskNotification(
sysMsg: { [key: string]: unknown },
input: AgentBackendInput,
): void {
const taskMsg = sysMsg as unknown as {
task_id: string;
status: string;
summary: string;
};
if (taskMsg.status === 'completed' && input.progressReporter.onTaskCompleted) {
input.progressReporter.onTaskCompleted(taskMsg.task_id, '', taskMsg.summary);
}
input.logWriter('INFO', 'Task notification', {
taskId: taskMsg.task_id,
status: taskMsg.status,
summary: taskMsg.summary,
});
}

/**
* Claude Code SDK backend for CASCADE.
*
Expand Down Expand Up @@ -357,10 +386,12 @@ export class ClaudeCodeBackend implements AgentBackend {
await input.progressReporter.onIteration(turnCount, input.maxIterations);
processAssistantMessage(assistantMsg, turnCount, input);
} else if (message.type === 'system') {
processSystemMessage(
message as { subtype: string; [key: string]: unknown },
input.logWriter,
);
const sysMsg = message as { subtype: string; [key: string]: unknown };
if (sysMsg.subtype === 'task_notification') {
processTaskNotification(sysMsg, input);
} else {
processSystemMessage(sysMsg, input.logWriter);
}
} else if (message.type === 'result') {
resultMessage = message as SDKResultMessage;
}
Expand Down
9 changes: 9 additions & 0 deletions src/backends/claude-code/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const CLAUDE_CODE_MODELS = [
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
{ value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' },
{ value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
] as const;

export const CLAUDE_CODE_MODEL_IDS: string[] = CLAUDE_CODE_MODELS.map((m) => m.value);

export const DEFAULT_CLAUDE_CODE_MODEL = 'claude-sonnet-4-5-20250929';
95 changes: 63 additions & 32 deletions src/backends/progressModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,73 @@ export interface ProgressContext {
iteration: number;
maxIterations: number;
todos: Todo[];
recentToolCalls: { name: string; timestamp: number }[];
recentToolCalls: { name: string; detail?: string; timestamp: number }[];
recentTextSnippets?: { text: string; timestamp: number }[];
completedTasks?: { subject: string; summary: string; timestamp: number }[];
}

const PROGRESS_SYSTEM_PROMPT = `You are a progress reporter for an AI coding agent called CASCADE. Write a brief, informative progress update based on the agent's current state. Be concise (3-5 sentences max). Focus on what has been accomplished, what's currently in progress, and what remains. Use markdown formatting. Start with a bold header like "**implementation agent progress** (X min)". Do not include a progress bar — the system adds that separately.`;
const PROGRESS_SYSTEM_PROMPT = `You are a progress reporter for an AI coding agent called CASCADE. Write a brief, informative progress update based on the agent's current state. Be concise (3-5 sentences max). Focus on what has been accomplished, what's currently in progress, and what remains. Synthesize the agent's own commentary, tool call details (file paths, commands), and completed task summaries into a coherent narrative — do not just list tool names. Use markdown formatting. Start with a bold header like "**implementation agent progress** (X min)". Do not include a progress bar — the system adds that separately.`;

function formatProgressUserPrompt(context: ProgressContext): string {
const { agentType, taskDescription, elapsedMinutes, iteration, todos, recentToolCalls } = context;

const todoLines =
todos.length > 0
? todos
.map((t) => {
const icon = t.status === 'done' ? '✅' : t.status === 'in_progress' ? '🔄' : '⬜';
return `${icon} ${t.content}`;
})
.join('\n')
: '(no todos)';

const recentActivity =
recentToolCalls.length > 0
? recentToolCalls
.slice(-10)
.map((tc) => tc.name)
.join(', ')
: '(no recent activity)';

return `Agent: ${agentType}
Task: ${taskDescription.slice(0, 500)}
Time elapsed: ${Math.round(elapsedMinutes)} minutes
Iterations: ${iteration}

## Todo List
${todoLines}

## Recent Activity
${recentActivity}`;
const {
agentType,
taskDescription,
elapsedMinutes,
iteration,
todos,
recentToolCalls,
recentTextSnippets,
completedTasks,
} = context;

const sections: string[] = [
`Agent: ${agentType}`,
`Task: ${taskDescription.slice(0, 500)}`,
`Time elapsed: ${Math.round(elapsedMinutes)} minutes`,
`Iterations: ${iteration}`,
];

// Todos — only include if there are any (avoids noise for claude-code backend)
if (todos.length > 0) {
const todoLines = todos
.map((t) => {
const icon = t.status === 'done' ? '✅' : t.status === 'in_progress' ? '🔄' : '⬜';
return `${icon} ${t.content}`;
})
.join('\n');
sections.push(`\n## Todo List\n${todoLines}`);
}

// Recent activity with tool details (file paths, commands)
if (recentToolCalls.length > 0) {
const activityLines = recentToolCalls
.slice(-10)
.map((tc) => (tc.detail ? `${tc.name}: ${tc.detail}` : tc.name))
.join('\n');
sections.push(`\n## Recent Activity\n${activityLines}`);
}

// Agent commentary — what the LLM said it's doing
if (recentTextSnippets && recentTextSnippets.length > 0) {
const commentaryLines = recentTextSnippets
.slice(-5)
.map((s) => s.text)
.join('\n---\n');
sections.push(`\n## Agent Commentary\n${commentaryLines}`);
}

// Completed tasks — subagent task summaries
if (completedTasks && completedTasks.length > 0) {
const taskLines = completedTasks
.map((t) => {
const label = t.subject ? `**${t.subject}**: ` : '';
return `- ${label}${t.summary}`;
})
.join('\n');
sections.push(`\n## Completed Tasks\n${taskLines}`);
}

return sections.join('\n');
}

/**
Expand Down
53 changes: 51 additions & 2 deletions src/backends/progressMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,30 @@ export interface ProgressMonitorConfig {
}

const RING_BUFFER_MAX = 20;
const TEXT_SNIPPETS_MAX = 10;
const COMPLETED_TASKS_MAX = 5;

/**
* Extract a meaningful detail string from tool call params.
* Returns file paths, commands, or search patterns — the most useful
* context for progress reporting.
*/
function summarizeToolParams(_toolName: string, params?: Record<string, unknown>): string {
if (!params) return '';
if (params.file_path) return String(params.file_path);
if (params.filePath) return String(params.filePath);
if (params.command) return String(params.command).slice(0, 100);
if (params.pattern) {
const detail = String(params.pattern);
return params.path ? `${detail} in ${params.path}` : detail;
}
return '';
}

export class ProgressMonitor implements ProgressReporter {
private recentToolCalls: { name: string; timestamp: number }[] = [];
private recentToolCalls: { name: string; detail?: string; timestamp: number }[] = [];
private recentTextSnippets: { text: string; timestamp: number }[] = [];
private completedTasks: { subject: string; summary: string; timestamp: number }[] = [];
private currentIteration = 0;
private maxIterations = 0;
private startTime = Date.now();
Expand All @@ -52,17 +73,43 @@ export class ProgressMonitor implements ProgressReporter {
}

onToolCall(toolName: string, params?: Record<string, unknown>): void {
this.recentToolCalls.push({ name: toolName, timestamp: Date.now() });
const detail = summarizeToolParams(toolName, params);
this.recentToolCalls.push({
name: toolName,
detail: detail || undefined,
timestamp: Date.now(),
});
if (this.recentToolCalls.length > RING_BUFFER_MAX) {
this.recentToolCalls.shift();
}
this.config.logWriter('INFO', 'Tool call', { toolName, params });
}

onText(content: string): void {
if (content.trim()) {
this.recentTextSnippets.push({
text: content.slice(0, 200),
timestamp: Date.now(),
});
if (this.recentTextSnippets.length > TEXT_SNIPPETS_MAX) {
this.recentTextSnippets.shift();
}
}
this.config.logWriter('INFO', 'Agent text output', { length: content.length });
}

onTaskCompleted(taskId: string, subject: string, summary: string): void {
this.completedTasks.push({
subject,
summary: summary.slice(0, 300),
timestamp: Date.now(),
});
if (this.completedTasks.length > COMPLETED_TASKS_MAX) {
this.completedTasks.shift();
}
this.config.logWriter('INFO', 'Task completed', { taskId, subject });
}

// ── Lifecycle ──

start(): void {
Expand Down Expand Up @@ -99,6 +146,8 @@ export class ProgressMonitor implements ProgressReporter {
maxIterations: this.maxIterations,
todos,
recentToolCalls: [...this.recentToolCalls],
recentTextSnippets: [...this.recentTextSnippets],
completedTasks: [...this.completedTasks],
};

let summary: string;
Expand Down
1 change: 1 addition & 0 deletions src/backends/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface ProgressReporter {
onIteration(iteration: number, maxIterations: number): Promise<void>;
onToolCall(toolName: string, params?: Record<string, unknown>): void;
onText(content: string): void;
onTaskCompleted?(taskId: string, subject: string, summary: string): void;
}

/**
Expand Down
50 changes: 36 additions & 14 deletions src/github/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,29 +257,51 @@ export const githubClient = {
},

async getCheckSuiteStatus(owner: string, repo: string, ref: string): Promise<CheckSuiteStatus> {
logger.debug('Fetching check runs for ref', { owner, repo, ref });
const { data } = await getClient().checks.listForRef({
logger.debug('Fetching workflow runs for ref', { owner, repo, ref });
const client = getClient();

// Use Actions API (workflow runs + jobs) instead of Checks API,
// because fine-grained PATs cannot access the Checks API.
const { data: runsData } = await client.actions.listWorkflowRunsForRepo({
owner,
repo,
ref,
head_sha: ref,
per_page: 100,
});

const checkRuns = data.check_runs.map((cr) => ({
name: cr.name,
status: cr.status,
conclusion: cr.conclusion,
}));
// Fetch jobs for each workflow run to get per-job granularity
const jobResults = await Promise.all(
runsData.workflow_runs.map((run) =>
client.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: run.id,
per_page: 100,
}),
),
);

// All checks pass if every completed check has success/skipped/neutral conclusion
const allPassing = checkRuns.every(
(cr) =>
cr.status === 'completed' &&
(cr.conclusion === 'success' || cr.conclusion === 'skipped' || cr.conclusion === 'neutral'),
const checkRuns = jobResults.flatMap(({ data }) =>
data.jobs.map((job) => ({
name: job.name,
status: job.status,
conclusion: job.conclusion,
})),
);

// All checks pass if every completed check has success/skipped/neutral conclusion
const allPassing =
checkRuns.length > 0 &&
checkRuns.every(
(cr) =>
cr.status === 'completed' &&
(cr.conclusion === 'success' ||
cr.conclusion === 'skipped' ||
cr.conclusion === 'neutral'),
);

return {
totalCount: data.total_count,
totalCount: checkRuns.length,
checkRuns,
allPassing,
};
Expand Down
Loading