feat: implement project action tools for enhanced project management capabilities
This commit is contained in:
51
progress.txt
51
progress.txt
@@ -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')`
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user