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
27 changes: 27 additions & 0 deletions src/agent/curator-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,33 @@ test('well-formed JSON that fails the schema produces a distinguishably differen
assert.doesNotMatch(result.error ?? '', /not valid JSON/)
})

const emptyCuratorOutput = JSON.stringify({ repos: [] })

test('treats an empty curation as a failure, not a quiet day', async () => {
const fakeCurate = async () => emptyCuratorOutput

const result = await runCuratorGraph(sampleRepos, fakeCurate)

assert.equal(result.curated, null, 'an empty result must not be reported as success')
assert.match(result.error ?? '', /none of the 1 repos/)
})

test('retries an empty curation and tells the model which name to echo', async () => {
let calls = 0
const feedback: (string | undefined)[] = []
const fakeCurate = async (_repos: TrendingRepo[], fb?: string) => {
calls++
feedback.push(fb)
return calls === 1 ? emptyCuratorOutput : validCuratorOutput
}

const result = await runCuratorGraph(sampleRepos, fakeCurate)

assert.equal(calls, 2, 'an empty curation should be retried')
assert.equal(result.curated?.[0].repo_name, 'foo/bar')
assert.match(feedback[1] ?? '', /foo\/bar/, 'the retry should name the repo the model failed to match')
})

test('an exception thrown by curate() is caught and recorded instead of crashing the graph', async () => {
const fakeCurate = async () => {
throw new Error('rate limited')
Expand Down
13 changes: 11 additions & 2 deletions src/agent/curator.graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@ async function curateNode(state: { repos: TrendingRepo[]; error: string | null;
}

const parsed = CuratedRepoOutputSchema.safeParse(json)
if (parsed.success) return { curated: parsed.data.repos, error: null }
if (!parsed.success) {
return { error: `curator output did not match the expected schema: ${parsed.error.message}`, attempts: state.attempts + 1 }
}

if (!parsed.data.repos.length) {
return {
error: `curator returned summaries for none of the ${state.repos.length} repos it was given — repo_name must match exactly, e.g. "${state.repos[0]?.name}"`,
attempts: state.attempts + 1,
}
}

return { error: `curator output did not match the expected schema: ${parsed.error.message}`, attempts: state.attempts + 1 }
return { curated: parsed.data.repos, error: null }
}

export async function runCuratorGraph(repos: TrendingRepo[], curate: CurateFn): Promise<CuratorResult> {
Expand Down
8 changes: 1 addition & 7 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,7 @@ export class WorkCoordinator {
return WorkCoordinator.rankByGrowth(allRepos)
}

// Step 3 with its failure handling: null means stop, and the alert has already
// been sent. An empty result is a skip, not a failure — nothing to alert on.
// Step 3 with its failure handling: null means stop, and the alert has already been sent.
private static async curateOrNotify(topRepos: TrendingRepo[]): Promise<CuratedRepo[] | null> {
const { curated, error } = await runCuratorGraph(topRepos, WorkCoordinator.curateRepos)

Expand All @@ -341,11 +340,6 @@ export class WorkCoordinator {
return null
}

if (!curated.length) {
logger.info('⏭️ No repos curated — skipping send and save.')
return null
}

return curated
}

Expand Down
Loading