-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add manual agent triggering and run retry #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
dbbbc07
Merge pull request #302 from zbigniewsobiecki/dev
zbigniewsobiecki 09d6d29
Merge pull request #305 from zbigniewsobiecki/dev
zbigniewsobiecki 7a8e305
Merge pull request #309 from zbigniewsobiecki/dev
zbigniewsobiecki 220d5b6
Merge pull request #311 from zbigniewsobiecki/dev
zbigniewsobiecki 71ac5cf
feat: add manual agent triggering and run retry functionality
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { Args, Flags } from '@oclif/core'; | ||
| import { DashboardCommand } from '../_shared/base.js'; | ||
|
|
||
| export default class RunsRetry extends DashboardCommand { | ||
| static override description = 'Retry a previous agent run.'; | ||
|
|
||
| static override args = { | ||
| id: Args.string({ description: 'Run ID (UUID)', required: true }), | ||
| }; | ||
|
|
||
| static override flags = { | ||
| ...DashboardCommand.baseFlags, | ||
| model: Flags.string({ description: 'Override model (optional)' }), | ||
| }; | ||
|
|
||
| async run(): Promise<void> { | ||
| const { args, flags } = await this.parse(RunsRetry); | ||
|
|
||
| try { | ||
| const result = await this.client.runs.retry.mutate({ | ||
| runId: args.id, | ||
| model: flags.model, | ||
| }); | ||
|
|
||
| if (flags.json) { | ||
| this.outputJson(result); | ||
| } else { | ||
| this.log('Run retry triggered successfully.'); | ||
| } | ||
| } catch (err) { | ||
| this.handleError(err); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { Flags } from '@oclif/core'; | ||
| import { DashboardCommand } from '../_shared/base.js'; | ||
|
|
||
| export default class RunsTrigger extends DashboardCommand { | ||
| static override description = 'Manually trigger an agent run.'; | ||
|
|
||
| static override flags = { | ||
| ...DashboardCommand.baseFlags, | ||
| project: Flags.string({ description: 'Project ID', required: true }), | ||
| 'agent-type': Flags.string({ description: 'Agent type to run', required: true }), | ||
| 'card-id': Flags.string({ description: 'Card ID (optional)' }), | ||
| 'pr-number': Flags.integer({ description: 'PR number (optional)' }), | ||
| 'pr-branch': Flags.string({ description: 'PR branch (optional)' }), | ||
| 'repo-full-name': Flags.string({ description: 'Repository full name (optional)' }), | ||
| 'head-sha': Flags.string({ description: 'Git SHA (optional)' }), | ||
| model: Flags.string({ description: 'Override model (optional)' }), | ||
| }; | ||
|
|
||
| async run(): Promise<void> { | ||
| const { flags } = await this.parse(RunsTrigger); | ||
|
|
||
| try { | ||
| const result = await this.client.runs.trigger.mutate({ | ||
| projectId: flags.project, | ||
| agentType: flags['agent-type'], | ||
| cardId: flags['card-id'], | ||
| prNumber: flags['pr-number'], | ||
| prBranch: flags['pr-branch'], | ||
| repoFullName: flags['repo-full-name'], | ||
| headSha: flags['head-sha'], | ||
| model: flags.model, | ||
| }); | ||
|
|
||
| if (flags.json) { | ||
| this.outputJson(result); | ||
| } else { | ||
| this.log('Agent run triggered successfully.'); | ||
| } | ||
| } catch (err) { | ||
| this.handleError(err); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| import { runAgent } from '../../agents/registry.js'; | ||
| import { getRunById } from '../../db/repositories/runsRepository.js'; | ||
| import type { AgentInput, AgentResult, CascadeConfig, ProjectConfig } from '../../types/index.js'; | ||
| import { logger } from '../../utils/logging.js'; | ||
|
|
||
| /** | ||
| * In-memory tracking to prevent duplicate concurrent manual triggers. | ||
| */ | ||
| const runningTriggers = new Map<string, boolean>(); | ||
|
|
||
| function generateTriggerKey( | ||
| projectId: string, | ||
| agentType: string, | ||
| cardId?: string, | ||
| prNumber?: number, | ||
| ): string { | ||
| return `${projectId}:${agentType}:${cardId ?? 'no-card'}:${prNumber ?? 'no-pr'}`; | ||
| } | ||
|
|
||
| function markTriggerRunning(key: string): void { | ||
| runningTriggers.set(key, true); | ||
| } | ||
|
|
||
| function markTriggerComplete(key: string): void { | ||
| runningTriggers.delete(key); | ||
| } | ||
|
|
||
| export function isTriggerRunning(key: string): boolean { | ||
| return runningTriggers.has(key); | ||
| } | ||
|
|
||
| /** | ||
| * Clear all trigger tracking (test utility). | ||
| */ | ||
| export function clearTriggerTracking(): void { | ||
| runningTriggers.clear(); | ||
| } | ||
|
|
||
| /** | ||
| * Input for manual agent triggers. | ||
| */ | ||
| export interface ManualTriggerInput { | ||
| projectId: string; | ||
| agentType: string; | ||
| cardId?: string; | ||
| prNumber?: number; | ||
| prBranch?: string; | ||
| repoFullName?: string; | ||
| headSha?: string; | ||
| modelOverride?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Trigger a manual agent run. | ||
| * | ||
| * This runs fire-and-forget (does not await runAgent completion). | ||
| * Status tracking is handled via in-memory map to prevent duplicates. | ||
| */ | ||
| export async function triggerManualRun( | ||
| input: ManualTriggerInput, | ||
| project: ProjectConfig, | ||
| config: CascadeConfig, | ||
| ): Promise<void> { | ||
| const triggerKey = generateTriggerKey( | ||
| input.projectId, | ||
| input.agentType, | ||
| input.cardId, | ||
| input.prNumber, | ||
| ); | ||
|
|
||
| if (isTriggerRunning(triggerKey)) { | ||
| throw new Error( | ||
| `Manual trigger already running for project=${input.projectId}, agent=${input.agentType}, card=${input.cardId ?? 'N/A'}, pr=${input.prNumber ?? 'N/A'}`, | ||
| ); | ||
| } | ||
|
|
||
| logger.info('Triggering manual agent run', { | ||
| projectId: input.projectId, | ||
| agentType: input.agentType, | ||
| cardId: input.cardId, | ||
| prNumber: input.prNumber, | ||
| modelOverride: input.modelOverride, | ||
| }); | ||
|
|
||
| markTriggerRunning(triggerKey); | ||
|
|
||
| const agentInput: AgentInput & { project: ProjectConfig; config: CascadeConfig } = { | ||
| cardId: input.cardId, | ||
| prNumber: input.prNumber, | ||
| prBranch: input.prBranch, | ||
| repoFullName: input.repoFullName, | ||
| headSha: input.headSha, | ||
| modelOverride: input.modelOverride, | ||
| triggerType: 'manual', | ||
| project, | ||
| config, | ||
| }; | ||
|
|
||
| // Fire-and-forget execution | ||
| runAgent(input.agentType, agentInput) | ||
| .then((result: AgentResult) => { | ||
| logger.info('Manual agent run completed', { | ||
| projectId: input.projectId, | ||
| agentType: input.agentType, | ||
| success: result.success, | ||
| runId: result.runId, | ||
| }); | ||
| }) | ||
| .catch((err) => { | ||
| logger.error('Manual agent run failed', { | ||
| projectId: input.projectId, | ||
| agentType: input.agentType, | ||
| error: String(err), | ||
| }); | ||
| }) | ||
| .finally(() => { | ||
| markTriggerComplete(triggerKey); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Retry a previous agent run. | ||
| * | ||
| * Reads the original run from DB, extracts parameters, and triggers a new manual run. | ||
| */ | ||
| export async function triggerRetryRun( | ||
| runId: string, | ||
| project: ProjectConfig, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
| config: CascadeConfig, | ||
| modelOverride?: string, | ||
| ): Promise<void> { | ||
| const run = await getRunById(runId); | ||
| if (!run) { | ||
| throw new Error(`Run not found: ${runId}`); | ||
| } | ||
|
|
||
| if (!run.projectId) { | ||
| throw new Error(`Run ${runId} has no associated project`); | ||
| } | ||
|
|
||
| logger.info('Retrying agent run', { | ||
| originalRunId: runId, | ||
| agentType: run.agentType, | ||
| projectId: run.projectId, | ||
| modelOverride, | ||
| }); | ||
|
|
||
| // Extract params from original run | ||
| const triggerInput: ManualTriggerInput = { | ||
| projectId: run.projectId, | ||
| agentType: run.agentType, | ||
| cardId: run.cardId ?? undefined, | ||
| prNumber: run.prNumber ?? undefined, | ||
| modelOverride: modelOverride ?? run.model ?? undefined, | ||
| }; | ||
|
|
||
| // For PR-based agents, we don't store branch/SHA in DB, so we can't restore them. | ||
| // The retry will fetch fresh data from GitHub if needed. | ||
|
|
||
| await triggerManualRun(triggerInput, project, config); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor:
agentType: z.string()— other routers usez.string().min(1)for agent type validation. Without it, an empty string silently fails in the fire-and-forget path.