personal memory agent
0
fork

Configure Feed

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

Merge branch 'hopper-n6eplbo2-agents-sol-rename'

+216 -62
-2
apps/agent/__init__.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc
+1 -1
apps/agent/call.py apps/sol/call.py
··· 3 3 4 4 """CLI commands for the agent identity system. 5 5 6 - Auto-discovered by ``think.call`` and mounted as ``sol call agent ...``. 6 + Auto-discovered by ``think.call`` and mounted as ``sol call sol ...``. 7 7 """ 8 8 9 9 import json
-5
apps/agents/app.json
··· 1 - { 2 - "icon": "🤖", 3 - "label": "Agents", 4 - "date_nav": true 5 - }
apps/agents/maint/000_migrate_agent_layout.py apps/sol/maint/000_migrate_agent_layout.py
apps/agents/maint/001_migrate_agent_run_logs.py apps/sol/maint/001_migrate_agent_run_logs.py
apps/agents/maint/__init__.py apps/sol/maint/__init__.py
+26 -3
apps/agents/routes.py apps/sol/routes.py
··· 22 22 from think.utils import updated_days 23 23 24 24 agents_bp = Blueprint( 25 - "app:agents", 25 + "app:sol", 26 26 __name__, 27 - url_prefix="/app/agents", 27 + url_prefix="/app/sol", 28 28 ) 29 29 30 30 ··· 353 353 def index() -> Any: 354 354 """Redirect to today's agent history.""" 355 355 today = date.today().strftime("%Y%m%d") 356 - return redirect(url_for("app:agents.agents_day", day=today)) 356 + return redirect(url_for("app:sol.agents_day", day=today)) 357 357 358 358 359 359 @agents_bp.route("/<day>") ··· 640 640 return jsonify(updated_days(exclude={today})) 641 641 except Exception: 642 642 return jsonify([]) 643 + 644 + 645 + @agents_bp.route("/api/identity") 646 + def api_identity() -> Any: 647 + """Return agent identity and thickness signals.""" 648 + try: 649 + from think.awareness import compute_thickness 650 + from think.utils import get_config 651 + 652 + config = get_config() 653 + agent = config.get("agent", {}) 654 + identity = config.get("identity", {}) 655 + thickness = compute_thickness() 656 + 657 + return jsonify( 658 + { 659 + "agent": agent, 660 + "identity": identity, 661 + "thickness": thickness, 662 + } 663 + ) 664 + except Exception: 665 + return jsonify({"error": "Unable to load identity data"}), 500
+120 -1
apps/agents/workspace.html apps/sol/workspace.html
··· 999 999 white-space: nowrap; 1000 1000 border: 0; 1001 1001 } 1002 + 1003 + /* Identity Header */ 1004 + .sol-identity { 1005 + max-width: 1200px; 1006 + margin: 0 auto; 1007 + padding: 1rem 2rem 0; 1008 + } 1009 + 1010 + .sol-identity-card { 1011 + display: flex; 1012 + align-items: center; 1013 + gap: 1.5rem; 1014 + padding: 1rem 1.25rem; 1015 + background: var(--card-bg, #faf9f7); 1016 + border: 1px solid var(--facet-border, #e5e0db); 1017 + border-radius: 8px; 1018 + font-size: 0.9rem; 1019 + color: var(--text-secondary, #6b6560); 1020 + } 1021 + 1022 + .sol-identity-card .identity-name { 1023 + font-size: 1.1rem; 1024 + font-weight: 600; 1025 + color: var(--text-primary, #2c2825); 1026 + } 1027 + 1028 + .sol-identity-card .identity-status { 1029 + font-size: 0.8rem; 1030 + padding: 0.15rem 0.5rem; 1031 + border-radius: 4px; 1032 + background: var(--facet-border, #e5e0db); 1033 + } 1034 + 1035 + .sol-identity-card .identity-signals { 1036 + display: flex; 1037 + gap: 1rem; 1038 + margin-left: auto; 1039 + } 1040 + 1041 + .sol-identity-card .signal { 1042 + text-align: center; 1043 + } 1044 + 1045 + .sol-identity-card .signal-value { 1046 + font-weight: 600; 1047 + color: var(--text-primary, #2c2825); 1048 + } 1049 + 1050 + .sol-identity-card .signal-label { 1051 + font-size: 0.75rem; 1052 + } 1053 + 1054 + .sol-identity-card .ready-badge { 1055 + padding: 0.15rem 0.5rem; 1056 + border-radius: 4px; 1057 + font-size: 0.8rem; 1058 + font-weight: 600; 1059 + } 1060 + 1061 + .sol-identity-card .ready-badge.ready-true { 1062 + background: #e8f5e9; 1063 + color: #2e7d32; 1064 + } 1065 + 1066 + .sol-identity-card .ready-badge.ready-false { 1067 + background: #fff3e0; 1068 + color: #e65100; 1069 + } 1002 1070 </style> 1071 + 1072 + <div class="sol-identity" id="sol-identity"> 1073 + <div class="sol-identity-card"> 1074 + <div> 1075 + <span class="identity-name" id="identity-name">...</span> 1076 + <span class="identity-status" id="identity-status"></span> 1077 + </div> 1078 + <div style="color: var(--text-secondary, #6b6560)"> 1079 + <span id="identity-owner"></span> 1080 + </div> 1081 + <div class="identity-signals" id="identity-signals"></div> 1082 + </div> 1083 + </div> 1003 1084 1004 1085 <div class="agents-content"> 1005 1086 <h2 class="sr-only">Agent Runs</h2> ··· 1111 1192 // Get current day from URL path 1112 1193 function getDayFromUrl() { 1113 1194 const path = window.location.pathname; 1114 - const match = path.match(/\/app\/agents\/(\d{8})$/); 1195 + const match = path.match(/\/app\/sol\/(\d{8})$/); 1115 1196 return match ? match[1] : null; 1197 + } 1198 + 1199 + function loadIdentity() { 1200 + fetch('/app/sol/api/identity') 1201 + .then(r => r.json()) 1202 + .then(data => { 1203 + if (data.error) { 1204 + document.getElementById('sol-identity').style.display = 'none'; 1205 + return; 1206 + } 1207 + const agent = data.agent || {}; 1208 + const identity = data.identity || {}; 1209 + const thickness = data.thickness || {}; 1210 + 1211 + document.getElementById('identity-name').textContent = agent.name || 'sol'; 1212 + const statusEl = document.getElementById('identity-status'); 1213 + statusEl.textContent = agent.name_status || 'default'; 1214 + 1215 + const ownerEl = document.getElementById('identity-owner'); 1216 + if (identity.name) { 1217 + ownerEl.textContent = 'Owner: ' + identity.name; 1218 + } 1219 + 1220 + const signalsEl = document.getElementById('identity-signals'); 1221 + signalsEl.innerHTML = [ 1222 + { label: 'Entities', value: thickness.entity_depth ?? '—' }, 1223 + { label: 'Conversations', value: thickness.conversation_count ?? '—' }, 1224 + { label: 'Days', value: thickness.journal_days ?? '—' }, 1225 + { label: 'Facets', value: thickness.facet_count ?? '—' }, 1226 + ].map(s => '<div class="signal"><div class="signal-value">' + s.value + '</div><div class="signal-label">' + s.label + '</div></div>').join('') 1227 + + '<div class="ready-badge ready-' + (thickness.ready ? 'true' : 'false') + '">' 1228 + + (thickness.ready ? 'Ready' : 'Growing') 1229 + + '</div>'; 1230 + }) 1231 + .catch(() => { 1232 + document.getElementById('sol-identity').style.display = 'none'; 1233 + }); 1116 1234 } 1117 1235 1118 1236 // Format timestamp to time string ··· 2413 2531 }); 2414 2532 2415 2533 // Initial load 2534 + loadIdentity(); 2416 2535 loadUpdatedBanner(); 2417 2536 loadAgents(); 2418 2537 })();
+5
apps/sol/app.json
··· 1 + { 2 + "icon": "🦾", 3 + "label": "Sol", 4 + "date_nav": true 5 + }
+14
convey/apps.py
··· 290 290 config = load_convey_config() 291 291 apps_dict = apply_app_order(apps_dict, config) 292 292 293 + # Override sol label if agent has a chosen name 294 + if "sol" in apps_dict: 295 + try: 296 + from think.utils import get_config as _get_journal_config 297 + 298 + journal_config = _get_journal_config() 299 + agent_block = journal_config.get("agent", {}) 300 + if agent_block.get("name_status") in ("chosen", "self-named"): 301 + agent_name = agent_block.get("name", "").strip() 302 + if agent_name: 303 + apps_dict["sol"]["label"] = agent_name 304 + except Exception: 305 + pass # Keep default label on any error 306 + 293 307 # Get starred apps list 294 308 starred_apps = config.get("apps", {}).get("starred", []) 295 309
+1 -1
docs/SOLCLI.md
··· 311 311 | `speakers` | `apps/speakers/call.py` | list, show, detect-owner, confirm-owner, clusters, suggest | 312 312 | `transcripts` | `apps/transcripts/call.py` | list, read, segments | 313 313 | `support` | `apps/support/call.py` | register, search, article, create, list, show, reply, attach, feedback, announcements, diagnose | 314 - | `agent` | `apps/agent/call.py` | name, set-name, reset, thickness, set-owner, sol-init | 314 + | `sol` | `apps/sol/call.py` | name, set-name, reset, thickness, set-owner, sol-init | 315 315 | `awareness` | `apps/awareness/call.py` | status, imports, log, log-read | 316 316 | `journal` | `think/tools/call.py` | search, events, facets, facet (show/create/update/rename/mute/unmute/delete/merge), news, agents, read, imports, import, retention purge, storage-summary | 317 317 | `routines` | `think/tools/routines.py` | list, templates, create, edit, delete, run, output, suggestions, suggest-respond, suggest-state |
+1 -1
talent/chat.md
··· 289 289 290 290 ## Naming Awareness 291 291 292 - If the journal is still using its default name ("sol"), you may — when the moment feels right after enough shared history — offer to suggest a name or let the owner choose one. Check naming readiness with `sol call agent thickness` before offering. Only once per session. 292 + If the journal is still using its default name ("sol"), you may — when the moment feels right after enough shared history — offer to suggest a name or let the owner choose one. Check naming readiness with `sol call sol thickness` before offering. Only once per session. 293 293 294 294 ## Location Context 295 295
+11 -11
talent/naming.md
··· 10 10 11 11 Before this talent runs, two checks must pass silently (no output on failure): 12 12 13 - 1. **Thickness gate** — Run `sol call agent thickness`. If `ready` is `false`, exit silently. 14 - 2. **Name gate** — Run `sol call agent name`. If `name_status` is not `"default"`, exit silently. 13 + 1. **Thickness gate** — Run `sol call sol thickness`. If `ready` is `false`, exit silently. 14 + 2. **Name gate** — Run `sol call sol name`. If `name_status` is not `"default"`, exit silently. 15 15 16 16 If both pass, proceed. 17 17 ··· 21 21 22 22 1. `sol call entities list` — the people, projects, and tools in their world 23 23 2. `sol call journal facets` — how they've organized their journal 24 - 3. `sol call agent name` — current name and status (confirms default) 25 - 4. `sol call agent thickness` — the thickness signals (confirms readiness) 24 + 3. `sol call sol name` — current name and status (confirms default) 25 + 4. `sol call sol thickness` — the thickness signals (confirms readiness) 26 26 27 27 Look for patterns: recurring entity names, facet themes, areas of focus. This is the raw material for a name proposal. 28 28 ··· 37 37 38 38 ### Path 1: Owner names you 39 39 40 - 1. Run `sol call agent set-name "NAME" --status chosen` — this also updates `sol/self.md` with the new name. 40 + 1. Run `sol call sol set-name "NAME" --status chosen` — this also updates `sol/self.md` with the new name. 41 41 2. Respond warmly: "NAME it is. That feels right." 42 42 43 43 ### Path 2: Owner asks you to suggest ··· 53 53 > How about **NAME**? [one sentence connecting the name to something from their journal]. 54 54 55 55 Then: 56 - - **Accept**: Run `sol call agent set-name "NAME" --status self-named` 57 - - **Counter-proposal**: Run `sol call agent set-name "THEIR_NAME" --status chosen` 58 - - **Keep sol**: Run `sol call agent set-name "sol" --status chosen` 56 + - **Accept**: Run `sol call sol set-name "NAME" --status self-named` 57 + - **Counter-proposal**: Run `sol call sol set-name "THEIR_NAME" --status chosen` 58 + - **Keep sol**: Run `sol call sol set-name "sol" --status chosen` 59 59 60 60 `set-name` updates `sol/self.md` automatically — no extra step needed. 61 61 ··· 67 67 - Increment `proposal_count` in the agent config 68 68 - Set `last_proposal_date` to today's date (YYYY-MM-DD) 69 69 70 - Do this by running `sol call agent set-name` with the current name and status, plus updating these fields via the agent config mechanism. 70 + Do this by running `sol call sol set-name` with the current name and status, plus updating these fields via the agent config mechanism. 71 71 72 72 ## Proposal Cap 73 73 74 - If `proposal_count` from `sol call agent name` is 3 or more, do NOT propose. Instead say: 74 + If `proposal_count` from `sol call sol name` is 3 or more, do NOT propose. Instead say: 75 75 76 76 > I've offered a few times already. If you ever want to name me, you can do it in Settings or just tell me in the chat bar. 77 77 ··· 79 79 80 80 ## Cooldown 81 81 82 - If `last_proposal_date` from `sol call agent name` is within the last 14 days, exit silently. Do not re-propose. 82 + If `last_proposal_date` from `sol call sol name` is within the last 14 days, exit silently. Do not re-propose. 83 83 84 84 ## Tone 85 85
+2 -2
talent/triage.md
··· 95 95 96 96 Check whether the naming ceremony should trigger: 97 97 98 - 1. Run `sol call agent name` to check status. 99 - 2. If `name_status` is `"default"`, run `sol call agent thickness` to check readiness. 98 + 1. Run `sol call sol name` to check status. 99 + 2. If `name_status` is `"default"`, run `sol call sol thickness` to check readiness. 100 100 3. If `ready` is `true`, mention that you've been getting to know the owner and offer to suggest a name — or let the naming talent handle it. 101 101 4. Only do this once per session. If you've already checked or offered, don't repeat. 102 102 5. If `name_status` is `"chosen"` or `"self-named"`, do nothing.
tests/baselines/api/agents/agents-day.json tests/baselines/api/sol/agents-day.json
tests/baselines/api/agents/badge-count.json tests/baselines/api/sol/badge-count.json
tests/baselines/api/agents/preview.json tests/baselines/api/sol/preview.json
tests/baselines/api/agents/run-detail.json tests/baselines/api/sol/run-detail.json
tests/baselines/api/agents/stats-month.json tests/baselines/api/sol/stats-month.json
tests/baselines/api/agents/updated-days.json tests/baselines/api/sol/updated-days.json
tests/baselines/visual/agents/smoke.jpg tests/baselines/visual/sol/smoke.jpg
+2 -2
tests/fixtures/journal/sol/partner.md
··· 13 13 ### learn their name 14 14 15 15 Ask what they'd like to be called. Record it: 16 - - `sol call agent set-owner "NAME"` 17 - - With context: `sol call agent set-owner "NAME" --bio "SHORT_BIO"` 16 + - `sol call sol set-owner "NAME"` 17 + - With context: `sol call sol set-owner "NAME" --bio "SHORT_BIO"` 18 18 19 19 As you learn about them, update your partner profile: 20 20 - `sol call identity partner --update-section 'SECTION' --value 'what you observed'`
+7 -7
tests/test_app_agents.py tests/test_app_sol.py
··· 9 9 10 10 import pytest 11 11 12 - from apps.agents.routes import _resolve_output_path 12 + from apps.sol.routes import _resolve_output_path 13 13 from think.talent import _resolve_agent_path, get_agent, get_talent_configs 14 14 15 15 ··· 246 246 """Create a Flask test client with agents blueprint and tmp journal.""" 247 247 from flask import Flask 248 248 249 - from apps.agents.routes import agents_bp 249 + from apps.sol.routes import agents_bp 250 250 from convey import state 251 251 252 252 app = Flask(__name__) ··· 273 273 274 274 def test_serves_day_relative_file(self, agents_client): 275 275 """Day-relative paths resolve under {journal}/{day}/.""" 276 - resp = agents_client.get("/app/agents/api/output/20260214/agents/flow.md") 276 + resp = agents_client.get("/app/sol/api/output/20260214/agents/flow.md") 277 277 assert resp.status_code == 200 278 278 data = resp.get_json() 279 279 assert data["content"] == "# Day agent output" ··· 283 283 def test_serves_facet_scoped_activity_file(self, agents_client): 284 284 """Paths starting with facets/ resolve from journal root.""" 285 285 resp = agents_client.get( 286 - "/app/agents/api/output/20260214/" 286 + "/app/sol/api/output/20260214/" 287 287 "facets/work/activities/20260214/coding_100/summary.md" 288 288 ) 289 289 assert resp.status_code == 200 ··· 293 293 294 294 def test_rejects_invalid_day_format(self, agents_client): 295 295 """Non-YYYYMMDD day returns 400.""" 296 - resp = agents_client.get("/app/agents/api/output/bad-day/agents/flow.md") 296 + resp = agents_client.get("/app/sol/api/output/bad-day/agents/flow.md") 297 297 assert resp.status_code == 400 298 298 299 299 def test_rejects_path_traversal(self, agents_client): 300 300 """Path traversal attempts return 403.""" 301 - resp = agents_client.get("/app/agents/api/output/20260214/../../etc/passwd") 301 + resp = agents_client.get("/app/sol/api/output/20260214/../../etc/passwd") 302 302 assert resp.status_code in (403, 404) 303 303 304 304 def test_missing_file_returns_404(self, agents_client): 305 305 """Non-existent file returns 404.""" 306 306 resp = agents_client.get( 307 - "/app/agents/api/output/20260214/agents/nonexistent.md" 307 + "/app/sol/api/output/20260214/agents/nonexistent.md" 308 308 ) 309 309 assert resp.status_code == 404
+7 -7
tests/test_awareness.py
··· 543 543 544 544 545 545 class TestThicknessCLI: 546 - """Tests for the thickness CLI command in apps/agent/call.py.""" 546 + """Tests for the thickness CLI command in apps/sol/call.py.""" 547 547 548 548 def test_thickness_command_returns_json(self): 549 549 from typer.testing import CliRunner 550 550 551 - from apps.agent.call import app 551 + from apps.sol.call import app 552 552 553 553 mock_result = { 554 554 "entity_depth": 5, ··· 933 933 def test_sol_init_command(self, tmp_path): 934 934 from typer.testing import CliRunner 935 935 936 - from apps.agent.call import app 936 + from apps.sol.call import app 937 937 938 938 result = CliRunner().invoke(app, ["sol-init"]) 939 939 assert result.exit_code == 0 ··· 944 944 945 945 946 946 class TestSetOwnerCLI: 947 - """Tests for sol call agent set-owner.""" 947 + """Tests for sol call sol set-owner.""" 948 948 949 949 def test_set_owner_name_only(self, tmp_path): 950 950 """set-owner saves identity.name to config and updates self.md.""" ··· 962 962 963 963 from typer.testing import CliRunner 964 964 965 - from apps.agent.call import app as agent_app 965 + from apps.sol.call import app as agent_app 966 966 967 967 runner = CliRunner() 968 968 with unittest.mock.patch("subprocess.run"): ··· 994 994 995 995 from typer.testing import CliRunner 996 996 997 - from apps.agent.call import app as agent_app 997 + from apps.sol.call import app as agent_app 998 998 999 999 runner = CliRunner() 1000 1000 with unittest.mock.patch("subprocess.run"): ··· 1029 1029 1030 1030 from typer.testing import CliRunner 1031 1031 1032 - from apps.agent.call import app as agent_app 1032 + from apps.sol.call import app as agent_app 1033 1033 1034 1034 runner = CliRunner() 1035 1035 # Mock subprocess.run to avoid `make skills`
+1 -1
tests/test_formatters.py
··· 1743 1743 1744 1744 assert len(chunks) == 1 1745 1745 assert ( 1746 - "**Agent:** [1765870373972](/app/agents/1765870373972)" 1746 + "**Agent:** [1765870373972](/app/sol/1765870373972)" 1747 1747 in chunks[0]["markdown"] 1748 1748 ) 1749 1749
+13 -13
tests/verify_api.py
··· 24 24 "params": {}, 25 25 "status": 200, 26 26 }, 27 - # apps/agents/routes.py 27 + # apps/sol/routes.py 28 28 { 29 - "app": "agents", 29 + "app": "sol", 30 30 "name": "agents-day", 31 - "path": "/app/agents/api/agents/20260304", 31 + "path": "/app/sol/api/agents/20260304", 32 32 "params": {"facet": "work"}, 33 33 "status": 200, 34 34 }, 35 35 { 36 - "app": "agents", 36 + "app": "sol", 37 37 "name": "run-detail", 38 - "path": "/app/agents/api/run/1700000000001", 38 + "path": "/app/sol/api/run/1700000000001", 39 39 "params": {}, 40 40 "status": 200, 41 41 }, 42 42 { 43 - "app": "agents", 43 + "app": "sol", 44 44 "name": "preview", 45 - "path": "/app/agents/api/preview/unified", 45 + "path": "/app/sol/api/preview/unified", 46 46 "params": {}, 47 47 "status": 200, 48 48 }, 49 49 { 50 - "app": "agents", 50 + "app": "sol", 51 51 "name": "stats-month", 52 - "path": "/app/agents/api/stats/202603", 52 + "path": "/app/sol/api/stats/202603", 53 53 "params": {}, 54 54 "status": 200, 55 55 }, 56 56 { 57 - "app": "agents", 57 + "app": "sol", 58 58 "name": "badge-count", 59 - "path": "/app/agents/api/badge-count", 59 + "path": "/app/sol/api/badge-count", 60 60 "params": {}, 61 61 "status": 200, 62 62 }, 63 63 { 64 - "app": "agents", 64 + "app": "sol", 65 65 "name": "updated-days", 66 - "path": "/app/agents/api/updated-days", 66 + "path": "/app/sol/api/updated-days", 67 67 "params": {}, 68 68 "status": 200, 69 69 "sandbox_only": True, # live indexer computes differently than Flask test client
+2 -2
tests/verify_browser.py
··· 24 24 SCENARIOS: list[dict[str, Any]] = [ 25 25 # smoke scenarios 26 26 { 27 - "app": "agents", 27 + "app": "sol", 28 28 "name": "smoke", 29 29 "steps": [ 30 - {"do": "navigate", "path": "/app/agents/20260304"}, 30 + {"do": "navigate", "path": "/app/sol/20260304"}, 31 31 {"do": "wait", "ms": 1000}, 32 32 {"do": "screenshot"}, 33 33 ],
+2 -2
think/awareness.py
··· 77 77 ### learn their name 78 78 79 79 Ask what they'd like to be called. Record it: 80 - - `sol call agent set-owner "NAME"` 81 - - With context: `sol call agent set-owner "NAME" --bio "SHORT_BIO"` 80 + - `sol call sol set-owner "NAME"` 81 + - With context: `sol call sol set-owner "NAME" --bio "SHORT_BIO"` 82 82 83 83 As you learn about them, update your partner profile: 84 84 - `sol call identity partner --update-section 'SECTION' --value 'what you observed'`
+1 -1
think/facets.py
··· 1082 1082 1083 1083 # Agent link if present 1084 1084 if agent_id: 1085 - lines.append(f"**Agent:** [{agent_id}](/app/agents/{agent_id})") 1085 + lines.append(f"**Agent:** [{agent_id}](/app/sol/{agent_id})") 1086 1086 1087 1087 lines.append("") 1088 1088