personal memory agent
0
fork

Configure Feed

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

Unify agent save field with insight output field

Replace agent-specific "save" frontmatter (full filename) with unified
"output" field (format only: "md" or "json"), matching insight behavior.
Output paths are now derived from persona name and schedule context.

- Add shared get_output_path() to think/utils.py for DRY path derivation
- Update think/insight.py to use shared function
- Rename _save_agent_result to _write_output in cortex.py
- Pass segment in config from dream.py for segment agent path resolution
- Remove unused save parameter from cortex_client.py
- Update decisionalizer.md frontmatter and CORTEX.md docs

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

+176 -132
+10 -9
docs/CORTEX.md
··· 43 43 "disable_mcp": false, // Optional: disable MCP tools for this request 44 44 "continue_from": "1234567890122", // Optional: continue from previous agent 45 45 "facet": "my-project", // Optional: project context 46 - "save": "analysis.md", // Optional: save result to file in day directory 46 + "output": "md", // Optional: output format ("md" or "json"), writes to insights/ 47 47 "day": "20250109", // Optional: YYYYMMDD format, defaults to current day 48 48 "env": { // Optional: environment variables for subprocess 49 49 "API_KEY": "secret", ··· 85 85 "prompt": "User's task or question", 86 86 "provider": "openai", 87 87 "persona": "default", 88 - "save": "output.md", 88 + "output": "md", 89 89 "day": "20250109", 90 90 "handoff": {}, 91 91 "handoff_from": "1234567890122" ··· 203 203 204 204 The frontend uses this to show real-time status updates as tools execute, changing from "running..." to "✓" when complete. 205 205 206 - ## Agent Result Saving 206 + ## Agent Output 207 207 208 - When an agent completes successfully, its result can be automatically saved to a file in the journal's day directory. 208 + When an agent completes successfully, its result can be automatically written to a file. This uses the same output path logic as insights. 209 209 210 - - Include a `save` field in the request with the desired filename 211 - - Optional `day` field specifies the target day in YYYYMMDD format (defaults to current day) 212 - - The result from the `finish` event is written to `<journal>/<day>/<filename>` 213 - - Saving occurs before any handoff processing 214 - - Save failures are logged but don't interrupt the agent flow 210 + - Include an `output` field in the agent's frontmatter with the format ("md" or "json") 211 + - Output path is derived from persona name + format + schedule: 212 + - Daily agents: `YYYYMMDD/insights/{persona}.{ext}` 213 + - Segment agents: `YYYYMMDD/{segment}/{persona}.{ext}` 214 + - Writing occurs before any handoff processing 215 + - Write failures are logged but don't interrupt the agent flow 215 216 - Commonly used for scheduled agents that generate daily reports 216 217 217 218 ## Agent Handoff
+1 -1
muse/decisionalizer.md
··· 3 3 "title": "Decision Dossier Generator", 4 4 "description": "Analyzes yesterday's top decision-actions to create detailed dossiers identifying gaps and stakeholder impacts", 5 5 "schedule": "daily", 6 - "save": "decision_dossiers.md", 6 + "output": "md", 7 7 "tools": "default" 8 8 9 9 }
+86 -60
tests/test_cortex.py
··· 463 463 assert set(status["agent_ids"]) == {"111", "222"} 464 464 465 465 466 - def test_save_agent_result(cortex_service, mock_journal): 467 - """Test saving agent result to file in day directory.""" 466 + def test_write_output(cortex_service, mock_journal): 467 + """Test writing agent output to insights directory.""" 468 468 # Mock datetime to return a specific date 469 469 test_date = "20240115" 470 470 from datetime import datetime as dt ··· 473 473 with patch("think.utils.datetime") as mock_datetime: 474 474 mock_datetime.now.return_value = mock_dt 475 475 476 - # Test saving result 476 + # Test writing output 477 477 agent_id = "test_agent" 478 478 result = "This is the agent result content" 479 - save_filename = "test_output.md" 479 + config = {"output": "md", "persona": "my_agent"} 480 480 481 - cortex_service._save_agent_result(agent_id, result, save_filename) 481 + cortex_service._write_output(agent_id, result, config) 482 482 483 - # Check file was created in correct location 484 - expected_path = mock_journal / test_date / save_filename 483 + # Check file was created in insights/ with persona-derived filename 484 + expected_path = mock_journal / test_date / "insights" / "my_agent.md" 485 485 assert expected_path.exists() 486 486 assert expected_path.read_text() == result 487 487 488 - # Check directory was created 489 - assert (mock_journal / test_date).is_dir() 488 + # Check directories were created 489 + assert (mock_journal / test_date / "insights").is_dir() 490 490 491 491 492 - def test_save_agent_result_with_error(cortex_service, mock_journal, caplog): 493 - """Test save agent result handles errors gracefully.""" 492 + def test_write_output_with_error(cortex_service, mock_journal, caplog): 493 + """Test write output handles errors gracefully.""" 494 494 import logging 495 495 496 496 # Make journal read-only to cause error 497 497 with patch("builtins.open", side_effect=PermissionError("Cannot write")): 498 498 with caplog.at_level(logging.ERROR): 499 - cortex_service._save_agent_result("agent_id", "result", "output.md") 499 + config = {"output": "md", "persona": "test"} 500 + cortex_service._write_output("agent_id", "result", config) 500 501 501 502 # Check error was logged but didn't raise 502 - assert "Failed to save agent agent_id result" in caplog.text 503 + assert "Failed to write agent agent_id output" in caplog.text 503 504 504 505 505 - def test_save_agent_result_with_day_parameter(cortex_service, mock_journal): 506 - """Test saving agent result to a specific day directory.""" 507 - # Test saving result with explicit day parameter 506 + def test_write_output_with_day_parameter(cortex_service, mock_journal): 507 + """Test writing agent output to a specific day directory.""" 508 + # Test writing output with explicit day parameter 508 509 agent_id = "test_agent" 509 510 result = "This is the agent result content" 510 - save_filename = "test_output.md" 511 511 specified_day = "20240201" 512 + config = {"output": "md", "persona": "reporter", "day": specified_day} 512 513 513 - cortex_service._save_agent_result( 514 - agent_id, result, save_filename, day=specified_day 515 - ) 514 + cortex_service._write_output(agent_id, result, config) 516 515 517 - # Check file was created in specified day directory 518 - expected_path = mock_journal / specified_day / save_filename 516 + # Check file was created in specified day's insights directory 517 + expected_path = mock_journal / specified_day / "insights" / "reporter.md" 519 518 assert expected_path.exists() 520 519 assert expected_path.read_text() == result 521 520 522 - # Check directory was created 523 - assert (mock_journal / specified_day).is_dir() 521 + # Check directories were created 522 + assert (mock_journal / specified_day / "insights").is_dir() 524 523 525 524 526 - def test_save_agent_result_with_invalid_day(cortex_service, mock_journal, caplog): 527 - """Test save agent result with invalid day format.""" 528 - import logging 525 + def test_write_output_with_segment(cortex_service, mock_journal): 526 + """Test writing segment agent output to segment directory.""" 527 + # Mock datetime to return a specific date 528 + test_date = "20240115" 529 + from datetime import datetime as dt 529 530 530 - # Test with invalid day format 531 - agent_id = "test_agent" 532 - result = "Test content" 533 - save_filename = "output.md" 534 - invalid_day = "2024-02-01" # Wrong format 531 + mock_dt = dt(2024, 1, 15, 12, 0, 0) 532 + with patch("think.utils.datetime") as mock_datetime: 533 + mock_datetime.now.return_value = mock_dt 535 534 536 - with caplog.at_level(logging.ERROR): 537 - cortex_service._save_agent_result( 538 - agent_id, result, save_filename, day=invalid_day 539 - ) 535 + agent_id = "segment_agent" 536 + result = "Segment analysis content" 537 + config = {"output": "md", "persona": "analyzer", "segment": "143000_600"} 540 538 541 - # Check error was logged 542 - assert "Failed to save agent test_agent result" in caplog.text 539 + cortex_service._write_output(agent_id, result, config) 543 540 544 - # File should not exist in invalid path 545 - assert not (mock_journal / invalid_day / save_filename).exists() 541 + # Check file was created in segment directory (not insights/) 542 + expected_path = mock_journal / test_date / "143000_600" / "analyzer.md" 543 + assert expected_path.exists() 544 + assert expected_path.read_text() == result 545 + 546 + 547 + def test_write_output_json_format(cortex_service, mock_journal): 548 + """Test writing agent output in JSON format.""" 549 + test_date = "20240115" 550 + from datetime import datetime as dt 551 + 552 + mock_dt = dt(2024, 1, 15, 12, 0, 0) 553 + with patch("think.utils.datetime") as mock_datetime: 554 + mock_datetime.now.return_value = mock_dt 555 + 556 + agent_id = "json_agent" 557 + result = '{"key": "value"}' 558 + config = {"output": "json", "persona": "data_agent"} 559 + 560 + cortex_service._write_output(agent_id, result, config) 546 561 562 + # Check file was created with .json extension 563 + expected_path = mock_journal / test_date / "insights" / "data_agent.json" 564 + assert expected_path.exists() 565 + assert expected_path.read_text() == result 547 566 548 - def test_monitor_stdout_with_save(cortex_service, mock_journal): 549 - """Test monitor_stdout saves result when save field is present.""" 567 + 568 + def test_monitor_stdout_with_output(cortex_service, mock_journal): 569 + """Test monitor_stdout writes output when output field is present.""" 550 570 from think.cortex import AgentProcess 551 571 552 - # Create agent with save in request 553 - agent_id = "save_test" 572 + # Create agent with output in request 573 + agent_id = "output_test" 554 574 active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 555 575 556 - # Store request with save field 576 + # Store request with output field (format only, path derived from persona) 557 577 cortex_service.agent_requests = { 558 - agent_id: {"event": "request", "prompt": "test", "save": "output.md"} 578 + agent_id: { 579 + "event": "request", 580 + "prompt": "test", 581 + "output": "md", 582 + "persona": "test_agent", 583 + } 559 584 } 560 585 561 586 # Create mock process with stdout ··· 581 606 with patch.object(cortex_service, "_has_finish_event", return_value=True): 582 607 cortex_service._monitor_stdout(agent) 583 608 584 - # Check result was saved 585 - save_path = mock_journal / test_date / "output.md" 586 - assert save_path.exists() 587 - assert save_path.read_text() == "Test result" 609 + # Check result was written to insights/ with persona-derived filename 610 + output_path = mock_journal / test_date / "insights" / "test_agent.md" 611 + assert output_path.exists() 612 + assert output_path.read_text() == "Test result" 588 613 589 614 590 - def test_monitor_stdout_with_save_and_day(cortex_service, mock_journal): 591 - """Test monitor_stdout saves result to specific day when day field is present.""" 615 + def test_monitor_stdout_with_output_and_day(cortex_service, mock_journal): 616 + """Test monitor_stdout writes output to specific day when day field is present.""" 592 617 from think.cortex import AgentProcess 593 618 594 - # Create agent with save and day in request 595 - agent_id = "save_day_test" 619 + # Create agent with output and day in request 620 + agent_id = "output_day_test" 596 621 active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 597 622 specified_day = "20240220" 598 623 599 - # Store request with save and day fields 624 + # Store request with output and day fields 600 625 cortex_service.agent_requests = { 601 626 agent_id: { 602 627 "event": "request", 603 628 "prompt": "test", 604 - "save": "report.md", 629 + "output": "md", 630 + "persona": "daily_reporter", 605 631 "day": specified_day, 606 632 } 607 633 } ··· 621 647 with patch.object(cortex_service, "_has_finish_event", return_value=True): 622 648 cortex_service._monitor_stdout(agent) 623 649 624 - # Check result was saved to specified day 625 - save_path = mock_journal / specified_day / "report.md" 626 - assert save_path.exists() 627 - assert save_path.read_text() == "Daily report content" 650 + # Check result was written to specified day's insights directory 651 + output_path = mock_journal / specified_day / "insights" / "daily_reporter.md" 652 + assert output_path.exists() 653 + assert output_path.read_text() == "Daily report content" 628 654 629 655 630 656 def test_recover_orphaned_agents(cortex_service, mock_journal):
+32 -21
think/cortex.py
··· 406 406 if self.mcp_server_url and not config.get("disable_mcp", False): 407 407 config.setdefault("mcp_server_url", self.mcp_server_url) 408 408 409 - # Store the config for later use (e.g., for save field) - thread safe 409 + # Store the config for later use (e.g., for output field) - thread safe 410 410 with self.lock: 411 411 self.agent_requests[agent_id] = config 412 412 ··· 535 535 536 536 # Handle finish or error event 537 537 if event.get("event") in ["finish", "error"]: 538 - # Check for save and handoff (only on finish) 538 + # Check for output and handoff (only on finish) 539 539 if event.get("event") == "finish": 540 540 result = event.get("result", "") 541 541 ··· 578 578 f"Failed to log token usage for agent {agent.agent_id}: {e}" 579 579 ) 580 580 581 - # Save result if requested 582 - if original_request and original_request.get("save"): 583 - self._save_agent_result( 581 + # Write output if requested 582 + if original_request and original_request.get("output"): 583 + self._write_output( 584 584 agent.agent_id, 585 585 result, 586 - original_request["save"], 587 - original_request.get( 588 - "day" 589 - ), # Pass optional day parameter 586 + original_request, 590 587 ) 591 588 592 589 # Handle handoff (prefer stored config captured at startup) ··· 728 725 except Exception as e: 729 726 self.logger.error(f"Failed to write error and complete: {e}") 730 727 731 - def _save_agent_result( 732 - self, agent_id: str, result: str, save_filename: str, day: Optional[str] = None 733 - ) -> None: 734 - """Save agent result to a file in the specified or current day directory.""" 728 + def _write_output(self, agent_id: str, result: str, config: Dict[str, Any]) -> None: 729 + """Write agent output to the appropriate location. 730 + 731 + Output path is derived from persona + output format + schedule: 732 + - Daily agents: YYYYMMDD/insights/{persona}.{ext} 733 + - Segment agents: YYYYMMDD/{segment}/{persona}.{ext} 734 + """ 735 735 try: 736 - from think.utils import day_path 736 + from think.utils import day_path, get_output_path 737 + 738 + output_format = config.get("output", "md") 739 + persona = config.get("persona", "default") 740 + segment = config.get("segment") # Set by dream.py for segment agents 741 + day = config.get("day") 737 742 738 - # day_path now handles None for today, creates dir, and returns Path 743 + # Get day directory 739 744 day_dir = day_path(day) 740 745 741 - # Write result to save file 742 - save_path = day_dir / save_filename 743 - with open(save_path, "w", encoding="utf-8") as f: 746 + # Derive output path using shared utility 747 + output_path = get_output_path( 748 + day_dir, persona, segment=segment, output_format=output_format 749 + ) 750 + 751 + # Ensure parent directory exists 752 + output_path.parent.mkdir(parents=True, exist_ok=True) 753 + 754 + with open(output_path, "w", encoding="utf-8") as f: 744 755 f.write(result) 745 756 746 - self.logger.info(f"Saved agent {agent_id} result to {save_path}") 757 + self.logger.info(f"Wrote agent {agent_id} output to {output_path}") 747 758 748 759 except Exception as e: 749 - self.logger.error(f"Failed to save agent {agent_id} result: {e}") 750 - # Don't raise - continue with normal flow even if save fails 760 + self.logger.error(f"Failed to write agent {agent_id} output: {e}") 761 + # Don't raise - continue with normal flow even if write fails 751 762 752 763 def _spawn_handoff( 753 764 self, parent_id: str, result: str, handoff: Dict[str, Any]
-5
think/cortex_client.py
··· 25 25 provider: Optional[str] = None, 26 26 handoff_from: Optional[str] = None, 27 27 config: Optional[Dict[str, Any]] = None, 28 - save: Optional[str] = None, 29 28 ) -> str: 30 29 """Create a Cortex agent request via Callosum broadcast. 31 30 ··· 35 34 provider: AI provider - openai, google, or anthropic 36 35 handoff_from: Previous agent ID if this is a handoff request 37 36 config: Provider-specific configuration (model, max_tokens, etc.) 38 - save: Optional filename to save result to in current day directory 39 37 40 38 Returns: 41 39 Agent ID (timestamp-based string) ··· 77 75 78 76 if handoff_from: 79 77 request["handoff_from"] = handoff_from 80 - 81 - if save: 82 - request["save"] = save 83 78 84 79 # Broadcast request to Callosum 85 80 # Note: callosum_send() signature is send(tract, event, **fields)
+1 -1
think/dream.py
··· 454 454 cortex_request( 455 455 prompt=f"Processing segment {segment} from {day}. Use available tools to analyze this specific recording window.", 456 456 persona=persona_id, 457 - config={"env": {"SEGMENT_KEY": segment}}, 457 + config={"segment": segment, "env": {"SEGMENT_KEY": segment}}, 458 458 ) 459 459 spawned += 1 460 460 logging.info(f"Spawned segment agent: {persona_id}")
+3 -35
think/insight.py
··· 20 20 day_path, 21 21 format_day, 22 22 format_segment_times, 23 - get_insight_topic, 24 23 get_insights, 24 + get_output_path, 25 25 load_insight_hook, 26 26 load_prompt, 27 27 segment_parse, ··· 29 29 ) 30 30 31 31 32 - def _output_path( 33 - day_dir: os.PathLike[str], 34 - key: str, 35 - segment: str | None = None, 36 - output_format: str | None = None, 37 - ) -> Path: 38 - """Return output path for insight ``key`` in ``day_dir``. 39 - 40 - Args: 41 - day_dir: Day directory path (YYYYMMDD) 42 - key: Insight key (e.g., "activity" or "chat:sentiment") 43 - segment: Optional segment key (HHMMSS_LEN) 44 - output_format: Output format from insight metadata ("json" or None for markdown) 45 - 46 - Returns: 47 - Path to output file: 48 - - Daily: YYYYMMDD/insights/{topic}.{ext} 49 - - Segment: YYYYMMDD/{segment}/{topic}.{ext} 50 - Where ext is "json" if output_format=="json", else "md" 51 - """ 52 - day = Path(day_dir) 53 - topic = get_insight_topic(key) 54 - ext = "json" if output_format == "json" else "md" 55 - 56 - if segment: 57 - # Segment insights go directly in segment directory 58 - return day / segment / f"{topic}.{ext}" 59 - else: 60 - # Daily insights go in insights/ subdirectory 61 - return day / "insights" / f"{topic}.{ext}" 62 - 63 - 64 32 def scan_day(day: str) -> dict[str, list[str]]: 65 33 """Return lists of processed and pending daily insight output files. 66 34 ··· 75 43 pending: list[str] = [] 76 44 for key, meta in sorted(daily_insights.items()): 77 45 output_format = meta.get("output") 78 - output_path = _output_path(day_dir, key, output_format=output_format) 46 + output_path = get_output_path(day_dir, key, output_format=output_format) 79 47 if output_path.exists(): 80 48 processed.append(os.path.join("insights", output_path.name)) 81 49 else: ··· 447 415 if args.output: 448 416 output_path = Path(args.output) 449 417 else: 450 - output_path = _output_path( 418 + output_path = get_output_path( 451 419 day_dir, insight_key, segment=args.segment, output_format=output_format 452 420 ) 453 421
+43
think/utils.py
··· 763 763 return key 764 764 765 765 766 + def get_output_path( 767 + day_dir: os.PathLike[str], 768 + key: str, 769 + segment: str | None = None, 770 + output_format: str | None = None, 771 + ) -> Path: 772 + """Return output path for insight or agent output. 773 + 774 + Shared utility for determining where to write insight/agent results. 775 + Used by both think/insight.py and think/cortex.py. 776 + 777 + Parameters 778 + ---------- 779 + day_dir: 780 + Day directory path (YYYYMMDD). 781 + key: 782 + Insight key or agent persona (e.g., "activity", "chat:sentiment", 783 + "decisionalizer", "entities:observer"). 784 + segment: 785 + Optional segment key (HHMMSS_LEN) for segment-level output. 786 + output_format: 787 + Output format - "json" for JSON, anything else for markdown. 788 + 789 + Returns 790 + ------- 791 + Path 792 + Output file path: 793 + - With segment: YYYYMMDD/{segment}/{topic}.{ext} 794 + - Without segment: YYYYMMDD/insights/{topic}.{ext} 795 + Where topic is derived from key and ext is "json" or "md". 796 + """ 797 + day = Path(day_dir) 798 + topic = get_insight_topic(key) 799 + ext = "json" if output_format == "json" else "md" 800 + 801 + if segment: 802 + # Segment output goes directly in segment directory 803 + return day / segment / f"{topic}.{ext}" 804 + else: 805 + # Daily output goes in insights/ subdirectory 806 + return day / "insights" / f"{topic}.{ext}" 807 + 808 + 766 809 def _load_insight_metadata(md_path: Path) -> dict[str, object]: 767 810 """Load insight metadata from .md file with JSON frontmatter. 768 811