feat: implement project action tools for enhanced project management capabilities

This commit is contained in:
Roberto Musso
2026-02-24 15:53:32 +01:00
parent 5eb19e022e
commit 7a1aec0d9f
5 changed files with 547 additions and 58 deletions

View File

@@ -442,3 +442,54 @@
- `trpc.projects.get` query with `enabled: !!projectId` + `id: projectId ?? ''` avoids both the non-null assertion lint warning and unnecessary queries
- For scroll-to-user-message UX: track the last user message with a ref and use `scrollIntoView({ behavior: 'smooth', block: 'start' })` — do NOT auto-scroll on AI streaming to let the user read from the top
---
## 2026-02-24 - US-021
- What was implemented:
- 4 project action tools in `src/main/ai/orchestrator.ts` using `@langchain/core/tools` `tool()` helper, via new `buildProjectTools(projectId)` factory function:
- `read_project_notes`: fetches full note content from SQLite (no 500-char truncation unlike buildProjectContext)
- `add_task`: inserts task via `db.insert(tasks).run()`, returns `'Task added: [title]'`
- `get_summary`: calls nested `getLLM().invoke()` to generate 2-3 sentence summary, persists via `db.update(projects).set({ aiSummary })`
- `suggest_checkpoints`: calls nested `getLLM().invoke()` with structured prompt, returns JSON array `[{ title, date }]` with regex extraction fallback
- `classifyIntent` short-circuits: `chatContext.type === 'project' && chatContext.projectId` → immediately returns `{ route: 'project' }` (saves one LLM round-trip, prevents misrouting)
- `projectAgent` rewritten with agent loop (max 5 iterations):
- `supportsTools` runtime guard: `'bindTools' in llm && typeof llm.bindTools === 'function'`
- Copilot path (no bindTools): direct `llm.invoke()` with full context prompt
- OpenAI/Anthropic path: `llm.bindTools!(projectTools)` → agent loop with `AIMessage.isInstance()` type guard for `tool_calls` access
- Tool dispatch via `matched.invoke({ ...toolCall, type: 'tool_call' as const })` — StructuredTool.invoke() detects ToolCall object and extracts args via internal `_isToolCall()` check
- `ToolMessage` appended per tool call with `tool_call_id`; `messageHistory` accumulated across iterations
- `makeProjectAgentPrompt` updated to describe all 4 available tools and usage guidance
- Streaming unaffected: tool-calling rounds produce empty `chunk.content` (falsy), filtered by existing guard; final text response streams normally
- Files changed: `src/main/ai/orchestrator.ts`, `prd.json`, `progress.txt`
- **Learnings for future iterations:**
- `tool()` returns `DynamicStructuredTool<...>` — use `StructuredTool[]` as the function return type and cast with `as StructuredTool[]` to avoid generic type variance errors in strict mode
- `llm.bindTools` is typed as optional on `BaseChatModel` — even after a runtime `typeof === 'function'` guard, TypeScript still reports TS18048 ("possibly undefined"); use `llm.bindTools!()` with an eslint-disable comment after the guard
- `AIMessage.isInstance(response)` is the zero-unsafe-cast way to access `tool_calls` on a `BaseMessage` — avoids `as any`
- LangGraph `streamMode: 'messages'` naturally skips tool-calling rounds because `chunk.content` is `''` (falsy) for AIMessageChunks that have tool call deltas
- Nested `getLLM().invoke()` calls inside tool handlers (for `get_summary`, `suggest_checkpoints`) do NOT stream tokens to the IPC channel — they execute synchronously within the tool handler, outside LangGraph's stream interceptor
- Short-circuiting `classifyIntent` for project context saves cost and prevents misrouting when user asks general questions from within a project view
- Empty Zod schema `z.object({})` infers TypeScript type `{}` — use `Record<string, never>` as the handler parameter type to be explicit about intent in strict mode
---
## 2026-02-24 - US-021 bugfix
- Bug: `<tool_call>` XML appeared in chat and tasks weren't actually created
- Root cause: `ChatCopilot` extends `SimpleChatModel` which inherits `bindTools()` from `BaseChatModel` — so `'bindTools' in llm` returned TRUE. But `ChatCopilot._call()` ignores bound tools (no kwargs plumbing to the Copilot SDK). The model received tool descriptions in the system prompt but NOT via the API, so it hallucinated `<tool_call>{"name":"sql",...}` freeform text. `tool_calls` on the response was empty → tool not executed → fake success text streamed to UI
- Fix: Replaced runtime `'bindTools' in llm` check with provider-name whitelist (`TOOL_CALLING_PROVIDERS = new Set(['openai', 'anthropic'])`). Imported `getActiveProviderName` from `./provider`
- Fix: Copilot fallback path now uses `makeProjectAgentPrompt(contextData, false)` — no tool section in system prompt — preventing hallucinated tool calls text
- Files changed: `src/main/ai/orchestrator.ts`
- **Learnings for future iterations:**
- `BaseChatModel.bindTools()` exists as a default inherited method in modern LangChain — `'bindTools' in llm` is ALWAYS true. You CANNOT use this to detect actual tool calling support; must know the provider
- The safe check is provider-name based: only 'openai' and 'anthropic' have real bindTools support in this codebase
- When a model receives tool descriptions IN THE SYSTEM PROMPT but NOT via the API tool calling mechanism, it may hallucinate tool calls as freeform text (especially models trained on ReAct/ToolBench data)
- Remove tool mentions from system prompt when NOT using API-level tool calling
---
## 2026-02-24 - US-021 classifyIntent short-circuit bugfix
- Bug: After US-021, GitHub Copilot chat broke entirely with "Failed to list models" SDK error
- Root cause: The `classifyIntent` short-circuit (added in US-021 for project context) removed the FIRST LLM call from the graph. The Copilot SDK requires at least one prior `sendAndWait()` call to initialize its internal model list cache before a subsequent call succeeds. Without `classifyIntent`'s LLM invocation acting as warm-up, the cold `projectAgent` call triggered `runAgenticLoop → listModels` which failed
- Fix: Removed the short-circuit from `classifyIntent` entirely. The node always calls the LLM for routing (matching pre-US-021 behavior). The `metadata.langgraph_node !== 'classifyIntent'` check in the streaming loop already prevents the routing token from appearing in chat
- Files changed: `src/main/ai/orchestrator.ts`
- **Learnings for future iterations:**
- The Copilot SDK needs a "warm-up" LLM call before it can successfully process the main request. Never eliminate the first LLM call in the graph when Copilot is the provider
- Short-circuit optimizations that skip LLM nodes are only safe for providers where the SDK has no internal state to initialize (OpenAI, Anthropic)
- If you want to restore the short-circuit as an OpenAI/Anthropic optimization, gate it: `if (TOOL_CALLING_PROVIDERS.has(getActiveProviderName()) && state.chatContext.type === 'project')`
---