diff --git a/.agent-core/artifact/bugfix_plan.md b/.agent-core/artifact/bugfix_plan.md new file mode 100644 index 00000000000..804c1ebd194 --- /dev/null +++ b/.agent-core/artifact/bugfix_plan.md @@ -0,0 +1,174 @@ +# Comprehensive Bug Fix Implementation Plan + +**Date:** 2026-01-12 +**Scope:** MCP Reconnection, Type Safety, Code Patterns + +--- + +## Part 1: MCP Server Reconnection After Daemon Restart + +### Problem Analysis + +When the daemon restarts: + +1. MCP client connections in the daemon are lost +2. TUI continues to display "Connection closed" status +3. No automatic reconnection mechanism exists +4. User must manually restart TUI to re-establish MCP connections + +### Root Causes + +1. MCP state is stored per-Instance in `Instance.state()` (line 156) +2. When daemon restarts, Instance state is recreated fresh +3. MCP clients show "connected" initially but fail when actually used +4. The `tools()` function (line 542) catches errors and marks as failed, but only AFTER a tool call fails + +### Solution Design + +1. **Add health check for MCP connections** - Ping MCP servers before reporting "connected" +2. **Add reconnection logic** - Attempt reconnect when connection fails +3. **Add retry with exponential backoff** - Graceful reconnection +4. **Expose reconnect API** - Allow TUI/API to trigger reconnection + +### Implementation Steps + +#### Step 1.1: Add connection health check function + +```typescript +// In MCP namespace +export async function isHealthy(name: string): Promise +``` + +#### Step 1.2: Add reconnect function + +```typescript +export async function reconnect(name: string): Promise +``` + +#### Step 1.3: Add automatic reconnection on tool failure + +When `tools()` catches an error, attempt reconnect before fully failing. + +#### Step 1.4: Add periodic health check (optional) + +Background interval to check MCP health and reconnect if needed. + +--- + +## Part 2: Fix Type Safety Issues (`as any`) + +### Problem Analysis + +35+ occurrences of `as any` throughout the codebase bypass TypeScript's type checking: + +- Potential runtime errors not caught at compile time +- Makes refactoring harder +- Reduces IDE assistance + +### High-Priority Files + +1. `src/provider/provider.ts` - 2 occurrences +2. `src/provider/transform.ts` - 1 occurrence +3. `src/session/prompt.ts` - 2 occurrences +4. `src/session/processor.ts` - 1 occurrence + +### Solution Strategy + +For each `as any`: + +1. **Determine if it's necessary** - Some are valid workarounds for external lib types +2. **Add proper type definitions** - Most can be fixed with correct types +3. **Use type guards** - For runtime type checking +4. **Add `@ts-expect-error` with comments** - For external lib workarounds + +### Implementation Steps + +#### Step 2.1: Fix provider.ts Auth.get type mismatch + +```typescript +// Current: +const options = await plugin.auth.loader(() => Auth.get(providerID) as any, ...) + +// Fix: Define proper return type for loader callback +``` + +#### Step 2.2: Fix transform.ts providerOptions access + +```typescript +// Current: +...(msg.providerOptions as any)?.openaiCompatible + +// Fix: Define proper type for providerOptions +``` + +#### Step 2.3: Fix session/prompt.ts schema handling + +```typescript +// Current: +inputSchema: jsonSchema(schema as any) + +// Fix: Use proper JSONSchema type +``` + +#### Step 2.4: Fix session/processor.ts error handling + +```typescript +// Current: +error: (value.error as any).toString() + +// Fix: Add type guard for error +``` + +--- + +## Part 3: Additional Code Pattern Issues + +### 3.1: Missing unref() on intervals + +**Location:** Some setInterval calls don't use .unref() +**Impact:** May prevent process from exiting cleanly +**Fix:** Add .unref() to background intervals that shouldn't keep process alive + +### 3.2: Unsafe object property access + +**Pattern:** `obj[key]` where key could be undefined +**Fix:** Add null checks or use optional chaining + +### 3.3: Silent error handling + +**Pattern:** `.catch(() => {})` or `catch (e) {}` +**Fix:** At minimum log errors, or only catch specific expected errors + +### 3.4: Race conditions in async operations + +**Pattern:** State modifications without proper locking +**Fix:** Add state guards or use atomic operations + +--- + +## Execution Order + +1. **Part 1: MCP Reconnection** (High priority - user-visible bug) + - 1.1 Add health check + - 1.2 Add reconnect function + - 1.3 Add auto-reconnect on failure +2. **Part 2: Type Safety** (Medium priority - code quality) + - 2.1 Fix provider.ts + - 2.2 Fix transform.ts + - 2.3 Fix prompt.ts + - 2.4 Fix processor.ts + +3. **Part 3: Code Patterns** (Lower priority - code quality) + - 3.1 Add missing unref() + - 3.2 Review and document remaining as any uses + +--- + +## Testing Plan + +After implementation: + +1. Run `bun run build` to verify compilation +2. Run `./scripts/reload.sh` to restart daemon +3. Verify MCP reconnection works by restarting daemon while TUI is running +4. Run existing tests if available diff --git a/.agent-core/tool/canvas.ts b/.agent-core/tool/canvas.ts new file mode 100644 index 00000000000..e97385dd609 --- /dev/null +++ b/.agent-core/tool/canvas.ts @@ -0,0 +1,152 @@ +/** + * Canvas Tools - WezTerm-native canvas rendering + * + * Provides canvas display capabilities for all personas. + * Canvas types: text, calendar, document, table, diagram, graph, mindmap + */ + +import { tool } from "@opencode-ai/plugin" + +// Canvas spawn tool +export const canvasSpawn = tool({ + description: `Spawn a canvas to display content in a WezTerm pane. + +Canvas types: +- text: Simple text display with title and content +- calendar: Monthly calendar view with events +- document: Markdown-like document rendering +- table: Tabular data display +- diagram: Flowchart/architecture diagrams +- graph: Nodes and edges visualization +- mindmap: Hierarchical tree view + +Config options by kind: +- text: { title: string, content: string } +- calendar: { date?: "YYYY-MM-DD", events?: [{ date: string, title: string }] } +- document: { title: string, content: string (markdown) } +- table: { title: string, headers: string[], rows: string[][] } + +Examples: +- Display poem: { kind: "text", id: "poem", config: '{"title": "My Poem", "content": "Roses are red..."}' } +- Show calendar: { kind: "calendar", id: "cal", config: '{"date": "2026-01-15", "events": []}' } +- Show table: { kind: "table", id: "data", config: '{"title": "Portfolio", "headers": ["Symbol", "Value"], "rows": [["AAPL", "$100"]]}' }`, + args: { + kind: tool.schema + .enum(["text", "calendar", "document", "table", "diagram", "graph", "mindmap"]) + .describe("Canvas type"), + id: tool.schema.string().describe("Unique canvas identifier"), + config: tool.schema.string().describe("JSON configuration for the canvas content"), + }, + async execute(args) { + const { requestDaemon } = await import("../../../src/daemon/ipc-client.js") + + let config: Record + try { + config = JSON.parse(args.config) + } catch { + return `Invalid JSON config: ${args.config}` + } + + try { + const result = await requestDaemon<{ paneId: string; id: string; kind: string }>( + "canvas:spawn", + { kind: args.kind, id: args.id, config } + ) + return `Canvas "${args.id}" (${args.kind}) displayed in pane ${result.paneId}. + +Content: +${JSON.stringify(config, null, 2)}` + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + return `Failed to spawn canvas: ${msg} + +Note: Canvas requires the agent-core daemon to be running. +Start it with: agent-core daemon` + } + }, +}) + +// Canvas update tool +export const canvasUpdate = tool({ + description: `Update an existing canvas's content. + +Examples: +- Update text: { id: "poem", config: '{"content": "New poem content"}' } +- Update calendar: { id: "cal", config: '{"events": [{"date": "2026-01-20", "title": "Meeting"}]}' }`, + args: { + id: tool.schema.string().describe("Canvas identifier to update"), + config: tool.schema.string().describe("New JSON configuration"), + }, + async execute(args) { + const { requestDaemon } = await import("../../../src/daemon/ipc-client.js") + + let config: Record + try { + config = JSON.parse(args.config) + } catch { + return `Invalid JSON config: ${args.config}` + } + + try { + await requestDaemon("canvas:update", { id: args.id, config }) + return `Canvas "${args.id}" updated. + +New content: +${JSON.stringify(config, null, 2)}` + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + return `Failed to update canvas: ${msg}` + } + }, +}) + +// Canvas close tool +export const canvasClose = tool({ + description: `Close a canvas pane.`, + args: { + id: tool.schema.string().describe("Canvas identifier to close"), + }, + async execute(args) { + const { requestDaemon } = await import("../../../src/daemon/ipc-client.js") + + try { + await requestDaemon("canvas:close", { id: args.id }) + return `Canvas "${args.id}" closed.` + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + return `Failed to close canvas: ${msg}` + } + }, +}) + +// Canvas list tool +export const canvasList = tool({ + description: `List all active canvases.`, + args: {}, + async execute() { + const { requestDaemon } = await import("../../../src/daemon/ipc-client.js") + + try { + const canvases = await requestDaemon< + Array<{ + id: string + kind: string + paneId: string + createdAt: number + }> + >("canvas:list", {}) + + if (canvases.length === 0) { + return "No active canvases." + } + + const list = canvases.map((c) => `- ${c.id} (${c.kind}) in pane ${c.paneId}`).join("\n") + + return `${canvases.length} active canvas(es): +${list}` + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + return `Failed to list canvases: ${msg}` + } + }, +}) diff --git a/CLAUDE.md b/CLAUDE.md index b4c6792b5be..fb4b7c50343 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,12 +1,12 @@ # Agent-Core - The Engine -This is the **engine** that powers Artur's Agent System. agent-core is a fork of OpenCode with custom personas. +This is the **engine** that powers Agent-Core, a fork of OpenCode. ## CRITICAL: Naming Convention **NEVER use "opencode" in new code, documentation, or user-facing text.** -This project is `agent-core`, not OpenCode. While we're grateful to the original OpenCode maintainers, users must be able to run both projects without confusion: +This project is `agent-core`, not OpenCode. We are grateful to the OpenCode developers and believe users must be able to run both projects without confusion: - CLI command: `agent-core` (not `opencode`) - Config directory: `~/.config/agent-core/` @@ -20,15 +20,15 @@ Existing upstream code may still contain "opencode" references - that's fine. Bu **ALWAYS read these before making changes:** -1. **Tiara** (`vendor/tiara/`) - The orchestration submodule with claude-flow +1. **Tiara** (`vendor/tiara/`) - The orchestration submodule - `vendor/tiara/CLAUDE.md` - SPARC methodology, concurrent execution rules - `vendor/tiara/docs/` - Architecture, integrations, roadmaps 2. **The Triad** (`.claude/skills/`) - The three personas: - - `.claude/skills/zee/SKILL.md` - Personal assistant (memory, messaging, calendar) - - `.claude/skills/stanley/SKILL.md` - Investing system (markets, portfolio, NautilusTrader) - - `.claude/skills/johny/SKILL.md` - Learning system (knowledge graph, spaced repetition) - + - `.claude/skills/zee/SKILL.md` - Personal assistant (memory, messaging, calendar, and more) + - `.claude/skills/stanley/SKILL.md` - Investing assistant with access to a full platform (NautilusTrader, OpenBB, own GUI in rust) of APIS integration + - `.claude/skills/johny/SKILL.md` - Study assistant focused on diliberate practice, with knowledge graph and spaced repetition + - Each persona has its own configuration and capabilities, all have access to Tiara's orchestration offers 3. **Shared capabilities** (`.claude/skills/shared/`, `.claude/skills/personas/`) - Orchestration, WezTerm integration, drone spawning @@ -63,24 +63,28 @@ You are part of the **Personas** - three AI personas that share a common orchest ### Personas Capabilities (ALL Personas Have These) **1. Spawn Drones** - You can spawn background workers (drones) that: + - Maintain your persona identity (a "Zee drone" acts like Zee) - Execute tasks in parallel while you continue the conversation - Report results back to you - Run in separate WezTerm panes for visibility **2. Shared Memory** - All personas share: + - Qdrant vector memory for semantic search - Conversation continuity state (survives compacting) - Plan and objectives across sessions - Key facts extracted from conversations **3. Conversation Continuity** - When context gets compacted: + - A summary is saved to Qdrant automatically - Key facts are extracted and preserved - Plan/objectives persist across sessions - You can restore context from previous sessions **4. WezTerm Pane Management** - Visual orchestration: + - Each drone gets its own pane - Status pane shows Personas state - You can see what all workers are doing @@ -88,6 +92,7 @@ You are part of the **Personas** - three AI personas that share a common orchest ### How to Use Personas Capabilities **Spawning a Drone:** + ``` When you need to do heavy background work, you can spawn a drone: 1. Decide what task needs background processing @@ -97,6 +102,7 @@ When you need to do heavy background work, you can spawn a drone: ``` **Preserving Continuity:** + ``` Before context is compacted: 1. Summarize the conversation state @@ -106,6 +112,7 @@ Before context is compacted: ``` **Checking State:** + ``` You can always check: - What drones are running @@ -176,14 +183,6 @@ You can always check: No generic "build" or "plan" agents. Every interaction goes through a persona with domain expertise. The personas share orchestration (tiara) and memory (Qdrant) but have distinct purposes. -## Personas - -| Persona | Inspiration | Domain | Skills Location | -|---------|-------------|--------|-----------------| -| **Johny** | von Neumann | Study, learning | `.claude/skills/johny/` | -| **Stanley** | Druckenmiller | Trading, markets | `.claude/skills/stanley/` | -| **Zee** | Personal | Memory, messaging | `.claude/skills/zee/` | - ## Key Directories ``` @@ -208,6 +207,7 @@ agent-core/ ## Integration Skills are loaded from `.claude/skills/` and `~/.config/agent-core/skills/`: + ``` .claude/skills/johny/ → Johny persona .claude/skills/stanley/ → Stanley persona @@ -226,6 +226,7 @@ Skills are loaded from `.claude/skills/` and `~/.config/agent-core/skills/`: ## Experimental Features This system has experimental features enabled: + - LLM Council for multi-model deliberation - Knowledge graph with FIRe (Fractional Implicit Repetition) - Semantic memory via Qdrant @@ -233,27 +234,53 @@ This system has experimental features enabled: ## State Management -| Data | Location | -|------|----------| -| Johny profile | `~/.zee/johny/profile.json` | +| Data | Location | +| ----------------- | ------------------------------- | +| Johny profile | `~/.zee/johny/profile.json` | | Stanley portfolio | `~/.zee/stanley/portfolio.json` | -| Zee memories | `~/.zee/zee/memories.json` | -| Credentials | `~/.zee/credentials/` | +| Zee memories | `~/.zee/zee/memories.json` | +| Credentials | `~/.zee/credentials/` | ## Running Processes & Binary Updates +> ⚠️ **CRITICAL: Read `docs/OPS.md` before debugging fixes that "don't take effect"** +> +> The #1 cause of confusion is **not knowing which binary is running**: +> +> - **Dev mode** (`bun run dev`): Uses source files directly, restart takes effect +> - **Production** (`~/bin/agent-core`): Uses compiled binary, must rebuild + copy + restart +> +> Use `./scripts/reload.sh --status` to see what's running and if source is newer than binary. + ### Repository Location + **Source code:** `~/.local/src/agent-core` This hidden location prevents accidentally running from the repo directory (which can cause config confusion). ### Binary Location + **Installed binary:** `~/bin/agent-core` (also `$AGENT_CORE_BIN`) **Run from anywhere:** The binary can be launched from any directory. Just `cd` to your project folder and run `agent-core`. ### Rebuilding +**Recommended:** Use the reload script (handles kill, build, copy, restart): + +```bash +# Full reload - kill all, rebuild, restart daemon +./scripts/reload.sh + +# Just check status (what's running, version info) +./scripts/reload.sh --status + +# Restart without rebuilding (config changes only) +./scripts/reload.sh --no-build +``` + +**Manual method** (if script unavailable): + ```bash # Build from repo cd ~/.local/src/agent-core/packages/agent-core && bun run build @@ -264,6 +291,7 @@ cp ~/.local/src/agent-core/packages/agent-core/dist/agent-core-linux-x64/bin/age ``` ### Common Processes + When updating the binary, you may encounter "Text file busy". Check for: ```bash @@ -323,6 +351,7 @@ pgrep -af agent-core ``` **Key Points:** + - **Zee Gateway** = Transport layer only (handles WhatsApp/Telegram/Signal connections) - **agent-core daemon** = All agent logic, personas, memory, tools - **Persona routing** = Messages mentioning `@stanley` or `@johny` are routed to those personas @@ -331,11 +360,13 @@ pgrep -af agent-core ### Running the External Gateway Architecture 1. **Start agent-core daemon with external gateway mode:** + ```bash agent-core daemon --external-gateway --hostname 127.0.0.1 --port 3210 ``` 2. **Start zee gateway:** + ```bash cd ~/Repositories/personas/zee && pnpm zee gateway ``` @@ -348,6 +379,7 @@ pgrep -af agent-core ### Architecture Decision Built-in messaging gateways have been **removed** from agent-core to: + 1. Avoid duplication with zee gateway (ClawdBot fork) 2. Keep agent-core clean for upstream OpenCode 3. Centralize messaging transport in one place diff --git a/bun.lock b/bun.lock index f90fb742e8a..5b208d7047f 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/agent-core": { "name": "agent-core", - "version": "0.1.20250112", + "version": "0.1.20260112", "bin": { "agent-core": "./bin/agent-core", }, @@ -121,7 +121,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -169,7 +169,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -198,7 +198,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -225,7 +225,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -249,7 +249,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -273,7 +273,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -302,7 +302,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -331,7 +331,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -347,7 +347,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -367,7 +367,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.1.20250112", + "version": "0.1.20260112", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -378,7 +378,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -391,7 +391,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -430,7 +430,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "zod": "catalog:", }, @@ -441,7 +441,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "0.1.20250112", + "version": "0.1.20260112", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/docs/BUGS.md b/docs/BUGS.md new file mode 100644 index 00000000000..2d02f38ca3a --- /dev/null +++ b/docs/BUGS.md @@ -0,0 +1,92 @@ +# Agent-Core Bug Tracker + +> Last Updated: 2026-01-12 + +## Fixed in This Session + +### 1. ✅ Silent Error Swallowing in Cache Cleanup + +- **File:** `src/global/index.ts:74` +- **Issue:** Empty `catch (e) {}` block silently swallowed all errors +- **Fix:** Now only ignores ENOENT (expected), logs other errors + +### 2. ✅ EventEmitter Memory Leak Warning + +- **File:** `src/bus/global.ts` +- **Issue:** GlobalBus had default 10 max listeners, could trigger memory leak warning with many SSE connections +- **Fix:** Added `GlobalBus.setMaxListeners(100)` + +### 3. ✅ topP Parameter Error for Claude Thinking Models + +- **File:** `src/session/llm.ts:189-195` +- **Issue:** Undefined topP was being passed to provider APIs, causing errors for Claude thinking models +- **Fix:** Changed to spread-only-if-defined pattern: `...(params.topP !== undefined && { topP: params.topP })` + +### 4. ✅ MCP Servers Don't Reconnect After Daemon Restart + +- **File:** `src/mcp/index.ts` +- **Issue:** When daemon restarts, MCP connections were lost with no recovery +- **Fix:** Added: + - `isHealthy(name)` - Check if MCP connection is still alive + - `reconnect(name)` - Reconnect a single failed MCP server + - `reconnectAll()` - Reconnect all failed MCP servers + - `healthCheckAndReconnect()` - Health check and reconnect all + - Auto-reconnect on tool fetch failure +- **File:** `src/server/server.ts` +- **Fix:** Added API endpoints: + - `POST /mcp/:name/reconnect` - Reconnect single server + - `POST /mcp/reconnect-all` - Reconnect all failed + - `POST /mcp/health-check` - Health check and reconnect + +### 5. ✅ Type Safety Issues Fixed + +- **File:** `src/session/processor.ts:234` + - **Issue:** `(value.error as any).toString()` + - **Fix:** `value.error instanceof Error ? value.error.message : String(value.error)` +- **File:** `src/provider/transform.ts:125` + - **Issue:** `(msg.providerOptions as any)?.openaiCompatible` + - **Fix:** Type-safe access with `Record` and `typeof` check +- **File:** `src/session/prompt.ts:690,692` + - **Issue:** `as any` for AI SDK type compatibility + - **Fix:** Added explanatory comments documenting why assertions are needed + +### 6. ✅ Debug Status Command Type Errors + +- **File:** `src/cli/cmd/debug/status.ts` +- **Issue:** Incorrect access to `config.provider?.default` and `config.model?.default` +- **Fix:** Correctly parse `config.model` string (format: `provider/model`) + +## Known Issues (Not Fixed Yet) + +### 1. 🟡 Type Assertions (`as any`) Remaining + +- **Count:** ~30 occurrences (reduced from 35+) +- **Risk:** Type safety bypassed in some places +- **Note:** Many are necessary for AI SDK compatibility and now documented + +### 2. 🟡 setInterval Without .unref() in Some Places + +- **File:** `src/cli/cmd/debug/errors.ts:154` +- **Issue:** `setInterval(checkForNewErrors, 1000)` doesn't call `.unref()` +- **Impact:** Low (only affects `debug errors --follow`, which runs indefinitely by design) + +## Testing Notes + +To verify fixes: + +```bash +# Type check +bun run typecheck + +# Rebuild and restart +./scripts/reload.sh + +# Check status +./scripts/reload.sh --status + +# Or via CLI +agent-core debug status -v + +# Test MCP reconnection +curl -X POST http://127.0.0.1:3210/mcp/health-check +``` diff --git a/docs/OPS.md b/docs/OPS.md new file mode 100644 index 00000000000..2f28f7b2c0d --- /dev/null +++ b/docs/OPS.md @@ -0,0 +1,243 @@ +# Agent-Core Operations Guide + +> **CRITICAL LESSON LEARNED (2026-01-12):** The biggest source of confusion when debugging is **not knowing which binary is running**. Fixes made to source code won't take effect if: +> +> 1. You're running `bun run dev` (dev mode) instead of the compiled binary +> 2. The daemon is still running an old version +> 3. The TUI was started before the binary was updated + +## Quick Reference + +| Command | Purpose | +| -------------------------------- | ------------------------------------ | +| `./scripts/reload.sh` | Full rebuild, restart daemon, verify | +| `./scripts/reload.sh --status` | Show what's running and diagnostics | +| `./scripts/reload.sh --no-build` | Restart without rebuild | +| `agent-core debug status` | CLI diagnostics (after install) | + +## The Two Execution Modes + +### 1. Development Mode (`bun run dev`) + +```bash +cd ~/.local/src/agent-core/packages/agent-core +bun run dev --print-logs +``` + +**Characteristics:** + +- Runs directly from TypeScript source +- Changes take effect on restart (no build needed) +- Process shows as: `bun run dev --print-logs` or `bun run --conditions=browser ./src/index.ts` +- Useful for rapid iteration + +**How to identify:** + +```bash +pgrep -af "bun.*print-logs" +``` + +### 2. Production Mode (Compiled Binary) + +```bash +~/bin/agent-core --print-logs # TUI +~/bin/agent-core daemon --port 3210 # Daemon +``` + +**Characteristics:** + +- Runs from compiled binary at `~/bin/agent-core` +- Requires rebuild (`bun run build`) and copy to take effect +- Process shows as: `/home/artur/bin/agent-core` +- What gets deployed and used in production + +**How to identify:** + +```bash +pgrep -af "/bin/agent-core" +``` + +## Why Fixes "Don't Take Effect" + +### Root Cause Analysis + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ WHY FIXES DON'T TAKE EFFECT │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ You edit: ~/.local/src/agent-core/packages/agent-core/src/foo.ts │ +│ │ +│ BUT your TUI is running: │ +│ │ +│ CASE A: bun run dev (dev mode) │ +│ ├── Process: bun run --conditions=browser ./src/index.ts │ +│ ├── Uses: Source files directly │ +│ └── Fix: Just restart the TUI │ +│ │ +│ CASE B: ~/bin/agent-core (compiled binary) │ +│ ├── Process: /home/artur/bin/agent-core │ +│ ├── Uses: Bundled code from WHEN IT WAS BUILT │ +│ └── Fix: Must rebuild, copy, then restart │ +│ │ +│ CASE C: Daemon is separate │ +│ ├── TUI connects to daemon via HTTP │ +│ ├── Daemon runs its own bundled code │ +│ └── Fix: Must restart daemon too │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Common Mistakes + +| Mistake | Why It Happens | Fix | +| -------------------------------------- | ------------------------------------- | ----------------------------------- | +| Edit source, but TUI uses old code | Running compiled binary, not dev mode | Run `./scripts/reload.sh` | +| Kill daemon but fixes still don't work | TUI has its own embedded code | Kill TUI too, restart everything | +| Binary says "Text file busy" | Process still using the file | Kill ALL agent-core processes first | +| Version mismatch TUI vs daemon | Started at different times | Restart both from same build | + +## The Reload Script + +Located at: `~/.local/src/agent-core/scripts/reload.sh` + +### What It Does + +1. **Kills ALL agent-core processes** (daemon, TUI binary, AND dev mode) +2. **Rebuilds** from source (unless `--no-build`) +3. **Copies** new binary to `~/bin/agent-core` +4. **Starts daemon** (unless `--no-daemon`) +5. **Verifies** everything is working + +### Usage + +```bash +# Full reload (recommended after code changes) +./scripts/reload.sh + +# Just check status +./scripts/reload.sh --status + +# Restart without rebuilding (for config changes only) +./scripts/reload.sh --no-build + +# Rebuild but don't start daemon +./scripts/reload.sh --no-daemon +``` + +### Status Output Explained + +``` +═══════════════════════════════════════════════════════════════ + AGENT-CORE STATUS +═══════════════════════════════════════════════════════════════ + +Binary: /home/artur/bin/agent-core +[ OK ] Exists (modified: 2026-01-12 20:17:35) ← When binary was last updated + +Processes: +[ OK ] Daemon: PID 2454325 ← Daemon running +[ OK ] TUI (dev): PID 656637 ← Dev mode TUI (bun run dev) +[ OK ] TUI: PID 123456 ← Binary TUI + +Daemon API: http://127.0.0.1:3210 +[ OK ] Healthy (version: 0.0.0-main-202601121917) ← Daemon version + +Tool directories: +[ OK ] /home/artur/.config/agent-core/tool (1 tools) + - canvas.ts ← Custom tools loaded + +Source timestamps: +[ OK ] transform.ts (19:05:23) ← Source file modification times +[ WARN ] llm.ts (19:17:00) - NEWER than binary, rebuild needed! ← Source newer than binary! +``` + +## Process Hierarchy + +``` +When running bun run dev: + + shell + └── bun run dev --print-logs (PID: 656637) + └── bun run ./src/index.ts (PID: 656638, child) + +When running compiled binary: + + shell + └── /home/artur/bin/agent-core --print-logs (PID: 123456) + +Daemon (always compiled binary): + + nohup + └── /home/artur/bin/agent-core daemon (PID: 234567) + └── (gateway subprocess if --gateway) (PID: 234568) +``` + +## Debugging Checklist + +When a fix doesn't take effect, check in order: + +- [ ] **1. Which mode am I running?** + + ```bash + pgrep -af "bun.*print-logs" # Dev mode + pgrep -af "/bin/agent-core" # Compiled binary + ``` + +- [ ] **2. What version is the daemon?** + + ```bash + curl -s http://127.0.0.1:3210/global/health | jq .version + ``` + +- [ ] **3. When was the binary built?** + + ```bash + ls -la ~/bin/agent-core + ``` + +- [ ] **4. When was the source file modified?** + + ```bash + ls -la ~/.local/src/agent-core/packages/agent-core/src/path/to/file.ts + ``` + +- [ ] **5. Is source newer than binary?** + + ```bash + ./scripts/reload.sh --status # Shows warnings for newer source files + ``` + +- [ ] **6. Nuclear option - kill everything and restart** + ```bash + ./scripts/reload.sh + ``` + +## Location Reference + +| What | Path | +| ----------------- | -------------------------------------------------------------------------------------- | +| Source repository | `~/.local/src/agent-core` | +| Package source | `~/.local/src/agent-core/packages/agent-core/src/` | +| Compiled binary | `~/bin/agent-core` | +| Build output | `~/.local/src/agent-core/packages/agent-core/dist/agent-core-linux-x64/bin/agent-core` | +| Reload script | `~/.local/src/agent-core/scripts/reload.sh` | +| Config directory | `~/.config/agent-core/` | +| Custom tools | `~/.config/agent-core/tool/` | +| Daemon logs | `/tmp/agent-core-daemon.log` | + +## MCP Servers Note + +MCP servers (memory, calendar, portfolio) connect when the **daemon** starts. If they show "Connection closed": + +1. The daemon was restarted and MCP connections were lost +2. The TUI still has stale connection handles +3. **Fix:** Restart the TUI after restarting the daemon + +## Version Strings + +Version format: `0.0.0-main-YYYYMMDDHHMM` + +- Built from git commit at build time +- Can identify exactly when binary was built +- Compare TUI version vs daemon version to spot mismatches diff --git a/packages/agent-core/src/bus/global.ts b/packages/agent-core/src/bus/global.ts index 43386dd6b20..6f0d1d26db4 100644 --- a/packages/agent-core/src/bus/global.ts +++ b/packages/agent-core/src/bus/global.ts @@ -8,3 +8,7 @@ export const GlobalBus = new EventEmitter<{ }, ] }>() + +// Prevent memory leak warning during long daemon sessions with many SSE connections +GlobalBus.setMaxListeners(100) + diff --git a/packages/agent-core/src/cli/cmd/debug/index.ts b/packages/agent-core/src/cli/cmd/debug/index.ts index 05232d731d4..1b3f60275f7 100644 --- a/packages/agent-core/src/cli/cmd/debug/index.ts +++ b/packages/agent-core/src/cli/cmd/debug/index.ts @@ -12,6 +12,7 @@ import { RipgrepCommand } from "./ripgrep" import { ScrapCommand } from "./scrap" import { SkillCommand } from "./skill" import { SnapshotCommand } from "./snapshot" +import { StatusCommand } from "./status" import { TasksCommand } from "./tasks" import { AgentCommand } from "./agent" @@ -33,6 +34,7 @@ export const DebugCommand = cmd({ .command(SnapshotCommand) .command(TasksCommand) .command(AgentCommand) + .command(StatusCommand) .command(PathsCommand) .command(FlagsCommand) .command({ diff --git a/packages/agent-core/src/cli/cmd/debug/status.ts b/packages/agent-core/src/cli/cmd/debug/status.ts new file mode 100644 index 00000000000..1d794cf5e96 --- /dev/null +++ b/packages/agent-core/src/cli/cmd/debug/status.ts @@ -0,0 +1,352 @@ +import { cmd } from "../cmd" +import { bootstrap } from "../../bootstrap" +import { Installation } from "../../../installation" +import { Config } from "../../../config/config" +import { Global } from "../../../global" +import { Flag } from "../../../flag/flag" +import fs from "fs/promises" +import path from "path" + +export const StatusCommand = cmd({ + command: "status", + describe: "show agent-core system status and diagnostics", + builder: (yargs) => + yargs + .option("json", { + type: "boolean", + default: false, + describe: "output as JSON", + }) + .option("verbose", { + alias: "v", + type: "boolean", + default: false, + describe: "show verbose details", + }), + async handler(args) { + await bootstrap(process.cwd(), async () => { + const status = await collectStatus(args.verbose) + + if (args.json) { + console.log(JSON.stringify(status, null, 2)) + return + } + + printStatus(status, args.verbose) + }) + }, +}) + +interface SystemStatus { + version: string + binary: { + path: string + exists: boolean + modifiedAt?: string + modifiedTs?: number + } + daemon: { + running: boolean + pid?: number + port?: number + version?: string + healthy?: boolean + error?: string + } + processes: Array<{ + pid: number + type: string + cmd: string + }> + tools: { + directories: string[] + loaded: string[] + } + config: { + directories: string[] + provider?: string + model?: string + } + sources: Array<{ + file: string + modifiedAt: string + modifiedTs: number + newerThanBinary: boolean + }> + issues: string[] +} + +async function collectStatus(verbose: boolean): Promise { + const status: SystemStatus = { + version: Installation.VERSION, + binary: { + path: process.execPath, + exists: false, + }, + daemon: { + running: false, + }, + processes: [], + tools: { + directories: [], + loaded: [], + }, + config: { + directories: [], + }, + sources: [], + issues: [], + } + + // Binary info + const binaryPath = path.join(process.env.HOME || "", "bin", "agent-core") + try { + const stat = await fs.stat(binaryPath) + status.binary = { + path: binaryPath, + exists: true, + modifiedAt: stat.mtime.toISOString(), + modifiedTs: stat.mtime.getTime(), + } + } catch { + status.binary = { path: binaryPath, exists: false } + status.issues.push("Binary not found at ~/bin/agent-core") + } + + // Check for running processes + try { + const { execSync } = await import("child_process") + const psOutput = execSync("pgrep -af agent-core 2>/dev/null || true", { encoding: "utf-8" }) + const lines = psOutput.trim().split("\n").filter(Boolean) + + for (const line of lines) { + const match = line.match(/^(\d+)\s+(.*)$/) + if (!match) continue + const pid = parseInt(match[1]) + const cmd = match[2] + + // Skip ourselves and grep + if (cmd.includes("pgrep") || cmd.includes("status")) continue + + let type = "unknown" + if (cmd.includes("daemon")) type = "daemon" + else if (cmd.includes("print-logs") || cmd.match(/\/bin\/agent-core$/)) type = "tui" + + status.processes.push({ pid, type, cmd }) + + if (type === "daemon") { + status.daemon.running = true + status.daemon.pid = pid + } + } + } catch { + // Ignore process check errors + } + + // Daemon health check + const daemonPort = parseInt(process.env.AGENT_CORE_PORT || "3210") + const daemonHost = process.env.AGENT_CORE_HOST || "127.0.0.1" + status.daemon.port = daemonPort + + try { + const response = await fetch(`http://${daemonHost}:${daemonPort}/global/health`, { + signal: AbortSignal.timeout(2000), + }) + if (response.ok) { + const health = (await response.json()) as { healthy: boolean; version: string } + status.daemon.healthy = health.healthy + status.daemon.version = health.version + + // Check version mismatch + if (health.version !== Installation.VERSION) { + status.issues.push( + `Daemon version (${health.version}) differs from binary (${Installation.VERSION}) - restart needed`, + ) + } + } + } catch (e) { + status.daemon.healthy = false + status.daemon.error = e instanceof Error ? e.message : String(e) + if (status.daemon.running) { + status.issues.push("Daemon process running but health check failed") + } + } + + // Tool directories + const configDirs = await Config.directories() + status.config.directories = configDirs + + for (const dir of configDirs) { + const toolDir = path.join(dir, "tool") + try { + const files = await fs.readdir(toolDir) + const tsFiles = files.filter((f) => f.endsWith(".ts") || f.endsWith(".js")) + if (tsFiles.length > 0) { + status.tools.directories.push(toolDir) + status.tools.loaded.push(...tsFiles.map((f) => path.join(toolDir, f))) + } + } catch { + // Tool directory doesn't exist, that's fine + } + } + + // Check source file timestamps (for rebuild detection) + if (verbose && status.binary.modifiedTs) { + const srcRoot = path.join(process.env.HOME || "", ".local", "src", "agent-core", "packages", "agent-core", "src") + const keyFiles = ["provider/transform.ts", "provider/provider.ts", "server/server.ts"] + + for (const file of keyFiles) { + const fullPath = path.join(srcRoot, file) + try { + const stat = await fs.stat(fullPath) + const newerThanBinary = stat.mtime.getTime() > status.binary.modifiedTs! + status.sources.push({ + file, + modifiedAt: stat.mtime.toISOString(), + modifiedTs: stat.mtime.getTime(), + newerThanBinary, + }) + + if (newerThanBinary) { + status.issues.push(`Source ${file} is newer than binary - rebuild needed`) + } + } catch { + // File doesn't exist + } + } + } + + // Config info + try { + const config = await Config.get() + // config.model is a string in format "provider/model", e.g. "anthropic/claude-2" + if (config.model) { + const [providerPart, modelPart] = config.model.split("/") + status.config.provider = providerPart + status.config.model = modelPart || config.model + } + } catch { + // Ignore config errors + } + + return status +} + +function printStatus(status: SystemStatus, verbose: boolean) { + const GREEN = "\x1b[32m" + const RED = "\x1b[31m" + const YELLOW = "\x1b[33m" + const BLUE = "\x1b[34m" + const DIM = "\x1b[2m" + const RESET = "\x1b[0m" + + const ok = (s: string) => `${GREEN}✓${RESET} ${s}` + const err = (s: string) => `${RED}✗${RESET} ${s}` + const warn = (s: string) => `${YELLOW}!${RESET} ${s}` + + console.log("") + console.log("═══════════════════════════════════════════════════════════════") + console.log(" AGENT-CORE STATUS") + console.log("═══════════════════════════════════════════════════════════════") + console.log("") + + // Version + console.log(`${BLUE}Version:${RESET} ${status.version}`) + console.log("") + + // Binary + console.log(`${BLUE}Binary:${RESET}`) + if (status.binary.exists) { + console.log(` ${ok(`${status.binary.path}`)}`) + if (status.binary.modifiedAt) { + console.log(` ${DIM}Modified: ${new Date(status.binary.modifiedAt).toLocaleString()}${RESET}`) + } + } else { + console.log(` ${err("Not found at " + status.binary.path)}`) + } + console.log("") + + // Processes + console.log(`${BLUE}Processes:${RESET}`) + if (status.processes.length === 0) { + console.log(` ${warn("No agent-core processes running")}`) + } else { + for (const proc of status.processes) { + const typeLabel = proc.type.charAt(0).toUpperCase() + proc.type.slice(1) + console.log(` ${ok(`${typeLabel}: PID ${proc.pid}`)}`) + if (verbose) { + console.log(` ${DIM}${proc.cmd}${RESET}`) + } + } + } + console.log("") + + // Daemon + console.log(`${BLUE}Daemon:${RESET}`) + console.log(` Port: ${status.daemon.port}`) + if (status.daemon.healthy) { + console.log(` ${ok(`Healthy (version: ${status.daemon.version})`)}`) + } else if (status.daemon.running) { + console.log(` ${warn("Process running but not healthy")}`) + if (status.daemon.error) { + console.log(` ${DIM}Error: ${status.daemon.error}${RESET}`) + } + } else { + console.log(` ${err("Not running")}`) + } + console.log("") + + // Tools + console.log(`${BLUE}Tools:${RESET}`) + if (status.tools.directories.length === 0) { + console.log(` ${warn("No tool directories found")}`) + } else { + for (const dir of status.tools.directories) { + const tools = status.tools.loaded.filter((t) => t.startsWith(dir)) + console.log(` ${ok(dir)} (${tools.length} tools)`) + if (verbose) { + for (const tool of tools) { + console.log(` ${DIM}- ${path.basename(tool)}${RESET}`) + } + } + } + } + console.log("") + + // Source timestamps (verbose only) + if (verbose && status.sources.length > 0) { + console.log(`${BLUE}Sources:${RESET}`) + for (const src of status.sources) { + const modified = new Date(src.modifiedAt).toLocaleTimeString() + if (src.newerThanBinary) { + console.log(` ${warn(`${src.file} (${modified}) - NEWER than binary`)}`) + } else { + console.log(` ${ok(`${src.file} (${modified})`)}`) + } + } + console.log("") + } + + // Issues + if (status.issues.length > 0) { + console.log(`${BLUE}Issues:${RESET}`) + for (const issue of status.issues) { + console.log(` ${err(issue)}`) + } + console.log("") + } + + console.log("═══════════════════════════════════════════════════════════════") + + // Quick fix suggestions + if (status.issues.length > 0) { + console.log("") + console.log(`${BLUE}Quick fixes:${RESET}`) + if (status.issues.some((i) => i.includes("rebuild"))) { + console.log(" Rebuild: ~/.local/src/agent-core/scripts/reload.sh") + } + if (status.issues.some((i) => i.includes("restart"))) { + console.log(" Restart: ~/.local/src/agent-core/scripts/reload.sh --no-build") + } + } +} diff --git a/packages/agent-core/src/global/index.ts b/packages/agent-core/src/global/index.ts index 32223d16a21..eb275405fc7 100644 --- a/packages/agent-core/src/global/index.ts +++ b/packages/agent-core/src/global/index.ts @@ -71,6 +71,12 @@ if (version !== CACHE_VERSION) { }), ), ) - } catch (e) {} + } catch (e) { + // Ignore ENOENT (cache dir doesn't exist) - expected on first run + // Log other errors for debugging + if ((e as NodeJS.ErrnoException).code !== "ENOENT") { + console.error("[agent-core] Cache cleanup failed:", e) + } + } await Bun.file(path.join(Global.Path.cache, "version")).write(CACHE_VERSION) } diff --git a/packages/agent-core/src/mcp/index.ts b/packages/agent-core/src/mcp/index.ts index aca0c663152..9c4602c93e1 100644 --- a/packages/agent-core/src/mcp/index.ts +++ b/packages/agent-core/src/mcp/index.ts @@ -539,6 +539,129 @@ export namespace MCP { s.status[name] = { status: "disabled" } } + /** + * Check if an MCP server connection is healthy by attempting to list tools. + * Returns true if connected and responsive, false otherwise. + */ + export async function isHealthy(name: string): Promise { + const s = await state() + const client = s.clients[name] + + if (!client) { + return false + } + + if (s.status[name]?.status !== "connected") { + return false + } + + try { + // Attempt a simple operation to verify connection is alive + await withTimeout(client.listTools(), 5000) + return true + } catch (e) { + log.warn("MCP health check failed", { name, error: e instanceof Error ? e.message : String(e) }) + return false + } + } + + /** + * Reconnect to an MCP server that has failed or disconnected. + * Returns the new status after reconnection attempt. + */ + export async function reconnect(name: string): Promise { + const cfg = await Config.get() + const mcpConfig = cfg.mcp?.[name] + + if (!mcpConfig) { + log.error("MCP config not found for reconnect", { name }) + return { status: "failed", error: "MCP config not found" } + } + + if (!isMcpConfigured(mcpConfig)) { + log.error("MCP config invalid for reconnect", { name }) + return { status: "failed", error: "Invalid MCP configuration" } + } + + // Close existing client if any + const s = await state() + const existingClient = s.clients[name] + if (existingClient) { + await existingClient.close().catch((error) => { + log.debug("Failed to close existing MCP client during reconnect", { name, error }) + }) + delete s.clients[name] + } + + log.info("Attempting MCP reconnection", { name }) + + // Create new connection + const result = await create(name, { ...mcpConfig, enabled: true }) + + if (!result) { + s.status[name] = { status: "failed", error: "Unknown error during reconnection" } + return s.status[name] + } + + s.status[name] = result.status + if (result.mcpClient) { + s.clients[name] = result.mcpClient + log.info("MCP reconnection successful", { name }) + } else { + log.warn("MCP reconnection failed", { name, status: result.status }) + } + + return result.status + } + + /** + * Attempt to reconnect all failed MCP servers. + * Returns a map of server names to their new statuses. + */ + export async function reconnectAll(): Promise> { + const s = await state() + const results: Record = {} + + for (const [name, currentStatus] of Object.entries(s.status)) { + if (currentStatus.status === "failed") { + results[name] = await reconnect(name) + } else { + results[name] = currentStatus + } + } + + return results + } + + /** + * Check health of all connected MCPs and reconnect any that have failed. + * This can be called periodically or after daemon restart. + */ + export async function healthCheckAndReconnect(): Promise> { + const s = await state() + const results: Record = {} + + for (const [name, currentStatus] of Object.entries(s.status)) { + if (currentStatus.status === "connected") { + // Check if still healthy + const healthy = await isHealthy(name) + if (!healthy) { + log.warn("MCP connection unhealthy, attempting reconnect", { name }) + results[name] = await reconnect(name) + } else { + results[name] = currentStatus + } + } else if (currentStatus.status === "failed") { + // Attempt to reconnect failed connections + results[name] = await reconnect(name) + } else { + results[name] = currentStatus + } + } + + return results + } + export async function tools() { const result: Record = {} const s = await state() @@ -550,23 +673,39 @@ export namespace MCP { continue } - const toolsResult = await client.listTools().catch((e) => { - log.error("failed to get tools", { clientName, error: e.message }) - const failedStatus = { - status: "failed" as const, - error: e instanceof Error ? e.message : String(e), - } - s.status[clientName] = failedStatus - delete s.clients[clientName] + let toolsResult = await client.listTools().catch((e) => { + log.warn("failed to get tools, will attempt reconnect", { clientName, error: e.message }) return undefined }) + + // If initial fetch failed, attempt reconnection + if (!toolsResult) { + const reconnectStatus = await reconnect(clientName) + if (reconnectStatus.status === "connected") { + // Try again with new client + const newClient = s.clients[clientName] + if (newClient) { + toolsResult = await newClient.listTools().catch((e) => { + log.error("failed to get tools after reconnect", { clientName, error: e.message }) + const failedStatus = { + status: "failed" as const, + error: e instanceof Error ? e.message : String(e), + } + s.status[clientName] = failedStatus + delete s.clients[clientName] + return undefined + }) + } + } + } + if (!toolsResult) { continue } for (const mcpTool of toolsResult.tools) { const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_") const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_") - result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, client) + result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, s.clients[clientName] ?? client) } } return result diff --git a/packages/agent-core/src/provider/provider.ts b/packages/agent-core/src/provider/provider.ts index 07bb63fed1c..f30e7b32974 100644 --- a/packages/agent-core/src/provider/provider.ts +++ b/packages/agent-core/src/provider/provider.ts @@ -847,7 +847,7 @@ export namespace Provider { }, status: "active", headers: {}, - options: { topP: 0.95 }, // Claude thinking requires topP >= 0.95 + options: {}, // topP unset - Claude thinking requires topP >= 0.95 or unset cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, limit: { context: 200000, output: 64000 }, capabilities: { @@ -903,7 +903,7 @@ export namespace Provider { }, status: "active", headers: {}, - options: { topP: 0.95 }, // Claude thinking requires topP >= 0.95 + options: {}, // topP unset - Claude thinking requires topP >= 0.95 or unset cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, limit: { context: 200000, output: 64000 }, capabilities: { diff --git a/packages/agent-core/src/provider/transform.ts b/packages/agent-core/src/provider/transform.ts index 7623ab244bf..f3e4e708ac9 100644 --- a/packages/agent-core/src/provider/transform.ts +++ b/packages/agent-core/src/provider/transform.ts @@ -116,13 +116,15 @@ export namespace ProviderTransform { // Include reasoning_content directly on the message for all assistant messages if (reasoningText) { + // Type assertion: providerOptions is Record but we need to spread existing openaiCompatible options + const existingOptions = (msg.providerOptions as Record)?.openaiCompatible return { ...msg, content: filteredContent, providerOptions: { ...msg.providerOptions, openaiCompatible: { - ...(msg.providerOptions as any)?.openaiCompatible, + ...(typeof existingOptions === "object" ? existingOptions : {}), reasoning_content: reasoningText, }, }, @@ -254,14 +256,14 @@ export namespace ProviderTransform { export function topP(model: Provider.Model) { const id = model.id.toLowerCase() + // Claude thinking models (extended thinking) require topP >= 0.95 OR unset + // Return undefined to leave it unset and let Claude use its default + if (id.includes("claude") && id.includes("thinking")) return undefined if (id.includes("qwen")) return 1 if (id.includes("minimax-m2")) { return 0.95 } if (id.includes("gemini")) return 0.95 - // Claude thinking models (extended thinking) require topP >= 0.95 - // This applies to any provider serving Claude thinking models (Antigravity, Vertex, etc.) - if (id.includes("claude") && id.includes("thinking")) return 0.95 return undefined } diff --git a/packages/agent-core/src/server/server.ts b/packages/agent-core/src/server/server.ts index c88ddc917f3..991e281ab79 100644 --- a/packages/agent-core/src/server/server.ts +++ b/packages/agent-core/src/server/server.ts @@ -2010,10 +2010,18 @@ export namespace Server { c.status(200) c.header("Content-Type", "application/json") return stream(c, async (stream) => { - const sessionID = c.req.valid("param").sessionID - const body = c.req.valid("json") - const msg = await SessionPrompt.prompt({ ...body, sessionID }) - stream.write(JSON.stringify(msg)) + try { + const sessionID = c.req.valid("param").sessionID + const body = c.req.valid("json") + const msg = await SessionPrompt.prompt({ ...body, sessionID }) + stream.write(JSON.stringify(msg)) + } catch (err) { + // Ensure we always write valid JSON even on error + // This prevents "Unexpected end of JSON input" on the client + const errorMsg = err instanceof Error ? err.message : String(err) + Log.Default.error("session.prompt stream error", { error: errorMsg }) + stream.write(JSON.stringify({ error: errorMsg, info: null, parts: [] })) + } }) }, ) @@ -2039,13 +2047,17 @@ export namespace Server { ), validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })), async (c) => { - c.status(204) - c.header("Content-Type", "application/json") - return stream(c, async () => { - const sessionID = c.req.valid("param").sessionID - const body = c.req.valid("json") - SessionPrompt.prompt({ ...body, sessionID }) + const sessionID = c.req.valid("param").sessionID + const body = c.req.valid("json") + // Fire-and-forget: start the prompt but don't wait for it + SessionPrompt.prompt({ ...body, sessionID }).catch((err) => { + Log.Default.error("session.prompt_async error", { + error: err instanceof Error ? err.message : String(err), + sessionID, + }) }) + // 204 No Content - no body, no Content-Type header needed + return c.body(null, 204) }, ) .post( @@ -2996,6 +3008,74 @@ export namespace Server { return c.json(true) }, ) + .post( + "/mcp/:name/reconnect", + describeRoute({ + summary: "Reconnect an MCP server", + description: "Attempt to reconnect to an MCP server that has failed or disconnected.", + operationId: "mcp.reconnect", + responses: { + 200: { + description: "MCP reconnection result", + content: { + "application/json": { + schema: resolver(MCP.Status), + }, + }, + }, + }, + }), + validator("param", z.object({ name: z.string() })), + async (c) => { + const { name } = c.req.valid("param") + const status = await MCP.reconnect(name) + return c.json(status) + }, + ) + .post( + "/mcp/reconnect-all", + describeRoute({ + summary: "Reconnect all failed MCP servers", + description: "Attempt to reconnect to all MCP servers that are in a failed state.", + operationId: "mcp.reconnectAll", + responses: { + 200: { + description: "MCP reconnection results", + content: { + "application/json": { + schema: resolver(z.record(z.string(), MCP.Status)), + }, + }, + }, + }, + }), + async (c) => { + const results = await MCP.reconnectAll() + return c.json(results) + }, + ) + .post( + "/mcp/health-check", + describeRoute({ + summary: "Health check and reconnect MCP servers", + description: "Check health of all connected MCPs and attempt to reconnect any that have failed.", + operationId: "mcp.healthCheckAndReconnect", + responses: { + 200: { + description: "MCP health check results", + content: { + "application/json": { + schema: resolver(z.record(z.string(), MCP.Status)), + }, + }, + }, + }, + }), + async (c) => { + const results = await MCP.healthCheckAndReconnect() + return c.json(results) + }, + ) .get( "/experimental/resource", describeRoute({ diff --git a/packages/agent-core/src/session/llm.ts b/packages/agent-core/src/session/llm.ts index 05bea465d0c..a5a405d3874 100644 --- a/packages/agent-core/src/session/llm.ts +++ b/packages/agent-core/src/session/llm.ts @@ -186,12 +186,13 @@ export namespace LLM { toolName: "invalid", } }, - temperature: params.temperature, - topP: params.topP, - topK: params.topK, - frequencyPenalty: params.frequencyPenalty, - presencePenalty: params.presencePenalty, - seed: params.seed, + // Only include sampling parameters if defined - some providers (Google) reject undefined values + ...(params.temperature !== undefined && { temperature: params.temperature }), + ...(params.topP !== undefined && { topP: params.topP }), + ...(params.topK !== undefined && { topK: params.topK }), + ...(params.frequencyPenalty !== undefined && { frequencyPenalty: params.frequencyPenalty }), + ...(params.presencePenalty !== undefined && { presencePenalty: params.presencePenalty }), + ...(params.seed !== undefined && { seed: params.seed }), providerOptions: ProviderTransform.providerOptions(input.model, params.options), activeTools: Object.keys(tools).filter((x) => x !== "invalid"), tools, diff --git a/packages/agent-core/src/session/processor.ts b/packages/agent-core/src/session/processor.ts index d402a7feab7..69967768230 100644 --- a/packages/agent-core/src/session/processor.ts +++ b/packages/agent-core/src/session/processor.ts @@ -231,7 +231,7 @@ export namespace SessionProcessor { state: { status: "error", input: value.input, - error: (value.error as any).toString(), + error: value.error instanceof Error ? value.error.message : String(value.error), time: { start: match.state.time.start, end: Date.now(), diff --git a/packages/agent-core/src/session/prompt.ts b/packages/agent-core/src/session/prompt.ts index 8d6b87fe71c..f271676b0e3 100644 --- a/packages/agent-core/src/session/prompt.ts +++ b/packages/agent-core/src/session/prompt.ts @@ -687,8 +687,10 @@ export namespace SessionPrompt { for (const item of await ToolRegistry.tools(input.model.providerID, input.agent)) { const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters)) tools[item.id] = tool({ + // Type assertion needed: AI SDK tool() requires specific string literal types, but our dynamic IDs are string variables id: item.id as any, description: item.description, + // Type assertion needed: AI SDK jsonSchema() expects JSONSchema7 but zod's toJSONSchema output has subtle type differences inputSchema: jsonSchema(schema as any), async execute(args, options) { const ctx = context(args, options) diff --git a/scripts/reload.sh b/scripts/reload.sh new file mode 100755 index 00000000000..d8e2c53d14f --- /dev/null +++ b/scripts/reload.sh @@ -0,0 +1,260 @@ +#!/usr/bin/env bash +# +# agent-core reload script +# Usage: ./scripts/reload.sh [--no-build] [--no-daemon] [--status] +# +# This script: +# 1. Kills all agent-core processes +# 2. Rebuilds from source (unless --no-build) +# 3. Copies binary to ~/bin/agent-core +# 4. Starts daemon (unless --no-daemon) +# 5. Verifies everything is working +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" +PKG_DIR="$REPO_ROOT/packages/agent-core" +BINARY_SRC="$PKG_DIR/dist/agent-core-linux-x64/bin/agent-core" +BINARY_DST="$HOME/bin/agent-core" +DAEMON_PORT="${AGENT_CORE_PORT:-3210}" +DAEMON_HOST="${AGENT_CORE_HOST:-127.0.0.1}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log() { echo -e "${BLUE}[reload]${NC} $*"; } +ok() { echo -e "${GREEN}[ OK ]${NC} $*"; } +warn() { echo -e "${YELLOW}[ WARN ]${NC} $*"; } +err() { echo -e "${RED}[ERROR]${NC} $*"; } + +# Parse args +NO_BUILD=false +NO_DAEMON=false +STATUS_ONLY=false + +for arg in "$@"; do + case $arg in + --no-build) NO_BUILD=true ;; + --no-daemon) NO_DAEMON=true ;; + --status) STATUS_ONLY=true ;; + --help|-h) + echo "Usage: $0 [--no-build] [--no-daemon] [--status]" + echo "" + echo "Options:" + echo " --no-build Skip rebuilding (just restart)" + echo " --no-daemon Don't start daemon after reload" + echo " --status Show status and diagnostics only" + exit 0 + ;; + esac +done + +# Status/diagnostics function +show_status() { + echo "" + echo "═══════════════════════════════════════════════════════════════" + echo " AGENT-CORE STATUS" + echo "═══════════════════════════════════════════════════════════════" + echo "" + + # Binary info + echo "Binary: $BINARY_DST" + if [[ -f "$BINARY_DST" ]]; then + local mod_time=$(stat -c "%Y" "$BINARY_DST" 2>/dev/null || stat -f "%m" "$BINARY_DST" 2>/dev/null) + local mod_date=$(date -d "@$mod_time" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r "$mod_time" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) + ok "Exists (modified: $mod_date)" + else + err "Not found" + fi + echo "" + + # Running processes + echo "Processes:" + local procs=$(pgrep -af "agent-core|bun.*print-logs" 2>/dev/null | grep -v "reload.sh" | grep -v "pgrep" || true) + if [[ -n "$procs" ]]; then + echo "$procs" | while read -r line; do + local pid=$(echo "$line" | awk '{print $1}') + local cmd=$(echo "$line" | cut -d' ' -f2-) + if [[ "$cmd" == *"daemon"* ]]; then + ok "Daemon: PID $pid" + elif [[ "$cmd" == *"bun"*"print-logs"* ]]; then + ok "TUI (dev): PID $pid" + elif [[ "$cmd" == *"print-logs"* ]] || [[ "$cmd" == *"/bin/agent-core" ]]; then + ok "TUI: PID $pid" + else + echo " Other: PID $pid - $cmd" + fi + done + else + warn "No agent-core processes running" + fi + echo "" + + # Daemon health + echo "Daemon API: http://$DAEMON_HOST:$DAEMON_PORT" + local health=$(curl -sf "http://$DAEMON_HOST:$DAEMON_PORT/global/health" 2>/dev/null || echo "") + if [[ -n "$health" ]]; then + local version=$(echo "$health" | grep -o '"version":"[^"]*"' | cut -d'"' -f4) + ok "Healthy (version: $version)" + else + warn "Not responding" + fi + echo "" + + # Tool directories + echo "Tool directories:" + for dir in "$HOME/.config/agent-core/tool" "$REPO_ROOT/.agent-core/tool"; do + if [[ -d "$dir" ]]; then + local count=$(ls -1 "$dir"/*.ts 2>/dev/null | wc -l || echo 0) + ok "$dir ($count tools)" + ls -1 "$dir"/*.ts 2>/dev/null | while read -r f; do + echo " - $(basename "$f")" + done + else + echo " $dir (not found)" + fi + done + echo "" + + # Source vs binary timestamps + echo "Source timestamps:" + local src_files=( + "$PKG_DIR/src/provider/transform.ts" + "$PKG_DIR/src/provider/provider.ts" + "$PKG_DIR/src/server/server.ts" + "$PKG_DIR/src/session/llm.ts" + ) + local binary_time=$(stat -c "%Y" "$BINARY_DST" 2>/dev/null || echo 0) + for src in "${src_files[@]}"; do + if [[ -f "$src" ]]; then + local src_time=$(stat -c "%Y" "$src" 2>/dev/null || echo 0) + local src_date=$(date -d "@$src_time" "+%H:%M:%S" 2>/dev/null || date -r "$src_time" "+%H:%M:%S" 2>/dev/null) + local name=$(basename "$src") + if [[ $src_time -gt $binary_time ]]; then + warn "$name ($src_date) - NEWER than binary, rebuild needed!" + else + ok "$name ($src_date)" + fi + fi + done + echo "" + echo "═══════════════════════════════════════════════════════════════" +} + +if $STATUS_ONLY; then + show_status + exit 0 +fi + +echo "" +echo "═══════════════════════════════════════════════════════════════" +echo " AGENT-CORE RELOAD" +echo "═══════════════════════════════════════════════════════════════" +echo "" + +# Step 1: Kill all agent-core processes +log "Stopping all agent-core processes..." + +# Kill daemon +if pgrep -f "agent-core daemon" > /dev/null 2>&1; then + pkill -9 -f "agent-core daemon" 2>/dev/null + ok "Killed daemon" +else + warn "No daemon to kill" +fi + +# Kill TUI processes (compiled binary) +if pgrep -f "agent-core.*print-logs" > /dev/null 2>&1; then + pkill -9 -f "agent-core.*print-logs" 2>/dev/null + ok "Killed TUI (binary)" +else + warn "No binary TUI to kill" +fi + +# Kill dev mode TUI (bun run dev) +if pgrep -f "bun.*print-logs" > /dev/null 2>&1; then + pkill -9 -f "bun.*print-logs" 2>/dev/null + ok "Killed TUI (dev mode)" +else + warn "No dev TUI to kill" +fi + +# Give processes time to die +sleep 1 + +# Verify nothing is running +remaining=$(pgrep -f "agent-core" 2>/dev/null | grep -v $$ | grep -v "reload" || true) +if [[ -n "$remaining" ]]; then + warn "Some processes still running, force killing..." + echo "$remaining" | xargs -r kill -9 2>/dev/null || true + sleep 1 +fi +ok "All processes stopped" + +# Step 2: Rebuild +if ! $NO_BUILD; then + log "Rebuilding agent-core..." + cd "$PKG_DIR" + if bun run build 2>&1 | tail -5; then + ok "Build complete" + else + err "Build failed!" + exit 1 + fi +else + warn "Skipping build (--no-build)" +fi + +# Step 3: Copy binary +log "Installing binary..." +if [[ -f "$BINARY_SRC" ]]; then + cp "$BINARY_SRC" "$BINARY_DST" + chmod +x "$BINARY_DST" + ok "Installed to $BINARY_DST" +else + err "Binary not found at $BINARY_SRC" + exit 1 +fi + +# Step 4: Start daemon +if ! $NO_DAEMON; then + log "Starting daemon..." + nohup "$BINARY_DST" daemon --hostname "$DAEMON_HOST" --port "$DAEMON_PORT" --gateway > /tmp/agent-core-daemon.log 2>&1 & + DAEMON_PID=$! + sleep 2 + + # Verify daemon started + if kill -0 "$DAEMON_PID" 2>/dev/null; then + # Check health endpoint + for i in {1..5}; do + health=$(curl -sf "http://$DAEMON_HOST:$DAEMON_PORT/global/health" 2>/dev/null || echo "") + if [[ -n "$health" ]]; then + version=$(echo "$health" | grep -o '"version":"[^"]*"' | cut -d'"' -f4) + ok "Daemon started (PID: $DAEMON_PID, version: $version)" + break + fi + sleep 1 + done + if [[ -z "$health" ]]; then + warn "Daemon started but health check failed" + fi + else + err "Daemon failed to start! Check /tmp/agent-core-daemon.log" + tail -20 /tmp/agent-core-daemon.log + exit 1 + fi +else + warn "Skipping daemon start (--no-daemon)" +fi + +echo "" +echo "═══════════════════════════════════════════════════════════════" +echo " RELOAD COMPLETE" +echo "═══════════════════════════════════════════════════════════════" +show_status diff --git a/vendor/tiara b/vendor/tiara index 912b9393561..b86598edbb0 160000 --- a/vendor/tiara +++ b/vendor/tiara @@ -1 +1 @@ -Subproject commit 912b93935614eb9d567b6106346067f2e4e6268a +Subproject commit b86598edbb0487051056fcadd7d997365db6ac7b