-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy path60-github-coding-agent.ts
More file actions
395 lines (366 loc) · 12.6 KB
/
Copy path60-github-coding-agent.ts
File metadata and controls
395 lines (366 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/**
* 60 - GitHub Coding Agent — pick an issue, code the fix, create a PR.
*
* Demonstrates:
* - Swarm orchestration with 3 specialist agents + team coordinator
* - GitHub integration via gh CLI tools
* - Git operations (clone, branch, commit, push)
* - Code execution for writing and testing code
*
* Requirements:
* - Conductor server running
* - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable
* - gh CLI authenticated
* - Git configured with push access to the repo
*/
import { Agent, AgentRuntime, OnTextMention, tool } from '@io-orkes/conductor-javascript/agents';
import { execSync } from 'child_process';
import { randomBytes } from 'crypto';
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
const REPO = 'agentspan/codingexamples';
const WORK_DIR = `/tmp/codingexamples-${randomBytes(4).toString('hex')}`;
// -- GitHub & Git tools ------------------------------------------------------
const listGithubIssues = tool(
async (args: { state?: string; limit?: number }) => {
try {
const result = execSync(
`gh issue list --repo ${REPO} --state ${args.state ?? 'open'} --limit ${args.limit ?? 10} --json number,title,body,labels`,
{ encoding: 'utf-8', timeout: 30000 },
);
return result;
} catch (e) {
return `Error listing issues: ${e}`;
}
},
{
name: 'list_github_issues',
description: 'List GitHub issues from the repository.',
inputSchema: {
type: 'object',
properties: {
state: { type: 'string', description: 'Issue state filter -- \'open\', \'closed\', or \'all\'' },
limit: { type: 'number', description: 'Maximum number of issues to return' },
},
},
},
);
const getGithubIssue = tool(
async (args: { issueNumber: number }) => {
try {
const result = execSync(
`gh issue view ${args.issueNumber} --repo ${REPO} --json number,title,body,labels,comments`,
{ encoding: 'utf-8', timeout: 30000 },
);
return result;
} catch (e) {
return `Error getting issue: ${e}`;
}
},
{
name: 'get_github_issue',
description: 'Get details of a specific GitHub issue.',
inputSchema: {
type: 'object',
properties: {
issueNumber: { type: 'number', description: 'The issue number to fetch' },
},
required: ['issueNumber'],
},
},
);
const cloneRepo = tool(
async () => {
try {
execSync(`gh repo clone ${REPO} ${WORK_DIR}`, {
encoding: 'utf-8',
timeout: 60000,
});
return `Cloned ${REPO} to ${WORK_DIR}`;
} catch (e) {
return `Error cloning: ${e}`;
}
},
{
name: 'clone_repo',
description: 'Clone the GitHub repository to a unique /tmp directory.',
inputSchema: {
type: 'object',
properties: {
},
},
},
);
const gitCreateBranch = tool(
async (args: { branchName: string }) => {
try {
execSync(`git checkout -b ${args.branchName}`, {
encoding: 'utf-8',
timeout: 10000,
cwd: WORK_DIR,
});
return `Created and checked out branch: ${args.branchName}`;
} catch (e) {
return `Error creating branch: ${e}`;
}
},
{
name: 'git_create_branch',
description: 'Create and checkout a new git branch.',
inputSchema: {
type: 'object',
properties: {
branchName: { type: 'string', description: 'Name for the new branch' },
},
required: ['branchName'],
},
},
);
const writeFile = tool(
async (args: { path: string; content: string }) => {
const fullPath = join(WORK_DIR, args.path);
mkdirSync(dirname(fullPath), { recursive: true });
writeFileSync(fullPath, args.content);
return `Wrote ${args.content.length} bytes to ${args.path}`;
},
{
name: 'write_file',
description: 'Write content to a file in the cloned repo.',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Relative path within the repo (e.g. \'src/utils.py\')' },
content: { type: 'string', description: 'The file content to write' },
},
required: ['path', 'content'],
},
},
);
const readFile = tool(
async (args: { path: string }) => {
const fullPath = join(WORK_DIR, args.path);
if (!existsSync(fullPath)) return `File not found: ${args.path}`;
return readFileSync(fullPath, 'utf-8');
},
{
name: 'read_file',
description: 'Read a file from the cloned repo.',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Relative path within the repo (e.g. \'src/utils.py\')' },
},
required: ['path'],
},
},
);
const listFiles = tool(
async (args: { path?: string }) => {
const fullPath = join(WORK_DIR, args.path ?? '.');
try {
const result = execSync('find . -type f -not -path "./.git/*"', {
encoding: 'utf-8',
timeout: 10000,
cwd: fullPath,
});
return result || 'Empty directory';
} catch {
return `Not a directory: ${args.path}`;
}
},
{
name: 'list_files',
description: 'List files in a directory of the cloned repo.',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Relative directory path (default: repo root)' },
},
},
},
);
const gitCommitAndPush = tool(
async (args: { message: string }) => {
try {
execSync('git add -A', { cwd: WORK_DIR, timeout: 10000 });
execSync(`git commit -m "${args.message}"`, { cwd: WORK_DIR, timeout: 10000 });
execSync('git push -u origin HEAD', { cwd: WORK_DIR, timeout: 30000 });
return `Committed and pushed: ${args.message}`;
} catch (e) {
return `Error: ${e}`;
}
},
{
name: 'git_commit_and_push',
description: 'Stage all changes, commit, and push to the remote.',
inputSchema: {
type: 'object',
properties: {
message: { type: 'string', description: 'The commit message' },
},
required: ['message'],
},
},
);
const createPullRequest = tool(
async (args: { title: string; body: string; issueNumber?: number }) => {
let body = args.body;
if (args.issueNumber && args.issueNumber > 0) {
body = `${body}\n\nCloses #${args.issueNumber}`;
}
try {
const result = execSync(
`gh pr create --repo ${REPO} --title "${args.title}" --body "${body}"`,
{ encoding: 'utf-8', timeout: 30000, cwd: WORK_DIR },
);
return result.trim();
} catch (e) {
return `Error creating PR: ${e}`;
}
},
{
name: 'create_pull_request',
description: 'Create a GitHub pull request.',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'PR title' },
body: { type: 'string', description: 'PR description/body in markdown' },
issueNumber: { type: 'number', description: 'Issue number to link (0 to skip)' },
},
required: ['title', 'body'],
},
},
);
// -- Tool sets per agent -----------------------------------------------------
const githubTools = [
listGithubIssues, getGithubIssue, cloneRepo,
gitCreateBranch, gitCommitAndPush, createPullRequest,
];
const codingTools = [writeFile, readFile, listFiles];
const qaTools = [readFile, listFiles];
// -- GitHub Agent ------------------------------------------------------------
export const githubAgent = new Agent({
name: 'github_agent',
model: 'anthropic/claude-sonnet-4-20250514',
instructions:
'You are a GitHub operations specialist. You handle all git and GitHub CLI interactions.\n\n' +
'IMPORTANT: Read the conversation history carefully. If the conversation already contains ' +
'messages from [coder] and [qa_tester] (especially \'ALL TESTS PASSED\' or similar), then ' +
'the code is already implemented and tested -- you are in PHASE 2. Skip directly to step 6.\n\n' +
'PHASE 1 -- SETUP:\n' +
'1. Use list_github_issues to see open issues\n' +
'2. Use get_github_issue to read the full details\n' +
'3. Use clone_repo to clone the repository\n' +
'4. Use git_create_branch to create a feature branch\n' +
'5. Call transfer_to_coder with the issue details.\n\n' +
'PHASE 2 -- PR CREATION:\n' +
'6. Use git_commit_and_push to commit and push the changes\n' +
'7. Use create_pull_request to create the PR\n' +
'8. Output the PR URL as your final response.',
tools: githubTools,
thinkingBudgetTokens: 4096,
maxTokens: 16384,
});
// -- Coder -------------------------------------------------------------------
export const coderAgent = new Agent({
name: 'coder',
model: 'anthropic/claude-sonnet-4-20250514',
instructions:
'You are an expert developer. Write clean, well-structured code.\n\n' +
'WHEN YOU RECEIVE A TASK:\n' +
'1. Use list_files to understand the repo structure\n' +
'2. Write your code using write_file\n' +
'3. Execute your code to verify it works\n' +
'4. Call transfer_to_qa_tester for review\n\n' +
'IMPORTANT: You can ONLY use transfer_to_qa_tester. ' +
`The repo is cloned to ${WORK_DIR}.`,
tools: codingTools,
codeExecutionConfig: { enabled: true },
thinkingBudgetTokens: 4096,
maxTokens: 16384,
});
// -- QA Tester ---------------------------------------------------------------
export const qaTester = new Agent({
name: 'qa_tester',
model: 'anthropic/claude-sonnet-4-20250514',
instructions:
'You are a meticulous QA engineer. Review the code written by the coder.\n\n' +
'1. Use read_file to read the code that was written\n' +
'2. Execute test cases covering: normal inputs, edge cases, and boundary conditions\n' +
'3. If you find ANY bugs: call transfer_to_coder.\n' +
'4. If ALL tests pass: call transfer_to_github_agent.\n\n' +
'TRANSFER RULES:\n' +
' bugs found -> transfer_to_coder\n' +
' all tests pass -> transfer_to_github_agent',
tools: qaTools,
codeExecutionConfig: { enabled: true },
thinkingBudgetTokens: 4096,
maxTokens: 16384,
});
// -- Coding Team: swarm coordinator ------------------------------------------
export const codingTeam = new Agent({
name: 'coding_team',
model: 'anthropic/claude-sonnet-4-20250514',
instructions:
'You are a coding team coordinator. Delegate the incoming request ' +
'to github_agent to get started. Call transfer_to_github_agent now.',
agents: [githubAgent, coderAgent, qaTester],
strategy: 'swarm',
handoffs: [
new OnTextMention({ text: 'transfer_to_github_agent', target: 'github_agent' }),
new OnTextMention({ text: 'transfer_to_coder', target: 'coder' }),
new OnTextMention({ text: 'transfer_to_qa_tester', target: 'qa_tester' }),
],
allowedTransitions: {
coding_team: ['github_agent'],
github_agent: ['coder'],
coder: ['qa_tester'],
qa_tester: ['coder', 'github_agent'],
},
maxTurns: 30,
timeoutSeconds: 900,
});
// -- Run ---------------------------------------------------------------------
const prompt =
'Pick an open issue from the GitHub repository, implement the ' +
'feature or fix the bug, get it reviewed by QA, and create a PR.';
async function main() {
const runtime = new AgentRuntime();
try {
console.log('='.repeat(60));
console.log(' GitHub Coding Agent + QA Tester');
console.log(` Repo: ${REPO}`);
console.log(` Work dir: ${WORK_DIR}`);
console.log(' coding_team -> github_agent <-> coder <-> qa_tester (swarm)');
console.log('='.repeat(60));
console.log(`\nPrompt: ${prompt}\n`);
const result = await runtime.run(codingTeam, prompt);
const output = result.output;
const skipKeys = new Set(['finishReason', 'rejectionReason', 'is_transfer', 'transfer_to']);
if (output && typeof output === 'object' && !Array.isArray(output)) {
for (const [key, text] of Object.entries(output as Record<string, string>)) {
if (skipKeys.has(key) || !text) continue;
console.log(`\n${'─'.repeat(60)}`);
console.log(` [${key}]`);
console.log('─'.repeat(60));
console.log(text);
}
} else {
console.log(output);
}
console.log(`\nFinish reason: ${result.finishReason}`);
console.log(`Execution ID: ${result.executionId}`);
// Production pattern:
// 1. Deploy once during CI/CD (optional -- serve() below also deploys):
// await runtime.deploy(codingTeam);
// CLI alternative:
// agentspan deploy --package sdk/typescript/examples --agents coding_team
//
// 2. In a separate long-lived worker process (deploys + registers workers + starts polling):
// await runtime.serve(codingTeam);
} finally {
await runtime.shutdown();
}
}
main().catch(console.error);