caudate-cli 0.1.0__py3-none-any.whl
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.
- api/__init__.py +5 -0
- api/anthropic_compat.py +1518 -0
- api/artifact_viewer.py +366 -0
- api/caudate_middleware.py +618 -0
- api/forge_bootstrapper_routes.py +377 -0
- api/forge_routes.py +630 -0
- api/forge_system_routes.py +294 -0
- api/openai_compat.py +1993 -0
- api/server.py +667 -0
- api/storyboard_page.py +677 -0
- caudate_cli-0.1.0.dist-info/METADATA +354 -0
- caudate_cli-0.1.0.dist-info/RECORD +153 -0
- caudate_cli-0.1.0.dist-info/WHEEL +5 -0
- caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
- caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
- cognos_mcp/__init__.py +4 -0
- cognos_mcp/bridge.py +41 -0
- cognos_mcp/client.py +70 -0
- cognos_mcp/config.py +49 -0
- cognos_mcp/server.py +66 -0
- config.py +82 -0
- core/__init__.py +0 -0
- core/agent.py +468 -0
- core/agentic_loop.py +731 -0
- core/anthropic_auth.py +91 -0
- core/background.py +113 -0
- core/banner.py +134 -0
- core/bootstrap.py +292 -0
- core/citations.py +131 -0
- core/compaction.py +109 -0
- core/constitution.py +198 -0
- core/diff_viewer.py +87 -0
- core/export.py +85 -0
- core/file_refs.py +119 -0
- core/files.py +199 -0
- core/hooks.py +209 -0
- core/image.py +599 -0
- core/input.py +91 -0
- core/loop.py +238 -0
- core/memory_md.py +147 -0
- core/notifications.py +99 -0
- core/ownership.py +181 -0
- core/paste.py +81 -0
- core/permissions.py +210 -0
- core/plan_mode.py +215 -0
- core/sandbox_prompt.py +185 -0
- core/scheduler.py +195 -0
- core/schemas.py +202 -0
- core/session.py +90 -0
- core/settings.py +132 -0
- core/skills.py +398 -0
- core/slash_commands.py +977 -0
- core/statusline.py +61 -0
- core/subagent.py +300 -0
- core/thinking.py +50 -0
- core/updater.py +122 -0
- core/usage.py +109 -0
- core/worktree.py +93 -0
- execution/__init__.py +0 -0
- execution/executor.py +329 -0
- execution/plugins.py +108 -0
- execution/tools/__init__.py +0 -0
- execution/tools/agent_tool.py +107 -0
- execution/tools/agentic_tool.py +297 -0
- execution/tools/artifact_tool.py +191 -0
- execution/tools/ask_user_question_tool.py +137 -0
- execution/tools/base.py +81 -0
- execution/tools/calculator_tool.py +137 -0
- execution/tools/cognos_card_tool.py +124 -0
- execution/tools/cron_tool.py +215 -0
- execution/tools/datetime_tool.py +215 -0
- execution/tools/describe_image_tool.py +161 -0
- execution/tools/draw_tool.py +164 -0
- execution/tools/edit_image_tool.py +262 -0
- execution/tools/edit_tool.py +245 -0
- execution/tools/file_tool.py +90 -0
- execution/tools/find_anywhere_tool.py +255 -0
- execution/tools/forge_feature_tools.py +377 -0
- execution/tools/glob_tool.py +59 -0
- execution/tools/grep_tool.py +89 -0
- execution/tools/http_request_tool.py +224 -0
- execution/tools/load_skill_tool.py +104 -0
- execution/tools/longcat_avatar_tool.py +384 -0
- execution/tools/mcp_tool.py +100 -0
- execution/tools/notebook_tool.py +279 -0
- execution/tools/openapi_tool.py +440 -0
- execution/tools/plan_mode_tool.py +95 -0
- execution/tools/push_notification_tool.py +157 -0
- execution/tools/python_tool.py +61 -0
- execution/tools/respond_tool.py +40 -0
- execution/tools/sandbox_tool.py +378 -0
- execution/tools/search_tool.py +153 -0
- execution/tools/semantic_search_tool.py +106 -0
- execution/tools/shell_tool.py +283 -0
- execution/tools/speak_tool.py +134 -0
- execution/tools/storyboard_tool.py +727 -0
- execution/tools/system_info_tool.py +212 -0
- execution/tools/task_tool.py +323 -0
- execution/tools/think_tool.py +49 -0
- execution/tools/transcribe_audio_tool.py +86 -0
- execution/tools/update_memory_tool.py +92 -0
- execution/tools/web_fetch_tool.py +82 -0
- execution/tools/worktree_tool.py +174 -0
- llm/__init__.py +0 -0
- llm/fallback.py +116 -0
- llm/models.py +320 -0
- llm/provider.py +1356 -0
- llm/router.py +373 -0
- main.py +1889 -0
- memory/__init__.py +0 -0
- memory/episodic.py +99 -0
- memory/procedural.py +145 -0
- memory/semantic.py +71 -0
- memory/working.py +64 -0
- nn/__init__.py +43 -0
- nn/auto_evolve.py +245 -0
- nn/caudate.py +136 -0
- nn/config.py +141 -0
- nn/consolidator.py +81 -0
- nn/data.py +1635 -0
- nn/encoder.py +258 -0
- nn/forge_advisor.py +303 -0
- nn/format.py +235 -0
- nn/heads.py +432 -0
- nn/observer.py +994 -0
- nn/policy.py +214 -0
- nn/runtime.py +343 -0
- nn/scorer.py +175 -0
- nn/trainer.py +515 -0
- nn/vision.py +352 -0
- personality/__init__.py +23 -0
- personality/engine.py +129 -0
- personality/identity.py +144 -0
- personality/inner_voice.py +100 -0
- personality/mood.py +205 -0
- planning/__init__.py +0 -0
- planning/dev_server.py +221 -0
- planning/forge_models.py +718 -0
- planning/orchestrator.py +1363 -0
- planning/planner.py +451 -0
- planning/task_graph.py +61 -0
- reflection/__init__.py +0 -0
- reflection/meta_learner.py +156 -0
- reflection/reflector.py +127 -0
- ui/__init__.py +5 -0
- ui/display.py +88 -0
- voice/__init__.py +0 -0
- voice/conversation.py +125 -0
- voice/listener.py +111 -0
- voice/speaker.py +59 -0
- voice/stt.py +126 -0
- voice/tts.py +214 -0
planning/planner.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Planner — decomposes goals into executable task graphs using LLM reasoning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from config import MAX_PLAN_DEPTH, MAX_REPLAN_ATTEMPTS
|
|
12
|
+
from core.schemas import Goal, Plan, Task, Reflection, Perception
|
|
13
|
+
from llm.provider import LLMProvider
|
|
14
|
+
from planning.task_graph import TaskGraph
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TaskOutput(BaseModel):
|
|
20
|
+
"""One task in a plan, as emitted by the LLM."""
|
|
21
|
+
description: str
|
|
22
|
+
tool_name: str | None = None
|
|
23
|
+
tool_args: dict[str, Any] = Field(default_factory=dict)
|
|
24
|
+
dependencies: list[int] = Field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PlanOutput(BaseModel):
|
|
28
|
+
"""LLM-emitted plan structure."""
|
|
29
|
+
reasoning: str
|
|
30
|
+
tasks: list[TaskOutput] = Field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
PLAN_SYSTEM_PROMPT = """You are a planning module in a cognitive architecture.
|
|
33
|
+
Your job is to decompose a goal into concrete, executable tasks.
|
|
34
|
+
|
|
35
|
+
Each task should specify:
|
|
36
|
+
- description: what to do
|
|
37
|
+
- tool_name: which tool to use (or null if it's a reasoning step)
|
|
38
|
+
- tool_args: arguments for the tool (MUST provide all required args)
|
|
39
|
+
- dependencies: list of task indices (0-based) this task depends on
|
|
40
|
+
|
|
41
|
+
IMPORTANT RULES:
|
|
42
|
+
- The "Respond" tool REQUIRES a "message" argument. Results from prior tasks are automatically available, so write a helpful message that answers the user's goal. You can reference prior results — they will be appended.
|
|
43
|
+
- Always end your plan with a "Respond" task to present results to the user.
|
|
44
|
+
- For tool_args, provide concrete values. Don't leave required args empty.
|
|
45
|
+
- Results from completed tasks automatically flow to dependent tasks.
|
|
46
|
+
|
|
47
|
+
Available tools: {tools}
|
|
48
|
+
|
|
49
|
+
Respond with JSON:
|
|
50
|
+
{{
|
|
51
|
+
"reasoning": "your reasoning about how to achieve the goal",
|
|
52
|
+
"tasks": [
|
|
53
|
+
{{
|
|
54
|
+
"description": "step description",
|
|
55
|
+
"tool_name": "tool_name or null",
|
|
56
|
+
"tool_args": {{}},
|
|
57
|
+
"dependencies": []
|
|
58
|
+
}}
|
|
59
|
+
]
|
|
60
|
+
}}"""
|
|
61
|
+
|
|
62
|
+
REPLAN_SYSTEM_PROMPT = """You are a planning module re-evaluating a failed plan.
|
|
63
|
+
|
|
64
|
+
Original goal: {goal}
|
|
65
|
+
Original plan reasoning: {reasoning}
|
|
66
|
+
What failed: {failure}
|
|
67
|
+
Reflection: {reflection}
|
|
68
|
+
|
|
69
|
+
Create a revised plan that addresses the failure. You may reuse successful steps.
|
|
70
|
+
Respond with the same JSON format as before."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Planner:
|
|
74
|
+
"""Decomposes goals into task DAGs, with re-planning on failure."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, llm: LLMProvider):
|
|
77
|
+
self.llm = llm
|
|
78
|
+
|
|
79
|
+
def reset(self) -> None:
|
|
80
|
+
"""Reset state for a new goal."""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
async def create_plan(
|
|
84
|
+
self,
|
|
85
|
+
goal: Goal,
|
|
86
|
+
perception: Perception,
|
|
87
|
+
available_tools: list[str],
|
|
88
|
+
) -> Plan:
|
|
89
|
+
"""Decompose a goal into a plan (task DAG)."""
|
|
90
|
+
context_parts = []
|
|
91
|
+
if perception.working_memory:
|
|
92
|
+
context_parts.append(f"Current context: {json.dumps(perception.working_memory)}")
|
|
93
|
+
if perception.relevant_episodes:
|
|
94
|
+
eps = [e.action for e in perception.relevant_episodes[:3]]
|
|
95
|
+
context_parts.append(f"Relevant past actions: {eps}")
|
|
96
|
+
if perception.active_strategies:
|
|
97
|
+
strats = [f"{s.name}: {s.actions}" for s in perception.active_strategies]
|
|
98
|
+
context_parts.append(f"Applicable strategies: {strats}")
|
|
99
|
+
if perception.relevant_knowledge:
|
|
100
|
+
context_parts.append(f"Relevant knowledge: {perception.relevant_knowledge[:3]}")
|
|
101
|
+
|
|
102
|
+
context_str = "\n".join(context_parts) if context_parts else "No prior context."
|
|
103
|
+
|
|
104
|
+
prompt = f"""Goal: {goal.description}
|
|
105
|
+
Success criteria: {goal.success_criteria}
|
|
106
|
+
|
|
107
|
+
Context:
|
|
108
|
+
{context_str}
|
|
109
|
+
|
|
110
|
+
Decompose this goal into executable tasks."""
|
|
111
|
+
|
|
112
|
+
system = PLAN_SYSTEM_PROMPT.format(tools=", ".join(available_tools))
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
result = await self.llm.structured_output(
|
|
116
|
+
prompt=prompt, system=system, response_model=PlanOutput,
|
|
117
|
+
caller="planning",
|
|
118
|
+
)
|
|
119
|
+
tasks = self._build_tasks(result, goal.id)
|
|
120
|
+
plan = Plan(
|
|
121
|
+
goal_id=goal.id,
|
|
122
|
+
tasks=tasks,
|
|
123
|
+
reasoning=result.reasoning,
|
|
124
|
+
)
|
|
125
|
+
logger.info(f"Created plan with {len(tasks)} tasks for goal: {goal.description}")
|
|
126
|
+
return plan
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Planning failed: {e}")
|
|
129
|
+
# Fallback: single reasoning task
|
|
130
|
+
return Plan(
|
|
131
|
+
goal_id=goal.id,
|
|
132
|
+
tasks=[Task(
|
|
133
|
+
goal_id=goal.id,
|
|
134
|
+
description=f"Attempt to achieve: {goal.description}",
|
|
135
|
+
)],
|
|
136
|
+
reasoning=f"Planning failed ({e}), falling back to direct attempt.",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def replan(
|
|
140
|
+
self,
|
|
141
|
+
goal: Goal,
|
|
142
|
+
current_plan: Plan,
|
|
143
|
+
reflection: Reflection,
|
|
144
|
+
available_tools: list[str],
|
|
145
|
+
) -> Plan | None:
|
|
146
|
+
"""Create a revised plan based on reflection of failure."""
|
|
147
|
+
# Replan limit is managed by the cognitive loop, not here
|
|
148
|
+
|
|
149
|
+
failure_desc = reflection.what_failed or "Unknown failure"
|
|
150
|
+
system = REPLAN_SYSTEM_PROMPT.format(
|
|
151
|
+
goal=goal.description,
|
|
152
|
+
reasoning=current_plan.reasoning,
|
|
153
|
+
failure=failure_desc,
|
|
154
|
+
reflection=reflection.lesson,
|
|
155
|
+
)
|
|
156
|
+
prompt = f"Available tools: {', '.join(available_tools)}\nCreate a revised plan."
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
result = await self.llm.structured_output(
|
|
160
|
+
prompt=prompt, system=system, response_model=PlanOutput,
|
|
161
|
+
caller="planning",
|
|
162
|
+
)
|
|
163
|
+
tasks = self._build_tasks(result, goal.id)
|
|
164
|
+
plan = Plan(
|
|
165
|
+
goal_id=goal.id,
|
|
166
|
+
tasks=tasks,
|
|
167
|
+
reasoning=result.reasoning,
|
|
168
|
+
version=current_plan.version + 1,
|
|
169
|
+
)
|
|
170
|
+
logger.info(f"Re-planned (v{plan.version}) with {len(tasks)} tasks")
|
|
171
|
+
return plan
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Re-planning failed: {e}")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def _build_tasks(self, output: PlanOutput, goal_id: str) -> list[Task]:
|
|
177
|
+
"""Convert PlanOutput into Task objects with proper dependency IDs."""
|
|
178
|
+
tasks: list[Task] = [
|
|
179
|
+
Task(
|
|
180
|
+
goal_id=goal_id,
|
|
181
|
+
description=t.description,
|
|
182
|
+
tool_name=t.tool_name,
|
|
183
|
+
tool_args=t.tool_args,
|
|
184
|
+
)
|
|
185
|
+
for t in output.tasks
|
|
186
|
+
]
|
|
187
|
+
for i, raw in enumerate(output.tasks):
|
|
188
|
+
for idx in raw.dependencies:
|
|
189
|
+
if 0 <= idx < len(tasks):
|
|
190
|
+
tasks[i].dependencies.append(tasks[idx].id)
|
|
191
|
+
return tasks
|
|
192
|
+
|
|
193
|
+
# ───────────────────── Forge bootstrapper ─────────────────────────
|
|
194
|
+
# Decomposes a project description into a buildable feature backlog.
|
|
195
|
+
# Different output shape from `create_plan`: features carry detailed
|
|
196
|
+
# descriptions + acceptance criteria + a verify_command suggestion,
|
|
197
|
+
# not tool calls. The orchestrator (planning/orchestrator.py) is the
|
|
198
|
+
# one that turns features into actual subagent runs.
|
|
199
|
+
|
|
200
|
+
async def decompose_to_features(
|
|
201
|
+
self,
|
|
202
|
+
goal: str,
|
|
203
|
+
stack_hints: list[str] | None = None,
|
|
204
|
+
max_features: int = 15,
|
|
205
|
+
) -> "list[FeatureOutput]":
|
|
206
|
+
"""Decompose a project description into a feature backlog.
|
|
207
|
+
|
|
208
|
+
Returns a list of `FeatureOutput` ready to write to the
|
|
209
|
+
`forge_features` table. The caller (forge bootstrap CLI) is
|
|
210
|
+
responsible for inserting into the DB and wiring dependencies
|
|
211
|
+
by index.
|
|
212
|
+
|
|
213
|
+
`stack_hints` should be tags like ["python", "fastapi"] or
|
|
214
|
+
["nextjs", "react"] — they steer the verify_command suggestion
|
|
215
|
+
and the level of UI detail. Empty list = generic.
|
|
216
|
+
"""
|
|
217
|
+
stack_str = (
|
|
218
|
+
", ".join(stack_hints)
|
|
219
|
+
if stack_hints
|
|
220
|
+
else "(none specified — pick what fits the goal)"
|
|
221
|
+
)
|
|
222
|
+
system = FORGE_BOOTSTRAP_SYSTEM_PROMPT.format(
|
|
223
|
+
max_features=max_features,
|
|
224
|
+
stack=stack_str,
|
|
225
|
+
)
|
|
226
|
+
prompt = f"Project goal: {goal}\n\nProduce the feature backlog."
|
|
227
|
+
try:
|
|
228
|
+
# 15 features × full description + acceptance criteria can
|
|
229
|
+
# easily exceed the 4 k default and truncate the JSON
|
|
230
|
+
# response mid-string. Give the bootstrapper plenty of
|
|
231
|
+
# headroom; salvage handles overruns but avoiding them is
|
|
232
|
+
# cheaper.
|
|
233
|
+
result = await self.llm.structured_output(
|
|
234
|
+
prompt=prompt,
|
|
235
|
+
system=system,
|
|
236
|
+
response_model=FeatureBacklogOutput,
|
|
237
|
+
caller="bootstrapper",
|
|
238
|
+
max_tokens=16384,
|
|
239
|
+
)
|
|
240
|
+
logger.info(
|
|
241
|
+
f"Bootstrapper produced {len(result.features)} features "
|
|
242
|
+
f"for goal: {goal[:80]!r}"
|
|
243
|
+
)
|
|
244
|
+
return result.features
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"Bootstrapper LLM call failed: {e}")
|
|
247
|
+
raise
|
|
248
|
+
|
|
249
|
+
async def revise_feature(
|
|
250
|
+
self,
|
|
251
|
+
failed_feature: dict[str, Any],
|
|
252
|
+
failure_logs: str,
|
|
253
|
+
project_goal: str | None = None,
|
|
254
|
+
stack_hints: list[str] | None = None,
|
|
255
|
+
) -> list["FeatureOutput"]:
|
|
256
|
+
"""ADR 0003: ask the bootstrapper to re-decompose a feature
|
|
257
|
+
that hit the 3-failure cap. Returns one or more new features
|
|
258
|
+
that together replace the original.
|
|
259
|
+
|
|
260
|
+
The output is the same FeatureBacklogOutput shape as the main
|
|
261
|
+
bootstrapper, so the orchestrator can splice the replacements
|
|
262
|
+
into the existing backlog with deps inherited from the
|
|
263
|
+
original feature.
|
|
264
|
+
"""
|
|
265
|
+
stack_str = (
|
|
266
|
+
", ".join(stack_hints) if stack_hints else "(unspecified)"
|
|
267
|
+
)
|
|
268
|
+
goal_block = (
|
|
269
|
+
f"\n\nProject goal: {project_goal}" if project_goal else ""
|
|
270
|
+
)
|
|
271
|
+
system = FORGE_REVISE_SYSTEM_PROMPT.format(stack=stack_str)
|
|
272
|
+
prompt = (
|
|
273
|
+
f"# Feature that failed 3 times\n\n"
|
|
274
|
+
f"Title: {failed_feature.get('title', '')}\n\n"
|
|
275
|
+
f"Description:\n{failed_feature.get('description', '') or '(none)'}\n\n"
|
|
276
|
+
f"Acceptance criteria:\n"
|
|
277
|
+
f"{failed_feature.get('acceptance_criteria', '') or '(none)'}\n\n"
|
|
278
|
+
f"Verify command: {failed_feature.get('verify_command') or '(none)'}\n\n"
|
|
279
|
+
f"# Failure logs (truncated)\n\n```\n{failure_logs[-4000:]}\n```"
|
|
280
|
+
f"{goal_block}\n\n"
|
|
281
|
+
f"Re-decompose this feature. Output the same JSON schema "
|
|
282
|
+
f"as a normal backlog bootstrap, with 1-5 features."
|
|
283
|
+
)
|
|
284
|
+
try:
|
|
285
|
+
result = await self.llm.structured_output(
|
|
286
|
+
prompt=prompt,
|
|
287
|
+
system=system,
|
|
288
|
+
response_model=FeatureBacklogOutput,
|
|
289
|
+
caller="revise",
|
|
290
|
+
max_tokens=8192,
|
|
291
|
+
)
|
|
292
|
+
logger.info(
|
|
293
|
+
f"revise_feature produced {len(result.features)} "
|
|
294
|
+
f"replacement(s) for {failed_feature.get('title', '?')[:60]!r}"
|
|
295
|
+
)
|
|
296
|
+
return result.features
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.error(f"revise_feature LLM call failed: {e}")
|
|
299
|
+
raise
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class FeatureOutput(BaseModel):
|
|
303
|
+
"""One feature in a forge backlog, as emitted by the bootstrapper."""
|
|
304
|
+
title: str
|
|
305
|
+
description: str
|
|
306
|
+
# Stored in the DB as a single string; LLMs frequently emit a list
|
|
307
|
+
# of bullet strings (the natural shape for a checklist). The
|
|
308
|
+
# validator coerces list -> "- item\n- item" so the DB writer
|
|
309
|
+
# doesn't have to branch.
|
|
310
|
+
acceptance_criteria: str = ""
|
|
311
|
+
priority: int = 0
|
|
312
|
+
category: str = "functional" # functional | style
|
|
313
|
+
# Indices into the surrounding features list — same dependency
|
|
314
|
+
# idiom as PlanOutput, resolved to feature ids at insert time.
|
|
315
|
+
depends_on: list[int] = Field(default_factory=list)
|
|
316
|
+
# Suggested shell command that must exit 0 to mark the feature done.
|
|
317
|
+
# Bootstrapper picks one based on stack_hints; runner executes it
|
|
318
|
+
# post-implementation. May be empty if no good default fits.
|
|
319
|
+
verify_command: str | None = None
|
|
320
|
+
|
|
321
|
+
# Pydantic v2 validator: lift list[str] -> bullet-formatted string.
|
|
322
|
+
@classmethod
|
|
323
|
+
def _coerce_str_field(cls, v: Any) -> str:
|
|
324
|
+
if isinstance(v, list):
|
|
325
|
+
return "\n".join(
|
|
326
|
+
f"- {str(item).strip()}" for item in v if str(item).strip()
|
|
327
|
+
)
|
|
328
|
+
if v is None:
|
|
329
|
+
return ""
|
|
330
|
+
return str(v)
|
|
331
|
+
|
|
332
|
+
# Apply to fields that LLMs sometimes emit as lists.
|
|
333
|
+
from pydantic import field_validator as _fv
|
|
334
|
+
|
|
335
|
+
_coerce_ac = _fv("acceptance_criteria", mode="before")( # type: ignore[arg-type]
|
|
336
|
+
_coerce_str_field
|
|
337
|
+
)
|
|
338
|
+
_coerce_desc = _fv("description", mode="before")( # type: ignore[arg-type]
|
|
339
|
+
_coerce_str_field
|
|
340
|
+
)
|
|
341
|
+
_coerce_verify = _fv("verify_command", mode="before")( # type: ignore[arg-type]
|
|
342
|
+
lambda cls, v: None if v in (None, "", []) else
|
|
343
|
+
(v if isinstance(v, str) else
|
|
344
|
+
" && ".join(str(x) for x in v))
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class FeatureBacklogOutput(BaseModel):
|
|
349
|
+
"""Top-level structure the bootstrapper emits."""
|
|
350
|
+
reasoning: str
|
|
351
|
+
features: list[FeatureOutput] = Field(default_factory=list)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
FORGE_BOOTSTRAP_SYSTEM_PROMPT = """You are the Cognos Forge bootstrapper.
|
|
355
|
+
|
|
356
|
+
Your job: decompose a project description into a buildable backlog of
|
|
357
|
+
{max_features} or fewer features. A weaker local coding agent will
|
|
358
|
+
implement these one at a time, so each feature must be detailed,
|
|
359
|
+
self-contained, and unambiguous.
|
|
360
|
+
|
|
361
|
+
Stack hints from the user: {stack}
|
|
362
|
+
|
|
363
|
+
For each feature, produce:
|
|
364
|
+
- title: short imperative (under 100 chars). Start with a verb.
|
|
365
|
+
Good: "Build user registration form with email + password validation"
|
|
366
|
+
Bad: "User auth"
|
|
367
|
+
- description: 150-500 words. Cover UI specifics (components, layout),
|
|
368
|
+
data details (field names, types, validation), behavior on user
|
|
369
|
+
actions, edge cases (empty/loading/error states), and explicit
|
|
370
|
+
scope boundaries (what is NOT in this feature).
|
|
371
|
+
- acceptance_criteria: bulleted checklist of independently testable
|
|
372
|
+
criteria. 5-15 bullets. Each one a coding agent or human tester can
|
|
373
|
+
unambiguously verify.
|
|
374
|
+
- priority: integer, lower = built first. Use this to express order
|
|
375
|
+
even when there's no hard dependency.
|
|
376
|
+
- category: "functional" (default) or "style" (purely visual).
|
|
377
|
+
- depends_on: list of *indices* (0-based, into your own features list)
|
|
378
|
+
this feature genuinely needs before it can be built. Don't over-chain.
|
|
379
|
+
- verify_command: a shell command that must exit 0 to count this
|
|
380
|
+
feature as passing. Pick based on stack:
|
|
381
|
+
* Python: "pytest -q" or "python3 -m mypy ." (use `python3`,
|
|
382
|
+
NOT bare `python` — most Linux hosts don't symlink it)
|
|
383
|
+
* Node/web: "npx playwright test" or "npx tsc --noEmit"
|
|
384
|
+
* Rust: "cargo test"
|
|
385
|
+
* Go: "go test ./..."
|
|
386
|
+
* No good default: leave null.
|
|
387
|
+
|
|
388
|
+
Order features dependency-first:
|
|
389
|
+
Foundation -> data layer -> UI/behavior -> polish.
|
|
390
|
+
|
|
391
|
+
Return JSON:
|
|
392
|
+
{{
|
|
393
|
+
"reasoning": "why this breakdown",
|
|
394
|
+
"features": [
|
|
395
|
+
{{
|
|
396
|
+
"title": "...",
|
|
397
|
+
"description": "...",
|
|
398
|
+
"acceptance_criteria": "...",
|
|
399
|
+
"priority": 0,
|
|
400
|
+
"category": "functional",
|
|
401
|
+
"depends_on": [],
|
|
402
|
+
"verify_command": "pytest -q"
|
|
403
|
+
}}
|
|
404
|
+
]
|
|
405
|
+
}}"""
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
FORGE_REVISE_SYSTEM_PROMPT = """You are the Cognos Forge feature reviser.
|
|
409
|
+
|
|
410
|
+
A feature in an autonomous coding pipeline has failed 3 times in a
|
|
411
|
+
row. Your job is to look at the original feature and the failure
|
|
412
|
+
logs, then output 1-5 NEW features that together accomplish the
|
|
413
|
+
same goal but are more likely to succeed.
|
|
414
|
+
|
|
415
|
+
Stack: {stack}
|
|
416
|
+
|
|
417
|
+
Strategies:
|
|
418
|
+
- If the original feature is too BIG, split it into smaller steps
|
|
419
|
+
(each step should be ~10-30 min of agent work, not 2 hours).
|
|
420
|
+
- If the original feature's verify_command was the cause (wrong
|
|
421
|
+
binary, no tests yet, wrong path), rewrite the verify_command.
|
|
422
|
+
Prefer `python3` over `python`. Prefer narrow file checks
|
|
423
|
+
(`python3 -c "from app.main import app"`) over broad ones
|
|
424
|
+
(`pytest -q`) for foundation features.
|
|
425
|
+
- If the original feature's description was ambiguous, rewrite it
|
|
426
|
+
to spell out the exact files and APIs expected.
|
|
427
|
+
- If the failure logs show an environment issue (missing tool,
|
|
428
|
+
permission), DON'T retry the same way. Either change the
|
|
429
|
+
verify_command, change the description, or split out a
|
|
430
|
+
"set up environment X" preceding feature.
|
|
431
|
+
|
|
432
|
+
Output the same JSON schema as a normal forge bootstrap:
|
|
433
|
+
{{
|
|
434
|
+
"reasoning": "what you changed and why, 1-2 sentences",
|
|
435
|
+
"features": [
|
|
436
|
+
{{
|
|
437
|
+
"title": "short imperative",
|
|
438
|
+
"description": "what 'done' looks like, 1 paragraph",
|
|
439
|
+
"acceptance_criteria": "bullets",
|
|
440
|
+
"priority": 0,
|
|
441
|
+
"category": "functional",
|
|
442
|
+
"depends_on": [],
|
|
443
|
+
"verify_command": "shell command that exits 0 on success or null"
|
|
444
|
+
}}
|
|
445
|
+
]
|
|
446
|
+
}}
|
|
447
|
+
|
|
448
|
+
Hard rule: produce 1-5 features. Use `python3` not `python`.
|
|
449
|
+
If you really cannot improve the feature, output exactly one
|
|
450
|
+
feature identical to the input — the orchestrator will park it.
|
|
451
|
+
"""
|
planning/task_graph.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Task Graph — DAG-based task dependency management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from core.schemas import Task, TaskStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskGraph:
|
|
11
|
+
"""Directed Acyclic Graph of tasks with dependency tracking."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, tasks: list[Task] | None = None):
|
|
14
|
+
self._tasks: dict[str, Task] = {}
|
|
15
|
+
if tasks:
|
|
16
|
+
for task in tasks:
|
|
17
|
+
self._tasks[task.id] = task
|
|
18
|
+
|
|
19
|
+
def add_task(self, task: Task) -> None:
|
|
20
|
+
self._tasks[task.id] = task
|
|
21
|
+
|
|
22
|
+
def get_task(self, task_id: str) -> Task | None:
|
|
23
|
+
return self._tasks.get(task_id)
|
|
24
|
+
|
|
25
|
+
def get_ready_tasks(self) -> list[Task]:
|
|
26
|
+
"""Return tasks whose dependencies are all completed."""
|
|
27
|
+
completed = {tid for tid, t in self._tasks.items() if t.status == TaskStatus.COMPLETED}
|
|
28
|
+
return [
|
|
29
|
+
t for t in self._tasks.values()
|
|
30
|
+
if t.status == TaskStatus.PENDING
|
|
31
|
+
and all(dep in completed for dep in t.dependencies)
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def mark_completed(self, task_id: str, result: Any = None) -> None:
|
|
35
|
+
if task_id in self._tasks:
|
|
36
|
+
self._tasks[task_id].status = TaskStatus.COMPLETED
|
|
37
|
+
self._tasks[task_id].result = result
|
|
38
|
+
|
|
39
|
+
def mark_failed(self, task_id: str, error: str) -> None:
|
|
40
|
+
if task_id in self._tasks:
|
|
41
|
+
self._tasks[task_id].status = TaskStatus.FAILED
|
|
42
|
+
self._tasks[task_id].error = error
|
|
43
|
+
|
|
44
|
+
def is_complete(self) -> bool:
|
|
45
|
+
return all(t.status == TaskStatus.COMPLETED for t in self._tasks.values())
|
|
46
|
+
|
|
47
|
+
def has_failed(self) -> bool:
|
|
48
|
+
return any(t.status == TaskStatus.FAILED for t in self._tasks.values())
|
|
49
|
+
|
|
50
|
+
def failed_tasks(self) -> list[Task]:
|
|
51
|
+
return [t for t in self._tasks.values() if t.status == TaskStatus.FAILED]
|
|
52
|
+
|
|
53
|
+
def all_tasks(self) -> list[Task]:
|
|
54
|
+
return list(self._tasks.values())
|
|
55
|
+
|
|
56
|
+
def summary(self) -> str:
|
|
57
|
+
lines = []
|
|
58
|
+
for t in self._tasks.values():
|
|
59
|
+
deps = f" (deps: {t.dependencies})" if t.dependencies else ""
|
|
60
|
+
lines.append(f" [{t.status.value}] {t.description}{deps}")
|
|
61
|
+
return "\n".join(lines)
|
reflection/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Meta-Learner — adapts strategies based on reflection history (self-improvement)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from config import STRATEGY_UPDATE_INTERVAL
|
|
11
|
+
from core.schemas import Reflection, Strategy
|
|
12
|
+
from llm.provider import LLMProvider
|
|
13
|
+
from memory.procedural import ProceduralMemory
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NewStrategyOutput(BaseModel):
|
|
19
|
+
name: str
|
|
20
|
+
description: str
|
|
21
|
+
conditions: str
|
|
22
|
+
actions: str
|
|
23
|
+
confidence: float = Field(default=0.5, ge=0.0, le=1.0)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StrategyUpdateOutput(BaseModel):
|
|
27
|
+
id: str
|
|
28
|
+
confidence_delta: float = Field(default=0.0, ge=-1.0, le=1.0)
|
|
29
|
+
reason: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StrategyOutput(BaseModel):
|
|
33
|
+
"""LLM-emitted strategy extraction result."""
|
|
34
|
+
new_strategies: list[NewStrategyOutput] = Field(default_factory=list)
|
|
35
|
+
strategy_updates: list[StrategyUpdateOutput] = Field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
STRATEGY_PROMPT = """You are the meta-learning module of a cognitive architecture.
|
|
38
|
+
Analyze recent performance and extract or update strategies.
|
|
39
|
+
|
|
40
|
+
Recent reflections:
|
|
41
|
+
{reflections}
|
|
42
|
+
|
|
43
|
+
Existing strategies:
|
|
44
|
+
{strategies}
|
|
45
|
+
|
|
46
|
+
Based on the patterns in these reflections, suggest new strategies or updates.
|
|
47
|
+
Each strategy should capture a reusable heuristic.
|
|
48
|
+
|
|
49
|
+
Respond with JSON:
|
|
50
|
+
{{
|
|
51
|
+
"new_strategies": [
|
|
52
|
+
{{
|
|
53
|
+
"name": "short name",
|
|
54
|
+
"description": "what this strategy does",
|
|
55
|
+
"conditions": "when to apply it",
|
|
56
|
+
"actions": "what to do",
|
|
57
|
+
"confidence": 0.0 to 1.0
|
|
58
|
+
}}
|
|
59
|
+
],
|
|
60
|
+
"strategy_updates": [
|
|
61
|
+
{{
|
|
62
|
+
"id": "existing strategy id",
|
|
63
|
+
"confidence_delta": -0.1 to 0.1,
|
|
64
|
+
"reason": "why update"
|
|
65
|
+
}}
|
|
66
|
+
]
|
|
67
|
+
}}"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MetaLearner:
|
|
71
|
+
"""Learns from reflection history to adapt agent strategies over time."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, llm: LLMProvider, procedural_memory: ProceduralMemory):
|
|
74
|
+
self.llm = llm
|
|
75
|
+
self.procedural = procedural_memory
|
|
76
|
+
self._reflection_buffer: list[Reflection] = []
|
|
77
|
+
|
|
78
|
+
def add_reflection(self, reflection: Reflection) -> None:
|
|
79
|
+
"""Buffer a reflection for periodic strategy updates."""
|
|
80
|
+
self._reflection_buffer.append(reflection)
|
|
81
|
+
|
|
82
|
+
async def maybe_update_strategies(self) -> list[Strategy]:
|
|
83
|
+
"""Update strategies if enough reflections have accumulated."""
|
|
84
|
+
if len(self._reflection_buffer) < STRATEGY_UPDATE_INTERVAL:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
new_strategies = await self._extract_strategies()
|
|
88
|
+
self._reflection_buffer.clear()
|
|
89
|
+
return new_strategies
|
|
90
|
+
|
|
91
|
+
async def force_update(self) -> list[Strategy]:
|
|
92
|
+
"""Force a strategy update regardless of buffer size."""
|
|
93
|
+
if not self._reflection_buffer:
|
|
94
|
+
return []
|
|
95
|
+
new_strategies = await self._extract_strategies()
|
|
96
|
+
self._reflection_buffer.clear()
|
|
97
|
+
return new_strategies
|
|
98
|
+
|
|
99
|
+
async def _extract_strategies(self) -> list[Strategy]:
|
|
100
|
+
"""Use LLM to extract strategies from reflection buffer."""
|
|
101
|
+
reflections_text = "\n".join(
|
|
102
|
+
f"- Score: {r.score}, Lesson: {r.lesson}, Worked: {r.what_worked}, Failed: {r.what_failed}"
|
|
103
|
+
for r in self._reflection_buffer
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
existing = self.procedural.get_strategies()
|
|
107
|
+
strategies_text = "\n".join(
|
|
108
|
+
f"- [{s.id}] {s.name} (confidence: {s.confidence:.2f}): {s.description}"
|
|
109
|
+
for s in existing
|
|
110
|
+
) or "No existing strategies."
|
|
111
|
+
|
|
112
|
+
prompt = STRATEGY_PROMPT.format(
|
|
113
|
+
reflections=reflections_text,
|
|
114
|
+
strategies=strategies_text,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
data = await self.llm.structured_output(
|
|
119
|
+
prompt=prompt, response_model=StrategyOutput,
|
|
120
|
+
caller="meta",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
new_strategies = []
|
|
124
|
+
for raw in data.new_strategies:
|
|
125
|
+
strategy = Strategy(
|
|
126
|
+
name=raw.name,
|
|
127
|
+
description=raw.description,
|
|
128
|
+
conditions=raw.conditions,
|
|
129
|
+
actions=raw.actions,
|
|
130
|
+
confidence=raw.confidence,
|
|
131
|
+
)
|
|
132
|
+
self.procedural.store_strategy(strategy)
|
|
133
|
+
new_strategies.append(strategy)
|
|
134
|
+
logger.info(f"Learned new strategy: {strategy.name}")
|
|
135
|
+
|
|
136
|
+
# Apply updates to existing strategies
|
|
137
|
+
for update in data.strategy_updates:
|
|
138
|
+
for s in existing:
|
|
139
|
+
if s.id == update.id:
|
|
140
|
+
s.confidence = max(0.0, min(1.0, s.confidence + update.confidence_delta))
|
|
141
|
+
self.procedural.store_strategy(s)
|
|
142
|
+
logger.info(f"Updated strategy {s.name}: confidence {s.confidence:.2f}")
|
|
143
|
+
|
|
144
|
+
return new_strategies
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Meta-learning failed: {e}")
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
def get_applicable_strategies(self, context: str) -> list[Strategy]:
|
|
151
|
+
"""Get strategies that might apply to the current context.
|
|
152
|
+
|
|
153
|
+
For now, returns all strategies above minimum confidence.
|
|
154
|
+
Future: use embedding similarity on conditions.
|
|
155
|
+
"""
|
|
156
|
+
return self.procedural.get_strategies(min_confidence=0.3)
|