Skip to content

Instantly share code, notes, and snippets.

@NickCis
Last active February 2, 2026 17:07
Show Gist options
  • Select an option

  • Save NickCis/e7a724d54e39eb9fe6b946adc5d69443 to your computer and use it in GitHub Desktop.

Select an option

Save NickCis/e7a724d54e39eb9fe6b946adc5d69443 to your computer and use it in GitHub Desktop.
Example agent using ollama api
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