caudate-cli 0.1.87__tar.gz → 0.1.89__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {caudate_cli-0.1.87/caudate_cli.egg-info → caudate_cli-0.1.89}/PKG-INFO +1 -1
- {caudate_cli-0.1.87 → caudate_cli-0.1.89/caudate_cli.egg-info}/PKG-INFO +1 -1
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/caudate_cli.egg-info/SOURCES.txt +5 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/agent.py +31 -3
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/agentic_loop.py +59 -5
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/compaction.py +34 -2
- caudate_cli-0.1.89/core/goal.py +361 -0
- caudate_cli-0.1.89/core/goal_store.py +255 -0
- caudate_cli-0.1.89/core/outcomes.py +246 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/sandbox_prompt.py +36 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/slash_commands.py +262 -7
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/executor.py +2 -0
- caudate_cli-0.1.89/execution/tools/decompose_goal_tool.py +217 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/llm/router.py +17 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/main.py +112 -1
- caudate_cli-0.1.89/memory/strategies.py +285 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/config.py +19 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/data.py +21 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/heads.py +27 -3
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/observer.py +157 -3
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/runtime.py +19 -1
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/trainer.py +24 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/pyproject.toml +1 -1
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/LICENSE +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/README.md +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/anthropic_compat.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/artifact_viewer.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/caudate_middleware.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/forge_bootstrapper_routes.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/forge_routes.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/forge_system_routes.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/openai_compat.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/server.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/api/storyboard_page.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/caudate_cli.egg-info/dependency_links.txt +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/caudate_cli.egg-info/entry_points.txt +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/caudate_cli.egg-info/requires.txt +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/caudate_cli.egg-info/top_level.txt +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/cognos_mcp/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/cognos_mcp/bridge.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/cognos_mcp/client.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/cognos_mcp/config.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/cognos_mcp/server.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/config.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/anthropic_auth.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/auto_onboarding.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/background.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/banner.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/bootstrap.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/citations.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/constitution.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/custom_commands.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/diff_viewer.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/export.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/file_refs.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/files.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/hooks.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/image.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/input.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/loop.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/memory_md.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/notifications.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/ownership.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/paste.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/permissions.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/permissions_auto.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/plan_mode.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/scheduler.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/schemas.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/session.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/settings.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/skills.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/statusline.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/subagent.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/thinking.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/updater.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/usage.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/core/worktree.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/plugins.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/agent_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/agentic_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/artifact_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/ask_user_question_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/base.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/calculator_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/cognos_card_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/cron_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/datetime_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/describe_image_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/draw_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/edit_image_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/edit_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/file_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/find_anywhere_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/forge_feature_tools.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/glob_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/grep_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/http_request_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/load_skill_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/longcat_avatar_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/mcp_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/notebook_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/openapi_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/plan_mode_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/push_notification_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/python_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/respond_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/sandbox_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/search_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/semantic_search_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/shell_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/speak_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/storyboard_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/system_info_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/task_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/think_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/todo_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/transcribe_audio_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/update_memory_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/web_fetch_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/execution/tools/worktree_tool.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/llm/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/llm/fallback.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/llm/models.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/llm/models_dev.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/llm/provider.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/memory/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/memory/episodic.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/memory/procedural.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/memory/semantic.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/memory/working.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/auto_evolve.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/caudate.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/consolidator.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/encoder.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/forge_advisor.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/format.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/policy.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/scorer.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/nn/vision.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/personality/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/personality/engine.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/personality/identity.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/personality/inner_voice.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/personality/mood.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/planning/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/planning/dev_server.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/planning/forge_models.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/planning/orchestrator.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/planning/planner.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/planning/task_graph.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/reflection/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/reflection/meta_learner.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/reflection/reflector.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/setup.cfg +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_agentic_runaway.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_chat_format.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_citations.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_constrained_routing.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_contrastive_tool_head.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_feature_done_contract.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_feature_success_head.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_file_refs.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_forge_advisor.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_forge_force_stop.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_forge_models.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_forge_observer.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_forge_orchestrator_runner.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_json_salvage.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_llm_fallback.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_pi_compat.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_scheduler.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_settings.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_skills_lockfile.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_slash_commands.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_usage.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/tests/test_vram_estimates.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/ui/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/ui/display.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/voice/__init__.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/voice/conversation.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/voice/listener.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/voice/speaker.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/voice/stt.py +0 -0
- {caudate_cli-0.1.87 → caudate_cli-0.1.89}/voice/tts.py +0 -0
|
@@ -40,12 +40,15 @@ core/diff_viewer.py
|
|
|
40
40
|
core/export.py
|
|
41
41
|
core/file_refs.py
|
|
42
42
|
core/files.py
|
|
43
|
+
core/goal.py
|
|
44
|
+
core/goal_store.py
|
|
43
45
|
core/hooks.py
|
|
44
46
|
core/image.py
|
|
45
47
|
core/input.py
|
|
46
48
|
core/loop.py
|
|
47
49
|
core/memory_md.py
|
|
48
50
|
core/notifications.py
|
|
51
|
+
core/outcomes.py
|
|
49
52
|
core/ownership.py
|
|
50
53
|
core/paste.py
|
|
51
54
|
core/permissions.py
|
|
@@ -77,6 +80,7 @@ execution/tools/calculator_tool.py
|
|
|
77
80
|
execution/tools/cognos_card_tool.py
|
|
78
81
|
execution/tools/cron_tool.py
|
|
79
82
|
execution/tools/datetime_tool.py
|
|
83
|
+
execution/tools/decompose_goal_tool.py
|
|
80
84
|
execution/tools/describe_image_tool.py
|
|
81
85
|
execution/tools/draw_tool.py
|
|
82
86
|
execution/tools/edit_image_tool.py
|
|
@@ -120,6 +124,7 @@ memory/__init__.py
|
|
|
120
124
|
memory/episodic.py
|
|
121
125
|
memory/procedural.py
|
|
122
126
|
memory/semantic.py
|
|
127
|
+
memory/strategies.py
|
|
123
128
|
memory/working.py
|
|
124
129
|
nn/__init__.py
|
|
125
130
|
nn/auto_evolve.py
|
|
@@ -231,16 +231,44 @@ class CognosAgent:
|
|
|
231
231
|
self.llm.set_mood(self.personality.mood)
|
|
232
232
|
|
|
233
233
|
# Wrap personality's system_prompt provider with goal-injection.
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
234
|
+
# Legacy `self._goal` (free-text string) is preserved for
|
|
235
|
+
# backward-compat. Phase 2 introduces the structured
|
|
236
|
+
# `self.active_goal: Goal | None` — managed by the new
|
|
237
|
+
# /goal slash commands + core.goal_store, persists across
|
|
238
|
+
# sessions. 2.4 will inject its `render_for_prompt()` block.
|
|
237
239
|
self._goal: str = ""
|
|
240
|
+
try:
|
|
241
|
+
from core.goal import Goal as _Goal # noqa: F401 (type hint only)
|
|
242
|
+
except Exception:
|
|
243
|
+
pass
|
|
244
|
+
self.active_goal = None # type: "Goal | None"
|
|
245
|
+
# Auto-resume: if a previous session set an active goal pointer,
|
|
246
|
+
# load it so caudate boots back into that goal's context. Best-
|
|
247
|
+
# effort; missing / unreadable goals are silently ignored.
|
|
248
|
+
try:
|
|
249
|
+
from core.goal_store import get_store
|
|
250
|
+
self.active_goal = get_store().active()
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.debug(f"active goal auto-resume failed: {e}")
|
|
238
253
|
_personality_wrap = (
|
|
239
254
|
self.personality.wrap_system_prompt if self.personality else None
|
|
240
255
|
)
|
|
241
256
|
|
|
242
257
|
def system_prompt_provider(base: str) -> str:
|
|
243
258
|
wrapped = _personality_wrap(base) if _personality_wrap else base
|
|
259
|
+
# Phase 2.4: prefer the structured active_goal's render_for_prompt
|
|
260
|
+
# block when present — it carries the subtask tree, progress
|
|
261
|
+
# counts, and a "don't restart the plan" nudge that single-
|
|
262
|
+
# string goal injection can't express. Falls back to the
|
|
263
|
+
# legacy free-text string for users who set /goal <text>
|
|
264
|
+
# without going through `/goal new`.
|
|
265
|
+
ag = getattr(self, "active_goal", None)
|
|
266
|
+
if ag is not None:
|
|
267
|
+
try:
|
|
268
|
+
block = ag.render_for_prompt()
|
|
269
|
+
return f"{block}\n\n{wrapped}"
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.debug(f"active_goal render failed: {e}")
|
|
244
272
|
goal = (self._goal or "").strip()
|
|
245
273
|
if not goal:
|
|
246
274
|
return wrapped
|
|
@@ -151,6 +151,11 @@ class AgenticLoop:
|
|
|
151
151
|
self.documents = documents or []
|
|
152
152
|
self.last_citations = []
|
|
153
153
|
self._turn_aborted_by_user = False
|
|
154
|
+
# Phase 1.2: accumulate outcome signals across the turn so we
|
|
155
|
+
# can pass a real reward to Caudate at turn end instead of the
|
|
156
|
+
# success/failure heuristic.
|
|
157
|
+
from core.outcomes import OutcomeCollector
|
|
158
|
+
self._outcome_collector = OutcomeCollector()
|
|
154
159
|
await self.hooks.emit(HookEvent.USER_PROMPT_SUBMIT, {"message": user_message})
|
|
155
160
|
self._start_turn(user_message, attachments=attachments or [])
|
|
156
161
|
consecutive_errors = 0
|
|
@@ -198,22 +203,40 @@ class AgenticLoop:
|
|
|
198
203
|
result_block = await self._execute_call(call)
|
|
199
204
|
self._append_tool_result(result_block)
|
|
200
205
|
self._notify_caudate_tool_use(call.name, was_error=result_block.is_error)
|
|
206
|
+
# Phase 1.2: feed each tool result into the outcome collector
|
|
207
|
+
try:
|
|
208
|
+
self._outcome_collector.on_tool_result(
|
|
209
|
+
name=call.name, ok=not result_block.is_error,
|
|
210
|
+
)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
201
213
|
if self._turn_aborted_by_user:
|
|
202
214
|
break
|
|
203
215
|
|
|
204
216
|
if self._turn_aborted_by_user:
|
|
217
|
+
try:
|
|
218
|
+
self._outcome_collector.on_turn_aborted_by_user()
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
205
221
|
msg = "(turn aborted: user denied a tool call)"
|
|
206
222
|
await self.hooks.emit(HookEvent.STOP, {"text": msg})
|
|
207
223
|
self._notify_caudate_turn_end()
|
|
208
224
|
return msg
|
|
209
225
|
|
|
210
226
|
logger.warning(f"Agentic loop hit max_iterations={self.max_iterations}")
|
|
211
|
-
|
|
227
|
+
try:
|
|
228
|
+
self._outcome_collector.on_hit_max_iterations()
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
self._notify_caudate_turn_end() # outcome_collector now drives the reward
|
|
212
232
|
return "(max iterations reached)"
|
|
213
233
|
|
|
214
234
|
async def run_streaming(self, user_message: str) -> AsyncIterator[StreamEvent]:
|
|
215
235
|
"""Run the loop streaming tokens/events as they arrive."""
|
|
216
236
|
self._turn_aborted_by_user = False
|
|
237
|
+
# Phase 1.2: outcome collector for the streaming path too.
|
|
238
|
+
from core.outcomes import OutcomeCollector
|
|
239
|
+
self._outcome_collector = OutcomeCollector()
|
|
217
240
|
self._start_turn(user_message)
|
|
218
241
|
self._notify_caudate_turn_start(user_message)
|
|
219
242
|
tools = self._caudate_constrained_tools(self.executor.tool_definitions())
|
|
@@ -253,6 +276,12 @@ class AgenticLoop:
|
|
|
253
276
|
result_block = await self._execute_call(call)
|
|
254
277
|
self._append_tool_result(result_block)
|
|
255
278
|
self._notify_caudate_tool_use(call.name, was_error=result_block.is_error)
|
|
279
|
+
try:
|
|
280
|
+
self._outcome_collector.on_tool_result(
|
|
281
|
+
name=call.name, ok=not result_block.is_error,
|
|
282
|
+
)
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
256
285
|
yield StreamEvent(
|
|
257
286
|
type="tool_result",
|
|
258
287
|
tool_use_id=call.id,
|
|
@@ -262,6 +291,10 @@ class AgenticLoop:
|
|
|
262
291
|
break
|
|
263
292
|
|
|
264
293
|
if self._turn_aborted_by_user:
|
|
294
|
+
try:
|
|
295
|
+
self._outcome_collector.on_turn_aborted_by_user()
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
265
298
|
self._notify_caudate_turn_end()
|
|
266
299
|
yield StreamEvent(
|
|
267
300
|
type="message_stop",
|
|
@@ -270,6 +303,11 @@ class AgenticLoop:
|
|
|
270
303
|
return
|
|
271
304
|
|
|
272
305
|
logger.warning(f"Agentic loop (stream) hit max_iterations={self.max_iterations}")
|
|
306
|
+
try:
|
|
307
|
+
self._outcome_collector.on_hit_max_iterations()
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
self._notify_caudate_turn_end()
|
|
273
311
|
|
|
274
312
|
def reset(self) -> None:
|
|
275
313
|
"""Clear conversation history (new session)."""
|
|
@@ -764,13 +802,29 @@ class AgenticLoop:
|
|
|
764
802
|
def _notify_caudate_turn_end(self, reward: float | None = None) -> None:
|
|
765
803
|
if self.caudate is None:
|
|
766
804
|
return
|
|
805
|
+
# Phase 1.2: drain the outcome collector. When signals fired,
|
|
806
|
+
# pass `outcome_reward ∈ [-1, +1]` and let the observer derive
|
|
807
|
+
# its native [0, 1] reward. When no signals fired (e.g. plain
|
|
808
|
+
# chat turn), fall back to the legacy heuristic.
|
|
809
|
+
outcome_reward = None
|
|
810
|
+
try:
|
|
811
|
+
coll = getattr(self, "_outcome_collector", None)
|
|
812
|
+
if coll is not None:
|
|
813
|
+
sig = coll.finalize()
|
|
814
|
+
if sig.is_informative():
|
|
815
|
+
outcome_reward = sig.reward()
|
|
816
|
+
logger.debug(f"caudate outcome signal: {sig.summary()}")
|
|
817
|
+
except Exception as e:
|
|
818
|
+
logger.debug(f"outcome collector finalize failed: {e}")
|
|
819
|
+
|
|
767
820
|
try:
|
|
768
|
-
if reward is None and self.caudate._pending is not None:
|
|
769
|
-
#
|
|
770
|
-
#
|
|
821
|
+
if outcome_reward is None and reward is None and self.caudate._pending is not None:
|
|
822
|
+
# Legacy heuristic if neither outcome signals nor an
|
|
823
|
+
# explicit reward are available: 0.7 default, 0.3 if
|
|
824
|
+
# any tool errored mid-turn.
|
|
771
825
|
had_err = getattr(self.caudate._pending, "_had_error", False)
|
|
772
826
|
reward = 0.3 if had_err else 0.7
|
|
773
|
-
self.caudate.on_turn_end(reward=reward)
|
|
827
|
+
self.caudate.on_turn_end(reward=reward, outcome_reward=outcome_reward)
|
|
774
828
|
except Exception as e:
|
|
775
829
|
logger.debug(f"caudate.on_turn_end failed: {e}")
|
|
776
830
|
|
|
@@ -23,10 +23,26 @@ asked for, what tools were used and their key results, any decisions made, and
|
|
|
23
23
|
any open threads. Be concise (300 words max) but include concrete facts (file
|
|
24
24
|
paths, values, names). Do NOT add commentary — just the summary.
|
|
25
25
|
|
|
26
|
-
Conversation:
|
|
26
|
+
{goal_context}Conversation:
|
|
27
27
|
{conversation}"""
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
# Phase 2.6: when an active goal exists, prepend its block to the
|
|
31
|
+
# compaction prompt so the summarizer LLM knows what context matters
|
|
32
|
+
# most. Subtask transitions and attempt outcomes are easy to lose in
|
|
33
|
+
# a generic "summarize this" pass; this nudge keeps them anchored.
|
|
34
|
+
_GOAL_CONTEXT_HEADER = """The agent is working on this active goal:
|
|
35
|
+
{goal_block}
|
|
36
|
+
|
|
37
|
+
When summarizing, EXPLICITLY preserve:
|
|
38
|
+
- Any subtask status transitions (which moved from pending → in_progress → done)
|
|
39
|
+
- Any subtask that was blocked + why
|
|
40
|
+
- Attempt outcomes that mattered (rewards, errors, unexpected results)
|
|
41
|
+
- Decisions made about the goal's scope or approach
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
30
46
|
class ContextCompactor:
|
|
31
47
|
"""Summarizes older messages to fit a token budget."""
|
|
32
48
|
|
|
@@ -72,7 +88,23 @@ class ContextCompactor:
|
|
|
72
88
|
conversation = "\n\n".join(
|
|
73
89
|
f"[{m.get('role')}]: {_content_str(m)}" for m in to_summarize
|
|
74
90
|
)
|
|
75
|
-
|
|
91
|
+
# Phase 2.6: pull the active goal context (if any) into the
|
|
92
|
+
# compactor's prompt so the LLM-driven summary preserves
|
|
93
|
+
# subtask transitions + outcomes instead of dropping them as
|
|
94
|
+
# "implementation noise".
|
|
95
|
+
goal_context = ""
|
|
96
|
+
try:
|
|
97
|
+
from core.goal_store import get_store
|
|
98
|
+
active = get_store().active()
|
|
99
|
+
if active is not None:
|
|
100
|
+
goal_block = active.render_for_prompt()
|
|
101
|
+
goal_context = _GOAL_CONTEXT_HEADER.format(goal_block=goal_block)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.debug(f"compaction goal-context lookup failed: {e}")
|
|
104
|
+
prompt = COMPACT_PROMPT.format(
|
|
105
|
+
conversation=conversation[:12000],
|
|
106
|
+
goal_context=goal_context,
|
|
107
|
+
)
|
|
76
108
|
|
|
77
109
|
try:
|
|
78
110
|
response = await self.llm.complete(prompt=prompt, caller="compaction")
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Agent-state Goal model (Phase 2.1).
|
|
2
|
+
|
|
3
|
+
A `Goal` is the persistent intent the agent is working toward across
|
|
4
|
+
turns and sessions. Unlike `planning.forge_models.ForgeProject` (which
|
|
5
|
+
is the SQLAlchemy project-management surface), `Goal` here is the
|
|
6
|
+
agent's working memory of "what we're ultimately trying to build /
|
|
7
|
+
fix / explore."
|
|
8
|
+
|
|
9
|
+
The schema is deliberately lean — JSON-serializable so it can live
|
|
10
|
+
under `~/.caudate/goals/<id>.json` without a database dependency.
|
|
11
|
+
|
|
12
|
+
A goal is the unit of resumption:
|
|
13
|
+
caudate --goal <id> ← reboot into the goal
|
|
14
|
+
/goal resume <id> ← swap to a different goal mid-session
|
|
15
|
+
/goal status ← see current subtask tree
|
|
16
|
+
/goal complete ← mark done (positive Phase 1 signal)
|
|
17
|
+
/goal abandon ← mark abandoned (negative signal)
|
|
18
|
+
|
|
19
|
+
Subsequent Phase 2 sub-steps wire this in:
|
|
20
|
+
2.2 GoalStore (file persistence)
|
|
21
|
+
2.3 slash commands
|
|
22
|
+
2.4 system-prompt injection
|
|
23
|
+
2.5 sub-task decomposition tool
|
|
24
|
+
2.6 compaction-aware goal context
|
|
25
|
+
2.7 CLI flag
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import uuid
|
|
31
|
+
from dataclasses import dataclass, field, asdict
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------
|
|
38
|
+
# Status enums — string-valued so they round-trip through JSON cleanly.
|
|
39
|
+
# ---------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
class GoalStatus(str, Enum):
|
|
42
|
+
ACTIVE = "active"
|
|
43
|
+
PAUSED = "paused"
|
|
44
|
+
COMPLETED = "completed"
|
|
45
|
+
ABANDONED = "abandoned"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SubtaskStatus(str, Enum):
|
|
49
|
+
PENDING = "pending"
|
|
50
|
+
IN_PROGRESS = "in_progress"
|
|
51
|
+
DONE = "done"
|
|
52
|
+
BLOCKED = "blocked"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AttemptResult(str, Enum):
|
|
56
|
+
SUCCESS = "success"
|
|
57
|
+
PARTIAL = "partial"
|
|
58
|
+
ERROR = "error"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------
|
|
62
|
+
# Subtask — one step in the goal's decomposition.
|
|
63
|
+
# ---------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Subtask:
|
|
67
|
+
"""A single step in the goal's decomposition tree."""
|
|
68
|
+
|
|
69
|
+
id: str
|
|
70
|
+
description: str
|
|
71
|
+
status: SubtaskStatus = SubtaskStatus.PENDING
|
|
72
|
+
# IDs of other subtasks that must complete before this one can start.
|
|
73
|
+
# Empty list = independently dispatchable (Phase 6 sub-agents will
|
|
74
|
+
# parallelize these).
|
|
75
|
+
depends_on: list[str] = field(default_factory=list)
|
|
76
|
+
# AttemptLog ids that targeted this subtask.
|
|
77
|
+
attempts: list[str] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def new(cls, description: str, depends_on: list[str] | None = None) -> "Subtask":
|
|
81
|
+
return cls(
|
|
82
|
+
id=str(uuid.uuid4()),
|
|
83
|
+
description=description,
|
|
84
|
+
depends_on=list(depends_on or []),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"id": self.id,
|
|
90
|
+
"description": self.description,
|
|
91
|
+
"status": self.status.value,
|
|
92
|
+
"depends_on": list(self.depends_on),
|
|
93
|
+
"attempts": list(self.attempts),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_dict(cls, d: dict[str, Any]) -> "Subtask":
|
|
98
|
+
return cls(
|
|
99
|
+
id=str(d.get("id") or uuid.uuid4()),
|
|
100
|
+
description=str(d.get("description", "")),
|
|
101
|
+
status=SubtaskStatus(d.get("status", "pending")),
|
|
102
|
+
depends_on=list(d.get("depends_on") or []),
|
|
103
|
+
attempts=list(d.get("attempts") or []),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------
|
|
108
|
+
# AttemptLog — a turn-by-turn record of work toward the goal.
|
|
109
|
+
# Links into Phase 1's outcome signal: `reward` is the same scalar
|
|
110
|
+
# `outcome_reward` Phase 1.1+1.2 produce.
|
|
111
|
+
# ---------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class AttemptLog:
|
|
115
|
+
"""One turn's worth of work toward the goal."""
|
|
116
|
+
|
|
117
|
+
id: str
|
|
118
|
+
turn_index: int # ordinal across the goal's lifetime
|
|
119
|
+
timestamp: str # ISO 8601 UTC
|
|
120
|
+
subtask_id: str | None = None # which subtask was being worked on
|
|
121
|
+
tool_calls: list[str] = field(default_factory=list) # tool names in order
|
|
122
|
+
result: AttemptResult | None = None
|
|
123
|
+
reward: float | None = None # outcome_reward ∈ [-1, +1] from Phase 1
|
|
124
|
+
note: str = "" # one-liner from agent: "added Flask routes"
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def new(
|
|
128
|
+
cls,
|
|
129
|
+
turn_index: int,
|
|
130
|
+
subtask_id: str | None = None,
|
|
131
|
+
tool_calls: list[str] | None = None,
|
|
132
|
+
result: AttemptResult | None = None,
|
|
133
|
+
reward: float | None = None,
|
|
134
|
+
note: str = "",
|
|
135
|
+
) -> "AttemptLog":
|
|
136
|
+
return cls(
|
|
137
|
+
id=str(uuid.uuid4()),
|
|
138
|
+
turn_index=turn_index,
|
|
139
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
140
|
+
subtask_id=subtask_id,
|
|
141
|
+
tool_calls=list(tool_calls or []),
|
|
142
|
+
result=result,
|
|
143
|
+
reward=reward,
|
|
144
|
+
note=note,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def to_dict(self) -> dict[str, Any]:
|
|
148
|
+
return {
|
|
149
|
+
"id": self.id,
|
|
150
|
+
"turn_index": self.turn_index,
|
|
151
|
+
"timestamp": self.timestamp,
|
|
152
|
+
"subtask_id": self.subtask_id,
|
|
153
|
+
"tool_calls": list(self.tool_calls),
|
|
154
|
+
"result": self.result.value if self.result else None,
|
|
155
|
+
"reward": self.reward,
|
|
156
|
+
"note": self.note,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_dict(cls, d: dict[str, Any]) -> "AttemptLog":
|
|
161
|
+
return cls(
|
|
162
|
+
id=str(d.get("id") or uuid.uuid4()),
|
|
163
|
+
turn_index=int(d.get("turn_index") or 0),
|
|
164
|
+
timestamp=str(d.get("timestamp") or datetime.now(timezone.utc).isoformat()),
|
|
165
|
+
subtask_id=d.get("subtask_id"),
|
|
166
|
+
tool_calls=list(d.get("tool_calls") or []),
|
|
167
|
+
result=AttemptResult(d["result"]) if d.get("result") else None,
|
|
168
|
+
reward=d.get("reward"),
|
|
169
|
+
note=str(d.get("note", "")),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------
|
|
174
|
+
# Goal — the top-level persistent intent.
|
|
175
|
+
# ---------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class Goal:
|
|
179
|
+
"""Persistent agent-state intent. The unit of resumption."""
|
|
180
|
+
|
|
181
|
+
id: str
|
|
182
|
+
intent: str # user's original ask
|
|
183
|
+
cwd: str # workspace path
|
|
184
|
+
status: GoalStatus = GoalStatus.ACTIVE
|
|
185
|
+
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
186
|
+
updated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
187
|
+
subtasks: list[Subtask] = field(default_factory=list)
|
|
188
|
+
attempts: list[AttemptLog] = field(default_factory=list)
|
|
189
|
+
# Cross-link to a forge project if the user spun one up alongside
|
|
190
|
+
# this goal (planning/forge_models.ForgeProject). Optional —
|
|
191
|
+
# most goals won't have one.
|
|
192
|
+
forge_project_id: int | None = None
|
|
193
|
+
notes: str = ""
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def new(cls, intent: str, cwd: str) -> "Goal":
|
|
197
|
+
return cls(
|
|
198
|
+
id=str(uuid.uuid4()),
|
|
199
|
+
intent=intent,
|
|
200
|
+
cwd=cwd,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# ── State queries ──────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
def is_active(self) -> bool:
|
|
206
|
+
return self.status == GoalStatus.ACTIVE
|
|
207
|
+
|
|
208
|
+
def short_id(self) -> str:
|
|
209
|
+
"""First 8 chars of the UUID — what we show in the statusline / picker."""
|
|
210
|
+
return self.id[:8]
|
|
211
|
+
|
|
212
|
+
def subtask_counts(self) -> dict[str, int]:
|
|
213
|
+
"""{status: count} across all subtasks."""
|
|
214
|
+
out = {s.value: 0 for s in SubtaskStatus}
|
|
215
|
+
for st in self.subtasks:
|
|
216
|
+
out[st.status.value] = out.get(st.status.value, 0) + 1
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
def progress_fraction(self) -> float:
|
|
220
|
+
"""Fraction of subtasks marked done. 0.0 if none."""
|
|
221
|
+
if not self.subtasks:
|
|
222
|
+
return 0.0
|
|
223
|
+
done = sum(1 for st in self.subtasks if st.status == SubtaskStatus.DONE)
|
|
224
|
+
return done / len(self.subtasks)
|
|
225
|
+
|
|
226
|
+
def next_dispatchable(self) -> list[Subtask]:
|
|
227
|
+
"""Subtasks that are pending AND all their dependencies are done.
|
|
228
|
+
Phase 6 sub-agents will dispatch from this list in parallel."""
|
|
229
|
+
done_ids = {st.id for st in self.subtasks if st.status == SubtaskStatus.DONE}
|
|
230
|
+
return [
|
|
231
|
+
st for st in self.subtasks
|
|
232
|
+
if st.status == SubtaskStatus.PENDING
|
|
233
|
+
and all(dep in done_ids for dep in st.depends_on)
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
# ── Mutation helpers ───────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
def touch(self) -> None:
|
|
239
|
+
self.updated_at = datetime.now(timezone.utc).isoformat()
|
|
240
|
+
|
|
241
|
+
def add_subtask(self, description: str, depends_on: list[str] | None = None) -> Subtask:
|
|
242
|
+
st = Subtask.new(description, depends_on)
|
|
243
|
+
self.subtasks.append(st)
|
|
244
|
+
self.touch()
|
|
245
|
+
return st
|
|
246
|
+
|
|
247
|
+
def find_subtask(self, subtask_id: str) -> Subtask | None:
|
|
248
|
+
for st in self.subtasks:
|
|
249
|
+
if st.id == subtask_id or st.id.startswith(subtask_id):
|
|
250
|
+
return st
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
def set_subtask_status(self, subtask_id: str, status: SubtaskStatus) -> bool:
|
|
254
|
+
st = self.find_subtask(subtask_id)
|
|
255
|
+
if st is None:
|
|
256
|
+
return False
|
|
257
|
+
st.status = status
|
|
258
|
+
self.touch()
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
def add_attempt(self, attempt: AttemptLog) -> None:
|
|
262
|
+
self.attempts.append(attempt)
|
|
263
|
+
if attempt.subtask_id:
|
|
264
|
+
st = self.find_subtask(attempt.subtask_id)
|
|
265
|
+
if st is not None and attempt.id not in st.attempts:
|
|
266
|
+
st.attempts.append(attempt.id)
|
|
267
|
+
self.touch()
|
|
268
|
+
|
|
269
|
+
def complete(self) -> None:
|
|
270
|
+
self.status = GoalStatus.COMPLETED
|
|
271
|
+
self.touch()
|
|
272
|
+
|
|
273
|
+
def abandon(self, reason: str = "") -> None:
|
|
274
|
+
self.status = GoalStatus.ABANDONED
|
|
275
|
+
if reason:
|
|
276
|
+
self.notes = (self.notes + f"\nabandoned: {reason}").strip()
|
|
277
|
+
self.touch()
|
|
278
|
+
|
|
279
|
+
# ── Serialization ──────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
def to_dict(self) -> dict[str, Any]:
|
|
282
|
+
return {
|
|
283
|
+
"id": self.id,
|
|
284
|
+
"intent": self.intent,
|
|
285
|
+
"cwd": self.cwd,
|
|
286
|
+
"status": self.status.value,
|
|
287
|
+
"created_at": self.created_at,
|
|
288
|
+
"updated_at": self.updated_at,
|
|
289
|
+
"subtasks": [st.to_dict() for st in self.subtasks],
|
|
290
|
+
"attempts": [a.to_dict() for a in self.attempts],
|
|
291
|
+
"forge_project_id": self.forge_project_id,
|
|
292
|
+
"notes": self.notes,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def from_dict(cls, d: dict[str, Any]) -> "Goal":
|
|
297
|
+
return cls(
|
|
298
|
+
id=str(d.get("id") or uuid.uuid4()),
|
|
299
|
+
intent=str(d.get("intent", "")),
|
|
300
|
+
cwd=str(d.get("cwd", "")),
|
|
301
|
+
status=GoalStatus(d.get("status", "active")),
|
|
302
|
+
created_at=str(d.get("created_at") or datetime.now(timezone.utc).isoformat()),
|
|
303
|
+
updated_at=str(d.get("updated_at") or datetime.now(timezone.utc).isoformat()),
|
|
304
|
+
subtasks=[Subtask.from_dict(s) for s in (d.get("subtasks") or [])],
|
|
305
|
+
attempts=[AttemptLog.from_dict(a) for a in (d.get("attempts") or [])],
|
|
306
|
+
forge_project_id=d.get("forge_project_id"),
|
|
307
|
+
notes=str(d.get("notes", "")),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# ── Display ────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
def summary_line(self) -> str:
|
|
313
|
+
"""One-liner for the picker / statusline."""
|
|
314
|
+
counts = self.subtask_counts()
|
|
315
|
+
n_done = counts.get("done", 0)
|
|
316
|
+
n_total = len(self.subtasks)
|
|
317
|
+
progress = f"{n_done}/{n_total}" if n_total else "—"
|
|
318
|
+
intent_short = self.intent[:60].replace("\n", " ")
|
|
319
|
+
return (
|
|
320
|
+
f"[{self.short_id()}] {self.status.value:9} "
|
|
321
|
+
f"{progress:>5} {intent_short!r}"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def render_for_prompt(self) -> str:
|
|
325
|
+
"""Block injected into the system prompt by Phase 2.4. Compact
|
|
326
|
+
so it doesn't blow context."""
|
|
327
|
+
lines = [
|
|
328
|
+
"### Active goal",
|
|
329
|
+
f" ID: {self.short_id()}",
|
|
330
|
+
f" Intent: {self.intent}",
|
|
331
|
+
f" Cwd: {self.cwd}",
|
|
332
|
+
]
|
|
333
|
+
if self.subtasks:
|
|
334
|
+
n_done = sum(1 for s in self.subtasks if s.status == SubtaskStatus.DONE)
|
|
335
|
+
lines.append(f" Subtasks ({n_done}/{len(self.subtasks)} done):")
|
|
336
|
+
glyph = {
|
|
337
|
+
SubtaskStatus.DONE: "✓",
|
|
338
|
+
SubtaskStatus.IN_PROGRESS: "◐",
|
|
339
|
+
SubtaskStatus.PENDING: "☐",
|
|
340
|
+
SubtaskStatus.BLOCKED: "⊘",
|
|
341
|
+
}
|
|
342
|
+
for st in self.subtasks:
|
|
343
|
+
lines.append(f" {glyph[st.status]} {st.description}")
|
|
344
|
+
else:
|
|
345
|
+
# Phase 2.5 nudge — agent's first move on a fresh goal
|
|
346
|
+
# should be DecomposeGoal so the tree persists.
|
|
347
|
+
lines.append(" Subtasks: none yet.")
|
|
348
|
+
lines.append(
|
|
349
|
+
" → As your FIRST tool call, call DecomposeGoal with the "
|
|
350
|
+
"subtask breakdown. That persists the plan across turns "
|
|
351
|
+
"and sessions (unlike TodoWrite which is per-turn)."
|
|
352
|
+
)
|
|
353
|
+
if self.attempts:
|
|
354
|
+
recent_reward = next(
|
|
355
|
+
(a.reward for a in reversed(self.attempts) if a.reward is not None),
|
|
356
|
+
None,
|
|
357
|
+
)
|
|
358
|
+
if recent_reward is not None:
|
|
359
|
+
lines.append(f" Last attempt reward: {recent_reward:+.2f}")
|
|
360
|
+
lines.append(" Pick up where the last turn left off — don't restart the plan.")
|
|
361
|
+
return "\n".join(lines)
|