From 15aa7d71f090a43dfc697bba4375a7a7a2e45b9c Mon Sep 17 00:00:00 2001 From: TerminallyLazy Date: Wed, 24 Jun 2026 03:59:21 -0400 Subject: [PATCH 1/2] feat: add tiny local tool-use profile --- agents/AGENTS.md | 1 + agents/tiny-local/AGENTS.md | 36 ++++++++++ agents/tiny-local/agent.yaml | 3 + .../agent.system.main.communication.md | 29 +++++++++ .../prompts/agent.system.main.solving.md | 17 +++++ .../prompts/agent.system.tool.code_exe.md | 24 +++++++ .../prompts/agent.system.tool.response.md | 13 ++++ .../prompts/agent.system.tool.text_editor.md | 26 ++++++++ .../tiny-local/prompts/agent.system.tools.md | 17 +++++ docs/guides/agent-profiles.md | 4 ++ docs/guides/local-model-tool-use.md | 65 +++++++++++++++++++ plugins/_editor/AGENTS.md | 1 + .../sync-text-editor-results.js | 19 +++++- plugins/_editor/webui/editor-store.js | 9 ++- plugins/_text_editor/AGENTS.md | 1 + plugins/_text_editor/tools/text_editor.py | 17 +++-- tests/test_default_prompt_budget.py | 58 +++++++++++++++++ tests/test_office_canvas_setup.py | 4 ++ tests/test_text_editor_context_patch.py | 7 +- tests/test_tool_action_contracts.py | 15 +++++ 20 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 agents/tiny-local/AGENTS.md create mode 100644 agents/tiny-local/agent.yaml create mode 100644 agents/tiny-local/prompts/agent.system.main.communication.md create mode 100644 agents/tiny-local/prompts/agent.system.main.solving.md create mode 100644 agents/tiny-local/prompts/agent.system.tool.code_exe.md create mode 100644 agents/tiny-local/prompts/agent.system.tool.response.md create mode 100644 agents/tiny-local/prompts/agent.system.tool.text_editor.md create mode 100644 agents/tiny-local/prompts/agent.system.tools.md create mode 100644 docs/guides/local-model-tool-use.md diff --git a/agents/AGENTS.md b/agents/AGENTS.md index d2df985b1b..ae6bda3c53 100644 --- a/agents/AGENTS.md +++ b/agents/AGENTS.md @@ -41,3 +41,4 @@ Direct child DOX files: | [developer/AGENTS.md](developer/AGENTS.md) | Software development specialist profile. | | [hacker/AGENTS.md](hacker/AGENTS.md) | Cyber security and penetration testing specialist profile. | | [researcher/AGENTS.md](researcher/AGENTS.md) | Research, data analysis, and reporting specialist profile. | +| [tiny-local/AGENTS.md](tiny-local/AGENTS.md) | Small/local model profile with an action-first communication prompt. | diff --git a/agents/tiny-local/AGENTS.md b/agents/tiny-local/AGENTS.md new file mode 100644 index 0000000000..465b55bf3b --- /dev/null +++ b/agents/tiny-local/AGENTS.md @@ -0,0 +1,36 @@ +# Tiny Local Agent Profile DOX + +## Purpose + +- Own the bundled Tiny Local profile for small/local chat models. +- Keep local-model behavior prompt-only and isolated from core framework execution. + +## Ownership + +- `agent.yaml` owns profile metadata for discovery and profile switching. +- `prompts/agent.system.main.communication.md` owns the local-model communication contract. +- `prompts/agent.system.main.solving.md` owns the local-model problem-solving contract and suppresses inherited visible reasoning requirements. +- `prompts/agent.system.tools.md` owns the Tiny Local tools wrapper and final output-shape reminder after tool listing. +- `prompts/agent.system.tool.*.md` files own Tiny Local-specific tool examples that avoid inherited reasoning fields and repeated writes. + +## Local Contracts + +- Preserve the normal Agent Zero tool-call shape: `tool_name` plus `tool_args`. +- Do not add parser repair, duplicate suppression, model transport, memory, or text-editor runtime behavior here. +- Keep prompt text short enough for small local models to follow. +- Treat continuation requests such as `proceed` or `continue` as commands to execute the next unfinished step, not as prompts for another status response. +- Do not include user-specific provider names, API keys, local paths, or secrets. + +## Work Guidance + +- Prefer prompt wording changes over new files when tightening this profile, except when replacing inherited tool examples for local-model compliance. +- Keep this profile suitable for Ollama, LM Studio, Qwen, and comparable local models. + +## Verification + +- Render the `tiny-local` system prompt after communication prompt changes. +- Run `pytest tests/test_default_prompt_budget.py` for prompt and profile regressions. + +## Child DOX Index + +No child DOX files. diff --git a/agents/tiny-local/agent.yaml b/agents/tiny-local/agent.yaml new file mode 100644 index 0000000000..413dcbfbf4 --- /dev/null +++ b/agents/tiny-local/agent.yaml @@ -0,0 +1,3 @@ +title: Tiny Local +description: Action-first profile for small local models that need a minimal tool-call contract. +context: Use this agent when running small local chat models through Ollama, LM Studio, or similar providers and the model tends to explain actions instead of calling tools. diff --git a/agents/tiny-local/prompts/agent.system.main.communication.md b/agents/tiny-local/prompts/agent.system.main.communication.md new file mode 100644 index 0000000000..690220defe --- /dev/null +++ b/agents/tiny-local/prompts/agent.system.main.communication.md @@ -0,0 +1,29 @@ +## Communication + +You are Agent Zero. Act on the user's behalf. + +When the user asks you to do something, do it directly. Do not explain how the user could do it themselves. + +Your visible assistant message must be exactly one valid JSON object. + +Use exactly these top-level fields: `"tool_name"` and `"tool_args"`. + +Do not include markdown fences, prose before the JSON, prose after the JSON, hidden reasoning, analysis, thoughts, or headlines. + +Choose a tool from the tools listed in this system prompt. Do not invent tool names, action names, or generic names such as `read`, `write`, `terminal`, or `multi`. + +For a final user-facing answer, use the `response` tool. + +Use `response` only when the work is complete, blocked, or the user is only acknowledging completed work. + +If the user says "proceed", "continue", "go ahead", "do it", "excellent proceed", or similar after you named a next step or there is unfinished work, do not answer with a promise or status update. Call the next appropriate tool. + +Final-answer shape: + +`{"tool_name":"response","tool_args":{"text":"Answer briefly."}}` + +For work that requires a command, file action, browser action, or any other available tool, call the appropriate tool immediately. Do not explain what command the user could run manually. + +If the framework warns that your prior message was malformed, repeated, or reasoning-only, output a corrected JSON tool request immediately without explaining the warning. + +{{ include "agent.system.main.communication_additions.md" }} diff --git a/agents/tiny-local/prompts/agent.system.main.solving.md b/agents/tiny-local/prompts/agent.system.main.solving.md new file mode 100644 index 0000000000..6806760c87 --- /dev/null +++ b/agents/tiny-local/prompts/agent.system.main.solving.md @@ -0,0 +1,17 @@ +## Problem Solving + +Act directly and keep hidden reasoning out of the visible JSON. + +For simple questions, answer with the `response` tool. + +Continuation words such as "proceed", "continue", "go ahead", "do it", and "excellent proceed" mean execute the next unfinished step. Do not respond by saying you will begin, continue, start, proceed, or investigate. Use a real tool call unless the task is already complete or blocked. + +For tasks that need shell commands, files, browser actions, or other capabilities: +- choose the appropriate listed tool immediately +- keep one tool call per turn unless the `parallel` tool is listed and truly useful +- inspect outputs before deciding the next tool call +- never claim success from timeout output or a still-running command +- after a successful tool result, do not repeat the same exact tool call +- when finished, use the `response` tool with a brief result + +Do not include `thoughts`, `headline`, analysis, plans, or prose outside the JSON object. diff --git a/agents/tiny-local/prompts/agent.system.tool.code_exe.md b/agents/tiny-local/prompts/agent.system.tool.code_exe.md new file mode 100644 index 0000000000..a42a3314fa --- /dev/null +++ b/agents/tiny-local/prompts/agent.system.tool.code_exe.md @@ -0,0 +1,24 @@ +### code_execution_tool +Run terminal, Python, or Node.js commands. + +Arguments in `tool_args`: +- `runtime`: `terminal`, `python`, `nodejs`, or `output` +- `code`: command or script code +- `session`: terminal session id; default `0` +- `reset`: kill a session before running; `true` or `false` + +Rules: +- Put the command or script in `code`. +- Use `runtime=output` to poll running work. +- Use `input` for interactive terminal prompts. +- If a session is stuck, call this tool again with the same `session` and `reset=true`. +- Do not claim success from timeout output or a still-running command. +- When counting files, prefer `find` over `ls` so hidden files and type filters are handled. + +Examples: + +`{"tool_name":"code_execution_tool","tool_args":{"runtime":"terminal","session":0,"reset":false,"code":"ls -1 /tmp | wc -l"}}` + +`{"tool_name":"code_execution_tool","tool_args":{"runtime":"python","session":0,"reset":false,"code":"import os\nprint(os.getcwd())"}}` + +`{"tool_name":"code_execution_tool","tool_args":{"runtime":"output","session":0}}` diff --git a/agents/tiny-local/prompts/agent.system.tool.response.md b/agents/tiny-local/prompts/agent.system.tool.response.md new file mode 100644 index 0000000000..de0709179d --- /dev/null +++ b/agents/tiny-local/prompts/agent.system.tool.response.md @@ -0,0 +1,13 @@ +### response +Final answer to the user. + +Use this tool only when the task is done, blocked, or no tool is needed. + +Do not use this tool for "proceed", "continue", "go ahead", or similar continuation requests when there is an unfinished next step. Call a real tool instead. + +Arguments in `tool_args`: +- `text`: concise final answer text + +Example: + +`{"tool_name":"response","tool_args":{"text":"There are 24 files in /tmp."}}` diff --git a/agents/tiny-local/prompts/agent.system.tool.text_editor.md b/agents/tiny-local/prompts/agent.system.tool.text_editor.md new file mode 100644 index 0000000000..fdf7ad1417 --- /dev/null +++ b/agents/tiny-local/prompts/agent.system.tool.text_editor.md @@ -0,0 +1,26 @@ +### text_editor +Read, write, or patch Markdown and plain text files. + +Actions in `tool_args.action`: +- `read`: read a file +- `write`: create or overwrite a file +- `patch`: edit an existing file + +Common arguments: +- `path`: absolute file path +- `content`: full file content for `write` +- `open_in_canvas`: set `true` when the user explicitly asks to open a Markdown file in the Canvas or Editor + +Rules: +- Use this tool for `.md` and plain text files. +- Use `write` to create a new Markdown file. +- If the user asks to open the file in the Canvas or Editor, include `"open_in_canvas": true` in the same `write` or `patch` call. +- After a successful write or patch result, do not repeat the same tool call. Use the `response` tool unless a different action is needed. + +Examples: + +`{"tool_name":"text_editor","tool_args":{"action":"write","path":"/a0/usr/workdir/TODO.md","content":"# TODO\n- [ ] First item\n","open_in_canvas":true}}` + +`{"tool_name":"text_editor","tool_args":{"action":"read","path":"/a0/usr/workdir/TODO.md"}}` + +`{"tool_name":"text_editor","tool_args":{"action":"patch","path":"/a0/usr/workdir/TODO.md","old_text":"- [ ] First item","new_text":"- [x] First item"}}` diff --git a/agents/tiny-local/prompts/agent.system.tools.md b/agents/tiny-local/prompts/agent.system.tools.md new file mode 100644 index 0000000000..7c50f33b08 --- /dev/null +++ b/agents/tiny-local/prompts/agent.system.tools.md @@ -0,0 +1,17 @@ +## Available Tools + +Use only the tools listed below. Match tool names exactly. + +Every tool request must be exactly one JSON object with only these top-level fields: +- `tool_name` +- `tool_args` + +Action names are not tool names. Do not invent top-level `multi`, `read`, `write`, `terminal`, or generic batch tools. + +{{tools}} + +## Tiny Local Output Rule + +Some inherited tool examples may show `thoughts` or `headline`. Ignore that shape for this profile. + +Do not include `thoughts`, `headline`, analysis, markdown fences, or prose outside the JSON object. diff --git a/docs/guides/agent-profiles.md b/docs/guides/agent-profiles.md index 41b885fd27..b3e9d97510 100644 --- a/docs/guides/agent-profiles.md +++ b/docs/guides/agent-profiles.md @@ -66,6 +66,10 @@ These controls are related, but they solve different problems. | **Project** | Files, workspace, memories, instructions, secrets, and long-running context. | | **Model Preset** | Which models are used for the chat. | +For small local models that narrate instead of calling tools, use the bundled +**Tiny Local** profile or the project-scoped Prompt Include recipe in +[Local Model Tool Use](local-model-tool-use.md). + Example: - use a **Project** for a client repository; diff --git a/docs/guides/local-model-tool-use.md b/docs/guides/local-model-tool-use.md new file mode 100644 index 0000000000..803583b776 --- /dev/null +++ b/docs/guides/local-model-tool-use.md @@ -0,0 +1,65 @@ +# Local Model Tool Use + +Small local models can struggle with Agent Zero's full default communication shape. The safest first fix is prompt/profile/plugin-only: use a smaller behavior contract while leaving Agent Zero's core parser and execution code unchanged. + +Use this guide for Ollama, LM Studio, Qwen, and similar local chat models when the model explains commands instead of calling tools. + +## Use The Tiny Local Profile + +Choose the **Tiny Local** profile when starting or switching a chat that uses a small local model. + +The bundled profile lives at: + +```text +agents/tiny-local/ +``` + +Tiny Local keeps the normal Agent Zero tool-call shape, but removes visible reasoning fields from the communication prompt. It tells the model to emit one executable JSON object with `tool_name` and `tool_args`. + +## Use A Project Prompt Include + +If you want to keep your current profile, create a project-local file that matches the Prompt Include plugin pattern (`*.promptinclude.md`): + +```text +local-model-tool-use.promptinclude.md +``` + +Put this content in that file: + +```markdown +## Local model tool-use discipline + +You are Agent Zero. Act on the user's behalf. + +When the user asks you to do something, do it directly. Do not explain how the user could do it themselves. + +Your visible assistant message must be exactly one valid JSON object. + +Use exactly these top-level fields: `tool_name` and `tool_args`. + +Do not include markdown fences, prose before the JSON, prose after the JSON, hidden reasoning, analysis, thoughts, or headlines. + +Choose a tool from the tools listed in the system prompt. Do not invent tool names, action names, or generic names such as `read`, `write`, `terminal`, or `multi`. + +For a final user-facing answer, use the `response` tool: + +`{"tool_name":"response","tool_args":{"text":"Done."}}` + +Use `response` only when the work is complete, blocked, or no tool is needed. If the user says "proceed", "continue", "go ahead", or similar after the agent named a next step, call the next appropriate tool instead of replying with a promise or status update. + +For work that requires a command, file action, browser action, or any other available tool, call the appropriate tool immediately. + +If the framework warns that your prior message was malformed, repeated, or reasoning-only, output a corrected JSON tool request immediately without explaining the warning. +``` + +## Keep This Prompt-Only + +Do not change `agent.py` for this workflow. + +Do not change `helpers/extract_tools.py` for this workflow. + +Do not create parser repair code for this workflow. + +Do not add duplicate execution suppression, LiteLLM transport changes, memory runtime changes, or text-editor file operation changes for this workflow. + +If a specific local model still cannot follow the prompt/profile/plugin-only contract, capture the exact model, prompt, response, and tool warning before considering deeper framework changes. diff --git a/plugins/_editor/AGENTS.md b/plugins/_editor/AGENTS.md index f7c2f25b93..90d8dd4e31 100644 --- a/plugins/_editor/AGENTS.md +++ b/plugins/_editor/AGENTS.md @@ -19,6 +19,7 @@ - Keep the floating Editor modal on the shared surface modal chrome so the header remains draggable while existing Focus mode continues to work. - Keep Editor Open wired through the File Browser text picker so users can open one or more Markdown or plain text files with an obvious confirmation action. - Keep Save As distinct from Rename: Save As writes the current editor text to a chosen `.md` or `.txt` path and retargets the active session without removing the original file. +- Preserve source chat context ids when opening Markdown files from tool-result canvas handoffs. ## Work Guidance diff --git a/plugins/_editor/extensions/webui/set_messages_after_loop/sync-text-editor-results.js b/plugins/_editor/extensions/webui/set_messages_after_loop/sync-text-editor-results.js index a8ec4f7083..f4b2947f62 100644 --- a/plugins/_editor/extensions/webui/set_messages_after_loop/sync-text-editor-results.js +++ b/plugins/_editor/extensions/webui/set_messages_after_loop/sync-text-editor-results.js @@ -5,7 +5,7 @@ const SYNC_WINDOW_MS = 10 * 60 * 1000; const syncedTextEditorResults = new Set(); export default async function syncTextEditorResultsIntoOpenEditor(context) { - if (!context?.results?.length || context.historyEmpty) return; + if (!context?.results?.length) return; for (const { args } of context.results) { const payload = getTextEditorPayload(args); @@ -14,6 +14,8 @@ export default async function syncTextEditorResultsIntoOpenEditor(context) { const target = textEditorTarget(payload); if (!target.path || target.extension !== "md") continue; + const explicitOpen = shouldOpenEditorUiFromResult(payload, target); + if (context.historyEmpty && !explicitOpen) continue; const key = [ args?.id || "", @@ -26,10 +28,12 @@ export default async function syncTextEditorResultsIntoOpenEditor(context) { syncedTextEditorResults.add(key); globalThis.setTimeout(() => { - if (shouldOpenEditorUiFromResult(payload, target)) { + if (explicitOpen) { void openSurface("editor", { path: target.path || "", file_id: target.file_id || "", + ctxid: target.ctxid || target.context_id || "", + context_id: target.context_id || target.ctxid || "", refresh: true, source: "tool-result-open", }); @@ -60,6 +64,8 @@ function pickPayloadFields(args = {}) { "action", "extension", "format", + "context_id", + "ctxid", "last_modified", "open_canvas", "open_document", @@ -97,9 +103,12 @@ function isExplicitEditorUiRequest(payload = {}) { function textEditorTarget(payload = {}) { const path = String(payload.path || "").trim(); const extension = documentExtension(payload, { path }); + const contextId = contextIdFromPayload(payload); return { path, file_id: "", + context_id: contextId, + ctxid: contextId, extension, format: extension, version: payload.version || "", @@ -139,6 +148,8 @@ async function syncOpenEditorSurface(document = {}) { await editor.openSession?.({ path: document.path || "", file_id: document.file_id || "", + ctxid: document.ctxid || document.context_id || "", + context_id: document.context_id || document.ctxid || "", refresh: true, source: "tool-result-sync", }); @@ -175,6 +186,10 @@ function documentsMatch(entry = {}, document = {}) { ); } +function contextIdFromPayload(payload = {}) { + return String(payload.context_id || payload.ctxid || "").trim(); +} + function truthy(value) { if (value === true) return true; if (value === false || value == null) return false; diff --git a/plugins/_editor/webui/editor-store.js b/plugins/_editor/webui/editor-store.js index a5997e3870..894bfcb1a6 100644 --- a/plugins/_editor/webui/editor-store.js +++ b/plugins/_editor/webui/editor-store.js @@ -182,17 +182,19 @@ function taskLineIndexes(markdown = "") { } async function callEditor(action, payload = {}) { + const explicitContextId = String(payload.ctxid || payload.context_id || "").trim(); return await callJsonApi("/plugins/_editor/editor_session", { action, - ctxid: currentContextId(), ...payload, + ctxid: explicitContextId || currentContextId(), }); } async function requestEditor(eventType, payload = {}, timeoutMs = 5000) { + const explicitContextId = String(payload.ctxid || payload.context_id || "").trim(); const response = await editorSocket.request(eventType, { - ctxid: currentContextId(), ...payload, + ctxid: explicitContextId || currentContextId(), }, { timeoutMs }); const results = Array.isArray(response?.results) ? response.results : []; const first = results.find((item) => item?.ok === true && isEditorSocketData(item?.data)) @@ -284,9 +286,12 @@ const model = { await this.init(); await this.refresh(); if (payload?.path || payload?.file_id) { + const contextId = String(payload.ctxid || payload.context_id || "").trim(); await this.openSession({ path: payload.path || "", file_id: payload.file_id || "", + ctxid: contextId, + context_id: contextId, refresh: payload.refresh === true, source: payload.source || "", }); diff --git a/plugins/_text_editor/AGENTS.md b/plugins/_text_editor/AGENTS.md index f18d552d31..14f38fb386 100644 --- a/plugins/_text_editor/AGENTS.md +++ b/plugins/_text_editor/AGENTS.md @@ -16,6 +16,7 @@ - Preserve stale-read protection before patch operations. - Validate patch structures before applying edits. - Read back changed regions after writes or patches where the tool contract requires confirmation. +- Include the active agent context id in write/patch result metadata when available so canvas consumers can open the changed file in the correct chat context. ## Work Guidance diff --git a/plugins/_text_editor/tools/text_editor.py b/plugins/_text_editor/tools/text_editor.py index 52ec3ef0de..f804e65073 100644 --- a/plugins/_text_editor/tools/text_editor.py +++ b/plugins/_text_editor/tools/text_editor.py @@ -152,7 +152,7 @@ async def _write( return Response( message=msg, break_loop=False, - additional=_result_additional("write", info, kwargs), + additional=_result_additional("write", info, kwargs, agent=self.agent), ) # ------------------------------------------------------------------ @@ -256,7 +256,7 @@ async def _patch_edits( return Response( message=msg, break_loop=False, - additional=_result_additional("patch", post_info, options), + additional=_result_additional("patch", post_info, options, agent=self.agent), ) async def _patch_replace( @@ -315,7 +315,7 @@ async def _patch_replace( return Response( message=msg, break_loop=False, - additional=_result_additional("patch", post_info, options), + additional=_result_additional("patch", post_info, options, agent=self.agent), ) async def _patch_context( @@ -377,7 +377,7 @@ async def _patch_context( return Response( message=msg, break_loop=False, - additional=_result_additional("patch", post_info, options), + additional=_result_additional("patch", post_info, options, agent=self.agent), ) # ------------------------------------------------------------------ @@ -475,7 +475,7 @@ def _freshness_error_message(agent, info: FileInfo, code: str) -> str: return agent.read_prompt(prompt, path=info["expanded"]) -def _result_additional(action: str, info: FileInfo, options: dict | None = None) -> dict: +def _result_additional(action: str, info: FileInfo, options: dict | None = None, agent=None) -> dict: path = str(info.get("expanded") or "") extension = Path(path).suffix.lower().lstrip(".") options = options or {} @@ -484,7 +484,7 @@ def _result_additional(action: str, info: FileInfo, options: dict | None = None) or options.get("open_canvas") or options.get("open_document") ) - return { + additional = { "_tool_name": "text_editor", "action": action, "path": path, @@ -492,6 +492,11 @@ def _result_additional(action: str, info: FileInfo, options: dict | None = None) "extension": extension, "open_in_canvas": open_in_canvas, } + context_id = str(getattr(getattr(agent, "context", None), "id", "") or "").strip() + if context_id: + additional["context_id"] = context_id + additional["ctxid"] = context_id + return additional def _truthy(value) -> bool: diff --git a/tests/test_default_prompt_budget.py b/tests/test_default_prompt_budget.py index 323ce7edf8..b10cb3a32f 100644 --- a/tests/test_default_prompt_budget.py +++ b/tests/test_default_prompt_budget.py @@ -71,6 +71,64 @@ async def test_default_agent0_prompt_budget_and_guardrails(): assert "Computer Use enablement is scoped to the current CLI session" not in system_text +@pytest.mark.asyncio +async def test_tiny_local_profile_prompt_is_action_first_json_contract(): + system_text = await _build_system_text("tiny-local") + communication_prompt = ( + PROJECT_ROOT / "agents" / "tiny-local" / "prompts" / "agent.system.main.communication.md" + ).read_text(encoding="utf-8") + code_prompt = ( + PROJECT_ROOT / "agents" / "tiny-local" / "prompts" / "agent.system.tool.code_exe.md" + ).read_text(encoding="utf-8") + response_prompt = ( + PROJECT_ROOT / "agents" / "tiny-local" / "prompts" / "agent.system.tool.response.md" + ).read_text(encoding="utf-8") + text_editor_prompt = ( + PROJECT_ROOT / "agents" / "tiny-local" / "prompts" / "agent.system.tool.text_editor.md" + ).read_text(encoding="utf-8") + solving_prompt = ( + PROJECT_ROOT / "agents" / "tiny-local" / "prompts" / "agent.system.main.solving.md" + ).read_text(encoding="utf-8") + + assert "You are Agent Zero. Act on the user's behalf." in system_text + assert "Your visible assistant message must be exactly one valid JSON object." in system_text + assert 'Use exactly these top-level fields: `"tool_name"` and `"tool_args"`.' in system_text + assert 'For a final user-facing answer, use the `response` tool.' in system_text + assert "Use `response` only when the work is complete, blocked, or the user is only acknowledging completed work." in system_text + assert "If the user says \"proceed\", \"continue\", \"go ahead\", \"do it\", \"excellent proceed\"" in system_text + assert "Do not explain what command the user could run manually." in system_text + assert "output a corrected JSON tool request immediately" in system_text + assert "## Tiny Local Output Rule" in system_text + assert "~~~json" not in communication_prompt + assert "~~~json" not in code_prompt + assert "~~~json" not in response_prompt + assert "~~~json" not in text_editor_prompt + assert "No JSON in markdown fences" not in communication_prompt + assert "thoughts: array thoughts before execution" not in communication_prompt + assert "headline: short headline summary" not in communication_prompt + assert "explain each step in thoughts" not in solving_prompt + assert "Continuation words" in solving_prompt + assert "Do not respond by saying you will begin, continue, start, proceed, or investigate." in solving_prompt + assert "Do not use this tool for \"proceed\", \"continue\", \"go ahead\"" in response_prompt + assert "do not repeat the same exact tool call" in solving_prompt + assert '"open_in_canvas":true' in text_editor_prompt + assert "do not repeat the same tool call" in text_editor_prompt + assert '"headline"' not in code_prompt + assert '"headline"' not in response_prompt + assert '"headline"' not in text_editor_prompt + + +def test_tiny_local_profile_is_discoverable(): + from helpers import subagents + + profiles = { + str(item.get("key") or ""): str(item.get("label") or "") + for item in subagents.get_all_agents_list() + } + + assert profiles["tiny-local"] == "Tiny Local" + + def test_removed_small_profile_and_prompt_text_generic(): removed_profile = "a0" + "_" + "small" diff --git a/tests/test_office_canvas_setup.py b/tests/test_office_canvas_setup.py index 35fc39fd68..a7759a1dc1 100644 --- a/tests/test_office_canvas_setup.py +++ b/tests/test_office_canvas_setup.py @@ -401,8 +401,12 @@ def test_office_artifacts_only_open_desktop_from_explicit_document_ui_requests() assert "syncTextEditorResultsIntoOpenEditor" in editor_sync assert 'toolName(payload) !== "text_editor"' in editor_sync assert 'return ["write", "patch"].includes(action);' in editor_sync + assert "if (!context?.results?.length) return;" in editor_sync + assert "if (context.historyEmpty && !explicitOpen) continue;" in editor_sync assert "syncOpenEditorSurface" in editor_sync assert "isEditorSurfaceOpen" in editor_sync + assert "context_id" in editor_sync + assert "ctxid" in editor_sync assert "void syncOpenDocumentSurfaces(target);" in auto_open assert "void syncOpenDocumentSurfaces({ path, file_id: fileId });" not in auto_open assert "editorStore" not in auto_open diff --git a/tests/test_text_editor_context_patch.py b/tests/test_text_editor_context_patch.py index 0cbb260ce7..df58d55b98 100644 --- a/tests/test_text_editor_context_patch.py +++ b/tests/test_text_editor_context_patch.py @@ -379,8 +379,9 @@ def __init__( class _FakeAgent: - def __init__(self) -> None: + def __init__(self, context_id: str = "") -> None: self.data = {} + self.context = types.SimpleNamespace(id=context_id) if context_id else None def read_prompt(self, name: str, **kwargs) -> str: if name.endswith("read_ok.md"): @@ -556,7 +557,7 @@ def test_text_editor_write_result_carries_markdown_canvas_intent( ) -> None: module, _calls = _load_text_editor_tool(monkeypatch) target = tmp_path / "note.md" - tool = module.TextEditor(_FakeAgent(), "text_editor", "write", {}, "", None) + tool = module.TextEditor(_FakeAgent("ctx-write-1"), "text_editor", "write", {}, "", None) response = asyncio.run( tool._write( @@ -574,6 +575,8 @@ def test_text_editor_write_result_carries_markdown_canvas_intent( "format": "md", "extension": "md", "open_in_canvas": True, + "context_id": "ctx-write-1", + "ctxid": "ctx-write-1", } diff --git a/tests/test_tool_action_contracts.py b/tests/test_tool_action_contracts.py index 3dfb6de2a4..a38d7079ea 100644 --- a/tests/test_tool_action_contracts.py +++ b/tests/test_tool_action_contracts.py @@ -649,6 +649,21 @@ def test_tool_prompts_prevent_top_level_multi_tool(): assert 'Never use `tool_name: "multi"`' in browser_prompt +def test_local_model_tool_use_guide_stays_prompt_profile_plugin_only(): + guide = Path("docs/guides/local-model-tool-use.md").read_text(encoding="utf-8") + + assert "Tiny Local" in guide + assert "agents/tiny-local/" in guide + assert "*.promptinclude.md" in guide + assert "Do not change `agent.py`" in guide + assert "Do not change `helpers/extract_tools.py`" in guide + assert "Use exactly these top-level fields: `tool_name` and `tool_args`." in guide + assert '{"tool_name":"response","tool_args":{"text":"Done."}}' in guide + assert "If the user says \"proceed\", \"continue\", \"go ahead\", or similar" in guide + assert "call the next appropriate tool instead of replying with a promise or status update" in guide + assert "Do not create parser repair code for this workflow." in guide + + def _load_scheduler_tool(monkeypatch): _install_tool_stub(monkeypatch) From 2131a5f33658546b804a4af6bdf889115d9e7fb3 Mon Sep 17 00:00:00 2001 From: TerminallyLazy Date: Wed, 24 Jun 2026 04:37:32 -0400 Subject: [PATCH 2/2] fix: tighten tiny local repeat recovery --- agents/tiny-local/AGENTS.md | 4 +++- .../prompts/agent.system.main.communication.md | 2 ++ .../tiny-local/prompts/agent.system.main.solving.md | 1 + agents/tiny-local/prompts/fw.msg_repeat.md | 13 +++++++++++++ tests/test_default_prompt_budget.py | 7 +++++++ 5 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 agents/tiny-local/prompts/fw.msg_repeat.md diff --git a/agents/tiny-local/AGENTS.md b/agents/tiny-local/AGENTS.md index 465b55bf3b..e176f2422e 100644 --- a/agents/tiny-local/AGENTS.md +++ b/agents/tiny-local/AGENTS.md @@ -10,13 +10,15 @@ - `agent.yaml` owns profile metadata for discovery and profile switching. - `prompts/agent.system.main.communication.md` owns the local-model communication contract. - `prompts/agent.system.main.solving.md` owns the local-model problem-solving contract and suppresses inherited visible reasoning requirements. +- `prompts/fw.msg_repeat.md` owns Tiny Local's profile-specific recovery instructions when the framework rejects a duplicate assistant message. - `prompts/agent.system.tools.md` owns the Tiny Local tools wrapper and final output-shape reminder after tool listing. - `prompts/agent.system.tool.*.md` files own Tiny Local-specific tool examples that avoid inherited reasoning fields and repeated writes. ## Local Contracts - Preserve the normal Agent Zero tool-call shape: `tool_name` plus `tool_args`. -- Do not add parser repair, duplicate suppression, model transport, memory, or text-editor runtime behavior here. +- Do not add parser repair, duplicate suppression runtime, model transport, memory, or text-editor runtime behavior here. +- Duplicate-message handling may be tightened through profile prompts only. - Keep prompt text short enough for small local models to follow. - Treat continuation requests such as `proceed` or `continue` as commands to execute the next unfinished step, not as prompts for another status response. - Do not include user-specific provider names, API keys, local paths, or secrets. diff --git a/agents/tiny-local/prompts/agent.system.main.communication.md b/agents/tiny-local/prompts/agent.system.main.communication.md index 690220defe..65f79edefb 100644 --- a/agents/tiny-local/prompts/agent.system.main.communication.md +++ b/agents/tiny-local/prompts/agent.system.main.communication.md @@ -26,4 +26,6 @@ For work that requires a command, file action, browser action, or any other avai If the framework warns that your prior message was malformed, repeated, or reasoning-only, output a corrected JSON tool request immediately without explaining the warning. +When the warning says you sent the same message again, do not resend the same JSON. Change the tool, action, arguments, or final answer so the next message is meaningfully different. + {{ include "agent.system.main.communication_additions.md" }} diff --git a/agents/tiny-local/prompts/agent.system.main.solving.md b/agents/tiny-local/prompts/agent.system.main.solving.md index 6806760c87..2e410be4a5 100644 --- a/agents/tiny-local/prompts/agent.system.main.solving.md +++ b/agents/tiny-local/prompts/agent.system.main.solving.md @@ -12,6 +12,7 @@ For tasks that need shell commands, files, browser actions, or other capabilitie - inspect outputs before deciding the next tool call - never claim success from timeout output or a still-running command - after a successful tool result, do not repeat the same exact tool call +- after a repeated-message warning, do not repeat the same status response or exact tool request; choose the next different executable action or report a blocker - when finished, use the `response` tool with a brief result Do not include `thoughts`, `headline`, analysis, plans, or prose outside the JSON object. diff --git a/agents/tiny-local/prompts/fw.msg_repeat.md b/agents/tiny-local/prompts/fw.msg_repeat.md new file mode 100644 index 0000000000..10602a5583 --- /dev/null +++ b/agents/tiny-local/prompts/fw.msg_repeat.md @@ -0,0 +1,13 @@ +You have sent the same message again. You have to do something else. + +Your repeated JSON was recorded, but it did not execute another tool. Do not send the same JSON object again. + +Choose one different action now: +- If work is unfinished, call a real tool for the next unfinished step. +- If your previous JSON used `response` while work remains, replace it with the next real tool call. +- If a file write or patch already succeeded, read that file or answer with the observed result. +- If a command already ran, inspect its output or run a different next command. +- If the user only said "proceed" or "continue", continue with the next real tool call. +- If no different action is possible, use `response` with a brief blocker. + +Output exactly one JSON object with `tool_name` and `tool_args`. No prose or markdown. diff --git a/tests/test_default_prompt_budget.py b/tests/test_default_prompt_budget.py index b10cb3a32f..eddac9b44b 100644 --- a/tests/test_default_prompt_budget.py +++ b/tests/test_default_prompt_budget.py @@ -83,6 +83,9 @@ async def test_tiny_local_profile_prompt_is_action_first_json_contract(): response_prompt = ( PROJECT_ROOT / "agents" / "tiny-local" / "prompts" / "agent.system.tool.response.md" ).read_text(encoding="utf-8") + repeat_prompt = ( + PROJECT_ROOT / "agents" / "tiny-local" / "prompts" / "fw.msg_repeat.md" + ).read_text(encoding="utf-8") text_editor_prompt = ( PROJECT_ROOT / "agents" / "tiny-local" / "prompts" / "agent.system.tool.text_editor.md" ).read_text(encoding="utf-8") @@ -98,6 +101,7 @@ async def test_tiny_local_profile_prompt_is_action_first_json_contract(): assert "If the user says \"proceed\", \"continue\", \"go ahead\", \"do it\", \"excellent proceed\"" in system_text assert "Do not explain what command the user could run manually." in system_text assert "output a corrected JSON tool request immediately" in system_text + assert "do not resend the same JSON" in system_text assert "## Tiny Local Output Rule" in system_text assert "~~~json" not in communication_prompt assert "~~~json" not in code_prompt @@ -110,6 +114,9 @@ async def test_tiny_local_profile_prompt_is_action_first_json_contract(): assert "Continuation words" in solving_prompt assert "Do not respond by saying you will begin, continue, start, proceed, or investigate." in solving_prompt assert "Do not use this tool for \"proceed\", \"continue\", \"go ahead\"" in response_prompt + assert "Your repeated JSON was recorded, but it did not execute another tool." in repeat_prompt + assert "replace it with the next real tool call" in repeat_prompt + assert "do not repeat the same status response or exact tool request" in solving_prompt assert "do not repeat the same exact tool call" in solving_prompt assert '"open_in_canvas":true' in text_editor_prompt assert "do not repeat the same tool call" in text_editor_prompt