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:
@@ -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()
|
||||
|
||||
|
||||
162
app/src/main/java/com/pixel10/ai/server/AgentConfig.kt
Normal file
162
app/src/main/java/com/pixel10/ai/server/AgentConfig.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 /"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user