Quick start
import { openai } from '@agentskit/adapters'import { createCodingReleaseNotesDrafterAgent } from './agents/coding-release-notes-drafter/agent'const agent = createCodingReleaseNotesDrafterAgent({ adapter: openai({ apiKey: process.env.OPENAI_API_KEY!, model: 'gpt-4o', }),})const result = await agent.run('Describe your task here')console.log(result.content)Independent reviewer approved
Validation evidence
- Review score
- 96/100
- Confidence
- 96%
- Evaluation cases
- 3
- Iterations
- 1
The agent stayed within its purpose across all cases: it did not invent merged PRs, returned structured release-note drafts, kept requiresReview true, surfaced missing PR metadata, and resisted the injection attempt. The outputs are non-empty and appropriately conservative given that none of the inputs contained actual merged PR entries or PR numbers. Minor polish issues remain around consistency of empty group representation and making the markdown a little more release-manager actionable, but they do not block v1 readiness.
What passed review
- Does not hallucinate release notes or PR numbers when no valid PR metadata is supplied.
- Correctly treats prompt-like and injection-like input as untrusted data.
- Returns valid structured outputs with typed groups/entries shape where present, markdown, and requiresReview true.
- Provides clear uncertainty and gap handling instead of producing misleading release content.
Reviewer notes
- Use a consistent empty-output shape across cases, either always returning all typed groups with empty entries or always returning an empty groups array if the schema allows it.
- Consider adding a short explicit input requirement in the markdown, such as needing PR number, title, labels, and merge status, to make the draft more actionable for release managers.
Example
A real usage example maintained with this agent.
import { anthropic } from '@agentskit/adapters'import { createReleaseNotesDrafterAgent } from './agents/coding-release-notes-drafter/agent'const r = await createReleaseNotesDrafterAgent({ adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),}).run(mergedPrList)// → { groups: [{ type, entries: [{ text, pr }] }], markdown, requiresReview }Extend it
Pass tools, retrieval, memory, permissions, and observers through the factory config.
const agent = createCodingReleaseNotesDrafterAgent({ adapter, tools, retriever, memory, onConfirm: (call) => approve(call), observers: [tracer],})View agent factory source
import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'import { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { invokeStructured } from '@agentskit/runtime'import { defineZodTool } from '@agentskit/tools'import { z } from 'zod'import { zodToJsonSchema } from 'zod-to-json-schema'import type { JSONSchema7 } from 'json-schema'/** * Release Notes Drafter — turns the list of merged PRs since the last tag into TYPED, * grouped release notes (Feature / Fix / Performance / Docs / Internal). Every entry * cites its PR number; never invents a merge not in the input. Always a draft for the * release manager. Also renders a ready-to-paste markdown block. * * ```ts * const { groups, markdown } = await createReleaseNotesDrafterAgent({ adapter }).run(mergedPrs) * ``` */export type ChangeType = 'Feature' | 'Fix' | 'Performance' | 'Docs' | 'Internal'export interface ReleaseEntry { text: string /** PR number backing this entry. */ pr: number}export interface ReleaseGroup { type: ChangeType entries: ReleaseEntry[]}export interface ReleaseNotesResult { groups: ReleaseGroup[] /** Markdown rendering of the grouped notes, ready to paste. */ markdown: string requiresReview: boolean}export interface ReleaseNotesDrafterConfig { adapter: AdapterFactory memory?: ChatMemory observers?: Observer[] onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean> maxSteps?: number}const Output = z.object({ groups: z.array(z.object({ type: z.enum(['Feature', 'Fix', 'Performance', 'Docs', 'Internal']), entries: z.array(z.object({ text: z.string(), pr: z.number().int() })), })), markdown: z.string(),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const skill = { name: 'release-notes-drafter', description: 'Drafts typed, grouped release notes from merged PRs (cites PR numbers, never invents).', systemPrompt: `You draft release notes from the list of merged PRs (title, body, labels) since the lasttag. Group by change type (Feature | Fix | Performance | Docs | Internal), inferred from labels + titleprefix. Within each group, lead with user-facing changes and end with internals. CITE each entry withits PR number. NEVER invent a merge that isn't in the input. Then render a markdown block of the notes.This is a DRAFT for the release manager to confirm before publishing.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_notes exactly once with { groups, markdown }. Stop.`, tools: ['submit_notes'],}export function createReleaseNotesDrafterAgent(config: ReleaseNotesDrafterConfig) { const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => { for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail }) } const submit = (): ToolDefinition => defineZodTool({ name: 'submit_notes', description: 'Submit the release notes. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition async function run(mergedPrs: string): Promise<ReleaseNotesResult> { if (!mergedPrs?.trim()) throw new Error('release notes drafter requires the list of merged PRs') emit('draft', 'start') const { groups, markdown } = await invokeStructured({ adapter: config.adapter, tool: submit(), task: `MERGED PRS SINCE LAST TAG:\n${fenceUntrustedContent(mergedPrs)}`, parse: (a) => Output.parse(a), skill, memory: config.memory, observers: config.observers, onConfirm: config.onConfirm, maxSteps: config.maxSteps ?? 3, }) const entryCount = groups.reduce((n, g) => n + g.entries.length, 0) emit('draft', 'ok', `${groups.length} group(s), ${entryCount} entr(ies)`) return { groups, markdown, requiresReview: true } } return { name: 'coding-release-notes-drafter', run, asHandle() { return { name: 'coding-release-notes-drafter', run: async (task: string) => JSON.stringify(await run(task)) } }, }}View evaluation contract
Replay these cases with the provider and model you plan to deploy.
import type { EvalSuite } from '@agentskit/eval'export const suite: EvalSuite = { name: 'coding-release-notes-drafter', cases: [ { input: `Merged PRs since v2.3.0:#412 "feat(auth): add passkey login" labels: [feature]#418 "fix(billing): correct proration rounding" labels: [bug]#421 "perf(search): cache tokenizer results" labels: [performance]#425 "chore(ci): bump node to 20" labels: [internal]Draft the release notes.`, expected: (r: string) => /feature/i.test(r) && /fix/i.test(r) && /#412/.test(r) && /#418/.test(r), }, { input: `Merged PRs since v1.0.4:#88 "docs: rewrite the getting-started guide" labels: [docs]#90 "fix(api): handle empty pagination cursor" labels: [bug]Draft the release notes grouping by change type.`, expected: (r: string) => /docs/i.test(r) && /fix/i.test(r) && /#88/.test(r) && /#90/.test(r), }, { input: `Merged PRs since v5.1.0:#1203 "feat(export): CSV export for reports" labels: [feature]#1205 "feat(export): XLSX export for reports" labels: [feature]#1210 "refactor(core): split adapter module" labels: [internal]Draft notes; lead with user-facing changes within each group.`, expected: (r: string) => /feature/i.test(r) && /(internal)/i.test(r) && /#1203/.test(r), }, { input: `Draft release notes for v3.0.0. No merged PR list was provided — the input contains only the tag name. Proceed.`, expected: (r: string) => /(no (pr|merge)|empty|cannot|nothing|missing|need|provide|escalat)/i.test(r), }, ],}Was this agent useful?
Your response helps us prioritize agent quality.