Last active
February 2, 2026 17:07
-
-
Save NickCis/e7a724d54e39eb9fe6b946adc5d69443 to your computer and use it in GitHub Desktop.
Example agent using ollama api
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import fs from "fs"; | |
| import path from "path"; | |
| import readline from "readline"; | |
| import { execSync } from "child_process"; | |
| /* ---------------- CONFIG ---------------- */ | |
| const OLLAMA_URL = "http://localhost:11434/api/chat"; | |
| // const MODEL = "llama3"; | |
| // const MODEL = "gemma3:12b"; | |
| // const MODEL = "orieg/gemma3-tools:12b"; | |
| // const MODEL = "llama3.2:3b"; | |
| const MODEL = "gemma2:9b"; | |
| const MAX_AGENT_STEPS = 10; | |
| /** Fallback context length (tokens) when Ollama does not report it (e.g. /api/show unavailable). */ | |
| const FALLBACK_CONTEXT_LENGTH = 8192; | |
| /* ---------------- DEFAULT SYSTEM PROMPT ---------------- */ | |
| const DEFAULT_SYSTEM_PROMPT = ` | |
| You are a coding agent. | |
| Rules: | |
| - Always respond in JSON. No markdown, no extra backtics, just the plain json object. | |
| - Allowed actions: bash, file, respond, done. | |
| - Think step by step. | |
| - Use bash only when necessary. | |
| - Never hallucinate command output. | |
| - Never run destructive commands unless explicitly requested. | |
| JSON schema: | |
| { | |
| "thought": "short reasoning", | |
| "action": "bash | file | respond | done", | |
| "command": "string (only if bash)", | |
| "message": "string (only if respond or done)", | |
| "path": "string (only if file, file path)", | |
| "content": "string (only if file, file content)", | |
| } | |
| `.trim(); | |
| /* ---------------- CONTEXT ---------------- */ | |
| const CONTEXT = ` | |
| OS: Linux | |
| Environment: local machine | |
| Available tools: | |
| - bash (requires explicit user approval) | |
| Safety rules: | |
| - Commands must be confirmed by the user | |
| - Output must come from real command execution | |
| `.trim(); | |
| /* ---------------- READLINE ---------------- */ | |
| const rl = readline.createInterface({ | |
| input: process.stdin, | |
| output: process.stdout | |
| }); | |
| function ask(question) { | |
| return new Promise(resolve => rl.question(question, resolve)); | |
| } | |
| async function askYesNo(question) { | |
| const answer = await ask(`${question} (y/n): `); | |
| return answer.toLowerCase().startsWith("y"); | |
| } | |
| /* ---------------- LOGGING ---------------- */ | |
| function log(stage, data) { | |
| console.log(`\n[${new Date().toISOString()}] ${stage}`); | |
| if (data !== undefined) console.log(data); | |
| } | |
| /* ---------------- TOKEN TRACKING ---------------- */ | |
| let tokenStats = { | |
| totalPromptTokens: 0, | |
| totalCompletionTokens: 0, | |
| lastPromptEvalCount: 0 // full context size as seen by last request | |
| }; | |
| const VERBOSE_MODEL_KEYS = new Set(["tensors", "modelfile", "template", "license"]); | |
| function modelInfoForLog(info) { | |
| if (!info || typeof info !== "object") return null; | |
| const out = {}; | |
| for (const [key, value] of Object.entries(info)) { | |
| if (VERBOSE_MODEL_KEYS.has(key)) continue; | |
| if (typeof value === "string" && value.length > 200) continue; | |
| if (value != null && typeof value === "object" && !Array.isArray(value)) { | |
| const nested = {}; | |
| for (const [k, v] of Object.entries(value)) { | |
| if (VERBOSE_MODEL_KEYS.has(k)) continue; | |
| if (typeof v !== "string" || v.length <= 200) nested[k] = v; | |
| } | |
| out[key] = Object.keys(nested).length ? nested : value; | |
| } else { | |
| out[key] = value; | |
| } | |
| } | |
| return out; | |
| } | |
| async function getModelContextLength() { | |
| try { | |
| const res = await fetch(`http://localhost:11434/api/show`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ name: MODEL }) | |
| }); | |
| const info = await res.json(); | |
| return { | |
| contextLength: info.parameters?.num_ctx ?? null, | |
| modelInfo: info | |
| }; | |
| } catch { | |
| return { contextLength: null, modelInfo: null }; | |
| } | |
| } | |
| function logTokenUsage(promptEvalCount, evalCount, contextLength) { | |
| const prompt = promptEvalCount ?? 0; | |
| const completion = evalCount ?? 0; | |
| tokenStats.totalCompletionTokens += completion; | |
| if (prompt > 0) { | |
| tokenStats.lastPromptEvalCount = prompt; | |
| tokenStats.totalPromptTokens = prompt; // last request's full prompt size | |
| } | |
| const parts = [ | |
| `prompt=${prompt}`, | |
| `completion=${completion}`, | |
| `total_completion=${tokenStats.totalCompletionTokens}` | |
| ]; | |
| if (contextLength != null) { | |
| const currentContext = tokenStats.lastPromptEvalCount + completion; | |
| const pct = Math.round((currentContext / contextLength) * 100); | |
| parts.push(`context_window=${currentContext}/${contextLength} (${pct}%)`); | |
| if (pct >= 90) { | |
| console.warn("\n⚠ Context window usage is high; consider summarizing or starting a new session."); | |
| } | |
| } | |
| log("TOKENS", parts.join(" | ")); | |
| } | |
| /* ---------------- LLM ---------------- */ | |
| function unwrapTripleBackticks(text) { | |
| const trimmed = text.trim(); | |
| // Matches: | |
| // ```json | |
| // {...} | |
| // ``` | |
| // OR | |
| // ``` | |
| // {...} | |
| // ``` | |
| const match = trimmed.match( | |
| /^```(?:json)?\s*([\s\S]*?)\s*```$/i | |
| ); | |
| if (match) { | |
| return match[1].trim(); | |
| } | |
| return text; | |
| } | |
| async function chat(messages) { | |
| log("LLM REQUEST (last message)", messages[messages.length - 1]); | |
| const res = await fetch(OLLAMA_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| model: MODEL, | |
| messages, | |
| stream: false | |
| }) | |
| }); | |
| const json = await res.json(); | |
| if (json.error) | |
| throw new Error(json.error); | |
| log("LLM RAW RESPONSE", json.message.content); | |
| return { | |
| content: json.message.content, | |
| promptEvalCount: json.prompt_eval_count ?? null, | |
| evalCount: json.eval_count ?? null | |
| }; | |
| } | |
| /* ---------------- AGENT LOOP ---------------- */ | |
| async function runAgent(messages, contextLength) { | |
| for (let step = 1; step <= MAX_AGENT_STEPS; step++) { | |
| log(`AGENT STEP ${step}`, "Planning…"); | |
| const result = await chat(messages); | |
| logTokenUsage(result.promptEvalCount, result.evalCount, contextLength); | |
| const reply = result.content; | |
| let parsed; | |
| try { | |
| parsed = JSON.parse(unwrapTripleBackticks(reply)); | |
| } catch { | |
| throw new Error("LLM did not return valid JSON. Aborting agent loop."); | |
| } | |
| log("PARSED ACTION", parsed); | |
| messages.push({ role: "assistant", content: reply }); | |
| if (parsed.action === "done") { | |
| console.log("\n✔ Agent finished:", parsed.message); | |
| return; | |
| } | |
| if (parsed.action === "respond") { | |
| console.log("\n💬 Agent:", parsed.message); | |
| return; | |
| } | |
| if (parsed.action === "bash") { | |
| console.log(`\n⚠ Agent wants to run:\n$ ${parsed.command}`); | |
| const approved = await askYesNo("Run this command?"); | |
| if (!approved) { | |
| messages.push({ | |
| role: "user", | |
| content: "User denied command execution." | |
| }); | |
| continue; | |
| } | |
| log("EXECUTING COMMAND", parsed.command); | |
| let output; | |
| try { | |
| output = execSync(parsed.command, { encoding: "utf8" }); | |
| } catch (err) { | |
| output = err.stderr || err.message; | |
| } | |
| log("COMMAND OUTPUT", output); | |
| messages.push({ | |
| role: "user", | |
| content: `Command output:\n${output}` | |
| }); | |
| continue; | |
| } | |
| if (parsed.action === "file") { | |
| const fullPath = path.resolve(parsed.path); | |
| console.log(`\n⚠ Agent wants to create a file:\n${fullPath}\n${parsed.content}`); | |
| const approved = await askYesNo("Run this command?"); | |
| if (!approved) { | |
| messages.push({ | |
| role: "user", | |
| content: "User denied file actions." | |
| }); | |
| continue; | |
| } | |
| fs.mkdirSync(path.dirname(fullPath), { recursive: true }); | |
| fs.writeFileSync(fullPath, parsed.content, "utf8"); | |
| messages.push({ | |
| role: "system", | |
| content: `File operation ${parsed.action} on ${parsed.path} completed` | |
| }); | |
| continue; | |
| } | |
| messages.push({ | |
| role: "system", | |
| content: `Unknown action ${parsed.action}` | |
| }); | |
| continue; | |
| throw new Error(`Unknown action: ${parsed.action}`); | |
| } | |
| console.log("\n⚠ Max agent steps reached."); | |
| } | |
| /* ---------------- MAIN INTERACTIVE LOOP ---------------- */ | |
| async function main() { | |
| console.log("\n=== Interactive Ollama Agent ===\n"); | |
| const { contextLength: fromOllama, modelInfo } = await getModelContextLength(); | |
| const contextLength = fromOllama ?? FALLBACK_CONTEXT_LENGTH; | |
| if (modelInfo != null) { | |
| const loggable = modelInfoForLog(modelInfo); | |
| if (loggable != null && Object.keys(loggable).length) log("MODEL INFO (from /api/show)", loggable); | |
| } | |
| log( | |
| "MODEL CONTEXT LENGTH", | |
| fromOllama != null ? `${contextLength} tokens` : `${contextLength} tokens (fallback; Ollama /api/show unavailable)` | |
| ); | |
| // --- Log default system prompt at startup --- | |
| log("DEFAULT SYSTEM PROMPT", DEFAULT_SYSTEM_PROMPT); | |
| let systemPrompt = DEFAULT_SYSTEM_PROMPT; | |
| const messages = [ | |
| { role: "system", content: systemPrompt }, | |
| { role: "system", content: `Context:\n${CONTEXT}` } | |
| ]; | |
| while (true) { | |
| const userInput = await ask( | |
| "\nNext instruction (:prompt to change system prompt, :exit to quit):\n> " | |
| ); | |
| if (userInput === ":exit") break; | |
| if (userInput === ":prompt") { | |
| const newPrompt = await ask("\nEnter NEW SYSTEM PROMPT:\n> "); | |
| systemPrompt = newPrompt.trim(); | |
| messages.length = 0; | |
| messages.push( | |
| { role: "system", content: systemPrompt }, | |
| { role: "system", content: `Context:\n${CONTEXT}` } | |
| ); | |
| tokenStats = { | |
| totalPromptTokens: 0, | |
| totalCompletionTokens: 0, | |
| lastPromptEvalCount: 0 | |
| }; | |
| log("SYSTEM PROMPT UPDATED", systemPrompt); | |
| console.log("✔ Session reset with new system prompt."); | |
| continue; | |
| } | |
| messages.push({ role: "user", content: userInput }); | |
| try { | |
| await runAgent(messages, contextLength); | |
| } catch (err) { | |
| console.error("❌ Agent error:", err.message); | |
| } | |
| } | |
| rl.close(); | |
| console.log("\nBye 👋"); | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment