From 0524dc7b659e3cfe146eb384b6aa12273a66ee69 Mon Sep 17 00:00:00 2001 From: KoheiYamashita Date: Sun, 22 Feb 2026 14:21:27 +0900 Subject: [PATCH] feat: compact UI tree output and separate max_tokens from context_window Reduce get_ui_tree verbosity by skipping invisible nodes, omitting empty/default fields, and stripping common class prefixes. Lower defaults to max_nodes=300, max_depth=15. Introduce a dedicated context_window config field (default 128000) so summarization threshold is independent of the API response token limit (max_tokens). Co-Authored-By: Claude Opus 4.6 --- .../android/assistant/ToolRequestHandler.kt | 36 ++++++++++++------- config/config.example.json | 1 + pkg/agent/loop.go | 10 +++--- pkg/agent/loop_test.go | 21 +++++++++++ pkg/config/config.go | 2 ++ pkg/tools/android.go | 4 +-- 6 files changed, 56 insertions(+), 18 deletions(-) diff --git a/android/app/src/main/java/io/picoclaw/android/assistant/ToolRequestHandler.kt b/android/app/src/main/java/io/picoclaw/android/assistant/ToolRequestHandler.kt index e98bbe347..cfde0f0bf 100644 --- a/android/app/src/main/java/io/picoclaw/android/assistant/ToolRequestHandler.kt +++ b/android/app/src/main/java/io/picoclaw/android/assistant/ToolRequestHandler.kt @@ -183,8 +183,8 @@ class ToolRequestHandler( val index = request.params?.get("index")?.jsonPrimitive?.intOrNull ?: 0 val boundsX = request.params?.get("bounds_x")?.jsonPrimitive?.doubleOrNull val boundsY = request.params?.get("bounds_y")?.jsonPrimitive?.doubleOrNull - val maxDepth = request.params?.get("max_depth")?.jsonPrimitive?.intOrNull ?: 30 - val maxNodes = request.params?.get("max_nodes")?.jsonPrimitive?.intOrNull ?: 2000 + val maxDepth = request.params?.get("max_depth")?.jsonPrimitive?.intOrNull ?: 15 + val maxNodes = request.params?.get("max_nodes")?.jsonPrimitive?.intOrNull ?: 300 return try { withContext(Dispatchers.Main) { setOverlayVisibility(false) } @@ -262,20 +262,32 @@ class ToolRequestHandler( nodeCount: IntArray ) { if (nodeCount[0] >= maxNodes) return + // Skip invisible nodes + if (!node.isVisibleToUser) return nodeCount[0]++ val indent = " ".repeat(depth) val bounds = Rect() node.getBoundsInScreen(bounds) - sb.appendLine( - "${indent}[${node.className}] " + - "text=${node.text ?: ""} " + - "desc=${node.contentDescription ?: ""} " + - "bounds=${bounds} " + - "clickable=${node.isClickable} " + - "enabled=${node.isEnabled} " + - "visible=${node.isVisibleToUser} " + - "id=${node.viewIdResourceName ?: ""}" - ) + + // Strip common class name prefixes + val className = node.className?.toString() ?: "View" + val shortClass = className + .removePrefix("android.widget.") + .removePrefix("android.view.") + + sb.append("${indent}[${shortClass}]") + + // Only output non-empty fields + node.text?.takeIf { it.isNotEmpty() }?.let { sb.append(" text=$it") } + node.contentDescription?.takeIf { it.isNotEmpty() }?.let { sb.append(" desc=$it") } + sb.append(" bounds=$bounds") + // Only output non-default values: clickable=true (default is false), enabled=false (default is true) + if (node.isClickable) sb.append(" clickable") + if (!node.isEnabled) sb.append(" enabled=false") + node.viewIdResourceName?.let { sb.append(" id=$it") } + + sb.appendLine() + if (depth >= maxDepth) { if (node.childCount > 0) { sb.appendLine("${indent} [truncated: ${node.childCount} children at depth $depth]") diff --git a/config/config.example.json b/config/config.example.json index 63f960d8b..3f2e3811c 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -6,6 +6,7 @@ "restrict_to_workspace": true, "model": "glm-4.7", "max_tokens": 8192, + "context_window": 128000, "temperature": 0.7, "max_tool_iterations": 20 } diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 136a7691d..17552b545 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -36,7 +36,8 @@ type AgentLoop struct { provider providers.LLMProvider workspace string model string - contextWindow int // Maximum context window size in tokens + maxTokens int // Maximum tokens for API response + contextWindow int // Maximum context window size in tokens (for summarization) maxIterations int sessions *session.SessionManager state *state.Manager @@ -213,7 +214,8 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers provider: provider, workspace: workspace, model: cfg.Agents.Defaults.Model, - contextWindow: cfg.Agents.Defaults.MaxTokens, // Restore context window for summarization + maxTokens: cfg.Agents.Defaults.MaxTokens, + contextWindow: cfg.Agents.Defaults.ContextWindow, maxIterations: cfg.Agents.Defaults.MaxToolIterations, sessions: sessionsManager, state: stateManager, @@ -672,7 +674,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M "model": al.model, "messages_count": len(messages), "tools_count": len(providerToolDefs), - "max_tokens": 8192, + "max_tokens": al.maxTokens, "temperature": 0.7, "system_prompt_len": len(messages[0].Content), }) @@ -692,7 +694,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M maxRetries := 2 for retry := 0; retry <= maxRetries; retry++ { response, err = al.provider.Chat(ctx, messages, providerToolDefs, al.model, map[string]interface{}{ - "max_tokens": 8192, + "max_tokens": al.maxTokens, "temperature": 0.7, }) diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index b82ebedef..dedbc313d 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -45,6 +45,7 @@ func TestRecordLastChannel(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -91,6 +92,7 @@ func TestRecordLastChatID(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -137,6 +139,7 @@ func TestNewAgentLoop_StateInitialized(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -174,6 +177,7 @@ func TestToolRegistry_ToolRegistration(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -221,6 +225,7 @@ func TestToolContext_Updates(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -253,6 +258,7 @@ func TestToolRegistry_GetDefinitions(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -298,6 +304,7 @@ func TestAgentLoop_GetStartupInfo(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -346,6 +353,7 @@ func TestCreateToolRegistry_ExecDisabled(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -386,6 +394,7 @@ func TestCreateToolRegistry_ExecEnabled(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -431,6 +440,7 @@ func TestCreateToolRegistry_I2CDisabled(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -469,6 +479,7 @@ func TestCreateToolRegistry_I2CEnabled(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -512,6 +523,7 @@ func TestCreateToolRegistry_SPIDisabled(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -550,6 +562,7 @@ func TestCreateToolRegistry_SPIEnabled(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -593,6 +606,7 @@ func TestAgentLoop_Stop(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -716,6 +730,7 @@ func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -759,6 +774,7 @@ func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -825,6 +841,7 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -921,6 +938,7 @@ func TestRetryLoop_CancelledContextSkipsCompression(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -983,6 +1001,7 @@ func TestForceCompression_ToolGroupBoundary(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -1069,6 +1088,7 @@ func TestForceCompression_MidOnAssistantWithToolCalls(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, @@ -1156,6 +1176,7 @@ func TestForceCompression_NoteUsesUserRole(t *testing.T) { DataDir: tmpDir, Model: "test-model", MaxTokens: 4096, + ContextWindow: 128000, MaxToolIterations: 10, }, }, diff --git a/pkg/config/config.go b/pkg/config/config.go index 2267f3334..e396a9982 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -66,6 +66,7 @@ type AgentDefaults struct { Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` + ContextWindow int `json:"context_window" env:"PICOCLAW_AGENTS_DEFAULTS_CONTEXT_WINDOW"` Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` } @@ -281,6 +282,7 @@ func DefaultConfig() *Config { Provider: "", Model: "glm-4.7", MaxTokens: 8192, + ContextWindow: 128000, Temperature: 0.7, MaxToolIterations: 20, }, diff --git a/pkg/tools/android.go b/pkg/tools/android.go index f4344e8d6..044fb37ee 100644 --- a/pkg/tools/android.go +++ b/pkg/tools/android.go @@ -145,11 +145,11 @@ func (t *AndroidTool) Parameters() map[string]interface{} { }, "max_depth": map[string]interface{}{ "type": "integer", - "description": "Maximum traversal depth (for get_ui_tree, default 30)", + "description": "Maximum traversal depth (for get_ui_tree, default 15)", }, "max_nodes": map[string]interface{}{ "type": "integer", - "description": "Maximum number of nodes to output (for get_ui_tree, default 2000)", + "description": "Maximum number of nodes to output (for get_ui_tree, default 300)", }, }, "required": []string{"action"},