Skip to content

Commit e2948b7

Browse files
patnikoCopilot
andcommitted
Add query() convenience API for async iterator pattern
Adds a query() function that wraps CopilotClient + session creation into a simple async generator, similar to the Claude Agent SDK's query() API. - New QueryOptions type in types.ts - New query() async generator in query.ts (auto-creates client/session, yields SessionEvent, supports maxTurns, auto-reads COPILOT_CLI_URL) - Exported from index.ts - New todo-tracker.ts sample demonstrating the pattern Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fc63890 commit e2948b7

4 files changed

Lines changed: 244 additions & 0 deletions

File tree

nodejs/samples/todo-tracker.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* TodoTracker — using the Copilot SDK's query() convenience API.
3+
*
4+
* Run: COPILOT_CLI_URL=localhost:PORT npx tsx todo-tracker.ts
5+
* (start the CLI first with: copilot --headless)
6+
*/
7+
8+
import { z } from "zod";
9+
import { query, defineTool } from "@github/copilot-sdk";
10+
11+
class TodoTracker {
12+
private todos: any[] = [];
13+
14+
displayProgress() {
15+
if (this.todos.length === 0) return;
16+
const completed = this.todos.filter((t) => t.status === "completed").length;
17+
const inProgress = this.todos.filter((t) => t.status === "in_progress").length;
18+
const total = this.todos.length;
19+
console.log(`\nProgress: ${completed}/${total} completed`);
20+
console.log(`Currently working on: ${inProgress} task(s)\n`);
21+
this.todos.forEach((todo, index) => {
22+
const icon =
23+
todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";
24+
const text = todo.status === "in_progress" ? todo.activeForm : todo.content;
25+
console.log(`${index + 1}. ${icon} ${text}`);
26+
});
27+
}
28+
29+
todoWriteTool = defineTool("TodoWrite", {
30+
description: "Write or update the todo list for the current task.",
31+
parameters: z.object({
32+
todos: z.array(
33+
z.object({
34+
content: z.string(),
35+
status: z.enum(["completed", "in_progress", "pending"]),
36+
activeForm: z.string().optional(),
37+
}),
38+
),
39+
}),
40+
handler: ({ todos }) => {
41+
this.todos = todos;
42+
this.displayProgress();
43+
return "Todo list updated.";
44+
},
45+
});
46+
47+
async trackQuery(prompt: string) {
48+
for await (const event of query({ prompt, tools: [this.todoWriteTool], maxTurns: 20 })) {
49+
if (event.type === "assistant.message_delta") {
50+
process.stdout.write(event.data.deltaContent);
51+
}
52+
}
53+
}
54+
}
55+
56+
// Usage
57+
const tracker = new TodoTracker();
58+
await tracker.trackQuery("Build a complete authentication system with todos");

