-
Notifications
You must be signed in to change notification settings - Fork 908
Add query() convenience API for async iterator pattern #598
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** | ||
| * TodoTracker — using the Copilot SDK's query() convenience API. | ||
| * | ||
| * Run: COPILOT_CLI_URL=localhost:PORT npx tsx todo-tracker.ts | ||
| * (start the CLI first with: copilot --headless) | ||
| */ | ||
|
|
||
| import { z } from "zod"; | ||
| import { query, defineTool } from "@github/copilot-sdk"; | ||
|
|
||
| class TodoTracker { | ||
| private todos: any[] = []; | ||
|
|
||
| displayProgress() { | ||
| if (this.todos.length === 0) return; | ||
| const completed = this.todos.filter((t) => t.status === "completed").length; | ||
| const inProgress = this.todos.filter((t) => t.status === "in_progress").length; | ||
| const total = this.todos.length; | ||
| console.log(`\nProgress: ${completed}/${total} completed`); | ||
| console.log(`Currently working on: ${inProgress} task(s)\n`); | ||
| this.todos.forEach((todo, index) => { | ||
| const icon = | ||
| todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌"; | ||
| const text = todo.status === "in_progress" ? todo.activeForm : todo.content; | ||
| console.log(`${index + 1}. ${icon} ${text}`); | ||
| }); | ||
| } | ||
|
|
||
| todoWriteTool = defineTool("TodoWrite", { | ||
| description: "Write or update the todo list for the current task.", | ||
| parameters: z.object({ | ||
| todos: z.array( | ||
| z.object({ | ||
| content: z.string(), | ||
| status: z.enum(["completed", "in_progress", "pending"]), | ||
| activeForm: z.string().optional(), | ||
| }), | ||
| ), | ||
| }), | ||
| handler: ({ todos }) => { | ||
| this.todos = todos; | ||
| this.displayProgress(); | ||
| return "Todo list updated."; | ||
| }, | ||
| }); | ||
|
|
||
| async trackQuery(prompt: string) { | ||
| for await (const event of query({ prompt, tools: [this.todoWriteTool], maxTurns: 20 })) { | ||
| if (event.type === "assistant.message_delta") { | ||
| process.stdout.write(event.data.deltaContent); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Usage | ||
| const tracker = new TodoTracker(); | ||
| await tracker.trackQuery("Build a complete authentication system with todos"); | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||||||
| /*--------------------------------------------------------------------------------------------- | ||||||||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||
| *--------------------------------------------------------------------------------------------*/ | ||||||||
|
|
||||||||
| /** | ||||||||
| * `query()` — a convenience wrapper that provides a simple async-iterator API | ||||||||
| * over the Copilot SDK. It creates a client + session, sends a prompt, and | ||||||||
| * yields every {@link SessionEvent} as it arrives. | ||||||||
| * | ||||||||
| * @example | ||||||||
| * ```typescript | ||||||||
| * import { query, defineTool } from "@github/copilot-sdk"; | ||||||||
| * | ||||||||
| * for await (const event of query({ prompt: "Hello!", tools: [myTool] })) { | ||||||||
| * if (event.type === "assistant.message_delta") { | ||||||||
| * process.stdout.write(event.data.deltaContent); | ||||||||
| * } | ||||||||
| * } | ||||||||
| * ``` | ||||||||
| * | ||||||||
| * @module query | ||||||||
| */ | ||||||||
|
|
||||||||
| import { CopilotClient } from "./client.js"; | ||||||||
| import { approveAll, type QueryOptions, type SessionEvent } from "./types.js"; | ||||||||
|
|
||||||||
| /** | ||||||||
| * Send a prompt and yield every session event as an async iterator. | ||||||||
| * | ||||||||
| * Internally creates a {@link CopilotClient} and session, sends the prompt, | ||||||||
| * and tears everything down when the iterator finishes or is broken out of. | ||||||||
| * | ||||||||
| * The generator ends when: | ||||||||
| * - The session becomes idle (model finished), or | ||||||||
| * - `maxTurns` tool-calling turns have been reached, or | ||||||||
| * - The consumer breaks out of the `for await` loop. | ||||||||
| */ | ||||||||
| export async function* query(options: QueryOptions): AsyncGenerator<SessionEvent> { | ||||||||
| const cliUrl = options.cliUrl ?? process.env.COPILOT_CLI_URL; | ||||||||
| const client = new CopilotClient({ | ||||||||
| ...(cliUrl ? { cliUrl } : {}), | ||||||||
| ...(options.cliPath ? { cliPath: options.cliPath } : {}), | ||||||||
| ...(options.githubToken ? { githubToken: options.githubToken } : {}), | ||||||||
|
||||||||
| ...(options.githubToken ? { githubToken: options.githubToken } : {}), | |
| // When using cliUrl (including via COPILOT_CLI_URL), CopilotClient does not allow githubToken. | |
| ...(!cliUrl && options.githubToken ? { githubToken: options.githubToken } : {}), |
Copilot
AI
Feb 27, 2026
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.
The logic for handling maxTurns has a potential issue: it yields the final assistant.message event before calling finish(), but it doesn't handle subsequent events that might arrive before the session is fully terminated. This could lead to buffered events being lost or the iterator continuing to process events after the maxTurns limit is reached.
Additionally, when maxTurns is reached on line 79, the iterator finishes immediately after yielding that event. However, other events (like tool.execution_start, tool.execution_end) that are already in flight or buffered might not be yielded, potentially leaving the caller without complete information about the final turn.
This issue also appears in the following locations of the same file:
- line 55
- line 118
Copilot
AI
Feb 27, 2026
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.
The event handler does not handle session.error events, which means errors that occur during session execution will not terminate the async iterator or propagate to the caller. According to the SessionEvent types and the sendAndWait pattern in session.ts (lines 175-178), session.error events should be caught and handled appropriately.
Consider adding a handler for session.error that calls finish() to terminate the iterator and potentially throws or yields the error event so the caller can handle it appropriately.
This issue also appears on line 69 of the same file.
Copilot
AI
Feb 27, 2026
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.
The query() function creates a new session but never explicitly destroys it. While client.stop() is called in the finally block (line 130), which stops the client and closes all sessions, it would be more explicit and follow better resource management practices to call session.destroy() before client.stop(). This is the pattern used in the codebase examples (see nodejs/src/client.ts lines 105-106 and nodejs/samples/chat.ts).
Copilot
AI
Feb 27, 2026
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.
The new query() convenience API lacks test coverage. Given that the codebase has comprehensive E2E tests for other SDK features (see nodejs/test/e2e/ with tests for client lifecycle, session handling, tools, etc.), this new feature should have corresponding tests to verify:
- Basic query functionality with streaming events
- maxTurns limiting behavior
- Environment variable handling (COPILOT_CLI_URL)
- Cleanup on early termination (breaking out of the iterator)
- Error propagation from session errors
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1052,3 +1052,55 @@ export interface ForegroundSessionInfo { | |||||||||
| /** Workspace path of the foreground session */ | ||||||||||
| workspacePath?: string; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // ============================================================================ | ||||||||||
| // Query Options (convenience API) | ||||||||||
| // ============================================================================ | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Options for the `query()` convenience function. | ||||||||||
| * Combines the essential CopilotClient and SessionConfig options | ||||||||||
| * into a single flat configuration. | ||||||||||
| */ | ||||||||||
| export interface QueryOptions { | ||||||||||
| /** The user prompt to send. */ | ||||||||||
| prompt: string; | ||||||||||
|
|
||||||||||
| /** Tools exposed to the model. */ | ||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||
| tools?: Tool<any>[]; | ||||||||||
|
|
||||||||||
| /** Model to use (e.g. "gpt-5", "claude-sonnet-4.5"). */ | ||||||||||
| model?: string; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Maximum number of agentic turns (assistant responses that include tool calls). | ||||||||||
| * The generator will end after this many tool-calling turns. | ||||||||||
|
||||||||||
| * The generator will end after this many tool-calling turns. | |
| * Only assistant messages that include tool calls count toward this limit. | |
| * After this many tool-calling turns, no further tool calls will be made, | |
| * but a final assistant message without tool calls may still be returned. |
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.
The sample uses
any[]type for the todos array. Following TypeScript best practices and conventions visible in the codebase (which uses explicit types throughout), consider defining a proper type for todo items to provide better type safety.For example:
private todos: Array<{ content: string; status: 'completed' | 'in_progress' | 'pending'; activeForm?: string }> = [];