Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,23 @@ import { sendMessage, type Response } from "./client"
import { findTool } from "./tools"
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions"

class ToolApprovalRejectedError extends Error {
constructor(toolName: string) {
super(`Approval rejected for tool: ${toolName}`)
this.name = "ToolApprovalRejectedError"
}
}

const bold = (s: string) => `\x1b[1m${s}\x1b[0m`
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`

const approvalRequiredTools = new Set([
"write_file",
"edit_file",
"bash",
])

const conversation: ChatCompletionMessageParam[] = []

console.log(cyan(`
Expand All @@ -28,11 +41,37 @@ while (true) {
const input = prompt(bold("you> "))
if (!input) continue

if (input.trim().startsWith("run ")) {
const command = input.trim().slice(4)

console.log(dim(`\n Agent wants to use tool: ${cyan("bash")}`))
console.log(dim(` Input: ${JSON.stringify({ command }, null, 2)}`))

const approved = prompt(bold("approve? (y/n) "))

if (approved?.toLowerCase() !== "y") {
console.log(dim("\n Approval rejected. No changes were made."))
continue
}

const tool = findTool("bash")
const result = tool
? await tool.call({ command })
: "Error: bash tool not found"

console.log(dim(` [${cyan("bash")}] ${JSON.stringify({ command })}`))
console.log(String(result) || dim(" <no output>"))
continue
}

conversation.push({ role: "user", content: input })

try {
process.stdout.write("\n" + bold("agent> "))
let response: Response = await sendMessage(conversation)
if (!response.wantsToUseTools && response.content) {
process.stdout.write(response.content)
}

// The inference loop — keep going while the model wants to use tools
while (response.wantsToUseTools) {
Expand All @@ -41,13 +80,31 @@ while (true) {
// Execute all requested tools in parallel
const toolResults = await Promise.all(
response.toolCalls.map(async (tc) => {
const tool = findTool(tc.function.name)
const toolName = tc.function.name
const tool = findTool(toolName)
const input = JSON.parse(tc.function.arguments)

if (approvalRequiredTools.has(toolName)) {
console.log(dim(`\n Agent wants to use tool: ${cyan(toolName)}`))
console.log(dim(` Input: ${JSON.stringify(input, null, 2)}`))

const approved = prompt(bold("approve? (y/n) "))

if (approved?.toLowerCase() !== "y") {
throw new ToolApprovalRejectedError(toolName)
}
}

const result = tool
? await tool.call(input)
: `Error: unknown tool '${tc.function.name}'`
: `Error: unknown tool '${toolName}'`

console.log(dim(` [${cyan(toolName)}] ${JSON.stringify(input)}`))

console.log(dim(` [${cyan(tc.function.name)}] ${JSON.stringify(input)}`))
if (toolName === "bash") {
console.log(dim("\n Command output:"))
console.log(String(result) || dim(" <no output>"))
}

return {
role: "tool" as const,
Expand All @@ -64,6 +121,9 @@ while (true) {
// Ask the model again — it now has the tool results
process.stdout.write("\n" + bold("agent> "))
response = await sendMessage(conversation)
if (!response.wantsToUseTools && response.content) {
process.stdout.write(response.content)
}
}

// Text was already streamed, just record it
Expand All @@ -73,6 +133,11 @@ while (true) {
// Remove the user message we just pushed — the turn failed
conversation.pop()

if (e instanceof ToolApprovalRejectedError) {
console.log(dim("\n Approval rejected. No changes were made."))
continue
}

const code = e?.error?.code || e?.code
if (code === "ConnectionRefused" || code === "ECONNREFUSED") {
console.log(dim("\n Connection refused — is LM Studio running on localhost:1234?"))
Expand Down
34 changes: 31 additions & 3 deletions client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,39 @@ import type { ChatCompletionMessageParam } from "openai/resources/chat/completio
const MODEL = "qwen2.5-coder-14b-instruct"

const SYSTEM_PROMPT = `You are a helpful coding agent. You have access to tools that let you
read files, list directories, and edit code.
read files, list directories, edit code, and run shell commands.

IMPORTANT RULES:

- If the user asks to create, edit, delete, move, or inspect files:
ALWAYS use tools.
NEVER only describe how to do it manually.

- If the user asks to run terminal commands:
ALWAYS use the bash tool.
NEVER simulate command output.

- If a request requires a tool:
USE THE TOOL.
DO NOT explain how to do it manually instead.

- Do not pretend commands were executed if they were not.

- Do not provide hypothetical shell commands instead of using tools.

- If a tool execution is rejected by the user:
stop the current task immediately.
do not attempt workarounds.
do not retry automatically.
do not explain alternative manual steps unless explicitly asked.

Use your tools to look at actual files rather than guessing about their contents.
When you're done, respond with a clear summary of what you did or found.

RULE:
If a request requires a tool,
DO NOT explain how to do it manually.
USE THE TOOL.

## Environment
- User: ${Bun.spawnSync(["whoami"]).stdout.toString().trim()}
- OS: ${Bun.spawnSync(["uname", "-s"]).stdout.toString().trim()} ${Bun.spawnSync(["uname", "-r"]).stdout.toString().trim()}
Expand Down Expand Up @@ -48,6 +76,7 @@ export async function sendMessage(
model: MODEL,
messages,
tools: tools.map((t) => t.definition),
tool_choice: "auto",
max_tokens: 4096,
stream: true,
})
Expand All @@ -64,7 +93,6 @@ export async function sendMessage(

if (choice.delta.content) {
content += choice.delta.content
process.stdout.write(choice.delta.content)
}

if (choice.delta.tool_calls) {
Expand Down