Skip to content
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"test": "node --test",
"test:unit": "NODE_ENV=test npx tsx --test test/minimal.test.ts test/modelName.test.ts test/daemonEventIntake.test.ts test/githubEventIntakeMode.test.ts test/intakeModePrerequisites.test.ts test/validateRoutingUrl.test.ts test/routingWebSocketProtocol.test.ts test/routingWebSocketIntakeService.test.ts test/routingStatusPublisher.test.ts test/prSplit/commandAuthorization.test.ts test/prSplit/operationStore.test.ts test/prSplit/intake.test.ts test/prSplit/interception.test.ts packages/api/test/statusRoutes.test.ts packages/api/test/agentRuntimeRoutes.test.ts packages/api/test/instanceAuthorization.test.ts packages/api/test/routeAuthorization.test.ts",
"test:unit": "NODE_ENV=test npx tsx --test test/minimal.test.ts test/modelName.test.ts test/daemonEventIntake.test.ts test/githubEventIntakeMode.test.ts test/intakeModePrerequisites.test.ts test/validateRoutingUrl.test.ts test/routingWebSocketProtocol.test.ts test/routingWebSocketIntakeService.test.ts test/routingStatusPublisher.test.ts test/partialExecution.test.ts test/prSplit/commandAuthorization.test.ts test/prSplit/operationStore.test.ts test/prSplit/intake.test.ts test/prSplit/interception.test.ts test/prSplit/analysisPlanning.test.ts packages/api/test/statusRoutes.test.ts packages/api/test/agentRuntimeRoutes.test.ts packages/api/test/instanceAuthorization.test.ts packages/api/test/routeAuthorization.test.ts",
"test:e2e": "npx tsx --test test/e2e.test.ts",
"test:docker": "docker-compose run --rm -e REDIS_HOST=redis -e NODE_ENV=test worker npx tsx --test test/*.test.ts test/prSplit/*.test.ts",
"test:docker:single": "docker-compose run --rm -e REDIS_HOST=redis -e NODE_ENV=test worker npx tsx --test",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/agents/impl/AntigravityAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export class AntigravityAgent implements Agent {
}