nodejs/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
export { CopilotClient } from "./client.js";
1212
export { CopilotSession, type AssistantMessageEvent } from "./session.js";
1313
export { defineTool, approveAll } from "./types.js";
14+
export { query } from "./query.js";
1415
export type {
1516
ConnectionState,
1617
CopilotClientOptions,
@@ -30,6 +31,7 @@ export type {
3031
PermissionHandler,
3132
PermissionRequest,
3233
PermissionRequestResult,
34+
QueryOptions,
3335
ResumeSessionConfig,
3436
SessionConfig,
3537
SessionEvent,

nodejs/src/query.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
/**
6+
* `query()` — a convenience wrapper that provides a simple async-iterator API
7+
* over the Copilot SDK. It creates a client + session, sends a prompt, and
8+
* yields every {@link SessionEvent} as it arrives.
9+
*
10+
* @example
11+
* ```typescript
12+
* import { query, defineTool } from "@github/copilot-sdk";
13+
*
14+
* for await (const event of query({ prompt: "Hello!", tools: [myTool] })) {
15+
* if (event.type === "assistant.message_delta") {
16+
* process.stdout.write(event.data.deltaContent);
17+
* }
18+
* }
19+
* ```
20+
*
21+
* @module query
22+
*/
23+
24+
import { CopilotClient } from "./client.js";
25+
import { approveAll, type QueryOptions, type SessionEvent } from "./types.js";
26+
27+
/**
28+
* Send a prompt and yield every session event as an async iterator.
29+
*
30+
* Internally creates a {@link CopilotClient} and session, sends the prompt,
31+
* and tears everything down when the iterator finishes or is broken out of.
32+
*
33+
* The generator ends when:
34+
* - The session becomes idle (model finished), or
35+
* - `maxTurns` tool-calling turns have been reached, or
36+
* - The consumer breaks out of the `for await` loop.
37+
*/
38+
export async function* query(options: QueryOptions): AsyncGenerator<SessionEvent> {
39+
const cliUrl = options.cliUrl ?? process.env.COPILOT_CLI_URL;
40+
const client = new CopilotClient({
41+
...(cliUrl ? { cliUrl } : {}),
42+
...(options.cliPath ? { cliPath: options.cliPath } : {}),
43+
...(options.githubToken ? { githubToken: options.githubToken } : {}),
44+
});
45+
46+
try {
47+
const session = await client.createSession({
48+
model: options.model,
49+
tools: options.tools ?? [],
50+
streaming: options.streaming ?? true,
51+
systemMessage: options.systemMessage,
52+
onPermissionRequest: options.onPermissionRequest ?? approveAll,
53+
});
54+
55+
// Bridge the event-driven API to an async iterator via a simple queue.
56+
let resolve: ((value: IteratorResult<SessionEvent>) => void) | null = null;
57+
const buffer: SessionEvent[] = [];
58+
let done = false;
59+
let turns = 0;
60+
61+
const finish = () => {
62+
done = true;
63+
if (resolve) {
64+
resolve({ value: undefined as unknown as SessionEvent, done: true });
65+
resolve = null;
66+
}
67+
};
68+
69+
session.on((event: SessionEvent) => {
70+
if (done) return;
71+
72+
// Count tool-calling turns for maxTurns support.
73+
if (
74+
options.maxTurns &&
75+
event.type === "assistant.message" &&
76+
event.data.toolRequests?.length
77+
) {
78+
turns++;
79+
if (turns >= options.maxTurns) {
80+
if (resolve) {
81+
resolve({ value: event, done: false });
82+
resolve = null;
83+
} else {
84+
buffer.push(event);
85+
}
86+
finish();
87+
return;
88+
}
89+
}
90+
91+
if (event.type === "session.idle") {
92+
if (resolve) {
93+
resolve({ value: event, done: false });
94+
resolve = null;
95+
} else {
96+
buffer.push(event);
97+
}
98+
finish();
99+
return;
100+
}
101+
102+
if (resolve) {
103+
resolve({ value: event, done: false });
104+
resolve = null;
105+
} else {
106+
buffer.push(event);
107+
}
108+
});
109+
110+
await session.send({ prompt: options.prompt });
111+
112+
while (!done || buffer.length > 0) {
113+
if (buffer.length > 0) {
114+
yield buffer.shift()!;
115+
} else if (done) {
116+
break;
117+
} else {
118+
yield await new Promise<SessionEvent>((r) => {
119+
resolve = (result) => {
120+
if (result.done) {
121+
r(undefined as unknown as SessionEvent);
122+
} else {
123+
r(result.value);
124+
}
125+
};
126+
});
127+
}
128+
}
129+
} finally {
130+
await client.stop();
131+
}
132+
}

nodejs/src/types.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,3 +1052,55 @@ export interface ForegroundSessionInfo {
10521052
/** Workspace path of the foreground session */
10531053
workspacePath?: string;
10541054
}
1055+
1056+
// ============================================================================
1057+
// Query Options (convenience API)
1058+
// ============================================================================
1059+
1060+
/**
1061+
* Options for the `query()` convenience function.
1062+
* Combines the essential CopilotClient and SessionConfig options
1063+
* into a single flat configuration.
1064+
*/
1065+
export interface QueryOptions {
1066+
/** The user prompt to send. */
1067+
prompt: string;
1068+
1069+
/** Tools exposed to the model. */
1070+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1071+
tools?: Tool<any>[];
1072+
1073+
/** Model to use (e.g. "gpt-5", "claude-sonnet-4.5"). */
1074+
model?: string;
1075+
1076+
/**
1077+
* Maximum number of agentic turns (assistant responses that include tool calls).
1078+
* The generator will end after this many tool-calling turns.
1079+
* If not set, the agent runs until it is idle.
1080+
*/
1081+
maxTurns?: number;
1082+
1083+
/** Enable streaming delta events. @default true */
1084+
streaming?: boolean;
1085+
1086+
/**
1087+
* URL of an existing Copilot CLI server (e.g. "localhost:8080").
1088+
* When provided, the client will not spawn a CLI process.
1089+
*/
1090+
cliUrl?: string;
1091+
1092+
/** Path to the CLI executable. */
1093+
cliPath?: string;
1094+
1095+
/** GitHub token for authentication. */
1096+
githubToken?: string;
1097+
1098+
/**
1099+
* Handler for permission requests.
1100+
* @default approveAll
1101+
*/
1102+
onPermissionRequest?: PermissionHandler;
1103+
1104+
/** System message configuration. */
1105+
systemMessage?: SystemMessageConfig;
1106+
}

0 commit comments

Comments
 (0)