personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove model/provider from agent persona configs, use context-based resolution

Simplifies agent configuration by removing explicit model and provider
fields from persona JSON configs. Models are now resolved automatically
from context tiers (agent.{app}.{name}) using the providers system.

Key changes:
- Add agent.* context pattern to CONTEXT_DEFAULTS with TIER_FLASH
- Add _resolve_tier() and resolve_model_for_provider() helpers
- Change doctor.json from provider:"claude" to claude:true flag
- Remove provider/model UI from agents app workspace
- Clean up legacy provider/model fields on agent save
- Update CORTEX.md documentation

The claude:true flag is a special case for Claude Code SDK and is NOT
inherited by handoff agents. Provider can still be overridden at request
time for power users via CLI --provider flag.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+178 -117
+4 -22
apps/agents/routes.py
··· 106 106 "priority": metadata.get("priority"), 107 107 "multi_facet": metadata.get("multi_facet", False), 108 108 "tools": metadata.get("tools"), 109 - "provider": metadata.get("provider"), 110 - "model": metadata.get("model"), 111 109 } 112 110 ) 113 111 except Exception: ··· 334 332 priority = data.get("priority") # Can be None or 0-99 335 333 tools = data.get("tools") # Can be None or comma-separated string 336 334 multi_facet = data.get("multi_facet") # Can be None or boolean 337 - provider = data.get("provider") # Can be None or provider name 338 - model = data.get("model") # Can be None or model name 339 335 340 336 if not new_title or not new_content: 341 337 return {"error": "Title and content are required"}, 400 ··· 372 368 item_config["tools"] = tools 373 369 if multi_facet is not None and multi_facet: 374 370 item_config["multi_facet"] = True 375 - if provider: 376 - item_config["provider"] = provider 377 - if model: 378 - item_config["model"] = model 379 371 else: 380 372 # Update existing JSON file 381 373 with open(json_path, "r", encoding="utf-8") as f: ··· 409 401 del item_config["multi_facet"] 410 402 # Don't delete if multi_facet is None (not provided) 411 403 412 - # Provider 413 - if provider: 414 - item_config["provider"] = provider 415 - elif "provider" in item_config: 416 - del item_config["provider"] 417 - 418 - # Model 419 - if model: 420 - item_config["model"] = model 421 - elif "model" in item_config: 422 - del item_config["model"] 404 + # Remove legacy provider/model fields if present 405 + item_config.pop("provider", None) 406 + item_config.pop("model", None) 423 407 424 408 # Write JSON file 425 409 with open(json_path, "w", encoding="utf-8") as f: ··· 480 464 481 465 if not prompt_value: 482 466 return jsonify({"error": "Prompt is required"}), 400 483 - provider = data.get("provider", "openai") 484 467 persona = data.get("persona", "default") 485 468 config = data.get("config", {}) 486 469 487 470 try: 488 471 from convey.utils import spawn_agent 489 472 490 - # Create the agent request 473 + # Create the agent request (provider resolved from context) 491 474 agent_id = spawn_agent( 492 475 prompt=prompt_value, 493 476 persona=persona, 494 - provider=provider, 495 477 config=config, 496 478 ) 497 479
+1 -70
apps/agents/workspace.html
··· 191 191 color: #388e3c; 192 192 } 193 193 194 - .metadata-badge.provider { 195 - background: #fce4ec; 196 - color: #c2185b; 197 - } 198 - 199 194 .metadata-badge .badge-icon { 200 195 font-size: 0.9rem; 201 196 } ··· 1482 1477 </div> 1483 1478 1484 1479 <div class="metadata-item"> 1485 - <label>Provider</label> 1486 - <select id="modalProvider" onchange="saveMetadata()"> 1487 - <option value="">Default</option> 1488 - <option value="openai">OpenAI</option> 1489 - <option value="anthropic">Anthropic</option> 1490 - <option value="google">Google</option> 1491 - <option value="claude">Claude</option> 1492 - </select> 1493 - </div> 1494 - 1495 - <div class="metadata-item"> 1496 - <label>Model</label> 1497 - <input type="text" id="modalModel" placeholder="Auto" 1498 - onblur="saveMetadata()" onkeydown="if(event.key === 'Escape') this.blur();"> 1499 - </div> 1500 - 1501 - <div class="metadata-item"> 1502 1480 <label>Tools</label> 1503 1481 <select id="modalTools" onchange="saveMetadata()"> 1504 1482 <option value="">Default</option> ··· 1578 1556 <p style="font-size: 0.9rem; color: #666; margin-bottom: 1rem;"> 1579 1557 <em>Note: Prompt generation uses Gemini. Configuration below applies to agent execution.</em> 1580 1558 </p> 1581 - 1582 - <div class="config-section"> 1583 - <label for="agentProvider">Agent Provider</label> 1584 - <select id="agentProvider"> 1585 - <option value="openai">OpenAI</option> 1586 - <option value="anthropic">Anthropic</option> 1587 - <option value="google">Google</option> 1588 - </select> 1589 - </div> 1590 - 1591 - <div class="config-section"> 1592 - <label for="agentModel">Model (optional)</label> 1593 - <input type="text" id="agentModel" placeholder="Leave empty for default"> 1594 - </div> 1595 1559 1596 1560 <div class="config-section"> 1597 1561 <label for="maxTokens">Max Tokens</label> ··· 1803 1767 } 1804 1768 } 1805 1769 1806 - // Provider badge 1807 - if (item.provider) { 1808 - badges.push(`<span class="metadata-badge provider"><span class="badge-icon">🤖</span>${item.provider}</span>`); 1809 - } 1810 - 1811 1770 if (badges.length > 0) { 1812 1771 metadataHtml = `<div class="agent-metadata">${badges.join('')}</div>`; 1813 1772 } ··· 1933 1892 document.getElementById('modalSchedule').checked = false; 1934 1893 document.getElementById('modalPriority').value = ''; 1935 1894 document.getElementById('modalMultiFacet').checked = false; 1936 - document.getElementById('modalProvider').value = ''; 1937 - document.getElementById('modalModel').value = ''; 1938 1895 document.getElementById('modalTools').value = ''; 1939 1896 document.getElementById('modalToolsCustom').style.display = 'none'; 1940 1897 ··· 1947 1904 } 1948 1905 if (item.multi_facet) { 1949 1906 document.getElementById('modalMultiFacet').checked = true; 1950 - } 1951 - if (item.provider) { 1952 - document.getElementById('modalProvider').value = item.provider; 1953 - } 1954 - if (item.model) { 1955 - document.getElementById('modalModel').value = item.model; 1956 1907 } 1957 1908 if (item.tools) { 1958 1909 const toolsSelect = document.getElementById('modalTools'); ··· 2123 2074 requestBody.multi_facet = true; 2124 2075 } 2125 2076 2126 - // Provider 2127 - const provider = document.getElementById('modalProvider').value; 2128 - if (provider) { 2129 - requestBody.provider = provider; 2130 - } 2131 - 2132 - // Model 2133 - const model = document.getElementById('modalModel').value.trim(); 2134 - if (model) { 2135 - requestBody.model = model; 2136 - } 2137 - 2138 2077 // Tools 2139 2078 const toolsSelect = document.getElementById('modalTools').value; 2140 2079 const toolsCustom = document.getElementById('modalToolsCustom').value.trim(); ··· 2311 2250 return; 2312 2251 } 2313 2252 2314 - const provider = document.getElementById('agentProvider').value; 2315 - const model = document.getElementById('agentModel').value.trim(); 2316 2253 const maxTokens = parseInt(document.getElementById('maxTokens').value) || 0; 2317 2254 const persona = document.getElementById('agentPersona').value; 2318 2255 2319 2256 // Build config object 2320 2257 const config = {}; 2321 - if (model) config.model = model; 2322 2258 if (maxTokens) config.max_tokens = maxTokens; 2323 2259 2324 2260 const startBtn = document.getElementById('startAgentBtn'); ··· 2333 2269 }, 2334 2270 body: JSON.stringify({ 2335 2271 prompt: prompt, 2336 - provider: provider, 2337 2272 config: config, 2338 2273 persona: persona 2339 2274 }) ··· 2443 2378 restartBtn.dataset.agentId = a.id; 2444 2379 restartBtn.dataset.prompt = a.prompt; 2445 2380 restartBtn.dataset.persona = a.persona; 2446 - restartBtn.dataset.provider = a.provider; 2447 2381 2448 2382 restartBtn.onclick = (e) => { 2449 2383 e.stopPropagation(); ··· 2555 2489 const agentId = buttonElement.dataset.agentId; 2556 2490 const prompt = buttonElement.dataset.prompt; 2557 2491 const persona = buttonElement.dataset.persona; 2558 - const provider = buttonElement.dataset.provider; 2559 2492 2560 2493 // Confirmation dialog 2561 2494 const confirmed = confirm( 2562 2495 `Are you sure you want to restart this agent?\n\n` + 2563 2496 `Prompt: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}\n` + 2564 - `Agent: ${persona}\n` + 2565 - `Provider: ${provider}\n\n` + 2497 + `Agent: ${persona}\n\n` + 2566 2498 `This will create a new agent run with the same configuration.` 2567 2499 ); 2568 2500 ··· 2578 2510 }, 2579 2511 body: JSON.stringify({ 2580 2512 prompt: prompt, 2581 - provider: provider, 2582 2513 persona: persona, 2583 2514 config: {} 2584 2515 })
+1 -3
apps/entities/agents/entity_assist.json
··· 1 1 { 2 2 "title": "Entity Assistant", 3 3 "description": "Quick entity addition with intelligent type detection and automatic description generation", 4 - "tools": "journal, entities", 5 - "model": "claude-haiku-4-5", 6 - "provider": "anthropic" 4 + "tools": "journal, entities" 7 5 }
+2 -2
convey/utils.py
··· 96 96 Args: 97 97 prompt: The task or question for the agent 98 98 persona: Agent persona - system (e.g., "default") or app-qualified (e.g., "entities:entity_assist") 99 - provider: AI provider - openai, google, anthropic, or claude 100 - config: Provider-specific configuration (model, max_tokens, facet, continue, etc.) 99 + provider: Optional provider override (openai, google, anthropic) 100 + config: Additional configuration (max_tokens, facet, continue_from, etc.) 101 101 102 102 Returns: 103 103 agent_id string (timestamp-based)
+26 -6
docs/CORTEX.md
··· 37 37 "event": "request", 38 38 "ts": 1234567890123, // Required: millisecond timestamp (must match filename) 39 39 "prompt": "Analyze this code for security issues", // Required: the task or question 40 - "provider": "openai", // Required: openai, google, anthropic, or claude 41 40 "persona": "default", // Optional: agent persona from muse/agents/*.txt 42 - "model": "gpt-4o", // Optional: provider-specific override 41 + "provider": "openai", // Optional: override provider (openai, google, anthropic) 43 42 "max_tokens": 8192, // Optional: token limit (if supported) 44 43 "disable_mcp": false, // Optional: disable MCP tools for this request 45 44 "continue_from": "1234567890122", // Optional: continue from previous agent ··· 59 58 } 60 59 ``` 61 60 62 - All provider overrides (for example `model`, `max_tokens`, `disable_mcp`) are supplied as 63 - top-level keys to keep the schema flat and aligned with the agent providers. 61 + The model is automatically resolved based on the agent context (`agent.{app}.{persona}`) 62 + and the configured tier in `journal.json`. Provider can optionally be overridden at 63 + request time, which will resolve the appropriate model for that provider at the same tier. 64 64 65 65 ### Conversation Continuations 66 66 ··· 240 240 ### Persona Configuration Options 241 241 242 242 The `.json` file for a persona can include: 243 - - `provider`: Default provider (openai, google, anthropic, claude) 244 - - `model`: Default model name for the provider 243 + - `claude`: Boolean flag to use Claude Code SDK instead of API providers 244 + - When true, uses filesystem tools instead of MCP; requires `facet` in request 245 + - This flag is NOT inherited by handoff agents 245 246 - `max_tokens`: Maximum response token limit 246 247 - `tools`: MCP tools configuration (string or array) 247 248 - String: Comma-separated pack names (e.g., `"journal"`, `"journal, todo"`) - expanded via `get_tools()` ··· 264 265 - Request-level `env` overrides persona defaults 265 266 - Inherited by handoff agents unless explicitly overridden 266 267 - Note: `JOURNAL_PATH` cannot be overridden (always set by Cortex) 268 + 269 + ### Model Resolution 270 + 271 + Models are resolved automatically based on context and tier: 272 + 1. Each agent has a context pattern: `agent.{app}.{persona}` (e.g., `agent.system.default`) 273 + 2. The context determines the tier (pro/flash/lite) from `journal.json` or system defaults 274 + 3. The tier + provider determines the actual model to use 275 + 276 + This allows controlling model selection via tier configuration rather than hardcoding models: 277 + ```json 278 + { 279 + "providers": { 280 + "contexts": { 281 + "agent.system.doctor": {"tier": 1}, 282 + "agent.*": {"tier": 2} 283 + } 284 + } 285 + } 286 + ``` 267 287 268 288 ## MCP Tools Integration 269 289
+1 -1
muse/agents/doctor.json
··· 1 1 { 2 2 "title": "System Diagnostics", 3 - "provider": "claude" 3 + "claude": true 4 4 }
+28 -6
muse/cli.py
··· 18 18 import json 19 19 import sys 20 20 import threading 21 - import time 22 - from typing import Any, Callable, Dict, Optional, TextIO 21 + from typing import Any, Dict, Optional, TextIO 23 22 24 23 from think.callosum import CallosumConnection 25 24 from think.utils import setup_cli ··· 107 106 Mirrors the config building logic in cortex.py for consistency. 108 107 """ 109 108 from muse.mcp import get_tools 109 + from muse.models import resolve_model_for_provider, resolve_provider 110 110 from think.utils import get_agent 111 111 112 112 # Load persona configuration 113 113 config = get_agent(persona) 114 114 115 - # Apply provider override if specified 116 - if provider: 117 - config["provider"] = provider 118 - 119 115 # Apply any additional overrides 120 116 config.update({k: v for k, v in overrides.items() if v is not None}) 121 117 122 118 # Set prompt 123 119 config["prompt"] = prompt 124 120 config["persona"] = persona 121 + 122 + # Resolve provider and model from context 123 + # Context format: agent.{app}.{name} where app="system" for system agents 124 + if ":" in persona: 125 + app, name = persona.split(":", 1) 126 + else: 127 + app, name = "system", persona 128 + agent_context = f"agent.{app}.{name}" 129 + 130 + # Check for claude: true flag (special case for Claude Code SDK) 131 + if config.get("claude"): 132 + config["provider"] = "claude" 133 + # Claude SDK doesn't need model - it uses its own 134 + else: 135 + # Resolve default provider and model from context 136 + default_provider, model = resolve_provider(agent_context) 137 + 138 + # Provider can be overridden by parameter or persona config 139 + final_provider = provider or config.get("provider") or default_provider 140 + 141 + # If provider was overridden, re-resolve model for that provider 142 + if final_provider != default_provider: 143 + model = resolve_model_for_provider(agent_context, final_provider) 144 + 145 + config["provider"] = final_provider 146 + config["model"] = model 125 147 126 148 # Expand tools if it's a string (tool pack name) 127 149 tools_config = config.get("tools")
+36 -6
muse/cortex.py
··· 313 313 config.update({k: v for k, v in request.items() if v is not None}) 314 314 config["agent_id"] = agent_id 315 315 316 + # Resolve provider and model from context 317 + # Context format: agent.{app}.{name} where app="system" for system agents 318 + from muse.models import resolve_model_for_provider, resolve_provider 319 + 320 + if ":" in persona: 321 + app, name = persona.split(":", 1) 322 + else: 323 + app, name = "system", persona 324 + agent_context = f"agent.{app}.{name}" 325 + 326 + # Check for claude: true flag (special case for Claude Code SDK) 327 + if config.get("claude"): 328 + config["provider"] = "claude" 329 + # Claude SDK doesn't need model - it uses its own 330 + else: 331 + # Resolve default provider and model from context 332 + default_provider, model = resolve_provider(agent_context) 333 + 334 + # Provider can be overridden by request or persona config 335 + # Model is always resolved from context tier + final provider 336 + provider = config.get("provider") or default_provider 337 + 338 + # If provider was overridden, re-resolve model for that provider 339 + if provider != default_provider: 340 + model = resolve_model_for_provider(agent_context, provider) 341 + 342 + config["provider"] = provider 343 + config["model"] = model 344 + 316 345 # Capture handoff configuration for post-run processing while 317 346 # leaving it in the merged config for logging transparency. 318 347 handoff_config = config.get("handoff") ··· 724 753 # Determine prompt/provider/persona before pruning extra keys. 725 754 prompt = handoff_config.pop("prompt", None) or result 726 755 persona = handoff_config.pop("persona", None) or "default" 756 + 757 + # Provider can be explicitly set in handoff config, otherwise let 758 + # the handoff persona resolve its own provider from context 727 759 provider = handoff_config.pop("provider", None) 728 - if provider is None: 729 - with self.lock: 730 - provider = self.agent_requests.get(parent_id, {}).get("provider") 731 - if provider is None: 732 - provider = "openai" 733 760 734 - # Ensure we do not propagate parent handoff metadata. 761 + # Ensure we do not propagate parent handoff metadata or claude flag. 762 + # Each persona must declare claude: true in its own config. 735 763 handoff_config.pop("handoff", None) 736 764 handoff_config.pop("handoff_from", None) 765 + handoff_config.pop("claude", None) 766 + handoff_config.pop("model", None) 737 767 738 768 # Inherit env from parent if not explicitly set in handoff config 739 769 if "env" not in handoff_config:
+77
muse/models.py
··· 143 143 "label": "Chat Title Generation", 144 144 "group": "Apps", 145 145 }, 146 + # Agent runs - AI agent execution via Cortex 147 + "agent.*": { 148 + "tier": TIER_FLASH, 149 + "label": "Agent Runs", 150 + "group": "Muse", 151 + }, 146 152 } 147 153 148 154 155 + def _resolve_tier(context: str) -> int: 156 + """Resolve context to tier number. 157 + 158 + Checks journal config contexts first, then CONTEXT_DEFAULTS with glob matching. 159 + 160 + Parameters 161 + ---------- 162 + context 163 + Context string (e.g., "agent.system.default", "insight.meetings"). 164 + 165 + Returns 166 + ------- 167 + int 168 + Tier number (1=pro, 2=flash, 3=lite). 169 + """ 170 + import fnmatch 171 + 172 + from think.utils import get_config 173 + 174 + journal_config = get_config() 175 + providers_config = journal_config.get("providers", {}) 176 + contexts = providers_config.get("contexts", {}) 177 + 178 + # Check journal config contexts first (exact match) 179 + if context in contexts: 180 + return contexts[context].get("tier", DEFAULT_TIER) 181 + 182 + # Check CONTEXT_DEFAULTS (exact match) 183 + if context in CONTEXT_DEFAULTS: 184 + return CONTEXT_DEFAULTS[context]["tier"] 185 + 186 + # Check glob patterns in both 187 + for pattern, ctx_config in contexts.items(): 188 + if fnmatch.fnmatch(context, pattern): 189 + return ctx_config.get("tier", DEFAULT_TIER) 190 + 191 + for pattern, ctx_default in CONTEXT_DEFAULTS.items(): 192 + if fnmatch.fnmatch(context, pattern): 193 + return ctx_default["tier"] 194 + 195 + return DEFAULT_TIER 196 + 197 + 149 198 def _resolve_model(provider: str, tier: int, config_models: Dict[str, Any]) -> str: 150 199 """Resolve tier to model string for a given provider. 151 200 ··· 190 239 provider, PROVIDER_DEFAULTS[DEFAULT_PROVIDER] 191 240 ) 192 241 return provider_defaults.get(DEFAULT_TIER, GEMINI_FLASH) 242 + 243 + 244 + def resolve_model_for_provider(context: str, provider: str) -> str: 245 + """Resolve model for a specific provider based on context tier. 246 + 247 + Use this when provider is overridden from the default - resolves the 248 + appropriate model for the given provider at the context's tier. 249 + 250 + Parameters 251 + ---------- 252 + context 253 + Context string (e.g., "agent.system.default"). 254 + provider 255 + Provider name ("google", "openai", "anthropic"). 256 + 257 + Returns 258 + ------- 259 + str 260 + Model identifier string for the provider at the context's tier. 261 + """ 262 + from think.utils import get_config 263 + 264 + tier = _resolve_tier(context) 265 + journal_config = get_config() 266 + providers_config = journal_config.get("providers", {}) 267 + config_models = providers_config.get("models", {}) 268 + 269 + return _resolve_model(provider, tier, config_models) 193 270 194 271 195 272 def resolve_provider(context: str) -> tuple[str, str]:
+2 -1
tests/test_cortex.py
··· 431 431 cortex_service._spawn_handoff(parent_id, result, handoff) 432 432 433 433 # Check cortex_request was called with explicit prompt 434 + # Provider is None when not explicitly set - let the persona resolve its own 434 435 mock_request.assert_called_once_with( 435 436 prompt="Review this analysis", # Uses explicit prompt 436 437 persona="reviewer", 437 - provider="openai", 438 + provider=None, 438 439 handoff_from=parent_id, 439 440 config=None, 440 441 )