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
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Bootstrapper chat-session routes — back-and-forth before committing.
|
|
2
|
+
|
|
3
|
+
Mirrors LocalForge's
|
|
4
|
+
GET/POST /api/projects/:id/bootstrapper-session
|
|
5
|
+
GET/POST /api/agent-sessions/:id/messages
|
|
6
|
+
POST /api/agent-sessions/:id/generate-features
|
|
7
|
+
POST /api/agent-sessions/:id/close
|
|
8
|
+
|
|
9
|
+
In Cognos, we reuse:
|
|
10
|
+
- ``forge_sessions`` rows where ``session_type="bootstrapper"``
|
|
11
|
+
- ``forge_chat_messages`` for the conversation
|
|
12
|
+
- ``LLMProvider.stream`` for the SSE response
|
|
13
|
+
- ``ForgeFeatureTools`` for agent-driven feature creation in
|
|
14
|
+
/generate-features (the agent already has these tools registered;
|
|
15
|
+
we just bind the project context with ``forge_project_scope``).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from typing import Any, AsyncIterator
|
|
25
|
+
|
|
26
|
+
from fastapi import APIRouter, HTTPException
|
|
27
|
+
from fastapi.responses import StreamingResponse
|
|
28
|
+
from pydantic import BaseModel
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Steering prompt for the conversational stage.
|
|
34
|
+
BOOTSTRAPPER_CHAT_SYSTEM_PROMPT = (
|
|
35
|
+
"You are Cognos Forge's AI bootstrapper. You help the user describe an "
|
|
36
|
+
"app they want to build. You are friendly, concise, and speak in 2-4 "
|
|
37
|
+
"sentences per reply. Ask one or two follow-up questions at a time "
|
|
38
|
+
"about the app's users, core features, data, and UI. When the user "
|
|
39
|
+
"seems satisfied with the plan, say so and invite them to click "
|
|
40
|
+
"'Generate feature list' to commit. Never produce code, markdown "
|
|
41
|
+
"tables, or long bullet walls — keep replies conversational."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Steering prompt for the structured-output commit stage.
|
|
45
|
+
FEATURE_GEN_SYSTEM_PROMPT = (
|
|
46
|
+
"You are Cognos Forge's feature-generation agent.\n\n"
|
|
47
|
+
"You are given a user/assistant chat describing an app. Your job is "
|
|
48
|
+
"to turn that conversation into a complete backlog of 6-15 features "
|
|
49
|
+
"by calling the provided forge_* tools.\n\n"
|
|
50
|
+
"Workflow:\n"
|
|
51
|
+
"1. Call forge_list_features to see what (if anything) is already "
|
|
52
|
+
"in the backlog.\n"
|
|
53
|
+
"2. Plan the build in dependency order: foundational work first "
|
|
54
|
+
"(schema, app shell, core data model), then behaviour, then polish.\n"
|
|
55
|
+
"3. Call forge_create_feature for each item, in order. Pass "
|
|
56
|
+
"depends_on with the ids of earlier features that must complete "
|
|
57
|
+
"first. Depends_on must ONLY reference features that already exist.\n"
|
|
58
|
+
"4. Make sure the backlog covers persistence, UI, and at least one "
|
|
59
|
+
"polish/style feature.\n"
|
|
60
|
+
"5. When you are done creating features, STOP calling tools and "
|
|
61
|
+
"reply with a single short sentence summarising how many features "
|
|
62
|
+
"you created.\n\n"
|
|
63
|
+
"Rules:\n"
|
|
64
|
+
"- Title: short imperative sentence under 100 characters.\n"
|
|
65
|
+
"- Description: one paragraph describing what 'done' looks like.\n"
|
|
66
|
+
"- category: 'functional' or 'style' (default functional).\n\n"
|
|
67
|
+
"Do NOT output code, markdown fences, or JSON in your replies — "
|
|
68
|
+
"all structured output goes through the tools."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ChatMessageIn(BaseModel):
|
|
73
|
+
content: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CloseSessionIn(BaseModel):
|
|
77
|
+
status: str | None = None # completed | failed | terminated
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ─────────────────────────── Helpers ────────────────────────────────
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _lazy():
|
|
84
|
+
from planning import forge_models as fm
|
|
85
|
+
return fm
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_active_bootstrapper(project_id: int) -> dict[str, Any] | None:
|
|
89
|
+
fm = _lazy()
|
|
90
|
+
with fm.session_scope() as sess:
|
|
91
|
+
row = (
|
|
92
|
+
sess.query(fm.ForgeSession)
|
|
93
|
+
.filter_by(
|
|
94
|
+
project_id=project_id,
|
|
95
|
+
session_type="bootstrapper",
|
|
96
|
+
status="in_progress",
|
|
97
|
+
)
|
|
98
|
+
.order_by(fm.ForgeSession.id.desc())
|
|
99
|
+
.first()
|
|
100
|
+
)
|
|
101
|
+
if row is None:
|
|
102
|
+
return None
|
|
103
|
+
return _session_dict(row)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _session_dict(row) -> dict[str, Any]:
|
|
107
|
+
return {
|
|
108
|
+
"id": row.id,
|
|
109
|
+
"project_id": row.project_id,
|
|
110
|
+
"feature_id": row.feature_id,
|
|
111
|
+
"session_type": row.session_type,
|
|
112
|
+
"status": row.status,
|
|
113
|
+
"started_at": row.started_at.isoformat() if row.started_at else None,
|
|
114
|
+
"ended_at": row.ended_at.isoformat() if row.ended_at else None,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _list_messages(session_id: int) -> list[dict[str, Any]]:
|
|
119
|
+
fm = _lazy()
|
|
120
|
+
with fm.session_scope() as sess:
|
|
121
|
+
rows = (
|
|
122
|
+
sess.query(fm.ForgeChatMessage)
|
|
123
|
+
.filter_by(session_id=session_id)
|
|
124
|
+
.order_by(fm.ForgeChatMessage.id.asc())
|
|
125
|
+
.all()
|
|
126
|
+
)
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
"id": r.id,
|
|
130
|
+
"session_id": r.session_id,
|
|
131
|
+
"role": r.role,
|
|
132
|
+
"content": r.content,
|
|
133
|
+
"created_at": r.created_at.isoformat() if r.created_at else None,
|
|
134
|
+
}
|
|
135
|
+
for r in rows
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _append_message(
|
|
140
|
+
session_id: int, role: str, content: str,
|
|
141
|
+
) -> dict[str, Any]:
|
|
142
|
+
fm = _lazy()
|
|
143
|
+
with fm.session_scope() as sess:
|
|
144
|
+
row = fm.ForgeChatMessage(
|
|
145
|
+
session_id=session_id, role=role, content=content,
|
|
146
|
+
)
|
|
147
|
+
sess.add(row)
|
|
148
|
+
sess.flush()
|
|
149
|
+
return {
|
|
150
|
+
"id": row.id, "session_id": row.session_id,
|
|
151
|
+
"role": row.role, "content": row.content,
|
|
152
|
+
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _close_session(session_id: int, status: str) -> dict[str, Any] | None:
|
|
157
|
+
fm = _lazy()
|
|
158
|
+
valid = {"completed", "failed", "terminated"}
|
|
159
|
+
if status not in valid:
|
|
160
|
+
status = "completed"
|
|
161
|
+
with fm.session_scope() as sess:
|
|
162
|
+
row = sess.get(fm.ForgeSession, session_id)
|
|
163
|
+
if row is None:
|
|
164
|
+
return None
|
|
165
|
+
if row.status == "in_progress":
|
|
166
|
+
row.status = status
|
|
167
|
+
row.ended_at = datetime.utcnow()
|
|
168
|
+
return _session_dict(row)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ─────────────────────────── Router ─────────────────────────────────
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_router() -> APIRouter:
|
|
175
|
+
router = APIRouter(prefix="/forge", tags=["forge-bootstrapper"])
|
|
176
|
+
|
|
177
|
+
# ── Session create / fetch ──────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
@router.get("/projects/{project_id}/bootstrapper-session")
|
|
180
|
+
async def get_bootstrapper(project_id: int) -> dict[str, Any]:
|
|
181
|
+
fm = _lazy()
|
|
182
|
+
if fm.get_project(project_id) is None:
|
|
183
|
+
raise HTTPException(404, "project not found")
|
|
184
|
+
return {"session": _get_active_bootstrapper(project_id)}
|
|
185
|
+
|
|
186
|
+
@router.post("/projects/{project_id}/bootstrapper-session")
|
|
187
|
+
async def post_bootstrapper(project_id: int) -> dict[str, Any]:
|
|
188
|
+
fm = _lazy()
|
|
189
|
+
if fm.get_project(project_id) is None:
|
|
190
|
+
raise HTTPException(404, "project not found")
|
|
191
|
+
existing = _get_active_bootstrapper(project_id)
|
|
192
|
+
if existing is not None:
|
|
193
|
+
return {"session": existing, "reused": True}
|
|
194
|
+
with fm.session_scope() as sess:
|
|
195
|
+
row = fm.ForgeSession(
|
|
196
|
+
project_id=project_id,
|
|
197
|
+
session_type="bootstrapper",
|
|
198
|
+
status="in_progress",
|
|
199
|
+
)
|
|
200
|
+
sess.add(row)
|
|
201
|
+
sess.flush()
|
|
202
|
+
return {"session": _session_dict(row), "reused": False}
|
|
203
|
+
|
|
204
|
+
# ── Messages ────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
@router.get("/agent-sessions/{session_id}/messages")
|
|
207
|
+
async def list_messages(session_id: int) -> dict[str, Any]:
|
|
208
|
+
fm = _lazy()
|
|
209
|
+
with fm.session_scope() as sess:
|
|
210
|
+
row = sess.get(fm.ForgeSession, session_id)
|
|
211
|
+
if row is None:
|
|
212
|
+
raise HTTPException(404, "session not found")
|
|
213
|
+
return {"messages": _list_messages(session_id)}
|
|
214
|
+
|
|
215
|
+
@router.post("/agent-sessions/{session_id}/messages")
|
|
216
|
+
async def post_message(
|
|
217
|
+
session_id: int, body: ChatMessageIn,
|
|
218
|
+
) -> StreamingResponse:
|
|
219
|
+
fm = _lazy()
|
|
220
|
+
with fm.session_scope() as sess:
|
|
221
|
+
srow = sess.get(fm.ForgeSession, session_id)
|
|
222
|
+
if srow is None:
|
|
223
|
+
raise HTTPException(404, "session not found")
|
|
224
|
+
if srow.status != "in_progress":
|
|
225
|
+
raise HTTPException(
|
|
226
|
+
409, "this session is closed; start a new conversation",
|
|
227
|
+
)
|
|
228
|
+
project_id = srow.project_id
|
|
229
|
+
|
|
230
|
+
content = (body.content or "").strip()
|
|
231
|
+
if not content:
|
|
232
|
+
raise HTTPException(400, "field 'content' is required")
|
|
233
|
+
|
|
234
|
+
# Persist user message immediately so it survives downstream errors.
|
|
235
|
+
user_msg = _append_message(session_id, "user", content)
|
|
236
|
+
history = _list_messages(session_id)
|
|
237
|
+
|
|
238
|
+
async def gen() -> AsyncIterator[bytes]:
|
|
239
|
+
yield f"data: {json.dumps({'type': 'user', 'message': user_msg})}\n\n".encode()
|
|
240
|
+
from llm.provider import LLMProvider
|
|
241
|
+
from core.settings import Settings
|
|
242
|
+
from config import SYSTEM_1_MODEL, SYSTEM_2_MODEL, LLM_MODEL
|
|
243
|
+
from core.anthropic_auth import subscription_auth_scope
|
|
244
|
+
|
|
245
|
+
s = Settings.load()
|
|
246
|
+
chosen = (
|
|
247
|
+
s.get("system1") or SYSTEM_1_MODEL
|
|
248
|
+
or s.get("system2") or SYSTEM_2_MODEL
|
|
249
|
+
or LLM_MODEL
|
|
250
|
+
)
|
|
251
|
+
llm = LLMProvider(model=chosen)
|
|
252
|
+
messages = [
|
|
253
|
+
{"role": "system", "content": BOOTSTRAPPER_CHAT_SYSTEM_PROMPT},
|
|
254
|
+
] + [
|
|
255
|
+
{"role": m["role"], "content": m["content"]}
|
|
256
|
+
for m in history
|
|
257
|
+
]
|
|
258
|
+
full = ""
|
|
259
|
+
errored = False
|
|
260
|
+
try:
|
|
261
|
+
with subscription_auth_scope():
|
|
262
|
+
async for ev in llm.stream(messages):
|
|
263
|
+
et = getattr(ev, "type", None)
|
|
264
|
+
if et == "text_delta":
|
|
265
|
+
# core.schemas.StreamEvent uses .delta for text
|
|
266
|
+
delta = (
|
|
267
|
+
getattr(ev, "delta", None)
|
|
268
|
+
or getattr(ev, "text", "")
|
|
269
|
+
or ""
|
|
270
|
+
)
|
|
271
|
+
if delta:
|
|
272
|
+
full += delta
|
|
273
|
+
yield (
|
|
274
|
+
f"data: {json.dumps({'type': 'delta', 'content': delta})}\n\n"
|
|
275
|
+
).encode()
|
|
276
|
+
elif et in ("message_stop", "message_end"):
|
|
277
|
+
break
|
|
278
|
+
except asyncio.CancelledError:
|
|
279
|
+
raise
|
|
280
|
+
except Exception as e:
|
|
281
|
+
errored = True
|
|
282
|
+
yield (
|
|
283
|
+
f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
|
284
|
+
).encode()
|
|
285
|
+
|
|
286
|
+
if not errored:
|
|
287
|
+
assistant_msg = _append_message(
|
|
288
|
+
session_id, "assistant",
|
|
289
|
+
full.strip() or "(empty response)",
|
|
290
|
+
)
|
|
291
|
+
yield (
|
|
292
|
+
f"data: {json.dumps({'type': 'assistant', 'message': assistant_msg})}\n\n"
|
|
293
|
+
).encode()
|
|
294
|
+
yield b'data: {"type": "done"}\n\n'
|
|
295
|
+
|
|
296
|
+
return StreamingResponse(gen(), media_type="text/event-stream")
|
|
297
|
+
|
|
298
|
+
# ── Close session ───────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
@router.post("/agent-sessions/{session_id}/close")
|
|
301
|
+
async def close_session(
|
|
302
|
+
session_id: int, body: CloseSessionIn | None = None,
|
|
303
|
+
) -> dict[str, Any]:
|
|
304
|
+
fm = _lazy()
|
|
305
|
+
with fm.session_scope() as sess:
|
|
306
|
+
row = sess.get(fm.ForgeSession, session_id)
|
|
307
|
+
if row is None:
|
|
308
|
+
raise HTTPException(404, "session not found")
|
|
309
|
+
if row.status != "in_progress":
|
|
310
|
+
return {"session": _session_dict(row), "closed": False}
|
|
311
|
+
status = (body.status if body else None) or "completed"
|
|
312
|
+
updated = _close_session(session_id, status)
|
|
313
|
+
return {"session": updated, "closed": True}
|
|
314
|
+
|
|
315
|
+
# ── Generate features (commit transcript → backlog) ─────────────
|
|
316
|
+
|
|
317
|
+
@router.post("/agent-sessions/{session_id}/generate-features")
|
|
318
|
+
async def generate_features(session_id: int) -> dict[str, Any]:
|
|
319
|
+
fm = _lazy()
|
|
320
|
+
with fm.session_scope() as sess:
|
|
321
|
+
srow = sess.get(fm.ForgeSession, session_id)
|
|
322
|
+
if srow is None:
|
|
323
|
+
raise HTTPException(404, "session not found")
|
|
324
|
+
if srow.session_type != "bootstrapper":
|
|
325
|
+
raise HTTPException(400, "not a bootstrapper session")
|
|
326
|
+
project_id = srow.project_id
|
|
327
|
+
history = _list_messages(session_id)
|
|
328
|
+
if not history:
|
|
329
|
+
raise HTTPException(
|
|
330
|
+
400, "no conversation yet — send a message first",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Prefix the transcript so the agent has the context.
|
|
334
|
+
transcript = "\n\n".join(
|
|
335
|
+
f"{m['role'].upper()}: {m['content']}" for m in history
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Snapshot existing features so we can compute what was created.
|
|
339
|
+
existing_before = len(fm.list_features(project_id))
|
|
340
|
+
|
|
341
|
+
from core.agent import CognosAgent
|
|
342
|
+
from execution.tools.forge_feature_tools import forge_project_scope
|
|
343
|
+
from core.anthropic_auth import subscription_auth_scope
|
|
344
|
+
|
|
345
|
+
prompt = (
|
|
346
|
+
f"{FEATURE_GEN_SYSTEM_PROMPT}\n\n"
|
|
347
|
+
f"The bootstrapper chat transcript follows. Generate the "
|
|
348
|
+
f"backlog now via tool calls.\n\n{transcript}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
agent = CognosAgent()
|
|
353
|
+
with subscription_auth_scope(), forge_project_scope(project_id):
|
|
354
|
+
resp = await agent.chat(prompt, stream=False)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.exception("generate-features agent failed")
|
|
357
|
+
raise HTTPException(502, f"agent failed: {e}")
|
|
358
|
+
|
|
359
|
+
summary = str(getattr(resp, "content", resp) or "")[:500]
|
|
360
|
+
features_after = fm.list_features(project_id)
|
|
361
|
+
created = len(features_after) - existing_before
|
|
362
|
+
if created <= 0:
|
|
363
|
+
raise HTTPException(
|
|
364
|
+
502,
|
|
365
|
+
"agent finished without creating any features. Try again "
|
|
366
|
+
"with a clearer description.",
|
|
367
|
+
)
|
|
368
|
+
# Auto-close the bootstrapper session
|
|
369
|
+
_close_session(session_id, "completed")
|
|
370
|
+
return {
|
|
371
|
+
"count": created,
|
|
372
|
+
"total": len(features_after),
|
|
373
|
+
"project_id": project_id,
|
|
374
|
+
"summary": summary,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return router
|