From 242fc74b9cb98ec18d7d7eef44b17ad67181b5b4 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Sun, 24 May 2026 22:29:32 +0800 Subject: [PATCH] feat(coding-agent): add streaming agent loop with DeepSeek Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/coding-agent/src/agent.ts | 101 +++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/examples/coding-agent/src/agent.ts b/examples/coding-agent/src/agent.ts index 4d7feeb..8583214 100644 --- a/examples/coding-agent/src/agent.ts +++ b/examples/coding-agent/src/agent.ts @@ -1,4 +1,5 @@ import { exec } from "node:child_process"; +import OpenAI from "openai"; export interface Message { role: "user" | "assistant" | "tool"; @@ -22,6 +23,11 @@ export interface AgentOptions { requestApproval: (command: string) => Promise; } +const client = new OpenAI({ + apiKey: process.env["DEEPSEEK_API_KEY"], + baseURL: "https://api.deepseek.com", +}); + const BASH_TOOL = { type: "function" as const, function: { @@ -68,3 +74,98 @@ export function executeBash(command: string): Promise { ); }); } + +export async function runAgentLoop( + userMessage: string, + messages: Message[], + options: AgentOptions, +): Promise { + messages = [...messages, { role: "user", content: userMessage }]; + + while (true) { + const stream = await client.chat.completions.create({ + model: "deepseek-v4-pro", + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + ...(messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[]), + ], + tools: [BASH_TOOL], + stream: true, + }); + + let contentBuffer = ""; + let toolCalls: ToolCall[] = []; + const toolCallArgs: Map = new Map(); + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta; + if (!delta) continue; + + if (delta.content) { + contentBuffer += delta.content; + options.onToken(delta.content); + } + + if (delta.tool_calls) { + for (const tc of delta.tool_calls) { + if (tc.id) { + toolCalls.push({ + id: tc.id, + type: "function", + function: { name: tc.function?.name ?? "", arguments: "" }, + }); + } + if (tc.function?.arguments) { + const existing = toolCallArgs.get(tc.index) ?? ""; + toolCallArgs.set(tc.index, existing + tc.function.arguments); + } + } + } + } + + // Finalize accumulated tool call arguments + for (const [index, args] of toolCallArgs) { + if (toolCalls[index]) { + toolCalls[index].function.arguments = args; + } + } + + // Append the full assistant message to history + const assistantMsg: Message = { + role: "assistant", + content: contentBuffer || undefined, + tool_calls: toolCalls.length > 0 ? toolCalls : undefined, + }; + messages = [...messages, assistantMsg]; + + // If no tool calls, we're done + if (!assistantMsg.tool_calls) { + options.onComplete(); + return messages; + } + + // Execute each tool call + for (const tc of assistantMsg.tool_calls) { + const parsed = JSON.parse(tc.function.arguments); + const command: string = parsed.command; + + options.onToolCall(command); + + if (!options.autoApprove) { + const approved = await options.requestApproval(command); + if (!approved) { + messages = [ + ...messages, + { role: "tool", tool_call_id: tc.id, content: "(skipped by user)" }, + ]; + continue; + } + } + + const output = await executeBash(command); + options.onToolResult(output); + messages = [...messages, { role: "tool", tool_call_id: tc.id, content: output }]; + } + // Loop back to call the API again with updated messages + } +}