async analyze(prompt: string, options?: AnalyzeOptions): Promise<AnalysisResult> {
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, responseFormat = 'text', suppressLlmLog } = options || {};
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, signal, responseFormat = 'text', suppressLlmLog } = options || {};
const startTime = Date.now();
logger.info({ agentAlias: this.config.alias, promptLength: prompt.length, hasContext: !!context, requestedModel: model, taskId, executionType }, 'Running lightweight analysis via Antigravity agent...');
const effectiveModel = model || 'antigravity-gemini-3.5-flash-medium';
Expand All @@ -302,7 +302,7 @@ export class AntigravityAgent implements Agent {

const { result, usageMetrics } = await executeWithUsageTracking(
this.getRuntimeName(),
async () => executeDockerCommand('docker', dockerArgs, { timeout: timeoutMs ?? 1800000, stdinData: fullPrompt, taskId }),
async () => executeDockerCommand('docker', dockerArgs, { timeout: timeoutMs ?? 1800000, stdinData: fullPrompt, taskId, signal }),
ANALYSIS_AGENT_TANK_TIMEOUT_MS
);
const executionTimeMs = Date.now() - startTime;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/agents/impl/ClaudeAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class ClaudeAgent implements Agent {

/** Runs a lightweight, read-only analysis for planning, summarization, and PR reviews. */
async analyze(prompt: string, options?: AnalyzeOptions): Promise<AnalysisResult> {
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, responseFormat = 'text', reasoningLevel, useConfiguredReasoningLevel = false, suppressLlmLog } = options || {};
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, signal, responseFormat = 'text', reasoningLevel, useConfiguredReasoningLevel = false, suppressLlmLog } = options || {};
const startTime = Date.now();

logger.info({
Expand Down Expand Up @@ -192,7 +192,7 @@ export class ClaudeAgent implements Agent {
const { result, usageMetrics } = await executeWithUsageTracking(
'claude',
async () => executeDockerCommand('docker', dockerArgs, {
timeout: timeoutMs ?? 1800000, stdinData: analysisPrompt, taskId
timeout: timeoutMs ?? 1800000, stdinData: analysisPrompt, taskId, signal
}),
ANALYSIS_AGENT_TANK_TIMEOUT_MS
);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/agents/impl/CodexAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export class CodexAgent implements Agent {
}

async analyze(prompt: string, options?: AnalyzeOptions): Promise<AnalysisResult> {
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, responseFormat = 'text', reasoningLevel, useConfiguredReasoningLevel = false, suppressLlmLog } = options || {};
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, signal, responseFormat = 'text', reasoningLevel, useConfiguredReasoningLevel = false, suppressLlmLog } = options || {};
const startTime = Date.now();
const effectiveModel = model || this.config.defaultModel || 'unknown';

Expand All @@ -232,7 +232,7 @@ export class CodexAgent implements Agent {
const { result, usageMetrics } = await executeWithUsageTracking(
'codex',
async () => executeDockerCommand('docker', dockerArgs, {
timeout: timeoutMs ?? 1800000, stdinData: analysisPrompt, taskId
timeout: timeoutMs ?? 1800000, stdinData: analysisPrompt, taskId, signal
}),
ANALYSIS_AGENT_TANK_TIMEOUT_MS
);
Expand Down
29 changes: 19 additions & 10 deletions packages/core/src/agents/impl/OpenCodeAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ function resolveOpenCodeExecutionOutcome(
return { success: result.exitCode === 0 && !parsedOutput.error && !terminationReason, terminationReason };
}

function buildAnalysisPrompt(prompt: string, context: string | undefined, responseFormat: 'text' | 'json'): string {
const suffix = responseFormat === 'json'
? '\n\nCRITICAL: Do not modify any files. Do not run any commands. Return only valid JSON matching the requested schema. Do not include markdown or explanatory text.'
: '\n\nCRITICAL: Do not modify any files. Do not run any commands. Only provide your analysis as plain text output.';
return context ? `${prompt}\n\nContext:\n${context}${suffix}` : `${prompt}${suffix}`;
}

export class OpenCodeAgent implements Agent {
readonly config: AgentConfig;
private readonly timeoutMs: number;
Expand Down Expand Up @@ -121,11 +128,10 @@ export class OpenCodeAgent implements Agent {
}

async analyze(prompt: string, options?: AnalyzeOptions): Promise<AnalysisResult> {
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, suppressLlmLog } = options || {};
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, signal, responseFormat = 'text', suppressLlmLog } = options || {};
const startTime = Date.now();
const effectiveModel = model || this.config.defaultModel || 'unknown';
const suffix = '\n\nCRITICAL: Do not modify any files. Do not run any commands. Only provide your analysis as plain text output.';
const analysisPrompt = context ? `${prompt}\n\nContext:\n${context}${suffix}` : `${prompt}${suffix}`;
const analysisPrompt = buildAnalysisPrompt(prompt, context, responseFormat);
const analysisWorkspace = this.ensureAnalysisWorkspace();
const analysisConfigPath = this.createAnalysisConfigSnapshot();
const analysisDataPath = this.resolveAnalysisDataPath();
Expand All @@ -134,7 +140,7 @@ export class OpenCodeAgent implements Agent {
const dockerArgs = await this.buildDockerArgs({ worktreePath: analysisWorkspace, githubToken: process.env.GITHUB_TOKEN || '', modelName: effectiveModel === 'unknown' ? undefined : effectiveModel, issueNumber: 0, taskId, executionType, readOnlyWorkspace: true, configPath: analysisConfigPath, dataPath: analysisDataPath });
const { result, usageMetrics } = await executeWithUsageTracking(
'opencode',
async () => executeDockerCommand('docker', dockerArgs, { timeout: 1800000, stdinData: analysisPrompt, taskId })
async () => executeDockerCommand('docker', dockerArgs, { timeout: timeoutMs ?? 1800000, stdinData: analysisPrompt, taskId, signal })
);
const executionTimeMs = Date.now() - startTime;
const parsedOutput = this.parseOpenCodeJsonl(result.stdout);
Expand All @@ -144,19 +150,15 @@ export class OpenCodeAgent implements Agent {
const success = !result.timedOut && result.exitCode === 0 && !parsedOutput.error && analysisText.length > 0;

const errorMsg = parsedOutput.error || result.stderr || 'No assistant text returned';
if (!suppressLlmLog) {
await this.persistAnalysisLogSafely({ executionType, modelUsed, executionTimeMs, success, error: success ? undefined : errorMsg, sessionId: parsedOutput.sessionId, taskId, correlationId, repository, metadata, taskNumber, prNumber, tokenUsage: parsedOutput.tokenUsage, usageMetrics });
}
await this.persistAnalysisLogUnlessSuppressed(suppressLlmLog, { executionType, modelUsed, executionTimeMs, success, error: success ? undefined : errorMsg, sessionId: parsedOutput.sessionId, taskId, correlationId, repository, metadata, taskNumber, prNumber, tokenUsage: parsedOutput.tokenUsage, usageMetrics });
return success
? { response: analysisText, modelUsed, executionTimeMs, success: true, sessionId: parsedOutput.sessionId, tokenUsage: parsedOutput.tokenUsage }
: { response: analysisText, modelUsed, executionTimeMs, success: false, error: `Analysis failed: ${errorMsg}`, tokenUsage: parsedOutput.tokenUsage };
} catch (error) {
const executionTimeMs = Date.now() - startTime;
const err = error as Error;
logger.error({ agentAlias: this.config.alias, error: err.message, executionTimeMs }, 'OpenCode lightweight analysis failed');
if (!suppressLlmLog) {
await this.persistAnalysisLogSafely({ executionType, modelUsed: effectiveModel, executionTimeMs, success: false, error: err.message, taskId, correlationId, repository, metadata, taskNumber, prNumber });
}
await this.persistAnalysisLogUnlessSuppressed(suppressLlmLog, { executionType, modelUsed: effectiveModel, executionTimeMs, success: false, error: err.message, taskId, correlationId, repository, metadata, taskNumber, prNumber });
return { response: '', modelUsed: effectiveModel, executionTimeMs, success: false, error: err.message };
} finally {
this.cleanupAnalysisWorkspace(analysisWorkspace);
Expand Down Expand Up @@ -255,6 +257,13 @@ export class OpenCodeAgent implements Agent {
}
}

private async persistAnalysisLogUnlessSuppressed(
suppressLlmLog: boolean | undefined,
opts: Parameters<OpenCodeAgent['persistAnalysisLogSafely']>[0]
): Promise<void> {
if (!suppressLlmLog) await this.persistAnalysisLogSafely(opts);
}

private ensureAnalysisWorkspace(): string {
const workspace = this.createAnalysisTempDir('workspace-');
try {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/agents/impl/VibeAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export class VibeAgent implements Agent {

// eslint-disable-next-line complexity
async analyze(prompt: string, options?: AnalyzeOptions): Promise<AnalysisResult> {
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, responseFormat = 'text', suppressLlmLog } = options || {};
const { context, model, taskId, taskNumber, prNumber, executionType, correlationId, repository, metadata, timeoutMs, signal, responseFormat = 'text', suppressLlmLog } = options || {};
const startTime = Date.now();
const effectiveModel = model || this.config.defaultModel;
if (!effectiveModel) throw new NoDefaultModelConfiguredError();
Expand Down Expand Up @@ -226,7 +226,8 @@ export class VibeAgent implements Agent {
'vibe',
async () => executeDockerCommand('docker', dockerArgs, {
timeout: timeoutMs ?? parseInt(process.env.VIBE_ANALYSIS_TIMEOUT_MS || '1800000', 10),
taskId
taskId,
signal
})
);
const executionTimeMs = Date.now() - startTime;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/agents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export interface AnalyzeOptions {
metadata?: Record<string, unknown>;
/** Optional timeout for lightweight analysis execution. */
timeoutMs?: number;
/** Cancels the underlying analysis process and its agent container. */
signal?: AbortSignal;
/** Expected response format. Defaults to plain text analysis. */
responseFormat?: 'text' | 'json';
/** Optional per-analysis reasoning level override. */
Expand Down
Loading
Loading