Add coding agent system prompt, tool set, and /v1/agent endpoint

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 <noreply@anthropic.com>
This commit is contained in:
alexpolo1
2026-02-28 22:03:32 +01:00
parent ec3beea01e
commit 6147f539ac
3 changed files with 205 additions and 3 deletions

View File

@@ -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()

View File

@@ -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<Tool> = 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<String, JsonObject>,
required: List<String> = 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)
}
}

View File

@@ -156,9 +156,10 @@ data class ServerStatus(
val uptime_seconds: Long,
val requests_served: Long,
val endpoints: List<String> = 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 /"
)