From 6147f539ace816eb92f43ea8b852ae57a219c0e8 Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Sat, 28 Feb 2026 22:03:32 +0100 Subject: [PATCH] Add coding agent system prompt, tool set, and /v1/agent endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentConfig.kt defines the full agent configuration optimised for Gemini Nano on the Tensor G5 chip: System prompt (~700 tokens): - Five explicit workflow stages: EXPLORE → PLAN → CHANGE → VERIFY → DONE - Hard rules: one tool per turn, 1-3 sentence replies, 120-line read limit, max 3 files per task, patch_file preferred over write_file Seven tools (OpenAI function-calling format): read_file(path, start_line?, end_line?) — sectioned reads, max 120 lines write_file(path, content) — new files / full rewrites < 80 ln patch_file(path, old_str, new_str) — targeted in-place edits (preferred) list_dir(path, depth?) — directory structure search_code(pattern, path?, include?) — regex search across files run_command(command, cwd?) — build, test, lint task_done(summary, files_changed?) — explicit completion signal AIApiServer changes: - Auto-injects the agent system prompt when the conversation has no system message, and auto-injects DEFAULT_TOOLS when the request provides none. Makes the server zero-config for any OpenAI-compatible agent client. - New GET /v1/agent endpoint returns system_prompt + tools + notes as JSON so clients like OpenClaw can fetch the config and apply it automatically. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/pixel10/ai/server/AIApiServer.kt | 43 ++++- .../java/com/pixel10/ai/server/AgentConfig.kt | 162 ++++++++++++++++++ .../java/com/pixel10/ai/server/ApiModels.kt | 3 +- 3 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/pixel10/ai/server/AgentConfig.kt diff --git a/app/src/main/java/com/pixel10/ai/server/AIApiServer.kt b/app/src/main/java/com/pixel10/ai/server/AIApiServer.kt index 7345b22..8e4bd05 100644 --- a/app/src/main/java/com/pixel10/ai/server/AIApiServer.kt +++ b/app/src/main/java/com/pixel10/ai/server/AIApiServer.kt @@ -50,6 +50,7 @@ class AIApiServer( method == Method.OPTIONS -> corsPreflightResponse() uri == "/" || uri == "/health" -> handleHealth() uri == "/v1/models" && method == Method.GET -> handleModels() + uri == "/v1/agent" && method == Method.GET -> handleAgentConfig() uri == "/v1/chat/completions" && method == Method.POST -> handleChatCompletions(session) uri == "/v1/completions" && method == Method.POST -> handleCompletions(session) else -> errorResponse(404, "Not found: $uri") @@ -79,14 +80,52 @@ class AIApiServer( return jsonResponse(200, gson.toJson(ModelList())) } + /** + * GET /v1/agent + * + * Returns the recommended system prompt and tool definitions for using this + * server as a coding agent brain. Clients (OpenClaw, Open WebUI, etc.) can + * fetch this once and inject it into every conversation automatically. + * + * Example: + * curl http://phone:8080/v1/agent | jq .system_prompt + */ + private fun handleAgentConfig(): Response { + val config = mapOf( + "system_prompt" to AgentConfig.SYSTEM_PROMPT, + "tools" to AgentConfig.DEFAULT_TOOLS, + "model" to "pixel10", + "notes" to mapOf( + "context_window" to "~32K tokens input", + "max_output_tokens" to 1024, + "tip" to "Keep each task small and focused. One file change per tool call. " + + "Use patch_file for edits, write_file for new files only." + ) + ) + return jsonResponse(200, gson.toJson(config)) + } + private fun handleChatCompletions(session: IHTTPSession): Response { val body = readBody(session) - val request = gson.fromJson(body, ChatRequest::class.java) + val raw = gson.fromJson(body, ChatRequest::class.java) - if (request.messages.isEmpty()) { + if (raw.messages.isEmpty()) { return errorResponse(400, "messages array is required and must not be empty") } + // Auto-inject agent system prompt if the conversation has no system message. + // Auto-inject default tools if the request provides none. + // This makes the server zero-config as a coding agent for any OpenAI-compatible client. + val messages = if (raw.messages.none { it.role == "system" }) { + listOf(Message(role = "system", content = AgentConfig.SYSTEM_PROMPT)) + raw.messages + } else { + raw.messages + } + val request = raw.copy( + messages = messages, + tools = raw.tools.takeUnless { it.isNullOrEmpty() } ?: AgentConfig.DEFAULT_TOOLS + ) + val id = "chatcmpl-${UUID.randomUUID().toString().take(8)}" val hasTools = !request.tools.isNullOrEmpty() diff --git a/app/src/main/java/com/pixel10/ai/server/AgentConfig.kt b/app/src/main/java/com/pixel10/ai/server/AgentConfig.kt new file mode 100644 index 0000000..e175c4a --- /dev/null +++ b/app/src/main/java/com/pixel10/ai/server/AgentConfig.kt @@ -0,0 +1,162 @@ +package com.pixel10.ai.server + +import com.google.gson.JsonArray +import com.google.gson.JsonObject + +/** + * Default agent configuration for the Pixel10 AI coding agent. + * + * Designed for Gemini Nano on the Tensor G5 chip — a small, fast, + * fully on-device model. The system prompt and tool set are tuned to + * work within the model's context window by keeping every turn focused + * and minimal. No token is wasted. + * + * Tool use flow (OpenAI-compatible): + * 1. Client sends messages (+ tools from this config) + * 2. Server returns finish_reason=tool_calls with the tool to invoke + * 3. Client executes the tool locally, appends result as role=tool message + * 4. Client sends updated conversation back → repeat until task_done + */ +object AgentConfig { + + // ── System Prompt ────────────────────────────────────────────────────────── + // + // Target: ≤ 700 tokens. Every token here costs context on every turn. + // Written to get the most out of a small on-device model: + // - Numbered rules (easy to follow for small models) + // - Explicit workflow stages (reduces hallucination / aimless tool calls) + // - Hard size limits on reads/writes (prevents context overflow) + + const val SYSTEM_PROMPT = """You are a precise coding agent running on a Pixel 10's Tensor G5 chip. + +## Constraints +- One tool call per turn. Wait for the result before calling another. +- Your text reply must be 1-3 sentences max. Let tools do the work. +- Never output file contents in text — use read_file and write_file. +- Never change more than 3 files per task. If more are needed, stop and ask. + +## Workflow — follow in order every time +1. EXPLORE : list_dir to map the structure. read_file with start_line/end_line to read only what is relevant (max 120 lines per read). search_code to locate symbols. +2. PLAN : State in one sentence what you will change and why. +3. CHANGE : Use patch_file to replace exact text (preferred). Use write_file only for new files or full rewrites under 80 lines. +4. VERIFY : run_command to build or test after every change. Fix errors before continuing. +5. DONE : Call task_done with a one-paragraph summary of every file changed. + +## Rules +- Always read a file before modifying it. +- When reading large files, use start_line/end_line — never load the whole file. +- Use search_code before reading to find the exact lines you need. +- patch_file is preferred over write_file: specify the exact text to replace. +- If a command fails, read the error, fix the cause, retry once. If it fails again, call task_done with the error and what you tried.""" + + // ── Tool Definitions ─────────────────────────────────────────────────────── + // + // 7 tools covering the full coding agent surface. + // Descriptions are kept short — they repeat on every request turn. + + val DEFAULT_TOOLS: List = listOf( + + tool( + name = "read_file", + description = "Read a file. Use start_line/end_line to read a section (max 120 lines). Always prefer sections over full files.", + properties = mapOf( + "path" to strProp("Absolute or workspace-relative file path"), + "start_line" to intProp("First line to read, 1-indexed (optional)"), + "end_line" to intProp("Last line to read, 1-indexed (optional)") + ), + required = listOf("path") + ), + + tool( + name = "write_file", + description = "Create a new file or fully overwrite an existing one. Use only for new files or complete rewrites under 80 lines. Prefer patch_file for edits.", + properties = mapOf( + "path" to strProp("File path to write"), + "content" to strProp("Full file content to write") + ), + required = listOf("path", "content") + ), + + tool( + name = "patch_file", + description = "Replace an exact string inside a file. Preferred for edits — avoids rewriting the whole file. old_str must match exactly including whitespace.", + properties = mapOf( + "path" to strProp("File path to patch"), + "old_str" to strProp("Exact text to find and replace (must match exactly)"), + "new_str" to strProp("Replacement text") + ), + required = listOf("path", "old_str", "new_str") + ), + + tool( + name = "list_dir", + description = "List files and directories at a path. Use depth=1 for a flat listing, depth=2 to include one level of subdirectories.", + properties = mapOf( + "path" to strProp("Directory path to list"), + "depth" to intProp("Max depth: 1 (flat) or 2 (with subdirs). Default 1.") + ), + required = listOf("path") + ), + + tool( + name = "search_code", + description = "Search for a regex pattern in files. Returns matching lines with file path and line number. Use this before read_file to find exactly which lines to read.", + properties = mapOf( + "pattern" to strProp("Regex pattern to search for"), + "path" to strProp("Directory or file to search in (default: workspace root)"), + "include" to strProp("Glob filter, e.g. '*.kt' or '*.py' (optional)") + ), + required = listOf("pattern") + ), + + tool( + name = "run_command", + description = "Run a shell command and return stdout+stderr. Use for build, test, lint, install. Keep commands short and targeted.", + properties = mapOf( + "command" to strProp("Shell command to execute"), + "cwd" to strProp("Working directory (optional, defaults to workspace root)") + ), + required = listOf("command") + ), + + tool( + name = "task_done", + description = "Signal that the task is fully complete. Call this as the final action — never leave a task without calling it.", + properties = mapOf( + "summary" to strProp("One paragraph describing what was changed and why"), + "files_changed" to strProp("Comma-separated list of files that were modified or created") + ), + required = listOf("summary") + ) + ) + + // ── Helpers ──────────────────────────────────────────────────────────────── + + private fun tool( + name: String, + description: String, + properties: Map, + required: List = emptyList() + ): Tool { + val params = JsonObject().apply { + addProperty("type", "object") + add("properties", JsonObject().apply { + properties.forEach { (k, v) -> add(k, v) } + }) + if (required.isNotEmpty()) { + add("required", JsonArray().apply { required.forEach { add(it) } }) + } + } + return Tool(function = ToolFunction(name = name, description = description, parameters = params)) + } + + private fun strProp(description: String) = JsonObject().apply { + addProperty("type", "string") + addProperty("description", description) + } + + private fun intProp(description: String) = JsonObject().apply { + addProperty("type", "integer") + addProperty("description", description) + } +} diff --git a/app/src/main/java/com/pixel10/ai/server/ApiModels.kt b/app/src/main/java/com/pixel10/ai/server/ApiModels.kt index 2c55da2..dbeb0f3 100644 --- a/app/src/main/java/com/pixel10/ai/server/ApiModels.kt +++ b/app/src/main/java/com/pixel10/ai/server/ApiModels.kt @@ -156,9 +156,10 @@ data class ServerStatus( val uptime_seconds: Long, val requests_served: Long, val endpoints: List = listOf( - "POST /v1/chat/completions (tools, streaming, thinking supported)", + "POST /v1/chat/completions (tool calling + streaming)", "POST /v1/completions", "GET /v1/models", + "GET /v1/agent (system prompt + tool definitions)", "GET /health", "GET /" )