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.
Files changed (153) hide show
  1. api/__init__.py +5 -0
  2. api/anthropic_compat.py +1518 -0
  3. api/artifact_viewer.py +366 -0
  4. api/caudate_middleware.py +618 -0
  5. api/forge_bootstrapper_routes.py +377 -0
  6. api/forge_routes.py +630 -0
  7. api/forge_system_routes.py +294 -0
  8. api/openai_compat.py +1993 -0
  9. api/server.py +667 -0
  10. api/storyboard_page.py +677 -0
  11. caudate_cli-0.1.0.dist-info/METADATA +354 -0
  12. caudate_cli-0.1.0.dist-info/RECORD +153 -0
  13. caudate_cli-0.1.0.dist-info/WHEEL +5 -0
  14. caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
  15. caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  16. caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
  17. cognos_mcp/__init__.py +4 -0
  18. cognos_mcp/bridge.py +41 -0
  19. cognos_mcp/client.py +70 -0
  20. cognos_mcp/config.py +49 -0
  21. cognos_mcp/server.py +66 -0
  22. config.py +82 -0
  23. core/__init__.py +0 -0
  24. core/agent.py +468 -0
  25. core/agentic_loop.py +731 -0
  26. core/anthropic_auth.py +91 -0
  27. core/background.py +113 -0
  28. core/banner.py +134 -0
  29. core/bootstrap.py +292 -0
  30. core/citations.py +131 -0
  31. core/compaction.py +109 -0
  32. core/constitution.py +198 -0
  33. core/diff_viewer.py +87 -0
  34. core/export.py +85 -0
  35. core/file_refs.py +119 -0
  36. core/files.py +199 -0
  37. core/hooks.py +209 -0
  38. core/image.py +599 -0
  39. core/input.py +91 -0
  40. core/loop.py +238 -0
  41. core/memory_md.py +147 -0
  42. core/notifications.py +99 -0
  43. core/ownership.py +181 -0
  44. core/paste.py +81 -0
  45. core/permissions.py +210 -0
  46. core/plan_mode.py +215 -0
  47. core/sandbox_prompt.py +185 -0
  48. core/scheduler.py +195 -0
  49. core/schemas.py +202 -0
  50. core/session.py +90 -0
  51. core/settings.py +132 -0
  52. core/skills.py +398 -0
  53. core/slash_commands.py +977 -0
  54. core/statusline.py +61 -0
  55. core/subagent.py +300 -0
  56. core/thinking.py +50 -0
  57. core/updater.py +122 -0
  58. core/usage.py +109 -0
  59. core/worktree.py +93 -0
  60. execution/__init__.py +0 -0
  61. execution/executor.py +329 -0
  62. execution/plugins.py +108 -0
  63. execution/tools/__init__.py +0 -0
  64. execution/tools/agent_tool.py +107 -0
  65. execution/tools/agentic_tool.py +297 -0
  66. execution/tools/artifact_tool.py +191 -0
  67. execution/tools/ask_user_question_tool.py +137 -0
  68. execution/tools/base.py +81 -0
  69. execution/tools/calculator_tool.py +137 -0
  70. execution/tools/cognos_card_tool.py +124 -0
  71. execution/tools/cron_tool.py +215 -0
  72. execution/tools/datetime_tool.py +215 -0
  73. execution/tools/describe_image_tool.py +161 -0
  74. execution/tools/draw_tool.py +164 -0
  75. execution/tools/edit_image_tool.py +262 -0
  76. execution/tools/edit_tool.py +245 -0
  77. execution/tools/file_tool.py +90 -0
  78. execution/tools/find_anywhere_tool.py +255 -0
  79. execution/tools/forge_feature_tools.py +377 -0
  80. execution/tools/glob_tool.py +59 -0
  81. execution/tools/grep_tool.py +89 -0
  82. execution/tools/http_request_tool.py +224 -0
  83. execution/tools/load_skill_tool.py +104 -0
  84. execution/tools/longcat_avatar_tool.py +384 -0
  85. execution/tools/mcp_tool.py +100 -0
  86. execution/tools/notebook_tool.py +279 -0
  87. execution/tools/openapi_tool.py +440 -0
  88. execution/tools/plan_mode_tool.py +95 -0
  89. execution/tools/push_notification_tool.py +157 -0
  90. execution/tools/python_tool.py +61 -0
  91. execution/tools/respond_tool.py +40 -0
  92. execution/tools/sandbox_tool.py +378 -0
  93. execution/tools/search_tool.py +153 -0
  94. execution/tools/semantic_search_tool.py +106 -0
  95. execution/tools/shell_tool.py +283 -0
  96. execution/tools/speak_tool.py +134 -0
  97. execution/tools/storyboard_tool.py +727 -0
  98. execution/tools/system_info_tool.py +212 -0
  99. execution/tools/task_tool.py +323 -0
  100. execution/tools/think_tool.py +49 -0
  101. execution/tools/transcribe_audio_tool.py +86 -0
  102. execution/tools/update_memory_tool.py +92 -0
  103. execution/tools/web_fetch_tool.py +82 -0
  104. execution/tools/worktree_tool.py +174 -0
  105. llm/__init__.py +0 -0
  106. llm/fallback.py +116 -0
  107. llm/models.py +320 -0
  108. llm/provider.py +1356 -0
  109. llm/router.py +373 -0
  110. main.py +1889 -0
  111. memory/__init__.py +0 -0
  112. memory/episodic.py +99 -0
  113. memory/procedural.py +145 -0
  114. memory/semantic.py +71 -0
  115. memory/working.py +64 -0
  116. nn/__init__.py +43 -0
  117. nn/auto_evolve.py +245 -0
  118. nn/caudate.py +136 -0
  119. nn/config.py +141 -0
  120. nn/consolidator.py +81 -0
  121. nn/data.py +1635 -0
  122. nn/encoder.py +258 -0
  123. nn/forge_advisor.py +303 -0
  124. nn/format.py +235 -0
  125. nn/heads.py +432 -0
  126. nn/observer.py +994 -0
  127. nn/policy.py +214 -0
  128. nn/runtime.py +343 -0
  129. nn/scorer.py +175 -0
  130. nn/trainer.py +515 -0
  131. nn/vision.py +352 -0
  132. personality/__init__.py +23 -0
  133. personality/engine.py +129 -0
  134. personality/identity.py +144 -0
  135. personality/inner_voice.py +100 -0
  136. personality/mood.py +205 -0
  137. planning/__init__.py +0 -0
  138. planning/dev_server.py +221 -0
  139. planning/forge_models.py +718 -0
  140. planning/orchestrator.py +1363 -0
  141. planning/planner.py +451 -0
  142. planning/task_graph.py +61 -0
  143. reflection/__init__.py +0 -0
  144. reflection/meta_learner.py +156 -0
  145. reflection/reflector.py +127 -0
  146. ui/__init__.py +5 -0
  147. ui/display.py +88 -0
  148. voice/__init__.py +0 -0
  149. voice/conversation.py +125 -0
  150. voice/listener.py +111 -0
  151. voice/speaker.py +59 -0
  152. voice/stt.py +126 -0
  153. 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)